diff --git a/app/assets/stylesheets/layout.scss b/app/assets/stylesheets/layout.scss index 56a1fe187..6a22e92b9 100644 --- a/app/assets/stylesheets/layout.scss +++ b/app/assets/stylesheets/layout.scss @@ -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 // ---------- diff --git a/app/assets/stylesheets/participation.scss b/app/assets/stylesheets/participation.scss index 55645309f..81757d78c 100644 --- a/app/assets/stylesheets/participation.scss +++ b/app/assets/stylesheets/participation.scss @@ -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); + } +} diff --git a/app/controllers/admin/poll/polls_controller.rb b/app/controllers/admin/poll/polls_controller.rb index 8ba6e934e..90a31192c 100644 --- a/app/controllers/admin/poll/polls_controller.rb +++ b/app/controllers/admin/poll/polls_controller.rb @@ -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 diff --git a/app/controllers/admin/poll/shifts_controller.rb b/app/controllers/admin/poll/shifts_controller.rb index 89bb68999..1261f2951 100644 --- a/app/controllers/admin/poll/shifts_controller.rb +++ b/app/controllers/admin/poll/shifts_controller.rb @@ -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 diff --git a/app/controllers/polls/questions_controller.rb b/app/controllers/polls/questions_controller.rb index 407e6d984..e1fc73805 100644 --- a/app/controllers/polls/questions_controller.rb +++ b/app/controllers/polls/questions_controller.rb @@ -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 diff --git a/app/controllers/polls_controller.rb b/app/controllers/polls_controller.rb index a13bfd33d..708f68abb 100644 --- a/app/controllers/polls_controller.rb +++ b/app/controllers/polls_controller.rb @@ -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 diff --git a/app/helpers/shifts_helper.rb b/app/helpers/shifts_helper.rb index 3a55feb4f..7ea17baf2 100644 --- a/app/helpers/shifts_helper.rb +++ b/app/helpers/shifts_helper.rb @@ -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 diff --git a/app/models/abilities/administrator.rb b/app/models/abilities/administrator.rb index 704f9be24..1274229b5 100644 --- a/app/models/abilities/administrator.rb +++ b/app/models/abilities/administrator.rb @@ -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 diff --git a/app/models/abilities/everyone.rb b/app/models/abilities/everyone.rb index 73f3220ab..dd692c269 100644 --- a/app/models/abilities/everyone.rb +++ b/app/models/abilities/everyone.rb @@ -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 diff --git a/app/models/poll/booth.rb b/app/models/poll/booth.rb index 07e7d2456..b9cba45b1 100644 --- a/app/models/poll/booth.rb +++ b/app/models/poll/booth.rb @@ -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) diff --git a/app/models/poll/question.rb b/app/models/poll/question.rb index 066d223d9..6be729757 100644 --- a/app/models/poll/question.rb +++ b/app/models/poll/question.rb @@ -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 diff --git a/app/models/poll/question/answer.rb b/app/models/poll/question/answer.rb index ccbcf1abd..dbf2f2139 100644 --- a/app/models/poll/question/answer.rb +++ b/app/models/poll/question/answer.rb @@ -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 diff --git a/app/models/poll/stats.rb b/app/models/poll/stats.rb index 1df281434..ac45a2aae 100644 --- a/app/models/poll/stats.rb +++ b/app/models/poll/stats.rb @@ -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 \ No newline at end of file +end diff --git a/app/views/admin/poll/polls/_form.html.erb b/app/views/admin/poll/polls/_form.html.erb index aff19d9d7..c0439af7b 100644 --- a/app/views/admin/poll/polls/_form.html.erb +++ b/app/views/admin/poll/polls/_form.html.erb @@ -53,6 +53,17 @@ + <% if controller_name == "polls" && action_name == "edit" %> +
+
+ <%= t('admin.polls.new.show_results_and_stats') %> + <%= 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') %> +

<%= t('admin.polls.new.results_and_stats_reminder') %>

