Merge remote-tracking branch 'consul/master' into iagirre-admin-poll-stats

This commit is contained in:
iagirre
2017-10-24 10:37:47 +02:00
39 changed files with 760 additions and 345 deletions

View File

@@ -214,6 +214,8 @@ a {
margin-bottom: $line-height / 2;
li {
font-size: $base-font-size;
margin-bottom: 0;
margin-right: $line-height / 2;
@include breakpoint(medium) {
@@ -316,13 +318,16 @@ a {
}
.tabs {
border: {
left: 0;
right: 0;
top: 0;
};
border-left: 0;
border-right: 0;
border-top: 0;
margin-bottom: $line-height;
.tabs-title {
font-size: $base-font-size;
margin-bottom: 0;
}
.tabs-title > a {
color: $text-medium;
margin-bottom: rem-calc(-1);
@@ -376,6 +381,10 @@ a {
box-shadow: none;
}
.uppercase {
text-transform: uppercase;
}
// 02. Header
// ----------

View File

@@ -8,6 +8,7 @@
// 06. Budget
// 07. Proposals successful
// 08. Polls
// 09. Polls results and stats
//
// 01. Votes and supports
@@ -1799,3 +1800,59 @@
}
}
}
// 09. Polls results and stats
// ---------------------------
.polls-results-stats {
.sidebar {
border-bottom: 1px solid $border;
margin-bottom: $line-height;
@include breakpoint(medium) {
border-bottom: 0;
border-right: 1px solid $border;
}
.menu {
padding: 0;
li a {
color: $link;
line-height: $line-height;
}
}
}
table {
table-layout: fixed;
caption {
padding: $line-height / 2 0;
text-align: left;
}
th {
text-align: left;
&.win {
background: #009fde;
}
}
td {
&.win {
background: #ccedf8;
font-weight: bold;
}
}
}
.number {
font-size: rem-calc(60);
font-weight: bold;
line-height: rem-calc(60);
}
}

View File

@@ -58,9 +58,10 @@ class Admin::Poll::PollsController < Admin::Poll::BaseController
end
def poll_params
params.require(:poll).permit(:name, :starts_at, :ends_at, :geozone_restricted, :summary, :description,
geozone_ids: [],
image_attributes: [:id, :title, :attachment, :cached_attachment, :user_id, :_destroy])
params.require(:poll).permit(:name, :starts_at, :ends_at, :geozone_restricted,
:summary, :description, :results_enabled, :stats_enabled,
geozone_ids: [],
image_attributes: [:id, :title, :attachment, :cached_attachment, :user_id, :_destroy])
end
def search_params

View File

@@ -6,6 +6,8 @@ class Admin::Poll::ShiftsController < Admin::Poll::BaseController
def new
load_shifts
@shift = ::Poll::Shift.new
@voting_polls = @booth.polls.current_or_incoming
@recount_polls = @booth.polls.current_or_recounting_or_incoming
end
def create

View File

@@ -13,6 +13,9 @@ class Polls::QuestionsController < ApplicationController
answer.touch if answer.persisted?
answer.save!
answer.record_voter_participation(token)
@question.question_answers.where(question_id: @question).each do |answer|
answer.set_most_voted
end
@answers_by_question_id = { @question.id => params[:answer] }
end

View File

@@ -25,8 +25,13 @@ class PollsController < ApplicationController
@commentable = @poll
@comment_tree = CommentTree.new(@commentable, params[:page], @current_order)
end
def stats
@stats = Poll::Stats.new(@poll).generate
end
def results
end
end

View File

@@ -1,10 +1,12 @@
module ShiftsHelper
def shift_vote_collection_dates(booth, polls)
return [] if polls.blank?
date_options((start_date(polls)..end_date(polls)), Poll::Shift.tasks[:vote_collection], booth)
end
def shift_recount_scrutiny_dates(booth, polls)
return [] if polls.blank?
dates = polls.map(&:ends_at).map(&:to_date).sort.inject([]) do |total, date|
initial_date = date < Date.current ? Date.current : date
total << (initial_date..date + Poll::RECOUNT_DURATION).to_a

View File

@@ -56,7 +56,7 @@ module Abilities
can [:index, :create, :edit, :update, :destroy], Geozone
can [:read, :create, :update, :destroy, :add_question, :search_booths, :search_officers, :booth_assignments], Poll
can [:read, :create, :update, :destroy, :add_question, :search_booths, :search_officers, :booth_assignments, :results, :stats], Poll
can [:read, :create, :update, :destroy, :available], Poll::Booth
can [:search, :create, :index, :destroy], ::Poll::Officer
can [:create, :destroy, :manage], ::Poll::BoothAssignment

View File

@@ -7,6 +7,12 @@ module Abilities
can [:read, :map, :summary, :share], Proposal
can :read, Comment
can :read, Poll
can :results, Poll do |poll|
poll.expired? && poll.results_enabled?
end
can :stats, Poll do |poll|
poll.expired? && poll.stats_enabled?
end
can :read, Poll::Question
can [:read, :welcome], Budget
can :read, SpendingProposal
@@ -23,7 +29,6 @@ module Abilities
can [:read], Legislation::Question
can [:create], Legislation::Answer
can [:search, :comments, :read, :create, :new_comment], Legislation::Annotation
can :read_stats, Poll
end
end
end

View File

@@ -12,7 +12,7 @@ class Poll
end
def self.available
where(polls: { id: Poll.current_or_incoming }).includes(:polls)
where(polls: { id: Poll.current_or_recounting_or_incoming }).includes(:polls)
end
def assignment_on_poll(poll)

View File

@@ -55,4 +55,8 @@ class Poll::Question < ActiveRecord::Base
where(poll_id: Poll.answerable_by(user).pluck(:id))
end
def answers_total_votes
question_answers.map { |a| Poll::Answer.where(question_id: self, answer: a.title).count }.sum
end
end

View File

@@ -31,4 +31,24 @@ class Poll::Question::Answer < ActiveRecord::Base
def self.last_position(question_id)
where(question_id: question_id).maximum('given_order') || 0
end
def total_votes
Poll::Answer.where(question_id: question, answer: title).count
end
def most_voted?
self.most_voted
end
def total_votes_percentage
question.answers_total_votes == 0 ? 0 : (total_votes * 100) / question.answers_total_votes
end
def set_most_voted
answers = question.question_answers
.map { |a| Poll::Answer.where(question_id: a.question, answer: a.title).count }
is_most_voted = !answers.any?{ |a| a > self.total_votes }
self.update(most_voted: is_most_voted)
end
end

View File

@@ -16,110 +16,128 @@ class Poll
end
private
def total_participants
total_participants_web + total_participants_booth
stats_cache('total_participants') { total_participants_web + total_participants_booth }
end
def total_participants_web
total_web_valid + total_web_white + total_web_null
stats_cache('total_participants_web') { total_web_valid + total_web_white + total_web_null }
end
def total_participants_web_percentage
(total_participants) == 0 ? 0 : total_participants_web * 100 / total_participants
stats_cache('total_participants_web_percentage') {
(total_participants) == 0 ? 0 : total_participants_web * 100 / total_participants
}
end
def total_participants_booth
voters.where(origin: 'booth').count
stats_cache('total_participants_booth') { voters.where(origin: 'booth').count }
end
def total_participants_booth_percentage
(total_participants) == 0 ? 0 : total_participants_booth * 100 / total_participants.to_f
stats_cache('total_participants_booth_percentage') {
(total_participants) == 0 ? 0 : total_participants_booth * 100 / total_participants.to_f
}
end
def total_web_valid
voters.where(origin: 'web').count
stats_cache('total_web_valid') { voters.where(origin: 'web').count }
end
def valid_percentage_web
(total_valid_votes) == 0 ? 0 : total_web_valid * 100 / total_valid_votes.to_f
stats_cache('valid_percentage_web') {
(total_valid_votes) == 0 ? 0 : total_web_valid * 100 / total_valid_votes.to_f
}
end
def total_web_white
0
stats_cache('total_web_white') { 0 }
end
def white_percentage_web
0
stats_cache('white_percentage_web') { 0 }
end
def total_web_null
0
stats_cache('total_web_null') { 0 }
end
def null_percentage_web
0
stats_cache('null_percentage_web') { 0 }
end
def total_booth_valid
recounts.sum(:total_amount)
stats_cache('total_booth_valid') { recounts.sum(:total_amount) }
end
def valid_percentage_booth
(total_valid_votes) == 0 ? 0 : total_booth_valid * 100 / total_valid_votes.to_f
stats_cache('valid_percentage_booth') {
(total_valid_votes) == 0 ? 0 : total_booth_valid * 100 / total_valid_votes.to_f
}
end
def total_booth_white
recounts.sum(:white_amount)
def total_booth_white
stats_cache('total_booth_white') { recounts.sum(:white_amount) }
end
def white_percentage_booth
(total_white_votes) == 0 ? 0 : total_booth_white * 100 / total_white_votes.to_f
stats_cache('white_percentage_booth') {
(total_white_votes) == 0 ? 0 : total_booth_white * 100 / total_white_votes.to_f
}
end
def total_booth_null
recounts.sum(:null_amount)
stats_cache('total_booth_null') { recounts.sum(:null_amount) }
end
def null_percentage_booth
(total_null_votes == 0) ? 0 : total_booth_null * 100 / total_null_votes.to_f
stats_cache('null_percentage_booth') {
(total_null_votes == 0) ? 0 : total_booth_null * 100 / total_null_votes.to_f
}
end
def total_valid_votes
total_web_valid + total_booth_valid
stats_cache('total_valid_votes') { total_web_valid + total_booth_valid }
end
def total_valid_percentage
(total_participants) == 0 ? 0 : total_valid_votes * 100 / total_participants.to_f
stats_cache('total_valid_percentage'){
(total_participants) == 0 ? 0 : total_valid_votes * 100 / total_participants.to_f
}
end
def total_white_votes
total_web_white + total_booth_white
stats_cache('total_white_votes') { total_web_white + total_booth_white }
end
def total_white_percentage
(total_participants) == 0 ? 0 : total_white_votes * 100 / total_participants.to_f
stats_cache('total_white_percentage') {
(total_participants) == 0 ? 0 : total_white_votes * 100 / total_participants.to_f
}
end
def total_null_votes
total_web_null + total_booth_null
stats_cache('total_null_votes') { total_web_null + total_booth_null }
end
def total_null_percentage
(total_participants) == 0 ? 0 : total_null_votes * 100 / total_participants.to_f
stats_cache('total_null_percentage') {
(total_participants) == 0 ? 0 : total_null_votes * 100 / total_participants.to_f
}
end
def voters
@poll.voters
stats_cache('voters') { @poll.voters }
end
def recounts
@poll.recounts
stats_cache('recounts') { @poll.recounts }
end
def stats_cache(key, &block)
Rails.cache.fetch("polls_stats/#{@poll.id}/#{key}/v7", &block)
Rails.cache.fetch("polls_stats/#{@poll.id}/#{key}", &block)
end
end
end
end

View File

@@ -53,6 +53,17 @@
</div>
</div>
<% if controller_name == "polls" && action_name == "edit" %>
<div class="row">
<fieldset class="fieldset">
<legend><%= t('admin.polls.new.show_results_and_stats') %></legend>
<%= f.check_box :results_enabled, checked: @poll.results_enabled?, label: t('admin.polls.new.show_results') %>
<%= f.check_box :stats_enabled, checked: @poll.stats_enabled?, label: t('admin.polls.new.show_stats') %>
<p class="small"><%= t('admin.polls.new.results_and_stats_reminder') %></p>
</fieldset>
</div>
<% end %>
<div class="row">
<div class="small-12 medium-4 column">
<%= f.submit t("admin.polls.#{admin_submit_action(@poll)}.submit_button"),

View File

@@ -3,7 +3,7 @@
<fieldset class="fieldset">
<legend>
<%= t("admin.poll_shifts.new.new_shift") %>
<%= t("admin.poll_shifts.new.new_shift") %>
</legend>
<div class="small-12 medium-3 column highlight padding">
@@ -24,12 +24,12 @@
<div class="small-12 medium-3 column">
<label><%= t("admin.poll_shifts.new.date") %></label>
<%= select 'shift[date]', 'vote_collection_date',
options_for_select(shift_vote_collection_dates(@booth, @booth.polls.current_or_incoming)),
{ prompt: t("admin.poll_shifts.new.select_date"),
options_for_select(shift_vote_collection_dates(@booth, @voting_polls)),
{ prompt: @voting_polls.present? ? t("admin.poll_shifts.new.select_date") : t("admin.poll_shifts.new.no_voting_days"),
label: false },
class: 'js-shift-vote-collection-dates' %>
<%= select 'shift[date]', 'recount_scrutiny_date',
options_for_select(shift_recount_scrutiny_dates(@booth, @booth.polls.current_or_recounting_or_incoming)),
options_for_select(shift_recount_scrutiny_dates(@booth, @recount_polls)),
{ prompt: t("admin.poll_shifts.new.select_date"),
label: false },
class: 'js-shift-recount-scrutiny-dates',

View File

@@ -1,6 +1,6 @@
<div class="row">
<div class="row margin-top">
<div class="small-12 column">
<ul class="tabs" data-tabs id="polls-tabs">
<ul class="tabs" data-tabs id="polls_tabs">
<li class="tabs-title is-active">
<%= link_to "#tab-comments" do %>
<h3>

View File

@@ -19,11 +19,27 @@
<div class="dates"></div>
<% if poll.questions.count == 1 %>
<% poll.questions.each do |question| %>
<h4><%= link_to question.title, poll %></h4>
<h4>
<% if poll.results_enabled? %>
<%= link_to question.title, results_poll_path(poll) %>
<% elsif poll.stats_enabled? %>
<%= link_to question.title, stats_poll_path(poll) %>
<% else %>
<%= link_to question.title, poll %>
<% end %>
</h4>
<%= poll_dates(poll) %>
<% end %>
<% else %>
<h4><%= link_to poll.name, poll %></h4>
<h4>
<% if poll.results_enabled? %>
<%= link_to poll.name, results_poll_path(poll) %>
<% elsif poll.stats_enabled? %>
<%= link_to poll.name, stats_poll_path(poll) %>
<% else %>
<%= link_to poll.name, poll %>
<% end %>
</h4>
<%= poll_dates(poll) %>
<ul class="margin-top">
<% poll.questions.each do |question| %>

View File

@@ -0,0 +1,27 @@
<div class="expanded no-margin-top polls-show-header">
<div class="row">
<div class="small-12 medium-9 column padding">
<%= back_link_to polls_path, t("polls.show.back") %>
<h2><%= @poll.name %></h2>
<%= safe_html_with_links simple_format(@poll.summary) %>
<% if @poll.geozones.any? %>
<ul class="no-bullet margin-top tags">
<% @poll.geozones.each do |g| %>
<li class="inline-block"><span><%= g.name %></span></li>
<% end %>
</ul>
<% end %>
</div>
<aside class="small-12 medium-3 column margin-top">
<%= render partial: 'shared/social_share', locals: {
share_title: t("shared.share"),
title: @poll.name,
url: poll_url(@poll)
} %>
</aside>
</div>
</div>

View File

@@ -0,0 +1,36 @@
<% if current_user && current_user.administrator? ||
(@poll.expired? && (@poll.results_enabled? || @poll.stats_enabled?)) %>
<div class="row margin-top">
<div class="small-12 column">
<ul class="menu simple clear">
<% if current_user && current_user.administrator? || @poll.results_enabled? %>
<% if controller_name == "polls" && action_name == "results" %>
<li class="active">
<h2><%= t("polls.show.results_menu") %></h2>
</li>
<% else %>
<li><%= link_to t("polls.show.results_menu"), results_poll_path(@poll) %></li>
<% end %>
<% end %>
<% if current_user && current_user.administrator? || @poll.stats_enabled? %>
<% if controller_name == "polls" && action_name == "stats" %>
<li class="active">
<h2><%= t("polls.show.stats_menu") %></h2>
</li>
<% else %>
<li><%= link_to t("polls.show.stats_menu"), stats_poll_path(@poll) %></li>
<% end %>
<% end %>
<% if controller_name == "polls" && action_name == "show" %>
<li class="active">
<h2><%= t("polls.show.info_menu") %></h2>
</li>
<% else %>
<li><%= link_to t("polls.show.info_menu"), poll_path(@poll) %></li>
<% end %>
</ul>
</div>
</div>
<% end %>

View File

@@ -1,20 +0,0 @@
<div class="row">
<div class="small-12 column">
<ul class="tabs" data-tabs id="polls-tabs">
<li class="tabs-title">
<%= link_to "#tab-stats" do %>
<h3>
<%= t("polls.show.stats_menu") %> <!-- t("polls.show.comments_tab") -->
</h3>
<% end %>
</li>
<li class="tabs-title is-active">
<%= link_to "#tab-information" do %>
<h3>
<%= t("polls.show.info_menu") %>
</h3>
<% end %>
</li>
</ul>
</div>
</div>

View File

@@ -1,126 +0,0 @@
<div class="row margin">
<div class="small-12 medium-9 column">
<%= render "callout" %>
<% if @poll.voted_in_booth?(current_user) %>
<div class="callout warning">
<%= t("polls.show.already_voted_in_booth") %>
</div>
<% else %>
<% if current_user && @poll.voted_in_web?(current_user) %>
<div class="callout warning">
<%= t("polls.show.already_voted_in_web") %>
</div>
<% end %>
<% end %>
<% @questions.each do |question| %>
<%= render 'polls/questions/question', question: question, token: @token %>
<% end %>
<% if poll_voter_token(@poll, current_user).empty? %>
<div class="callout token-message js-token-message" style="display: none">
<%= t('poll_questions.show.voted_token') %>
</div>
<% end %>
<%= link_to t("polls.show.participate_in_other_polls"), polls_path, class: "button hollow" %>
</div>
</div>
<div class="expanded poll-more-info">
<div class="row margin">
<div class="small-12 medium-9 column">
<h3><%= t("polls.show.more_info_title") %></h3>
<%= safe_html_with_links simple_format(@poll.description) %>
</div>
<% if false %>
<aside class="small-12 medium-3 column">
<div class="sidebar-divider"></div>
<h2><%= t("polls.show.documents") %></h2>
</aside>
<% end %>
</div>
</div>
<div class="expanded poll-more-info-answers">
<div class="row padding">
<% @poll_questions_answers.each do |answer| %>
<div class="small-12 medium-6 column end answer <%= cycle('first', '') %>" id="answer_<%= answer.id %>">
<% if answer.description.present? %>
<h3><%= answer.title %></h3>
<% end %>
<% if answer.images.any? %>
<%= render "gallery", answer: answer %>
<% end %>
<% if answer.description.present? %>
<div class="margin-top">
<div id="answer_description_<%= answer.id %>" class="answer-description short" data-toggler="short">
<%= safe_html_with_links simple_format(answer.description) %>
</div>
<div class="margin">
<a id="read_more_<%= answer.id %>"
data-toggle="answer_description_<%= answer.id %> read_more_<%= answer.id %> read_less_<%= answer.id %>"
data-toggler="hide">
<%= t("polls.show.read_more", answer: answer.title) %>
</a>
<a id="read_less_<%= answer.id %>"
data-toggle="answer_description_<%= answer.id %> read_more_<%= answer.id %> read_less_<%= answer.id %>"
data-toggler="hide"
class="hide">
<%= t("polls.show.read_less", answer: answer.title) %>
</a>
</div>
</div>
<% end %>
<% if answer.documents.present? %>
<div class="document-link">
<p>
<span class="icon-document"></span>&nbsp;
<strong><%= t("polls.show.documents") %></strong>
</p>
<% answer.documents.each do |document| %>
<%= link_to document.title,
document.attachment.url,
target: "_blank",
rel: "nofollow" %><br>
<% end %>
</div>
<% end %>
<% if answer.videos.present? %>
<div class="video-link">
<p>
<span class="icon-video"></span>&nbsp;
<strong><%= t("polls.show.videos") %></strong>
</p>
<% answer.videos.each do |video| %>
<%= link_to video.title,
video.url,
target: "_blank",
rel: "nofollow" %><br>
<% end %>
</div>
<% end %>
</div>
<% end %>
</div>
</div>
<div class="tabs-content" data-tabs-content="proposals-tabs" role="tablist">
<%= render "filter_subnav" %>
<div class="tabs-panel is-active" id="tab-comments">
<%= render "comments" %>
</div>
</div>

View File

@@ -0,0 +1,49 @@
<% provide :title do %><%= @poll.name %><% end %>
<div class="polls-results-stats">
<%= render "poll_header" %>
<%= render "poll_subnav" %>
<div class="row margin" data-equalizer data-equalize-on="medium">
<div class="small-12 medium-3 column sidebar" data-equalizer-watch>
<p><strong><%= t("polls.show.results.title") %></strong></p>
<ul class="menu vertical">
<%- @poll.questions.each do |question| %>
<li><%=link_to question.title, "##{question.title.parameterize}" %></li>
<% end %>
</ul>
</div>
<div class="small-12 medium-9 column" data-equalizer-watch>
<%- @poll.questions.each do |question| %>
<table id="question_<%= question.id %>_results_table">
<h3 id="<%= question.title.parameterize %>"><%= question.title %></h3>
<thead>
<tr>
<%- question.question_answers.each do |answer| %>
<th scope="col" <%= answer.most_voted? ? "class=win" : "" %>>
<% if answer.most_voted %>
<span class="show-for-sr"><%= t("polls.show.results.most_voted_answer") %></span>
<% end %>
<%= answer.title %>
</th>
<% end %>
</tr>
</thead>
<tbody>
<tr>
<%- question.question_answers.each do |answer| %>
<td id="answer_<%= answer.id %>_result" <%= answer.most_voted? ? "class=win" : "" %>>
<%= answer.total_votes %>
(<%= answer.total_votes_percentage.round(2) %>%)
</td>
<% end %>
</tr>
</tbody>
</table>
<% end %>
</div>
</div>
</div>

View File

@@ -1,44 +1,134 @@
<% provide :title do %><%= @poll.name %><% end %>
<div class="polls-show">
<div class="expanded no-margin-top polls-show-header">
<div class="row">
<div class="small-12 medium-9 column padding">
<%= back_link_to polls_path, t("polls.show.back") %>
<%= render "poll_header" %>
<h2><%= @poll.name %></h2>
<%= render "poll_subnav" %>
<%= safe_html_with_links simple_format(@poll.summary) %>
<div class="row margin">
<div class="small-12 medium-9 column">
<%= render "callout" %>
<% if @poll.geozones.any? %>
<ul class="no-bullet margin-top tags">
<% @poll.geozones.each do |g| %>
<li class="inline-block"><span><%= g.name %></span></li>
<% end %>
</ul>
<% if @poll.voted_in_booth?(current_user) %>
<div class="callout warning">
<%= t("polls.show.already_voted_in_booth") %>
</div>
<% else %>
<% if current_user && @poll.voted_in_web?(current_user) && !@poll.expired? %>
<div class="callout warning">
<%= t("polls.show.already_voted_in_web") %>
</div>
<% end %>
<% end %>
<% @questions.each do |question| %>
<%= render 'polls/questions/question', question: question, token: @token %>
<% end %>
<% if poll_voter_token(@poll, current_user).empty? %>
<div class="callout token-message js-token-message" style="display: none">
<%= t('poll_questions.show.voted_token') %>
</div>
<% end %>
<%= link_to t("polls.show.participate_in_other_polls"), polls_path, class: "button hollow" %>
</div>
</div>
<div class="expanded poll-more-info">
<div class="row margin">
<div class="small-12 medium-9 column">
<h3><%= t("polls.show.more_info_title") %></h3>
<%= safe_html_with_links simple_format(@poll.description) %>
</div>
<aside class="small-12 medium-3 column margin-top">
<%= render partial: 'shared/social_share', locals: {
share_title: t("shared.share"),
title: @poll.name,
url: poll_url
} %>
</aside>
<% if false %>
<aside class="small-12 medium-3 column">
<div class="sidebar-divider"></div>
<h2><%= t("polls.show.documents") %></h2>
</aside>
<% end %>
</div>
</div>
<div class="tabs-content" data-tabs-content="polls-tabs" role="tablist">
<%= render "results_subnavigation" %>
<div id="tab-stats" class="tabs-panel">
<%= render "polls/stats/show" %>
<div class="expanded poll-more-info-answers">
<div class="row padding">
<% @poll_questions_answers.each do |answer| %>
<div class="small-12 medium-6 column end answer <%= cycle('first', '') %>" id="answer_<%= answer.id %>">
<% if answer.description.present? %>
<h3><%= answer.title %></h3>
<% end %>
<% if answer.images.any? %>
<%= render "gallery", answer: answer %>
<% end %>
<% if answer.description.present? %>
<div class="margin-top">
<div id="answer_description_<%= answer.id %>" class="answer-description short" data-toggler="short">
<%= safe_html_with_links simple_format(answer.description) %>
</div>
<div class="margin">
<a id="read_more_<%= answer.id %>"
data-toggle="answer_description_<%= answer.id %> read_more_<%= answer.id %> read_less_<%= answer.id %>"
data-toggler="hide">
<%= t("polls.show.read_more", answer: answer.title) %>
</a>
<a id="read_less_<%= answer.id %>"
data-toggle="answer_description_<%= answer.id %> read_more_<%= answer.id %> read_less_<%= answer.id %>"
data-toggler="hide"
class="hide">
<%= t("polls.show.read_less", answer: answer.title) %>
</a>
</div>
</div>
<% end %>
<% if answer.documents.present? %>
<div class="document-link">
<p>
<span class="icon-document"></span>&nbsp;
<strong><%= t("polls.show.documents") %></strong>
</p>
<% answer.documents.each do |document| %>
<%= link_to document.title,
document.attachment.url,
target: "_blank",
rel: "nofollow" %><br>
<% end %>
</div>
<% end %>
<% if answer.videos.present? %>
<div class="video-link">
<p>
<span class="icon-video"></span>&nbsp;
<strong><%= t("polls.show.videos") %></strong>
</p>
<% answer.videos.each do |video| %>
<%= link_to video.title,
video.url,
target: "_blank",
rel: "nofollow" %><br>
<% end %>
</div>
<% end %>
</div>
<% end %>
</div>
</div>
<div class="tabs-content" data-tabs-content="polls_tabs" role="tablist">
<%= render "filter_subnav" %>
<div class="tabs-panel is-active" id="tab-comments">
<%= render "comments" %>
</div>
<div id="tab-information" class="tabs-panel is-active">
<%= render "show" %>
</div>
</div>
</div>

View File

@@ -0,0 +1,96 @@
<% provide :title do %><%= @poll.name %><% end %>
<div class="polls-results-stats">
<%= render "poll_header" %>
<%= render "poll_subnav" %>
<div class="row margin" data-equalizer data-equalize-on="medium">
<div class="small-12 medium-3 column sidebar" data-equalizer-watch>
<p><strong><%= t("polls.show.stats.title") %></strong></p>
<ul class="menu vertical margin-top">
<li><a href="#total"><%= t("polls.show.stats.total_participation") %></a></li>
</ul>
</div>
<div class="small-12 medium-9 column" data-equalizer-watch>
<h3 id="total"><%= t("polls.show.stats.total_participation") %></h3>
<p class="margin-top uppercase">
<%= t("polls.show.stats.total_votes") %><br>
<span class="number"><%= @stats[:total_participants] %></span>
</p>
<table>
<thead>
<tr>
<th scope="col"><%= t("polls.show.stats.votes") %></th>
<th scope="col"><%= t("polls.show.stats.web") %></th>
<th scope="col"><%= t("polls.show.stats.booth") %></th>
<th scope="col"><%= t("polls.show.stats.total") %></th>
</tr>
</thead>
<tbody>
<tr>
<th scope="row"><%= t("polls.show.stats.valid") %></th>
<td>
<%= @stats[:total_web_valid] %>
<small><em>(<%= @stats[:valid_percentage_web].round(2) %>%)</em></small>
</td>
<td>
<%= @stats[:total_booth_valid] %>
<small><em>(<%= @stats[:valid_percentage_booth].round(2) %>%)</em></small>
</td>
<td>
<%= @stats[:total_valid_votes] %>
<small><em>(<%= @stats[:total_valid_percentage].round(2) %>%)</em></small>
</td>
</tr>
<tr>
<th scope="row"><%= t("polls.show.stats.white") %></th>
<td>
<%= @stats[:total_web_white] %>
<small><em>(<%= @stats[:white_percentage_web].round(2) %>%)</em></small>
</td>
<td>
<%= @stats[:total_booth_white] %>
<small><em>(<%= @stats[:white_percentage_booth].round(2) %>%)</em></small>
</td>
<td><%= @stats[:total_white_votes] %>
<small><em>(<%= @stats[:total_white_percentage].round(2) %>%)</em></small>
</td>
</tr>
<tr>
<th scope="row"><%= t("polls.show.stats.null_votes") %></th>
<td>
<%= @stats[:total_web_null] %>
<small><em>(<%= @stats[:null_percentage_web].round(2) %>%)</em></small>
</td>
<td>
<%= @stats[:total_booth_null] %>
<small><em>(<%= @stats[:null_percentage_booth].round(2) %>%)</em></small>
</td>
<td>
<%= @stats[:total_null_votes] %>
<small><em>(<%= @stats[:total_null_percentage].round(2) %>%)</em></small>
</td>
</tr>
<tr>
<th scope="row"><%= t("polls.show.stats.total") %></th>
<td>
<%= @stats[:total_participants_web] %>
<small><em>(<%= @stats[:total_participants_web_percentage].round(2) %>%)</em></small>
</td>
<td>
<%= @stats[:total_participants_booth] %>
<small><em>(<%= @stats[:total_participants_booth_percentage].round(2) %>%)</em></small>
</td>
<td><%= @stats[:total_participants_web] + @stats[:total_participants_booth] %></td>
</tr>
</tbody>
</table>
</div>
</div>
</div>

View File

@@ -1,85 +0,0 @@
<div class="row">
<div class="small-12 medium-3 column sidebar">
<p><%= t("polls.show.stats.title") %></p>
<ul class="menu vertical margin-top">
<li><a href="#total"><%= t("polls.show.stats.total_participation") %></a></li>
</ul>
</div>
<div class="small-12 medium-9 column">
<h3 id="total"><%= t("polls.show.stats.total_participation") %></h3>
<p class="stat-number margin-top">
<%= t("polls.show.stats.total_votes") %><br>
<span><%= @stats[:total_participants] %></span>
</p>
<table class="polls-total-stats">
<thead>
<tr>
<th scope="col"><%= t("polls.show.stats.votes") %></th>
<th scope="col"><%= t("polls.show.stats.web") %></th>
<th scope="col"><%= t("polls.show.stats.booth") %></th>
<th scope="col"><%= t("polls.show.stats.total") %></th>
</tr>
</thead>
<tbody>
<tr>
<th><%= t("polls.show.stats.valid") %></th>
<td><%= @stats[:total_web_valid] %> <small><em>
(<%= number_to_percentage(@stats[:valid_percentage_web],
strip_insignificant_zeros: true,
precision: 2) %>)</em></small></td>
<td><%= @stats[:total_booth_valid] %> <small><em>
(<%= number_to_percentage(@stats[:valid_percentage_booth],
strip_insignificant_zeros: true,
precision: 2) %>)</em></small></td>
<td><%= @stats[:total_valid_votes] %> <small><em>
(<%= number_to_percentage(@stats[:total_valid_percentage],
strip_insignificant_zeros: true,
precision: 2) %>)</em></small></td>
</tr>
<tr>
<th><%= t("polls.show.stats.white") %></th>
<td><%= @stats[:total_web_white] %> <small><em>
(<%= number_to_percentage(@stats[:white_percentage_web],
strip_insignificant_zeros: true,
precision: 2) %>)</em></small></td>
<td><%= @stats[:total_booth_white] %> <small><em>
(<%= number_to_percentage(@stats[:white_percentage_booth],
strip_insignificant_zeros: true,
precision: 2) %>)</em></small></td>
<td><%= @stats[:total_white_votes] %> <small><em>
(<%= number_to_percentage(@stats[:total_white_percentage],
strip_insignificant_zeros: true,
precision: 2) %>)</em></small></td>
</tr>
<tr>
<th><%= t("polls.show.stats.null_votes") %></th>
<td><%= @stats[:total_web_null] %> <small><em>
(<%= number_to_percentage(@stats[:null_percentage_web],
strip_insignificant_zeros: true,
precision: 2) %>)</em></small></td>
<td><%= @stats[:total_booth_null] %> <small><em>
(<%= number_to_percentage(@stats[:null_percentage_booth],
strip_insignificant_zeros: true,
precision: 2) %>)</em></small></td>
<td><%= @stats[:total_null_votes] %> <small><em>
(<%= number_to_percentage(@stats[:total_null_percentage],
strip_insignificant_zeros: true,
precision: 2) %>)</em></small></td>
</tr>
<tr>
<th><%= t("polls.show.stats.total") %></th>
<td><%= @stats[:total_participants_web] %> <small><em>
(<%= number_to_percentage(@stats[:total_participants_web_percentage],
strip_insignificant_zeros: true,
precision: 2) %>)</em></small></td>
<td><%= @stats[:total_participants_booth] %> <small><em>
(<%= number_to_percentage(@stats[:total_participants_booth_percentage],
strip_insignificant_zeros: true,
precision: 2) %>)</em></small></td>
<td><%= @stats[:total_participants_web] + @stats[:total_participants_booth] %></td>
</tr>
</tbody>
</table>
</div>
</div>

View File

@@ -515,6 +515,7 @@ en:
search_officer_placeholder: Search officer
search_officer_text: Search for an officer to assign a new shift
select_date: "Select day"
no_voting_days: "Los dias de votación terminaron"
select_task: "Select task"
table_shift: "Shift"
table_email: "Email"
@@ -569,6 +570,10 @@ en:
geozone_restricted: "Restricted to districts"
new:
title: "New poll"
show_results_and_stats: "Show results and stats"
show_results: "Show results"
show_stats: "Show stats"
results_and_stats_reminder: "Marking these checkboxes the results and/or stats of this poll will be publicly available and every user will see them."
submit_button: "Create poll"
edit:
title: "Edit poll"

View File

@@ -501,6 +501,7 @@ en:
videos: "External video"
info_menu: "Information"
stats_menu: "Participation statistics"
results_menu: "Poll results"
stats:
title: "Participation data"
total_participation: "Total participation"
@@ -512,6 +513,9 @@ en:
valid: "Valid"
white: "White votes"
null_votes: "Invalid"
results:
title: "Questions"
most_voted_answer: "Most voted answer: "
poll_questions:
create_question: "Create question"
show:

View File

@@ -515,6 +515,7 @@ es:
search_officer_placeholder: Buscar presidentes de mesa
search_officer_text: Busca al presidente de mesa para asignar un turno
select_date: "Seleccionar día"
no_voting_days: "Voting days ended"
select_task: "Seleccionar tarea"
table_shift: "Turno"
table_email: "Email"
@@ -569,6 +570,10 @@ es:
geozone_restricted: "Restringida a los distritos"
new:
title: "Nueva votación"
show_results_and_stats: "Mostrar resultados y estadísticas"
show_results: "Mostrar resultados"
show_stats: "Mostrar estadísticas"
results_and_stats_reminder: "Si marcas estas casillas los resultados y/o estadísticas de esta votación serán públicos y podrán verlos todos los usuarios."
submit_button: "Crear votación"
edit:
title: "Editar votación"

View File

@@ -501,6 +501,7 @@ es:
videos: "Vídeo externo"
info_menu: "Información"
stats_menu: "Estadísticas de participación"
results_menu: "Resultados de la votación"
stats:
title: "Datos de participación"
total_participation: "Participación total"
@@ -512,6 +513,9 @@ es:
valid: "Válidos"
white: "En blanco"
null_votes: "Nulos"
results:
title: "Preguntas"
most_voted_answer: "Respuesta más votada: "
poll_questions:
create_question: "Crear pregunta para votación"
show:

View File

@@ -113,6 +113,10 @@ Rails.application.routes.draw do
end
resources :polls, only: [:show, :index] do
member do
get :stats
get :results
end
resources :questions, controller: 'polls/questions', shallow: true do
post :answer, on: :member
end

View File

@@ -546,6 +546,13 @@ poll = Poll.create(name: "Upcoming Poll",
starts_at: 1.month.from_now,
ends_at: 2.months.from_now)
puts ""
print "Recounting Poll"
poll = Poll.create(name: "Recounting Poll",
# slug: "recounting-poll",
starts_at: 1.months.ago,
ends_at: 5.days.ago)
puts ""
print "Expired Poll"
poll = Poll.create(name: "Expired Poll",
@@ -553,6 +560,15 @@ poll = Poll.create(name: "Expired Poll",
starts_at: 2.months.ago,
ends_at: 1.month.ago)
puts ""
print "Expired Poll with Stats & Results"
poll = Poll.create(name: "Expired Poll with Stats & Results",
# slug: "expired-poll-with-stats-and-results",
starts_at: 2.months.ago,
ends_at: 1.month.ago,
results_enabled: true,
stats_enabled: true)
puts ""
print "Creating Poll Questions"

View File

@@ -0,0 +1,5 @@
class AddMostVotedToPollQuestionAnswer < ActiveRecord::Migration
def change
add_column :poll_question_answers, :most_voted, :boolean, default: false
end
end

View File

@@ -0,0 +1,6 @@
class AddResultsAndStatsToPolls < ActiveRecord::Migration
def change
add_column :polls, :results_enabled, :boolean, default: false
add_column :polls, :stats_enabled, :boolean, default: false
end
end

View File

@@ -11,7 +11,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 20171017221546) do
ActiveRecord::Schema.define(version: 20171020163240) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@@ -666,6 +666,7 @@ ActiveRecord::Schema.define(version: 20171017221546) do
t.text "description"
t.integer "question_id"
t.integer "given_order", default: 1
t.boolean "most_voted", default: false
end
add_index "poll_question_answers", ["question_id"], name: "index_poll_question_answers_on_question_id", using: :btree
@@ -759,6 +760,8 @@ ActiveRecord::Schema.define(version: 20171017221546) do
t.integer "comments_count", default: 0
t.integer "author_id"
t.datetime "hidden_at"
t.boolean "results_enabled", default: false
t.boolean "stats_enabled", default: false
end
add_index "polls", ["starts_at", "ends_at"], name: "index_polls_on_starts_at_and_ends_at", using: :btree

View File

@@ -60,6 +60,10 @@ feature 'Admin polls' do
fill_in 'poll_ends_at', with: end_date.strftime("%d/%m/%Y")
fill_in 'poll_summary', with: "Upcoming poll's summary. This poll..."
fill_in 'poll_description', with: "Upcomming poll's description. This poll..."
expect(page).to_not have_css("#poll_results_enabled")
expect(page).to_not have_css("#poll_stats_enabled")
click_button "Create poll"
expect(page).to have_content "Poll created successfully"
@@ -79,14 +83,25 @@ feature 'Admin polls' do
expect(page).to have_css("img[alt='#{poll.image.title}']")
expect(page).to have_css("#poll_results_enabled")
expect(page).to have_css("#poll_stats_enabled")
fill_in "poll_name", with: "Next Poll"
fill_in 'poll_ends_at', with: end_date.strftime("%d/%m/%Y")
check 'poll_results_enabled'
check 'poll_stats_enabled'
click_button "Update poll"
expect(page).to have_content "Poll updated successfully"
expect(page).to have_content "Next Poll"
expect(page).to have_content I18n.l(end_date.to_date)
click_link "Edit poll"
expect(page).to have_field('poll_results_enabled', checked: true)
expect(page).to have_field('poll_stats_enabled', checked: true)
end
scenario 'Edit from index' do

View File

@@ -59,6 +59,22 @@ feature 'Polls' do
expect(page).to have_link('Incoming')
expect(page).to_not have_link('Expired')
end
scenario "Poll title link to stats if enabled" do
poll = create(:poll, name: "Poll with stats", stats_enabled: true)
visit polls_path
expect(page).to have_link("Poll with stats", href: stats_poll_path(poll))
end
scenario "Poll title link to results if enabled" do
poll = create(:poll, name: "Poll with results", stats_enabled: true, results_enabled: true)
visit polls_path
expect(page).to have_link("Poll with results", href: results_poll_path(poll))
end
end
context 'Show' do
@@ -367,17 +383,74 @@ feature 'Polls' do
end
end
context "Results and stats" do
scenario "See polls statistics", :js do
scenario "Show poll results and stats if enabled and poll expired" do
poll = create(:poll, :expired, results_enabled: true, stats_enabled: true)
user = create(:user)
poll = create(:poll, summary: "Summary", description: "Description")
login_as user
visit poll_path(poll)
click_link "Participation statistics"
expect(page).to have_content("Total participation")
expect(page).to have_content("Poll results")
expect(page).to have_content("Participation statistics")
visit results_poll_path(poll)
expect(page).to have_content("Questions")
visit stats_poll_path(poll)
expect(page).to have_content("Participation data")
end
scenario "Don't show poll results and stats if not enabled" do
poll = create(:poll, :expired, results_enabled: false, stats_enabled: false)
user = create(:user)
login_as user
visit poll_path(poll)
expect(page).to_not have_content("Poll results")
expect(page).to_not have_content("Participation statistics")
visit results_poll_path(poll)
expect(page).to have_content("You do not have permission to carry out the action 'results' on poll.")
visit stats_poll_path(poll)
expect(page).to have_content("You do not have permission to carry out the action 'stats' on poll.")
end
scenario "Don't show poll results and stats if is not expired" do
poll = create(:poll, :current, results_enabled: true, stats_enabled: true)
user = create(:user)
login_as user
visit poll_path(poll)
expect(page).to_not have_content("Poll results")
expect(page).to_not have_content("Participation statistics")
visit results_poll_path(poll)
expect(page).to have_content("You do not have permission to carry out the action 'results' on poll.")
visit stats_poll_path(poll)
expect(page).to have_content("You do not have permission to carry out the action 'stats' on poll.")
end
scenario "Show poll results and stats if user is administrator" do
poll = create(:poll, :current, results_enabled: false, stats_enabled: false)
user = create(:administrator).user
login_as user
visit poll_path(poll)
expect(page).to have_content("Poll results")
expect(page).to have_content("Participation statistics")
visit results_poll_path(poll)
expect(page).to have_content("Questions")
visit stats_poll_path(poll)
expect(page).to have_content("Participation data")
end
end
end

View File

@@ -0,0 +1,55 @@
require 'rails_helper'
feature 'Poll Results' do
scenario 'List each Poll question', :js 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)
answer1 = create(:poll_question_answer, question: question1, title: 'Yes')
answer2 = create(:poll_question_answer, question: question1, title: 'No')
question2 = create(:poll_question, poll: poll)
answer3 = create(:poll_question_answer, question: question2, title: 'Blue')
answer4 = create(:poll_question_answer, question: question2, title: 'Green')
answer5 = create(:poll_question_answer, question: question2, title: 'Yellow')
login_as user1
vote_for_poll_via_web(poll, question1, 'Yes')
vote_for_poll_via_web(poll, question2, 'Blue')
expect(Poll::Voter.count).to eq(1)
logout
login_as user2
vote_for_poll_via_web(poll, question1, 'Yes')
vote_for_poll_via_web(poll, question2, 'Green')
expect(Poll::Voter.count).to eq(2)
logout
login_as user3
vote_for_poll_via_web(poll, question1, 'No')
vote_for_poll_via_web(poll, question2, 'Yellow')
expect(Poll::Voter.count).to eq(3)
logout
poll.update(ends_at: 1.day.ago)
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("#answer_#{answer1.id}_result")).to have_content("2 (66.0%)")
expect(find("#answer_#{answer2.id}_result")).to have_content("1 (33.0%)")
end
within("#question_#{question2.id}_results_table") do
expect(find("#answer_#{answer3.id}_result")).to have_content("1 (33.0%)")
expect(find("#answer_#{answer4.id}_result")).to have_content("1 (33.0%)")
expect(find("#answer_#{answer5.id}_result")).to have_content("1 (33.0%)")
end
end
end

View File

@@ -95,7 +95,8 @@ feature "Voter" do
scenario "Trying to vote in web and then in booth", :js do
login_as user
vote_for_poll_via_web(poll, question)
vote_for_poll_via_web(poll, question, 'Yes')
expect(Poll::Voter.count).to eq(1)
click_link "Sign out"
@@ -127,7 +128,8 @@ feature "Voter" do
scenario "Trying to vote in web again", :js do
login_as user
vote_for_poll_via_web(poll, question)
vote_for_poll_via_web(poll, question, 'Yes')
expect(Poll::Voter.count).to eq(1)
visit poll_path(poll)

View File

@@ -299,15 +299,13 @@ module CommonActions
end
end
def vote_for_poll_via_web(poll, question)
def vote_for_poll_via_web(poll, question, answer)
visit poll_path(poll)
within("#poll_question_#{question.id}_answers") do
click_link 'Yes'
expect(page).to_not have_link('Yes')
click_link "#{answer}"
expect(page).to_not have_link("#{answer}")
end
expect(Poll::Voter.count).to eq(1)
end
def vote_for_poll_via_booth