+
+
+ <% end %> +
<%= f.submit t("admin.polls.#{admin_submit_action(@poll)}.submit_button"), diff --git a/app/views/admin/poll/shifts/_form.html.erb b/app/views/admin/poll/shifts/_form.html.erb index 07e07381f..7e2930751 100644 --- a/app/views/admin/poll/shifts/_form.html.erb +++ b/app/views/admin/poll/shifts/_form.html.erb @@ -3,7 +3,7 @@
- <%= t("admin.poll_shifts.new.new_shift") %> + <%= t("admin.poll_shifts.new.new_shift") %>
@@ -24,12 +24,12 @@
<%= 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', diff --git a/app/views/polls/_filter_subnav.html.erb b/app/views/polls/_filter_subnav.html.erb index b096edaf2..88ee5beb9 100644 --- a/app/views/polls/_filter_subnav.html.erb +++ b/app/views/polls/_filter_subnav.html.erb @@ -1,6 +1,6 @@ -
+
-
    +
    • <%= link_to "#tab-comments" do %>

      diff --git a/app/views/polls/_poll_group.html.erb b/app/views/polls/_poll_group.html.erb index 529e9a99e..93174e157 100644 --- a/app/views/polls/_poll_group.html.erb +++ b/app/views/polls/_poll_group.html.erb @@ -19,11 +19,27 @@
      <% if poll.questions.count == 1 %> <% poll.questions.each do |question| %> -

      <%= link_to question.title, poll %>

      +

      + <% 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 %> +

      <%= poll_dates(poll) %> <% end %> <% else %> -

      <%= link_to poll.name, poll %>

      +

      + <% 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 %> +

      <%= poll_dates(poll) %>
        <% poll.questions.each do |question| %> diff --git a/app/views/polls/_poll_header.html.erb b/app/views/polls/_poll_header.html.erb new file mode 100644 index 000000000..db4918ab4 --- /dev/null +++ b/app/views/polls/_poll_header.html.erb @@ -0,0 +1,27 @@ +
        +
        +
        + <%= back_link_to polls_path, t("polls.show.back") %> + +

        <%= @poll.name %>

        + + <%= safe_html_with_links simple_format(@poll.summary) %> + + <% if @poll.geozones.any? %> +
          + <% @poll.geozones.each do |g| %> +
        • <%= g.name %>
        • + <% end %> +
        + <% end %> +
        + + +
        +
        diff --git a/app/views/polls/_poll_subnav.html.erb b/app/views/polls/_poll_subnav.html.erb new file mode 100644 index 000000000..112f267ee --- /dev/null +++ b/app/views/polls/_poll_subnav.html.erb @@ -0,0 +1,36 @@ +<% if current_user && current_user.administrator? || + (@poll.expired? && (@poll.results_enabled? || @poll.stats_enabled?)) %> +
        +
        + +
        +
        +<% end %> diff --git a/app/views/polls/_results_subnavigation.html.erb b/app/views/polls/_results_subnavigation.html.erb deleted file mode 100644 index 9c2a7e8b0..000000000 --- a/app/views/polls/_results_subnavigation.html.erb +++ /dev/null @@ -1,20 +0,0 @@ -
        -
        -
          -
        • - <%= link_to "#tab-stats" do %> -

          - <%= t("polls.show.stats_menu") %> -

          - <% end %> -
        • -
        • - <%= link_to "#tab-information" do %> -

          - <%= t("polls.show.info_menu") %> -

          - <% end %> -
        • -
        -
        -
        \ No newline at end of file diff --git a/app/views/polls/_show.html.erb b/app/views/polls/_show.html.erb deleted file mode 100644 index ee75ea5e3..000000000 --- a/app/views/polls/_show.html.erb +++ /dev/null @@ -1,126 +0,0 @@ -
        -
        - <%= render "callout" %> - - <% if @poll.voted_in_booth?(current_user) %> -
        - <%= t("polls.show.already_voted_in_booth") %> -
        - <% else %> - - <% if current_user && @poll.voted_in_web?(current_user) %> -
        - <%= t("polls.show.already_voted_in_web") %> -
        - <% end %> - <% end %> - - <% @questions.each do |question| %> - <%= render 'polls/questions/question', question: question, token: @token %> - <% end %> - - <% if poll_voter_token(@poll, current_user).empty? %> - - <% end %> - - <%= link_to t("polls.show.participate_in_other_polls"), polls_path, class: "button hollow" %> -
        -
        - -
        -
        -
        -

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

        - <%= safe_html_with_links simple_format(@poll.description) %> -
        - - <% if false %> - - <% end %> -
        -
        - -
        -
        - - <% @poll_questions_answers.each do |answer| %> -
        - - <% if answer.description.present? %> -

        <%= answer.title %>

        - <% end %> - - <% if answer.images.any? %> - <%= render "gallery", answer: answer %> - <% end %> - - <% if answer.description.present? %> - - <% end %> - - <% if answer.documents.present? %> - - <% end %> - - <% if answer.videos.present? %> - - <% end %> -
        - <% end %> -
        - -
        - -
        - <%= render "filter_subnav" %> - -
        - <%= render "comments" %> -
        -
        \ No newline at end of file diff --git a/app/views/polls/results.html.erb b/app/views/polls/results.html.erb new file mode 100644 index 000000000..4373da22d --- /dev/null +++ b/app/views/polls/results.html.erb @@ -0,0 +1,49 @@ +<% provide :title do %><%= @poll.name %><% end %> + +
        + <%= render "poll_header" %> + + <%= render "poll_subnav" %> + +
        + + +
        + <%- @poll.questions.each do |question| %> + +

        <%= question.title %>

        + + + <%- question.question_answers.each do |answer| %> + + <% end %> + + + + + <%- question.question_answers.each do |answer| %> + + <% end %> + + +
        > + <% if answer.most_voted %> + <%= t("polls.show.results.most_voted_answer") %> + <% end %> + <%= answer.title %> +
        > + <%= answer.total_votes %> + (<%= answer.total_votes_percentage.round(2) %>%) +
        + <% end %> + +
        +
        +
        diff --git a/app/views/polls/show.html.erb b/app/views/polls/show.html.erb index 880d0d421..709fcc2a2 100644 --- a/app/views/polls/show.html.erb +++ b/app/views/polls/show.html.erb @@ -1,44 +1,134 @@ <% provide :title do %><%= @poll.name %><% end %>
        -
        -
        -
        - <%= back_link_to polls_path, t("polls.show.back") %> + <%= render "poll_header" %> -

        <%= @poll.name %>

        + <%= render "poll_subnav" %> - <%= safe_html_with_links simple_format(@poll.summary) %> +
        +
        + <%= render "callout" %> - <% if @poll.geozones.any? %> -
          - <% @poll.geozones.each do |g| %> -
        • <%= g.name %>
        • - <% end %> -
        + <% if @poll.voted_in_booth?(current_user) %> +
        + <%= t("polls.show.already_voted_in_booth") %> +
        + <% else %> + + <% if current_user && @poll.voted_in_web?(current_user) && !@poll.expired? %> +
        + <%= t("polls.show.already_voted_in_web") %> +
        <% end %> + <% end %> + + <% @questions.each do |question| %> + <%= render 'polls/questions/question', question: question, token: @token %> + <% end %> + + <% if poll_voter_token(@poll, current_user).empty? %> + + <% end %> + + <%= link_to t("polls.show.participate_in_other_polls"), polls_path, class: "button hollow" %> +
        +
        + +
        +
        +
        +

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

        + <%= safe_html_with_links simple_format(@poll.description) %>
        - + <% if false %> + + <% end %>
        - -
        - <%= render "results_subnavigation" %> -
        - <%= render "polls/stats/show" %> +
        +
        + + <% @poll_questions_answers.each do |answer| %> +
        + + <% if answer.description.present? %> +

        <%= answer.title %>

        + <% end %> + + <% if answer.images.any? %> + <%= render "gallery", answer: answer %> + <% end %> + + <% if answer.description.present? %> + + <% end %> + + <% if answer.documents.present? %> + + <% end %> + + <% if answer.videos.present? %> + + <% end %> +
        + <% end %> +
        + +
        + +
        + <%= render "filter_subnav" %> + +
        + <%= render "comments" %>
        - -
        - <%= render "show" %> -
        - diff --git a/app/views/polls/stats.html.erb b/app/views/polls/stats.html.erb new file mode 100644 index 000000000..a89277ac4 --- /dev/null +++ b/app/views/polls/stats.html.erb @@ -0,0 +1,96 @@ +<% provide :title do %><%= @poll.name %><% end %> + +
        + <%= render "poll_header" %> + + <%= render "poll_subnav" %> + +
        + + +
        +

        <%= t("polls.show.stats.total_participation") %>

        + +

        + <%= t("polls.show.stats.total_votes") %>
        + <%= @stats[:total_participants] %> +

        + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
        <%= t("polls.show.stats.votes") %><%= t("polls.show.stats.web") %><%= t("polls.show.stats.booth") %><%= t("polls.show.stats.total") %>
        <%= t("polls.show.stats.valid") %> + <%= @stats[:total_web_valid] %> + (<%= @stats[:valid_percentage_web].round(2) %>%) + + <%= @stats[:total_booth_valid] %> + (<%= @stats[:valid_percentage_booth].round(2) %>%) + + <%= @stats[:total_valid_votes] %> + (<%= @stats[:total_valid_percentage].round(2) %>%) +
        <%= t("polls.show.stats.white") %> + <%= @stats[:total_web_white] %> + (<%= @stats[:white_percentage_web].round(2) %>%) + + <%= @stats[:total_booth_white] %> + (<%= @stats[:white_percentage_booth].round(2) %>%) + <%= @stats[:total_white_votes] %> + (<%= @stats[:total_white_percentage].round(2) %>%) +
        <%= t("polls.show.stats.null_votes") %> + <%= @stats[:total_web_null] %> + (<%= @stats[:null_percentage_web].round(2) %>%) + + <%= @stats[:total_booth_null] %> + (<%= @stats[:null_percentage_booth].round(2) %>%) + + <%= @stats[:total_null_votes] %> + (<%= @stats[:total_null_percentage].round(2) %>%) +
        <%= t("polls.show.stats.total") %> + <%= @stats[:total_participants_web] %> + (<%= @stats[:total_participants_web_percentage].round(2) %>%) + + <%= @stats[:total_participants_booth] %> + (<%= @stats[:total_participants_booth_percentage].round(2) %>%) + <%= @stats[:total_participants_web] + @stats[:total_participants_booth] %>
        +
        +
        + +
        diff --git a/app/views/polls/stats/_show.html.erb b/app/views/polls/stats/_show.html.erb deleted file mode 100644 index 1aeb7cf69..000000000 --- a/app/views/polls/stats/_show.html.erb +++ /dev/null @@ -1,85 +0,0 @@ -
        - -
        -

        <%= t("polls.show.stats.total_participation") %>

        - -

        - <%= t("polls.show.stats.total_votes") %>
        - <%= @stats[:total_participants] %> -

        - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
        <%= t("polls.show.stats.votes") %><%= t("polls.show.stats.web") %><%= t("polls.show.stats.booth") %><%= t("polls.show.stats.total") %>
        <%= t("polls.show.stats.valid") %><%= @stats[:total_web_valid] %> - (<%= number_to_percentage(@stats[:valid_percentage_web], - strip_insignificant_zeros: true, - precision: 2) %>)<%= @stats[:total_booth_valid] %> - (<%= number_to_percentage(@stats[:valid_percentage_booth], - strip_insignificant_zeros: true, - precision: 2) %>)<%= @stats[:total_valid_votes] %> - (<%= number_to_percentage(@stats[:total_valid_percentage], - strip_insignificant_zeros: true, - precision: 2) %>)
        <%= t("polls.show.stats.white") %><%= @stats[:total_web_white] %> - (<%= number_to_percentage(@stats[:white_percentage_web], - strip_insignificant_zeros: true, - precision: 2) %>)<%= @stats[:total_booth_white] %> - (<%= number_to_percentage(@stats[:white_percentage_booth], - strip_insignificant_zeros: true, - precision: 2) %>)<%= @stats[:total_white_votes] %> - (<%= number_to_percentage(@stats[:total_white_percentage], - strip_insignificant_zeros: true, - precision: 2) %>)
        <%= t("polls.show.stats.null_votes") %><%= @stats[:total_web_null] %> - (<%= number_to_percentage(@stats[:null_percentage_web], - strip_insignificant_zeros: true, - precision: 2) %>)<%= @stats[:total_booth_null] %> - (<%= number_to_percentage(@stats[:null_percentage_booth], - strip_insignificant_zeros: true, - precision: 2) %>)<%= @stats[:total_null_votes] %> - (<%= number_to_percentage(@stats[:total_null_percentage], - strip_insignificant_zeros: true, - precision: 2) %>)
        <%= t("polls.show.stats.total") %><%= @stats[:total_participants_web] %> - (<%= number_to_percentage(@stats[:total_participants_web_percentage], - strip_insignificant_zeros: true, - precision: 2) %>)<%= @stats[:total_participants_booth] %> - (<%= number_to_percentage(@stats[:total_participants_booth_percentage], - strip_insignificant_zeros: true, - precision: 2) %>)<%= @stats[:total_participants_web] + @stats[:total_participants_booth] %>
        -
        -
        \ No newline at end of file diff --git a/config/locales/en/admin.yml b/config/locales/en/admin.yml index a60600916..fa2ed56bb 100644 --- a/config/locales/en/admin.yml +++ b/config/locales/en/admin.yml @@ -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" diff --git a/config/locales/en/general.yml b/config/locales/en/general.yml index 9c16e40ec..926410b51 100644 --- a/config/locales/en/general.yml +++ b/config/locales/en/general.yml @@ -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: diff --git a/config/locales/es/admin.yml b/config/locales/es/admin.yml index ed2c36825..9ff7ab7f3 100644 --- a/config/locales/es/admin.yml +++ b/config/locales/es/admin.yml @@ -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" diff --git a/config/locales/es/general.yml b/config/locales/es/general.yml index e56f725c9..f82ba9ed7 100644 --- a/config/locales/es/general.yml +++ b/config/locales/es/general.yml @@ -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: diff --git a/config/routes.rb b/config/routes.rb index 2b7f41c60..cdc9fe1b1 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -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 diff --git a/db/dev_seeds.rb b/db/dev_seeds.rb index 8c09923d8..ba6ad3962 100644 --- a/db/dev_seeds.rb +++ b/db/dev_seeds.rb @@ -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" diff --git a/db/migrate/20171019095042_add_most_voted_to_poll_question_answer.rb b/db/migrate/20171019095042_add_most_voted_to_poll_question_answer.rb new file mode 100644 index 000000000..e4650643b --- /dev/null +++ b/db/migrate/20171019095042_add_most_voted_to_poll_question_answer.rb @@ -0,0 +1,5 @@ +class AddMostVotedToPollQuestionAnswer < ActiveRecord::Migration + def change + add_column :poll_question_answers, :most_voted, :boolean, default: false + end +end diff --git a/db/migrate/20171020163240_add_results_and_stats_to_polls.rb b/db/migrate/20171020163240_add_results_and_stats_to_polls.rb new file mode 100644 index 000000000..af6f82aaa --- /dev/null +++ b/db/migrate/20171020163240_add_results_and_stats_to_polls.rb @@ -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 diff --git a/db/schema.rb b/db/schema.rb index c3ccbeac7..7f3177760 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -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 diff --git a/spec/features/admin/poll/polls_spec.rb b/spec/features/admin/poll/polls_spec.rb index 2a2d26ea5..210b2ff6a 100644 --- a/spec/features/admin/poll/polls_spec.rb +++ b/spec/features/admin/poll/polls_spec.rb @@ -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 diff --git a/spec/features/polls/polls_spec.rb b/spec/features/polls/polls_spec.rb index 7bf51473f..8629a1087 100644 --- a/spec/features/polls/polls_spec.rb +++ b/spec/features/polls/polls_spec.rb @@ -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 diff --git a/spec/features/polls/results_spec.rb b/spec/features/polls/results_spec.rb new file mode 100644 index 000000000..326344dd9 --- /dev/null +++ b/spec/features/polls/results_spec.rb @@ -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 diff --git a/spec/features/polls/voter_spec.rb b/spec/features/polls/voter_spec.rb index ff9b71755..aee0ff522 100644 --- a/spec/features/polls/voter_spec.rb +++ b/spec/features/polls/voter_spec.rb @@ -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) diff --git a/spec/support/common_actions.rb b/spec/support/common_actions.rb index b87a54d8e..5023c1952 100644 --- a/spec/support/common_actions.rb +++ b/spec/support/common_actions.rb @@ -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