Merge pull request #3512 from consul/backport-stats
Change stats layout
BIN
app/assets/images/stats_booth.png
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
app/assets/images/stats_investments.png
Normal file
|
After Width: | Height: | Size: 2.9 KiB |
BIN
app/assets/images/stats_mail.png
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
BIN
app/assets/images/stats_man.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
BIN
app/assets/images/stats_users.png
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
BIN
app/assets/images/stats_web.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
app/assets/images/stats_woman.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
@@ -27,7 +27,7 @@
|
||||
//= require app
|
||||
//= require check_all_none
|
||||
//= require comments
|
||||
//= require dropdown
|
||||
//= require foundation_extras
|
||||
//= require ie_alert
|
||||
//= require location_changer
|
||||
//= require moderator_comment
|
||||
@@ -93,7 +93,7 @@ var initialize_modules = function() {
|
||||
App.Votes.initialize();
|
||||
App.AllowParticipation.initialize();
|
||||
App.Tags.initialize();
|
||||
App.Dropdown.initialize();
|
||||
App.FoundationExtras.initialize();
|
||||
App.LocationChanger.initialize();
|
||||
App.CheckAllNone.initialize();
|
||||
App.PreventDoubleSubmission.initialize();
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
App.Dropdown =
|
||||
|
||||
initialize: ->
|
||||
$(document).foundation()
|
||||
5
app/assets/javascripts/foundation_extras.js.coffee
Normal file
@@ -0,0 +1,5 @@
|
||||
App.FoundationExtras =
|
||||
|
||||
initialize: ->
|
||||
$(document).foundation()
|
||||
$(window).trigger "load.zf.sticky"
|
||||
@@ -119,3 +119,6 @@ $tab-content-border: $border;
|
||||
$orbit-bullet-diameter: 0.8rem;
|
||||
|
||||
$pagination-radius: $global-radius;
|
||||
|
||||
$show-header-for-stacked: true;
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
@import "legislation";
|
||||
@import "legislation_process";
|
||||
@import "community";
|
||||
@import "stats";
|
||||
@import "custom";
|
||||
@import "c3";
|
||||
@import "annotator.min";
|
||||
|
||||
@@ -1988,15 +1988,8 @@
|
||||
.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 {
|
||||
.menu:not(.vertical) {
|
||||
padding: 0;
|
||||
|
||||
li a {
|
||||
@@ -2030,10 +2023,4 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.number {
|
||||
font-size: rem-calc(60);
|
||||
font-weight: bold;
|
||||
line-height: rem-calc(60);
|
||||
}
|
||||
}
|
||||
|
||||
215
app/assets/stylesheets/stats.scss
Normal file
@@ -0,0 +1,215 @@
|
||||
.participation-stats {
|
||||
|
||||
@include breakpoint(medium) {
|
||||
|
||||
.stats-content {
|
||||
border-left: 1px solid $border;
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
border-bottom: 1px solid $border;
|
||||
margin-bottom: $line-height;
|
||||
|
||||
@include breakpoint(medium) {
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
ul {
|
||||
margin-left: $line-height / 2;
|
||||
margin-top: 0;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: rem-calc(24);
|
||||
border-bottom: 1px solid $border;
|
||||
}
|
||||
|
||||
.stats-group {
|
||||
margin-bottom: $line-height;
|
||||
margin-top: $line-height;
|
||||
}
|
||||
|
||||
.number-with-info {
|
||||
display: inline-block;
|
||||
|
||||
&::before {
|
||||
display: inline-block;
|
||||
margin-right: $line-height / 2;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
&.total-participants::before {
|
||||
content: image-url("stats_users.png");
|
||||
}
|
||||
|
||||
&.male::before {
|
||||
content: image-url("stats_man.png");
|
||||
}
|
||||
|
||||
&.female::before {
|
||||
content: image-url("stats_woman.png");
|
||||
}
|
||||
|
||||
&.total-investments::before {
|
||||
content: image-url("stats_investments.png");
|
||||
}
|
||||
|
||||
&.web::before {
|
||||
content: image-url("stats_web.png");
|
||||
}
|
||||
|
||||
&.booth::before {
|
||||
content: image-url("stats_booth.png");
|
||||
}
|
||||
|
||||
&.mail::before {
|
||||
content: image-url("stats_mail.png");
|
||||
}
|
||||
|
||||
.content {
|
||||
display: inline-block;
|
||||
margin-right: $line-height * 2;
|
||||
}
|
||||
|
||||
.number {
|
||||
display: block;
|
||||
font-size: rem-calc(36);
|
||||
font-weight: bold;
|
||||
line-height: rem-calc(36);
|
||||
}
|
||||
|
||||
.info {
|
||||
color: $text-medium;
|
||||
font-size: $small-font-size;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
&.total-participants {
|
||||
|
||||
.number {
|
||||
font-size: rem-calc(60);
|
||||
line-height: rem-calc(60);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.progress {
|
||||
background: #ebf0f4;
|
||||
border-radius: rem-calc(20);
|
||||
height: $line-height / 2;
|
||||
}
|
||||
|
||||
.progress-meter {
|
||||
background: #52a4ee;
|
||||
border-bottom-left-radius: rem-calc(20);
|
||||
border-top-left-radius: rem-calc(20);
|
||||
}
|
||||
|
||||
.gender-and-channel {
|
||||
|
||||
thead,
|
||||
tbody {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
thead,
|
||||
tr,
|
||||
th {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
td,
|
||||
th {
|
||||
text-align: center;
|
||||
text-transform: uppercase;
|
||||
|
||||
&::before {
|
||||
display: inline-block;
|
||||
transform: scale(0.6);
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
|
||||
th {
|
||||
font-weight: bold;
|
||||
font-size: rem-calc(21);
|
||||
|
||||
&.gender {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
&.web::before {
|
||||
content: image-url("stats_web.png");
|
||||
}
|
||||
|
||||
&.booth::before {
|
||||
content: image-url("stats_booth.png");
|
||||
}
|
||||
|
||||
&.mail::before {
|
||||
content: image-url("stats_mail.png");
|
||||
}
|
||||
}
|
||||
|
||||
td {
|
||||
|
||||
&.gender {
|
||||
font-weight: bold;
|
||||
font-size: rem-calc(21);
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
&.male::before {
|
||||
content: image-url("stats_man.png");
|
||||
}
|
||||
|
||||
&.female::before {
|
||||
content: image-url("stats_woman.png");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
table {
|
||||
|
||||
&.survey-districts {
|
||||
|
||||
@include breakpoint(medium down) {
|
||||
thead {
|
||||
font-size: rem-calc(12);
|
||||
|
||||
th {
|
||||
width: 25%;
|
||||
word-break: break-all;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tr th {
|
||||
border: 1px solid $border;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.phase-subheader {
|
||||
font-size: rem-calc(11);
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.success {
|
||||
background: #c1fdd2;
|
||||
}
|
||||
|
||||
.border-left-success {
|
||||
background: #c1fdd2;
|
||||
border-left: 2px solid #58ff87;
|
||||
}
|
||||
|
||||
.border-right-success {
|
||||
background: #c1fdd2;
|
||||
border-right: 2px solid #58ff87;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,16 +6,12 @@ module Budgets
|
||||
|
||||
def show
|
||||
authorize! :read_stats, @budget
|
||||
@stats = load_stats
|
||||
@stats = Budget::Stats.new(@budget)
|
||||
@headings = @budget.headings.sort_by_name
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def load_stats
|
||||
Budget::Stats.new(@budget).generate
|
||||
end
|
||||
|
||||
def load_budget
|
||||
@budget = Budget.find_by(slug: params[:budget_id]) || Budget.find_by(id: params[:budget_id])
|
||||
end
|
||||
|
||||
@@ -33,7 +33,7 @@ class PollsController < ApplicationController
|
||||
end
|
||||
|
||||
def stats
|
||||
@stats = Poll::Stats.new(@poll).generate
|
||||
@stats = Poll::Stats.new(@poll)
|
||||
end
|
||||
|
||||
def results
|
||||
|
||||
@@ -30,4 +30,16 @@ module StatsHelper
|
||||
opt[:data][:graph] = admin_api_stats_path(budget_investments: true)
|
||||
content_tag :div, "", opt
|
||||
end
|
||||
|
||||
def number_to_stats_percentage(number, options = {})
|
||||
number_to_percentage(number, { strip_insignificant_zeros: true, precision: 2 }.merge(options))
|
||||
end
|
||||
|
||||
def number_with_info_tags(number, text, html_class: "")
|
||||
content_tag :p, class: "number-with-info #{html_class}".strip do
|
||||
content_tag :span, class: "content" do
|
||||
content_tag(:span, number, class: "number") + content_tag(:span, text, class: "info")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -66,7 +66,6 @@ module Abilities
|
||||
can [:hide, :update, :toggle_selection], Budget::Investment
|
||||
can [:valuate, :comment_valuation], Budget::Investment
|
||||
can :create, Budget::ValuatorAssignment
|
||||
can :read_stats, Budget, phase: "reviewing_ballots"
|
||||
|
||||
can [:search, :edit, :update, :create, :index, :destroy], Banner
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ module Abilities
|
||||
can [:read], Budget::Group
|
||||
can [:read, :print, :json_data], Budget::Investment
|
||||
can [:read_results, :read_executions], Budget, phase: "finished"
|
||||
can :read_stats, Budget, phase: "finished"
|
||||
can(:read_stats, Budget) { |budget| budget.valuating_or_later? }
|
||||
can :new, DirectMessage
|
||||
can [:read, :debate, :draft_publication, :allegations, :result_publication,
|
||||
:proposals, :milestones], Legislation::Process, published: true
|
||||
|
||||
@@ -2,6 +2,7 @@ class Budget < ApplicationRecord
|
||||
|
||||
include Measurable
|
||||
include Sluggable
|
||||
include StatsVersionable
|
||||
|
||||
translates :name, touch: true
|
||||
include Globalizable
|
||||
@@ -125,8 +126,12 @@ class Budget < ApplicationRecord
|
||||
Budget::Phase::PUBLISHED_PRICES_PHASES.include?(phase)
|
||||
end
|
||||
|
||||
def valuating_or_later?
|
||||
current_phase&.valuating_or_later?
|
||||
end
|
||||
|
||||
def publishing_prices_or_later?
|
||||
publishing_prices? || balloting_or_later?
|
||||
current_phase&.publishing_prices_or_later?
|
||||
end
|
||||
|
||||
def balloting_process?
|
||||
@@ -134,7 +139,7 @@ class Budget < ApplicationRecord
|
||||
end
|
||||
|
||||
def balloting_or_later?
|
||||
balloting_process? || finished?
|
||||
current_phase&.balloting_or_later?
|
||||
end
|
||||
|
||||
def heading_price(heading)
|
||||
|
||||
@@ -28,16 +28,10 @@ class Budget
|
||||
|
||||
scope :enabled, -> { where(enabled: true) }
|
||||
scope :published, -> { enabled.where.not(kind: "drafting") }
|
||||
scope :drafting, -> { find_by_kind("drafting") }
|
||||
scope :informing, -> { find_by_kind("informing") }
|
||||
scope :accepting, -> { find_by_kind("accepting")}
|
||||
scope :reviewing, -> { find_by_kind("reviewing")}
|
||||
scope :selecting, -> { find_by_kind("selecting")}
|
||||
scope :valuating, -> { find_by_kind("valuating")}
|
||||
scope :publishing_prices, -> { find_by_kind("publishing_prices")}
|
||||
scope :balloting, -> { find_by_kind("balloting")}
|
||||
scope :reviewing_ballots, -> { find_by_kind("reviewing_ballots")}
|
||||
scope :finished, -> { find_by_kind("finished")}
|
||||
|
||||
PHASE_KINDS.each do |phase|
|
||||
define_singleton_method(phase) { find_by_kind(phase) }
|
||||
end
|
||||
|
||||
def next_enabled_phase
|
||||
next_phase&.enabled? ? next_phase : next_phase&.next_enabled_phase
|
||||
@@ -53,42 +47,58 @@ class Budget
|
||||
end
|
||||
end
|
||||
|
||||
def valuating_or_later?
|
||||
in_phase_or_later?("valuating")
|
||||
end
|
||||
|
||||
def publishing_prices_or_later?
|
||||
in_phase_or_later?("publishing_prices")
|
||||
end
|
||||
|
||||
def balloting_or_later?
|
||||
in_phase_or_later?("balloting")
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def adjust_date_ranges
|
||||
if enabled?
|
||||
next_enabled_phase&.update_column(:starts_at, ends_at)
|
||||
prev_enabled_phase&.update_column(:ends_at, starts_at)
|
||||
elsif enabled_changed?
|
||||
next_enabled_phase&.update_column(:starts_at, starts_at)
|
||||
end
|
||||
end
|
||||
|
||||
def touch_budget
|
||||
budget.touch
|
||||
end
|
||||
|
||||
def prev_phase_dates_valid?
|
||||
if enabled? && starts_at.present? && prev_enabled_phase.present?
|
||||
prev_enabled_phase.assign_attributes(ends_at: starts_at)
|
||||
if prev_enabled_phase.invalid_dates_range?
|
||||
phase_name = I18n.t("budgets.phase.#{prev_enabled_phase.kind}")
|
||||
error = I18n.t("budgets.phases.errors.prev_phase_dates_invalid", phase_name: phase_name)
|
||||
errors.add(:starts_at, error)
|
||||
def adjust_date_ranges
|
||||
if enabled?
|
||||
next_enabled_phase&.update_column(:starts_at, ends_at)
|
||||
prev_enabled_phase&.update_column(:ends_at, starts_at)
|
||||
elsif enabled_changed?
|
||||
next_enabled_phase&.update_column(:starts_at, starts_at)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def next_phase_dates_valid?
|
||||
if enabled? && ends_at.present? && next_enabled_phase.present?
|
||||
next_enabled_phase.assign_attributes(starts_at: ends_at)
|
||||
if next_enabled_phase.invalid_dates_range?
|
||||
phase_name = I18n.t("budgets.phase.#{next_enabled_phase.kind}")
|
||||
error = I18n.t("budgets.phases.errors.next_phase_dates_invalid", phase_name: phase_name)
|
||||
errors.add(:ends_at, error)
|
||||
def touch_budget
|
||||
budget.touch
|
||||
end
|
||||
|
||||
def prev_phase_dates_valid?
|
||||
if enabled? && starts_at.present? && prev_enabled_phase.present?
|
||||
prev_enabled_phase.assign_attributes(ends_at: starts_at)
|
||||
if prev_enabled_phase.invalid_dates_range?
|
||||
phase_name = I18n.t("budgets.phase.#{prev_enabled_phase.kind}")
|
||||
error = I18n.t("budgets.phases.errors.prev_phase_dates_invalid", phase_name: phase_name)
|
||||
errors.add(:starts_at, error)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def next_phase_dates_valid?
|
||||
if enabled? && ends_at.present? && next_enabled_phase.present?
|
||||
next_enabled_phase.assign_attributes(starts_at: ends_at)
|
||||
if next_enabled_phase.invalid_dates_range?
|
||||
phase_name = I18n.t("budgets.phase.#{next_enabled_phase.kind}")
|
||||
error = I18n.t("budgets.phases.errors.next_phase_dates_invalid", phase_name: phase_name)
|
||||
errors.add(:ends_at, error)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def in_phase_or_later?(phase)
|
||||
PHASE_KINDS.index(kind) >= PHASE_KINDS.index(phase)
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
|
||||
@@ -3,116 +3,145 @@ class Budget::Stats
|
||||
alias_method :budget, :resource
|
||||
|
||||
def self.stats_methods
|
||||
super +
|
||||
%i[total_participants_support_phase total_participants_vote_phase
|
||||
total_budget_investments total_votes total_selected_investments
|
||||
total_unfeasible_investments total_supports headings
|
||||
total_participants_web total_participants_booths]
|
||||
super + support_phase_methods + vote_phase_methods
|
||||
end
|
||||
|
||||
def self.support_phase_methods
|
||||
%i[total_participants_support_phase total_budget_investments
|
||||
total_selected_investments total_unfeasible_investments headings]
|
||||
end
|
||||
|
||||
def self.vote_phase_methods
|
||||
%i[total_votes total_participants_vote_phase]
|
||||
end
|
||||
|
||||
def stats_methods
|
||||
base_stats_methods + participation_methods + phase_methods
|
||||
end
|
||||
|
||||
def phases
|
||||
%w[support vote].select { |phase| send("#{phase}_phase_finished?") }
|
||||
end
|
||||
|
||||
def all_phases
|
||||
return phases unless phases.many?
|
||||
|
||||
[*phases, "every"]
|
||||
end
|
||||
|
||||
def support_phase_finished?
|
||||
budget.valuating_or_later?
|
||||
end
|
||||
|
||||
def vote_phase_finished?
|
||||
budget.finished?
|
||||
end
|
||||
|
||||
def total_participants
|
||||
participants.distinct.count
|
||||
end
|
||||
|
||||
def total_participants_support_phase
|
||||
voters.count
|
||||
end
|
||||
|
||||
def total_participants_vote_phase
|
||||
(balloters + poll_ballot_voters).uniq.count
|
||||
end
|
||||
|
||||
def total_budget_investments
|
||||
budget.investments.count
|
||||
end
|
||||
|
||||
def total_votes
|
||||
budget.ballots.pluck(:ballot_lines_count).inject(0) { |sum, x| sum + x }
|
||||
end
|
||||
|
||||
def total_selected_investments
|
||||
budget.investments.selected.count
|
||||
end
|
||||
|
||||
def total_unfeasible_investments
|
||||
budget.investments.unfeasible.count
|
||||
end
|
||||
|
||||
def headings
|
||||
groups = Hash.new(0)
|
||||
budget.headings.order("id ASC").each do |heading|
|
||||
groups[heading.id] = Hash.new(0).merge(calculate_heading_totals(heading))
|
||||
end
|
||||
|
||||
groups[:total] = Hash.new(0)
|
||||
groups[:total][:total_investments_count] = groups.collect {|_k, v| v[:total_investments_count]}.sum
|
||||
groups[:total][:total_participants_support_phase] = groups.collect {|_k, v| v[:total_participants_support_phase]}.sum
|
||||
groups[:total][:total_participants_vote_phase] = groups.collect {|_k, v| v[:total_participants_vote_phase]}.sum
|
||||
groups[:total][:total_participants_every_phase] = groups.collect {|_k, v| v[:total_participants_every_phase]}.sum
|
||||
|
||||
budget.headings.each do |heading|
|
||||
groups[heading.id].merge!(calculate_heading_stats_with_totals(groups[heading.id], groups[:total], heading.population))
|
||||
end
|
||||
|
||||
groups[:total][:percentage_participants_support_phase] = groups.collect {|_k, v| v[:percentage_participants_support_phase]}.sum
|
||||
groups[:total][:percentage_participants_vote_phase] = groups.collect {|_k, v| v[:percentage_participants_vote_phase]}.sum
|
||||
groups[:total][:percentage_participants_every_phase] = groups.collect {|_k, v| v[:percentage_participants_every_phase]}.sum
|
||||
|
||||
groups
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def total_participants
|
||||
participants.distinct.count
|
||||
def phase_methods
|
||||
phases.map { |phase| self.class.send("#{phase}_phase_methods") }.flatten
|
||||
end
|
||||
|
||||
def total_participants_support_phase
|
||||
voters.uniq.count
|
||||
def participant_ids
|
||||
phases.map { |phase| send("participant_ids_#{phase}_phase") }.flatten.uniq
|
||||
end
|
||||
|
||||
def total_participants_web
|
||||
(balloters - poll_ballot_voters).uniq.compact.count
|
||||
def participant_ids_support_phase
|
||||
(authors + voters).uniq
|
||||
end
|
||||
|
||||
def total_participants_booths
|
||||
poll_ballot_voters.uniq.count
|
||||
end
|
||||
|
||||
def total_participants_vote_phase
|
||||
balloters.uniq.count
|
||||
end
|
||||
|
||||
def total_budget_investments
|
||||
budget.investments.count
|
||||
end
|
||||
|
||||
def total_votes
|
||||
budget.ballots.pluck(:ballot_lines_count).inject(0) { |sum, x| sum + x }
|
||||
end
|
||||
|
||||
def total_selected_investments
|
||||
budget.investments.selected.count
|
||||
end
|
||||
|
||||
def total_unfeasible_investments
|
||||
budget.investments.unfeasible.count
|
||||
end
|
||||
|
||||
def total_supports
|
||||
supports(budget).count
|
||||
end
|
||||
|
||||
def participants
|
||||
User.where(id: (authors + voters + balloters + poll_ballot_voters).uniq.compact)
|
||||
def participant_ids_vote_phase
|
||||
(balloters + poll_ballot_voters).uniq
|
||||
end
|
||||
|
||||
def authors
|
||||
budget.investments.pluck(:author_id)
|
||||
@authors ||= budget.investments.pluck(:author_id)
|
||||
end
|
||||
|
||||
def voters
|
||||
supports(budget).pluck(:voter_id)
|
||||
@voters ||= supports(budget).distinct.pluck(:voter_id)
|
||||
end
|
||||
|
||||
def balloters
|
||||
budget.ballots.where("ballot_lines_count > ?", 0).pluck(:user_id)
|
||||
@balloters ||= budget.ballots.where("ballot_lines_count > ?", 0).distinct.pluck(:user_id).compact
|
||||
end
|
||||
|
||||
def poll_ballot_voters
|
||||
budget&.poll ? budget.poll.voters.pluck(:user_id) : []
|
||||
@poll_ballot_voters ||= budget.poll ? budget.poll.voters.pluck(:user_id) : []
|
||||
end
|
||||
|
||||
def balloters_by_heading(heading_id)
|
||||
stats_cache("balloters_by_heading_#{heading_id}") do
|
||||
budget.ballots.joins(:lines).where(budget_ballot_lines: {heading_id: heading_id}).pluck(:user_id)
|
||||
budget.ballots.joins(:lines)
|
||||
.where(budget_ballot_lines: { heading_id: heading_id} )
|
||||
.distinct.pluck(:user_id)
|
||||
end
|
||||
end
|
||||
|
||||
def voters_by_heading(heading)
|
||||
stats_cache("voters_by_heading_#{heading.id}") do
|
||||
supports(heading).pluck(:voter_id)
|
||||
supports(heading).distinct.pluck(:voter_id)
|
||||
end
|
||||
end
|
||||
|
||||
def headings
|
||||
groups = Hash.new(0)
|
||||
budget.headings.order("id ASC").each do |heading|
|
||||
groups[heading.id] = Hash.new(0).merge(calculate_heading_totals(heading))
|
||||
end
|
||||
|
||||
groups[:total] = Hash.new(0)
|
||||
groups[:total][:total_investments_count] = groups.collect {|_k, v| v[:total_investments_count]}.sum
|
||||
groups[:total][:total_participants_support_phase] = groups.collect {|_k, v| v[:total_participants_support_phase]}.sum
|
||||
groups[:total][:total_participants_vote_phase] = groups.collect {|_k, v| v[:total_participants_vote_phase]}.sum
|
||||
groups[:total][:total_participants_all_phase] = groups.collect {|_k, v| v[:total_participants_all_phase]}.sum
|
||||
|
||||
budget.headings.each do |heading|
|
||||
groups[heading.id].merge!(calculate_heading_stats_with_totals(groups[heading.id], groups[:total], heading.population))
|
||||
end
|
||||
|
||||
groups[:total][:percentage_participants_support_phase] = groups.collect {|_k, v| v[:percentage_participants_support_phase]}.sum
|
||||
groups[:total][:percentage_participants_vote_phase] = groups.collect {|_k, v| v[:percentage_participants_vote_phase]}.sum
|
||||
groups[:total][:percentage_participants_all_phase] = groups.collect {|_k, v| v[:percentage_participants_all_phase]}.sum
|
||||
|
||||
groups
|
||||
end
|
||||
|
||||
def calculate_heading_totals(heading)
|
||||
{
|
||||
total_investments_count: heading.investments.count,
|
||||
total_participants_support_phase: voters_by_heading(heading).uniq.count,
|
||||
total_participants_vote_phase: balloters_by_heading(heading.id).uniq.count,
|
||||
total_participants_all_phase: voters_and_balloters_by_heading(heading)
|
||||
total_participants_support_phase: voters_by_heading(heading).count,
|
||||
total_participants_vote_phase: balloters_by_heading(heading.id).count,
|
||||
total_participants_every_phase: voters_and_balloters_by_heading(heading)
|
||||
}
|
||||
end
|
||||
|
||||
@@ -122,8 +151,8 @@ class Budget::Stats
|
||||
percentage_district_population_support_phase: population_percent(population, heading_totals[:total_participants_support_phase]),
|
||||
percentage_participants_vote_phase: participants_percent(heading_totals, groups_totals, :total_participants_vote_phase),
|
||||
percentage_district_population_vote_phase: population_percent(population, heading_totals[:total_participants_vote_phase]),
|
||||
percentage_participants_all_phase: participants_percent(heading_totals, groups_totals, :total_participants_all_phase),
|
||||
percentage_district_population_all_phase: population_percent(population, heading_totals[:total_participants_all_phase])
|
||||
percentage_participants_every_phase: participants_percent(heading_totals, groups_totals, :total_participants_every_phase),
|
||||
percentage_district_population_every_phase: population_percent(population, heading_totals[:total_participants_every_phase])
|
||||
}
|
||||
end
|
||||
|
||||
@@ -141,14 +170,12 @@ class Budget::Stats
|
||||
end
|
||||
|
||||
def supports(supportable)
|
||||
ActsAsVotable::Vote.where(votable_type: "Budget::Investment", votable_id: supportable.investments.pluck(:id))
|
||||
Vote.where(votable: supportable.investments)
|
||||
end
|
||||
|
||||
stats_cache(*stats_methods)
|
||||
stats_cache :total_participants_with_gender
|
||||
stats_cache :voters, :participants, :authors, :balloters, :poll_ballot_voters
|
||||
|
||||
def stats_cache(key, &block)
|
||||
Rails.cache.fetch("budgets_stats/#{budget.id}/#{key}/v10", &block)
|
||||
Rails.cache.fetch("budgets_stats/#{budget.id}/#{phases.join}/#{key}/#{version}", &block)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,79 +1,175 @@
|
||||
module Statisticable
|
||||
extend ActiveSupport::Concern
|
||||
PARTICIPATIONS = %w[gender age geozone]
|
||||
|
||||
included do
|
||||
attr_reader :resource
|
||||
|
||||
def initialize(resource)
|
||||
@resource = resource
|
||||
end
|
||||
|
||||
def generate
|
||||
self.class.stats_methods.map { |stat_name| [stat_name, send(stat_name)] }.to_h
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def total_participants_with_gender
|
||||
participants.where.not(gender: nil).distinct.count
|
||||
end
|
||||
|
||||
def total_male_participants
|
||||
participants.where(gender: "male").count
|
||||
end
|
||||
|
||||
def total_female_participants
|
||||
participants.where(gender: "female").count
|
||||
end
|
||||
|
||||
def total_unknown_gender_or_age
|
||||
participants.where("gender IS NULL OR date_of_birth is NULL").uniq.count
|
||||
end
|
||||
|
||||
def male_percentage
|
||||
calculate_percentage(total_male_participants, total_participants_with_gender)
|
||||
end
|
||||
|
||||
def female_percentage
|
||||
calculate_percentage(total_female_participants, total_participants_with_gender)
|
||||
end
|
||||
|
||||
def age_groups
|
||||
groups = Hash.new(0)
|
||||
["16 - 19",
|
||||
"20 - 24",
|
||||
"25 - 29",
|
||||
"30 - 34",
|
||||
"35 - 39",
|
||||
"40 - 44",
|
||||
"45 - 49",
|
||||
"50 - 54",
|
||||
"55 - 59",
|
||||
"60 - 64",
|
||||
"65 - 69",
|
||||
"70 - 140"].each do |group|
|
||||
start, finish = group.split(" - ")
|
||||
group_name = (group == "70 - 140" ? "+ 70" : group)
|
||||
groups[group_name] = User.where(id: participants)
|
||||
.where("date_of_birth > ? AND date_of_birth < ?",
|
||||
finish.to_i.years.ago.beginning_of_year,
|
||||
start.to_i.years.ago.end_of_year).count
|
||||
end
|
||||
groups
|
||||
end
|
||||
|
||||
def calculate_percentage(fraction, total)
|
||||
return 0.0 if total.zero?
|
||||
|
||||
(fraction * 100.0 / total).round(3)
|
||||
end
|
||||
end
|
||||
|
||||
def initialize(resource)
|
||||
@resource = resource
|
||||
end
|
||||
|
||||
def generate
|
||||
stats_methods.each { |stat_name| send(stat_name) }
|
||||
end
|
||||
|
||||
def stats_methods
|
||||
base_stats_methods + participation_methods
|
||||
end
|
||||
|
||||
def participations
|
||||
PARTICIPATIONS.select { |participation| send("#{participation}?") }
|
||||
end
|
||||
|
||||
def gender?
|
||||
participants.male.any? || participants.female.any?
|
||||
end
|
||||
|
||||
def age?
|
||||
participants.between_ages(age_groups.flatten.min, age_groups.flatten.max).any?
|
||||
end
|
||||
|
||||
def geozone?
|
||||
participants.where(geozone: geozones).any?
|
||||
end
|
||||
|
||||
def participants
|
||||
@participants ||= User.unscoped.where(id: participant_ids)
|
||||
end
|
||||
|
||||
def total_male_participants
|
||||
participants.male.count
|
||||
end
|
||||
|
||||
def total_female_participants
|
||||
participants.female.count
|
||||
end
|
||||
|
||||
def total_no_demographic_data
|
||||
participants.where("gender IS NULL OR date_of_birth IS NULL OR geozone_id IS NULL").count
|
||||
end
|
||||
|
||||
def male_percentage
|
||||
calculate_percentage(total_male_participants, total_participants_with_gender)
|
||||
end
|
||||
|
||||
def female_percentage
|
||||
calculate_percentage(total_female_participants, total_participants_with_gender)
|
||||
end
|
||||
|
||||
def participants_by_age
|
||||
age_groups.map do |start, finish|
|
||||
count = participants.between_ages(start, finish).count
|
||||
|
||||
[
|
||||
"#{start} - #{finish}",
|
||||
{
|
||||
range: range_description(start, finish),
|
||||
count: count,
|
||||
percentage: calculate_percentage(count, total_participants)
|
||||
}
|
||||
]
|
||||
end.to_h
|
||||
end
|
||||
|
||||
def participants_by_geozone
|
||||
geozone_stats.map do |stats|
|
||||
[
|
||||
stats.name,
|
||||
{
|
||||
count: stats.count,
|
||||
percentage: stats.percentage
|
||||
}
|
||||
]
|
||||
end.to_h
|
||||
end
|
||||
|
||||
def calculate_percentage(fraction, total)
|
||||
PercentageCalculator.calculate(fraction, total)
|
||||
end
|
||||
|
||||
def version
|
||||
"v#{resource.find_or_create_stats_version.updated_at.to_i}"
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def base_stats_methods
|
||||
self.class.base_stats_methods
|
||||
end
|
||||
|
||||
def participation_methods
|
||||
participations.map { |participation| self.class.send("#{participation}_methods") }.flatten
|
||||
end
|
||||
|
||||
def total_participants_with_gender
|
||||
@total_participants_with_gender ||= participants.where.not(gender: nil).distinct.count
|
||||
end
|
||||
|
||||
def age_groups
|
||||
[[16, 19],
|
||||
[20, 24],
|
||||
[25, 29],
|
||||
[30, 34],
|
||||
[35, 39],
|
||||
[40, 44],
|
||||
[45, 49],
|
||||
[50, 54],
|
||||
[55, 59],
|
||||
[60, 64],
|
||||
[65, 69],
|
||||
[70, 74],
|
||||
[75, 79],
|
||||
[80, 84],
|
||||
[85, 89],
|
||||
[90, 300]
|
||||
]
|
||||
end
|
||||
|
||||
def participants_between_ages(from, to)
|
||||
participants.between_ages(from, to)
|
||||
end
|
||||
|
||||
def geozones
|
||||
Geozone.all.order("name")
|
||||
end
|
||||
|
||||
def geozone_stats
|
||||
geozones.map { |geozone| GeozoneStats.new(geozone, participants) }
|
||||
end
|
||||
|
||||
def range_description(start, finish)
|
||||
if finish > 200
|
||||
I18n.t("stats.age_more_than", start: start)
|
||||
else
|
||||
I18n.t("stats.age_range", start: start, finish: finish)
|
||||
end
|
||||
end
|
||||
|
||||
class_methods do
|
||||
def stats_methods
|
||||
%i[total_participants total_male_participants
|
||||
total_female_participants total_unknown_gender_or_age
|
||||
male_percentage female_percentage age_groups]
|
||||
base_stats_methods + gender_methods + age_methods + geozone_methods
|
||||
end
|
||||
|
||||
def base_stats_methods
|
||||
%i[total_participants participations] + participation_check_methods
|
||||
end
|
||||
|
||||
def participation_check_methods
|
||||
PARTICIPATIONS.map { |participation| :"#{participation}?" }
|
||||
end
|
||||
|
||||
def gender_methods
|
||||
%i[total_male_participants total_female_participants male_percentage female_percentage]
|
||||
end
|
||||
|
||||
def age_methods
|
||||
[:participants_by_age]
|
||||
end
|
||||
|
||||
def geozone_methods
|
||||
%i[participants_by_geozone total_no_demographic_data]
|
||||
end
|
||||
|
||||
def stats_cache(*method_names)
|
||||
|
||||
11
app/models/concerns/stats_versionable.rb
Normal file
@@ -0,0 +1,11 @@
|
||||
module StatsVersionable
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
has_one :stats_version, as: :process
|
||||
end
|
||||
|
||||
def find_or_create_stats_version
|
||||
stats_version || create_stats_version
|
||||
end
|
||||
end
|
||||
@@ -6,6 +6,7 @@ class Poll < ApplicationRecord
|
||||
include ActsAsParanoidAliases
|
||||
include Notifiable
|
||||
include Sluggable
|
||||
include StatsVersionable
|
||||
|
||||
translates :name, touch: true
|
||||
translates :summary, touch: true
|
||||
|
||||
@@ -2,122 +2,123 @@ class Poll::Stats
|
||||
include Statisticable
|
||||
alias_method :poll, :resource
|
||||
|
||||
CHANNELS = Poll::Voter::VALID_ORIGINS
|
||||
|
||||
def self.stats_methods
|
||||
%i[total_participants total_participants_web total_web_valid total_web_white total_web_null
|
||||
total_participants_booth total_booth_valid total_booth_white total_booth_null
|
||||
total_valid_votes total_white_votes total_null_votes valid_percentage_web valid_percentage_booth
|
||||
total_valid_percentage white_percentage_web white_percentage_booth total_white_percentage
|
||||
null_percentage_web null_percentage_booth total_null_percentage total_participants_web_percentage
|
||||
total_participants_booth_percentage]
|
||||
super +
|
||||
%i[total_valid_votes total_white_votes total_null_votes
|
||||
total_participants_web total_web_valid total_web_white total_web_null
|
||||
total_participants_booth total_booth_valid total_booth_white total_booth_null
|
||||
total_participants_letter total_letter_valid total_letter_white total_letter_null
|
||||
total_participants_web_percentage total_participants_booth_percentage
|
||||
total_participants_letter_percentage
|
||||
valid_percentage_web valid_percentage_booth valid_percentage_letter total_valid_percentage
|
||||
white_percentage_web white_percentage_booth white_percentage_letter total_white_percentage
|
||||
null_percentage_web null_percentage_booth null_percentage_letter total_null_percentage]
|
||||
end
|
||||
|
||||
def total_participants
|
||||
total_participants_web + total_participants_booth
|
||||
end
|
||||
|
||||
def channels
|
||||
CHANNELS.select { |channel| send(:"total_participants_#{channel}") > 0 }
|
||||
end
|
||||
|
||||
CHANNELS.each do |channel|
|
||||
define_method :"total_participants_#{channel}" do
|
||||
send(:"total_#{channel}_valid") +
|
||||
send(:"total_#{channel}_white") +
|
||||
send(:"total_#{channel}_null")
|
||||
end
|
||||
|
||||
define_method :"total_participants_#{channel}_percentage" do
|
||||
calculate_percentage(send(:"total_participants_#{channel}"), total_participants)
|
||||
end
|
||||
end
|
||||
|
||||
def total_web_valid
|
||||
voters.where(origin: "web").count - total_web_white
|
||||
end
|
||||
|
||||
def total_web_white
|
||||
0
|
||||
end
|
||||
|
||||
def total_web_null
|
||||
0
|
||||
end
|
||||
|
||||
def total_booth_valid
|
||||
recounts.sum(:total_amount)
|
||||
end
|
||||
|
||||
def total_booth_white
|
||||
recounts.sum(:white_amount)
|
||||
end
|
||||
|
||||
def total_booth_null
|
||||
recounts.sum(:null_amount)
|
||||
end
|
||||
|
||||
def total_letter_valid
|
||||
voters.where(origin: "letter").count # TODO: count only valid votes
|
||||
end
|
||||
|
||||
def total_letter_white
|
||||
0 # TODO
|
||||
end
|
||||
|
||||
def total_letter_null
|
||||
0 # TODO
|
||||
end
|
||||
|
||||
%i[valid white null].each do |type|
|
||||
CHANNELS.each do |channel|
|
||||
define_method :"#{type}_percentage_#{channel}" do
|
||||
calculate_percentage(send(:"total_#{channel}_#{type}"), send(:"total_#{type}_votes"))
|
||||
end
|
||||
end
|
||||
|
||||
define_method :"total_#{type}_votes" do
|
||||
send(:"total_web_#{type}") + send(:"total_booth_#{type}")
|
||||
end
|
||||
|
||||
define_method :"total_#{type}_percentage" do
|
||||
calculate_percentage(send(:"total_#{type}_votes"), total_participants)
|
||||
end
|
||||
end
|
||||
|
||||
def total_no_demographic_data
|
||||
super + total_unregistered_booth
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def total_participants
|
||||
total_participants_web + total_participants_booth
|
||||
end
|
||||
|
||||
def total_participants_web
|
||||
total_web_valid + total_web_white + total_web_null
|
||||
end
|
||||
|
||||
def total_participants_web_percentage
|
||||
calculate_percentage(total_participants_web, total_participants)
|
||||
end
|
||||
|
||||
def total_participants_booth
|
||||
total_booth_valid + total_booth_white + total_booth_null
|
||||
end
|
||||
|
||||
def total_participants_booth_percentage
|
||||
calculate_percentage(total_participants_booth, total_participants)
|
||||
end
|
||||
|
||||
def total_web_valid
|
||||
voters.where(origin: "web").count - total_web_white
|
||||
end
|
||||
|
||||
def valid_percentage_web
|
||||
calculate_percentage(total_web_valid, total_valid_votes)
|
||||
end
|
||||
|
||||
def total_web_white
|
||||
0
|
||||
end
|
||||
|
||||
def white_percentage_web
|
||||
calculate_percentage(total_web_white, total_white_votes)
|
||||
end
|
||||
|
||||
def total_web_null
|
||||
0
|
||||
end
|
||||
|
||||
def null_percentage_web
|
||||
calculate_percentage(total_web_null, total_null_votes)
|
||||
end
|
||||
|
||||
def total_booth_valid
|
||||
recounts.sum(:total_amount)
|
||||
end
|
||||
|
||||
def valid_percentage_booth
|
||||
calculate_percentage(total_booth_valid, total_valid_votes)
|
||||
end
|
||||
|
||||
def total_booth_white
|
||||
recounts.sum(:white_amount)
|
||||
end
|
||||
|
||||
def white_percentage_booth
|
||||
calculate_percentage(total_booth_white, total_white_votes)
|
||||
end
|
||||
|
||||
def total_booth_null
|
||||
recounts.sum(:null_amount)
|
||||
end
|
||||
|
||||
def null_percentage_booth
|
||||
calculate_percentage(total_booth_null, total_null_votes)
|
||||
end
|
||||
|
||||
def total_valid_votes
|
||||
total_web_valid + total_booth_valid
|
||||
end
|
||||
|
||||
def total_valid_percentage
|
||||
calculate_percentage(total_valid_votes, total_participants)
|
||||
end
|
||||
|
||||
def total_white_votes
|
||||
total_web_white + total_booth_white
|
||||
end
|
||||
|
||||
def total_white_percentage
|
||||
calculate_percentage(total_white_votes, total_participants)
|
||||
end
|
||||
|
||||
def total_null_votes
|
||||
total_web_null + total_booth_null
|
||||
end
|
||||
|
||||
def total_null_percentage
|
||||
calculate_percentage(total_null_votes, total_participants)
|
||||
def participant_ids
|
||||
voters
|
||||
end
|
||||
|
||||
def voters
|
||||
poll.voters
|
||||
@voters ||= poll.voters.select(:user_id)
|
||||
end
|
||||
|
||||
def recounts
|
||||
poll.recounts
|
||||
@recounts ||= poll.recounts
|
||||
end
|
||||
|
||||
def total_registered_booth
|
||||
voters.where(origin: "booth").count
|
||||
end
|
||||
|
||||
def total_unregistered_booth
|
||||
[total_participants_booth - total_registered_booth, 0].max
|
||||
end
|
||||
|
||||
stats_cache(*stats_methods)
|
||||
stats_cache :voters, :recounts
|
||||
|
||||
def stats_cache(key, &block)
|
||||
Rails.cache.fetch("polls_stats/#{poll.id}/#{key}/v12", &block)
|
||||
Rails.cache.fetch("polls_stats/#{poll.id}/#{key}/#{version}", &block)
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
5
app/models/stats_version.rb
Normal file
@@ -0,0 +1,5 @@
|
||||
class StatsVersion < ApplicationRecord
|
||||
validates :process, presence: true
|
||||
|
||||
belongs_to :process, polymorphic: true
|
||||
end
|
||||
@@ -55,6 +55,8 @@ class User < ApplicationRecord
|
||||
scope :moderators, -> { joins(:moderator) }
|
||||
scope :organizations, -> { joins(:organization) }
|
||||
scope :officials, -> { where("official_level > 0") }
|
||||
scope :male, -> { where(gender: "male") }
|
||||
scope :female, -> { where(gender: "female") }
|
||||
scope :newsletter, -> { where(newsletter: true) }
|
||||
scope :for_render, -> { includes(:organization) }
|
||||
scope :by_document, ->(document_type, document_number) do
|
||||
@@ -70,6 +72,13 @@ class User < ApplicationRecord
|
||||
string = "%#{search_string}%"
|
||||
where("username ILIKE ? OR email ILIKE ? OR document_number ILIKE ?", string, string, string)
|
||||
end
|
||||
scope :between_ages, -> (from, to) do
|
||||
where(
|
||||
"date_of_birth > ? AND date_of_birth < ?",
|
||||
to.years.ago.beginning_of_year,
|
||||
from.years.ago.end_of_year
|
||||
)
|
||||
end
|
||||
|
||||
before_validation :clean_document_number
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
<%= link_to t("budgets.results.link"), budget_results_path(@budget) %>
|
||||
</li>
|
||||
<li class="tabs-title">
|
||||
<%= link_to t("budgets.stats.link"), budget_stats_path(@budget) %>
|
||||
<%= link_to t("stats.budgets.link"), budget_stats_path(@budget) %>
|
||||
</li>
|
||||
<li class="tabs-title is-active">
|
||||
<%= link_to t("budgets.executions.link"), budget_executions_path(@budget), class: "is-active" %>
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
<%= link_to t("budgets.results.link"), budget_results_path(@budget), class: "is-active" %>
|
||||
</li>
|
||||
<li class="tabs-title">
|
||||
<%= link_to t("budgets.stats.link"), budget_stats_path(@budget) %>
|
||||
<%= link_to t("stats.budgets.link"), budget_stats_path(@budget) %>
|
||||
</li>
|
||||
<li class="tabs-title">
|
||||
<%= link_to t("budgets.executions.link"), budget_executions_path(@budget) %>
|
||||
|
||||
@@ -1,254 +1,163 @@
|
||||
<% provide :title do %>
|
||||
<%= t("budgets.stats.page_title", budget: @budget.name) %>
|
||||
<%= t("stats.budgets.page_title", budget: @budget.name) %>
|
||||
<% end %>
|
||||
<% provide :social_media_meta_tags do %>
|
||||
<%= render "shared/social_media_meta_tags",
|
||||
social_url: budget_url(@budget),
|
||||
social_url: budget_stats_url(@budget),
|
||||
social_title: @budget.name,
|
||||
social_description: @budget.description_finished %>
|
||||
social_description: @budget.description_for_phase("finished") %>
|
||||
<% end %>
|
||||
|
||||
<% cache [@stats] do %>
|
||||
<div class="budgets-stats">
|
||||
<div class="expanded no-margin-top padding header">
|
||||
<div class="row">
|
||||
<div class="small-12 column">
|
||||
<%= back_link_to budgets_path %>
|
||||
<h2 class="margin-top">
|
||||
<%= t("budgets.stats.title") %><br>
|
||||
<span><%= @budget.name %></span>
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row margin-top">
|
||||
<div class="small-12 column">
|
||||
<ul class="tabs">
|
||||
<li class="tabs-title">
|
||||
<span class="show-for-sr"><%= t("shared.you_are_in") %></span>
|
||||
<%= link_to t("budgets.results.link"), budget_results_path(@budget) %>
|
||||
</li>
|
||||
<li class="tabs-title is-active">
|
||||
<%= link_to t("budgets.stats.link"), budget_stats_path(@budget), class: "is-active" %>
|
||||
</li>
|
||||
<li class="tabs-title">
|
||||
<%= link_to t("budgets.executions.link"), budget_executions_path(@budget) %>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="small-12 medium-6 column">
|
||||
<div class="callout">
|
||||
<span class="uppercase"><%= t("budgets.stats.total_participants") %></span>
|
||||
<p id="total_participants" class="big-number-stat budget">
|
||||
<%= @stats[:total_participants] %>
|
||||
</p>
|
||||
|
||||
<span class="uppercase"><%= t("budgets.stats.total_budget_investments") %></span>
|
||||
<p class="big-number-stat budget">
|
||||
<%= @stats[:total_budget_investments] %>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="small-12 medium-6 column">
|
||||
<p>
|
||||
<span class="label feasible"></span>
|
||||
<span id="total_selected_investments">
|
||||
<span class="uppercase"><strong><%= t("budgets.stats.total_selected_investments") %></strong></span>:
|
||||
<%= @stats[:total_selected_investments] %><br>
|
||||
</span>
|
||||
|
||||
<span class="label unfeasible"></span>
|
||||
<span id="total_unfeasible_investments">
|
||||
<span class="uppercase"><strong><%= t("budgets.stats.total_unfeasible_investments") %></strong></span>:
|
||||
<%= @stats[:total_unfeasible_investments] %><br>
|
||||
</span>
|
||||
|
||||
<br>
|
||||
|
||||
<span class="label supports"></span>
|
||||
<span class="uppercase"><strong><%= t("budgets.stats.total_participants_support_phase") %></strong></span>:
|
||||
<span id="total_participants_support_phase">
|
||||
<%= @stats[:total_participants_support_phase] %> <em><%= t("budgets.stats.participants") %></em>,
|
||||
<%= @stats[:total_supports] %> <em><%= t("budgets.stats.supports") %></em><br>
|
||||
</span>
|
||||
|
||||
<br>
|
||||
|
||||
<span class="label"></span>
|
||||
<span class="uppercase"><strong><%= t("budgets.stats.total_participants_web") %></strong></span>:
|
||||
<span id="total_participants_web">
|
||||
<%= @stats[:total_participants_web] %><br>
|
||||
</span>
|
||||
|
||||
<span class="label"></span>
|
||||
<span class="uppercase"><strong><%= t("budgets.stats.total_participants_booths") %></strong></span>:
|
||||
<span id="total_participants_booths">
|
||||
<%= @stats[:total_participants_booths] %><br>
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row margin">
|
||||
<div class="small-12 column">
|
||||
<h2 class="margin-bottom"><%= t("budgets.stats.by_gender") %></h2>
|
||||
</div>
|
||||
|
||||
<div class="small-12 medium-6 column text-center">
|
||||
<%= image_tag ("budgets/stats/gender.png") %>
|
||||
</div>
|
||||
|
||||
<div class="small-12 medium-6 column">
|
||||
<div class="small-12 medium-6 column text-right">
|
||||
<p id="female_percentage">
|
||||
<span class="label" style="background: #FF9E01"></span>
|
||||
<%= t("budgets.stats.total_female_participants").upcase %>
|
||||
(<%= number_to_percentage(@stats[:female_percentage],
|
||||
strip_insignificant_zeros: true,
|
||||
precision: 2) %>)
|
||||
</p>
|
||||
<p id="total_female_participants" class="big-number-stat">
|
||||
<%= @stats[:total_female_participants] %>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="small-12 medium-6 column text-left">
|
||||
<p id="male_percentage">
|
||||
<span class="label" style="background: #FF6600"></span>
|
||||
<%= t("budgets.stats.total_male_participants").upcase %>
|
||||
(<%= number_to_percentage(@stats[:male_percentage],
|
||||
strip_insignificant_zeros: true,
|
||||
precision: 2) %>)
|
||||
</p>
|
||||
<p id="total_male_participants" class="big-number-stat">
|
||||
<%= @stats[:total_male_participants] %>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row margin">
|
||||
<div class="small-12 column">
|
||||
<h2 class="margin-bottom"><%= t("budgets.stats.by_age") %></h2>
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col" class="table-width border-right"><%= t("budgets.stats.age").upcase %></th>
|
||||
<th scope="col" class="border-left"><%= t("budgets.stats.total").upcase %></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<% all_ages_count = @stats[:age_groups].values.sum.to_f %>
|
||||
<% @stats[:age_groups].each do |age_group, count| %>
|
||||
<tr id="age_group_<%= age_group.gsub(" - ", "_to_").gsub("+ ", "up_to_") %>">
|
||||
<td class="border-right">
|
||||
<%= age_group.gsub("+", t("budgets.stats.more_than")) + " " + t("budgets.stats.years") %>
|
||||
</td>
|
||||
<td class="border-left">
|
||||
<strong>
|
||||
<%
|
||||
percentage_age_count = all_ages_count == 0 ? 0 : (count / all_ages_count * 100)
|
||||
formatted_percentage_age_count = number_to_percentage(percentage_age_count,
|
||||
strip_insignificant_zeros: true,
|
||||
precision: 2)
|
||||
%>
|
||||
<%= count %>
|
||||
(<%= formatted_percentage_age_count %>)
|
||||
</strong>
|
||||
<div class="progress" role="progressbar" tabindex="0" aria-valuenow="20" aria-valuemin="0" aria-valuetext="<%= percentage_age_count %>" aria-valuemax="100">
|
||||
<span class="progress-meter" style="width: <%= number_to_percentage(percentage_age_count*5,
|
||||
strip_insignificant_zeros: true,
|
||||
precision: 2, locale: :en) %>;"></span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<% end %>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="participation-stats budgets-stats">
|
||||
<div class="expanded no-margin-top padding header">
|
||||
<div class="row">
|
||||
<div class="small-12 column">
|
||||
<h2 class="margin-bottom"><%= t("budgets.stats.by_heading") %></h2>
|
||||
<%= back_link_to budgets_path %>
|
||||
<h2 class="margin-top">
|
||||
<%= t("stats.title") %><br>
|
||||
<span><%= @budget.name %></span>
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<table class="stats-districts survey-districts">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col" rowspan="2"><%= t("budgets.stats.heading") %></th>
|
||||
<th scope="col" rowspan="2"><%= t("budgets.stats.investments_sent_html") %></th>
|
||||
<th scope="col" colspan="3"><%= t("budgets.stats.participants_support_phase") %></th>
|
||||
<th scope="col" colspan="3"><%= t("budgets.stats.participants_voting_phase") %></th>
|
||||
<th scope="col" colspan="3"><%= t("budgets.stats.participants_total") %></th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="col" class="tiny"><%= t("budgets.stats.total") %></th>
|
||||
<th scope="col" class="tiny"><%= t("budgets.stats.percent_total_participants_html") %></th>
|
||||
<th scope="col" class="tiny"><%= t("budgets.stats.percent_heading_census_html") %></th>
|
||||
<th scope="col" class="tiny"><%= t("budgets.stats.total") %></th>
|
||||
<th scope="col" class="tiny"><%= t("budgets.stats.percent_total_participants_html") %></th>
|
||||
<th scope="col" class="tiny"><%= t("budgets.stats.percent_heading_census_html") %></th>
|
||||
<th scope="col" class="tiny"><%= t("budgets.stats.total") %></th>
|
||||
<th scope="col" class="tiny"><%= t("budgets.stats.percent_total_participants_html") %></th>
|
||||
<th scope="col" class="tiny"><%= t("budgets.stats.percent_heading_census_html") %></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="headings">
|
||||
<% @headings.each do |heading| %>
|
||||
<tr id="<%= heading.name.parameterize %>">
|
||||
<td class="border-left">
|
||||
<strong><%= heading.name %></strong>
|
||||
</td>
|
||||
<td id="total_spending_proposals_heading_<%= heading.id %>"
|
||||
class="text-center border-left border-right">
|
||||
<%= @stats[:headings][heading.id][:total_investments_count] %>
|
||||
</td>
|
||||
<div class="row margin-top">
|
||||
<div class="small-12 column">
|
||||
<ul class="tabs">
|
||||
<li class="tabs-title">
|
||||
<span class="show-for-sr"><%= t("shared.you_are_in") %></span>
|
||||
<%= link_to t("budgets.results.link"), budget_results_path(@budget) %>
|
||||
</li>
|
||||
<li class="tabs-title is-active">
|
||||
<%= link_to t("stats.budgets.link"), budget_stats_path(@budget), class: "is-active" %>
|
||||
</li>
|
||||
<li class="tabs-title">
|
||||
<%= link_to t("budgets.executions.link"), budget_executions_path(@budget) %>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<% ["support", "vote", "all"].each do |phase| %>
|
||||
<td id="total_participants_<%= phase %>_phase_heading_<%= heading.id %>"
|
||||
class="border-left text-center">
|
||||
<%= @stats[:headings][heading.id]["total_participants_#{phase}_phase".to_sym] %>
|
||||
</td>
|
||||
<td id="percentage_participants_<%= phase %>_phase_heading_<%= heading.id %>"
|
||||
class="border-left border-right text-center">
|
||||
<%= number_to_percentage(@stats[:headings][heading.id]["percentage_participants_#{phase}_phase".to_sym],
|
||||
strip_insignificant_zeros: true,
|
||||
precision: 2) %>
|
||||
</td>
|
||||
<td id="percentage_district_population_<%= phase %>_phase_heading_<%= heading.id %>"
|
||||
class="text-center border-right">
|
||||
<%= number_to_percentage(@stats[:headings][heading.id]["percentage_district_population_#{phase}_phase".to_sym],
|
||||
strip_insignificant_zeros: true,
|
||||
precision: 2) %>
|
||||
</td>
|
||||
<div class="row margin">
|
||||
<div class="small-12 medium-3 column sidebar">
|
||||
<%= render "shared/stats/links", stats: @stats %>
|
||||
|
||||
<p><strong><%= link_to t("stats.advanced"), "#advanced_statistics" %></strong></p>
|
||||
<ul class="menu vertical">
|
||||
<li>
|
||||
<%= link_to t("stats.budgets.total_investments"), "#total_investments" %>
|
||||
</li>
|
||||
<li>
|
||||
<%= link_to t("stats.budgets.by_phase"), "#stats_by_phase" %>
|
||||
</li>
|
||||
<li>
|
||||
<%= link_to t("stats.budgets.by_heading"), "#stats_by_heading" %>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="small-12 medium-9 column stats-content">
|
||||
<%= render "shared/stats/participation", stats: @stats %>
|
||||
|
||||
<div id="advanced_statistics">
|
||||
<h3 class="section-title"><%= t("stats.advanced") %></h3>
|
||||
|
||||
<div id="total_investments" class="stats-group">
|
||||
<h4><%= t("stats.budgets.total_investments") %></h4>
|
||||
|
||||
<%= number_with_info_tags(
|
||||
@stats.total_budget_investments,
|
||||
t("stats.budgets.total_investments"),
|
||||
html_class: "total-investments"
|
||||
) %>
|
||||
|
||||
<%= number_with_info_tags(@stats.total_unfeasible_investments,
|
||||
t("stats.budgets.total_unfeasible_investments")) %>
|
||||
<%= number_with_info_tags(@stats.total_selected_investments,
|
||||
t("stats.budgets.total_selected_investments")) %>
|
||||
</div>
|
||||
|
||||
<div id="stats_by_phase" class="stats-group">
|
||||
<h4><%= t("stats.budgets.by_phase") %></h4>
|
||||
|
||||
<% @stats.phases.each do |phase| %>
|
||||
<%= number_with_info_tags(
|
||||
@stats.send("total_participants_#{phase}_phase"),
|
||||
t("stats.budgets.participants_#{phase}_phase")
|
||||
) %>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<div id="stats_by_heading" class="stats-group">
|
||||
<h4 class="margin-bottom"><%= t("stats.budgets.by_heading") %></h4>
|
||||
|
||||
<table class="stats-districts survey-districts">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col" rowspan="2"><%= t("stats.budgets.heading") %></th>
|
||||
<th scope="col" rowspan="2"><%= t("stats.budgets.investments_sent_html") %></th>
|
||||
|
||||
<% @stats.all_phases.each do |phase| %>
|
||||
<th scope="col" colspan="3">
|
||||
<%= t("stats.budgets.participants_#{phase}_phase") %>
|
||||
</th>
|
||||
<% end %>
|
||||
</tr>
|
||||
<% end %>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<tr>
|
||||
<% @stats.all_phases.each do %>
|
||||
<th scope="col" class="phase-subheader"><%= t("stats.budgets.total") %></th>
|
||||
<th scope="col" class="phase-subheader"><%= t("stats.budgets.percent_total_participants") %></th>
|
||||
<th scope="col" class="phase-subheader"><%= t("stats.budgets.percent_heading_census") %></th>
|
||||
<% end %>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="headings">
|
||||
<% @headings.each do |heading| %>
|
||||
<tr id="<%= heading.name.parameterize %>">
|
||||
<td class="border-left">
|
||||
<strong><%= heading.name %></strong>
|
||||
</td>
|
||||
<td id="total_spending_proposals_heading_<%= heading.id %>"
|
||||
class="text-center border-left border-right">
|
||||
<%= @stats.headings[heading.id][:total_investments_count] %>
|
||||
</td>
|
||||
|
||||
<div class="row margin">
|
||||
<div class="small-12 column">
|
||||
<div id="total_unknown_gender_or_age">
|
||||
<p class="help-text">
|
||||
<%= t("budgets.stats.no_demographic_data", total: @stats[:total_unknown_gender_or_age]) %>
|
||||
</p>
|
||||
<p class="help-text">
|
||||
<%= t("budgets.stats.participatory_disclaimer") %>
|
||||
</p>
|
||||
<p class="help-text">
|
||||
<%= t("budgets.stats.heading_disclaimer") %>
|
||||
</p>
|
||||
<% @stats.all_phases.each do |phase| %>
|
||||
<td id="total_participants_<%= phase %>_phase_heading_<%= heading.id %>"
|
||||
class="border-left text-center">
|
||||
<%= @stats.headings[heading.id]["total_participants_#{phase}_phase".to_sym] %>
|
||||
</td>
|
||||
<td id="percentage_participants_<%= phase %>_phase_heading_<%= heading.id %>"
|
||||
class="border-left border-right text-center">
|
||||
<%= number_to_stats_percentage(@stats.headings[heading.id]["percentage_participants_#{phase}_phase".to_sym]) %>
|
||||
</td>
|
||||
<td id="percentage_district_population_<%= phase %>_phase_heading_<%= heading.id %>"
|
||||
class="text-center border-right">
|
||||
<%= number_to_stats_percentage(@stats.headings[heading.id]["percentage_district_population_#{phase}_phase".to_sym]) %>
|
||||
</td>
|
||||
<% end %>
|
||||
</tr>
|
||||
<% end %>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row margin">
|
||||
<div class="small-12 column">
|
||||
<div id="total_no_demographic_data">
|
||||
<p class="help-text">
|
||||
<%= t("stats.no_demographic_data", count: @stats.total_no_demographic_data) %>
|
||||
</p>
|
||||
<p class="help-text">
|
||||
<%= t("stats.budgets.participatory_disclaimer") %>
|
||||
</p>
|
||||
<p class="help-text">
|
||||
<%= t("stats.budgets.heading_disclaimer") %>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<%= link_to t("polls.poll_header.back_to_proposal"), [@poll.related] %>
|
||||
<% end %>
|
||||
|
||||
<h2><%= @poll.name %></h2>
|
||||
<h1><%= @poll.name %></h1>
|
||||
|
||||
<%= safe_html_with_links simple_format(@poll.summary) %>
|
||||
|
||||
|
||||
@@ -1,95 +1,126 @@
|
||||
<% provide :title do %><%= @poll.name %><% end %>
|
||||
|
||||
<div class="polls-results-stats">
|
||||
<div class="participation-stats 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>
|
||||
<div class="row margin">
|
||||
<div class="small-12 medium-3 column sidebar">
|
||||
<%= render "shared/stats/links", stats: @stats %>
|
||||
|
||||
<ul class="menu vertical margin-top">
|
||||
<li><a href="#total"><%= t("polls.show.stats.total_participation") %></a></li>
|
||||
<p><strong><%= link_to t("stats.advanced"), "#advanced_statistics" %></strong></p>
|
||||
<ul class="menu vertical">
|
||||
<li>
|
||||
<%= link_to t("stats.polls.by_channel"), "#stats_by_channel" %>
|
||||
</li>
|
||||
<li>
|
||||
<%= link_to t("stats.polls.vote_by_channel"), "#vote_stats_by_channel" %>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="small-12 medium-9 column" data-equalizer-watch>
|
||||
<h3 id="total"><%= t("polls.show.stats.total_participation") %></h3>
|
||||
<div class="small-12 medium-9 column stats-content">
|
||||
<%= render "shared/stats/participation", stats: @stats %>
|
||||
|
||||
<p class="margin-top uppercase">
|
||||
<%= t("polls.show.stats.total_votes") %><br>
|
||||
<span class="number"><%= @stats[:total_participants] %></span>
|
||||
</p>
|
||||
<div id="advanced_statistics">
|
||||
<h3 class="section-title"><%= t("stats.advanced") %></h3>
|
||||
|
||||
<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] %></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div id="stats_by_channel" class="stats-group">
|
||||
<h4><%= t("stats.polls.by_channel") %></h4>
|
||||
|
||||
<% @stats.channels.each do |channel| %>
|
||||
<%= number_with_info_tags(
|
||||
@stats.send("total_participants_#{channel}"),
|
||||
t("stats.polls.#{channel}_percentage",
|
||||
percentage: number_to_stats_percentage(@stats.send(:"total_participants_#{channel}_percentage"))
|
||||
),
|
||||
html_class: channel
|
||||
) %>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<div id="vote_stats_by_channel" class="stats-group">
|
||||
<h4><%= t("stats.polls.vote_by_channel") %></h4>
|
||||
|
||||
<table class="stack">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col"><%= t("polls.show.stats.votes") %></th>
|
||||
<% @stats.channels.each do |channel| %>
|
||||
<th scope="col"><%= t("polls.show.stats.#{channel}") %></th>
|
||||
<% end %>
|
||||
<th scope="col"><%= t("polls.show.stats.total") %></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th scope="row"><%= t("polls.show.stats.valid") %></th>
|
||||
|
||||
<% @stats.channels.each do |channel| %>
|
||||
<td>
|
||||
<%= @stats.send(:"total_#{channel}_valid") %>
|
||||
<small><em>(<%= @stats.send(:"valid_percentage_#{channel}").round(2) %>%)</em></small>
|
||||
</td>
|
||||
<% end %>
|
||||
|
||||
<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>
|
||||
|
||||
<% @stats.channels.each do |channel| %>
|
||||
<td>
|
||||
<%= @stats.send(:"total_#{channel}_white") %>
|
||||
<small><em>(<%= @stats.send(:"white_percentage_#{channel}").round(2) %>%)</em></small>
|
||||
</td>
|
||||
<% end %>
|
||||
|
||||
<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>
|
||||
|
||||
<% @stats.channels.each do |channel| %>
|
||||
<td>
|
||||
<%= @stats.send(:"total_#{channel}_null") %>
|
||||
<small><em>(<%= @stats.send(:"null_percentage_#{channel}").round(2) %>%)</em></small>
|
||||
</td>
|
||||
<% end %>
|
||||
|
||||
<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>
|
||||
|
||||
<% @stats.channels.each do |channel| %>
|
||||
<td>
|
||||
<%= @stats.send(:"total_participants_#{channel}") %>
|
||||
<small><em>(<%= @stats.send(:"total_participants_#{channel}_percentage").round(2) %>%)</em></small>
|
||||
</td>
|
||||
<% end %>
|
||||
|
||||
<td><%= @stats.total_participants %></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div id="total_no_demographic_data">
|
||||
<p class="help-text">
|
||||
<%= t("stats.no_demographic_data", count: @stats.total_no_demographic_data) %>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
28
app/views/shared/stats/_age.html.erb
Normal file
@@ -0,0 +1,28 @@
|
||||
<div id="participants_by_age" class="stats-group">
|
||||
<h4><%= t("stats.by_age") %></h4>
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="small-4"><%= t("stats.age") %></th>
|
||||
<th><%= t("stats.total") %></th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
<% stats.participants_by_age.values.each do |group| %>
|
||||
<tr>
|
||||
<td><%= group[:range] %></td>
|
||||
<td>
|
||||
<strong>
|
||||
<%= "#{group[:count]} (#{number_to_stats_percentage(group[:percentage])})" %>
|
||||
</strong>
|
||||
<div class="progress" tabindex="0">
|
||||
<span class="progress-meter" style="width: <%= group[:percentage] %>%"></span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<% end %>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
15
app/views/shared/stats/_gender.html.erb
Normal file
@@ -0,0 +1,15 @@
|
||||
<div id="participants_by_gender" class="stats-group">
|
||||
<h4><%= t("stats.by_gender") %></h4>
|
||||
|
||||
<%= number_with_info_tags(
|
||||
stats.total_male_participants,
|
||||
t("stats.men_percentage", percentage: number_to_stats_percentage(stats.male_percentage)),
|
||||
html_class: "participants male"
|
||||
) %>
|
||||
|
||||
<%= number_with_info_tags(
|
||||
stats.total_female_participants,
|
||||
t("stats.women_percentage", percentage: number_to_stats_percentage(stats.female_percentage)),
|
||||
html_class: "participants female"
|
||||
) %>
|
||||
</div>
|
||||
21
app/views/shared/stats/_geozone.html.erb
Normal file
@@ -0,0 +1,21 @@
|
||||
<div id="participants_by_geozone" class="stats-group">
|
||||
<h4><%= t("stats.by_geozone") %></h4>
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th><%= t("stats.geozone") %></th>
|
||||
<th><%= t("stats.total") %></th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
<% stats.participants_by_geozone.each do |geozone, participants| %>
|
||||
<tr>
|
||||
<td><%= geozone %></td>
|
||||
<td><%= "#{participants[:count]} (#{number_to_stats_percentage(participants[:percentage])})" %></td>
|
||||
</tr>
|
||||
<% end %>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
12
app/views/shared/stats/_links.html.erb
Normal file
@@ -0,0 +1,12 @@
|
||||
<p><strong><%= link_to t("stats.title"), "#participation_statistics" %></strong></p>
|
||||
<ul class="menu vertical">
|
||||
<li>
|
||||
<%= link_to t("stats.total_participants"), "#total_participants" %>
|
||||
</li>
|
||||
|
||||
<% stats.participations.each do |stat| %>
|
||||
<li>
|
||||
<%= link_to t("stats.by_#{stat}"), "#participants_by_#{stat}" %>
|
||||
</li>
|
||||
<% end %>
|
||||
</ul>
|
||||
17
app/views/shared/stats/_participation.html.erb
Normal file
@@ -0,0 +1,17 @@
|
||||
<div id="participation_statistics">
|
||||
<h3 class="section-title"><%= t("stats.title") %></h3>
|
||||
|
||||
<div id="total_participants" class="stats-group">
|
||||
<h4><%= t("stats.total_participants") %></h4>
|
||||
|
||||
<%= number_with_info_tags(
|
||||
stats.total_participants,
|
||||
"",
|
||||
html_class: "participants total-participants"
|
||||
) %>
|
||||
</div>
|
||||
|
||||
<% stats.participations.each do |participation| %>
|
||||
<%= render "shared/stats/#{participation}", stats: stats %>
|
||||
<% end %>
|
||||
</div>
|
||||
@@ -45,6 +45,7 @@ data:
|
||||
- config/locales/%{locale}/user_groups.yml
|
||||
- config/locales/%{locale}/i18n.yml
|
||||
- config/locales/%{locale}/milestones.yml
|
||||
- config/locales/%{locale}/stats.yml
|
||||
|
||||
# Locale files to write new keys to, based on a list of key pattern => file rules. Matched from top to bottom:
|
||||
# `i18n-tasks normalize -p` will force move the keys according to these rules
|
||||
@@ -193,6 +194,7 @@ ignore_unused:
|
||||
- "admin.site_customization.pages.page.status_*"
|
||||
- "admin.legislation.processes.process.*"
|
||||
- "legislation.processes.index.*"
|
||||
- "stats.budgets.participants_*_phase"
|
||||
- "votes.budget_investments.different_heading_assigned*"
|
||||
- "*.form.map_skip_checkbox"
|
||||
# - "{devise,kaminari,will_paginate}.*"
|
||||
|
||||
@@ -191,35 +191,3 @@ en:
|
||||
dates_range_invalid: "Start date can't be equal or later than End date"
|
||||
prev_phase_dates_invalid: "Start date must be later than the start date of the previous enabled phase (%{phase_name})"
|
||||
next_phase_dates_invalid: "End date must be earlier than the end date of the next enabled phase (%{phase_name})"
|
||||
stats:
|
||||
title: Participation stats
|
||||
link: Stats
|
||||
page_title: "%{budget} - Participation stats"
|
||||
total_participants: Total Participants
|
||||
total_budget_investments: Total Proposed Investments
|
||||
total_selected_investments: Proposals on final phase
|
||||
total_unfeasible_investments: Unfeasible proposals
|
||||
total_participants_support_phase: Support phase
|
||||
total_participants_web: Vote phase web participants
|
||||
total_participants_booths: Vote phase booth participants
|
||||
participants: Participants
|
||||
supports: Supports
|
||||
by_gender: "Participants by gender"
|
||||
total_male_participants: Mens
|
||||
total_female_participants: Women
|
||||
by_age: "Participants by age groups"
|
||||
age: Age
|
||||
total: Total
|
||||
more_than: More than
|
||||
years: years
|
||||
by_heading: "Participants by heading"
|
||||
heading: Heading
|
||||
investments_sent_html: "Investment proposals sent"
|
||||
participants_support_phase: Participants support phase
|
||||
participants_voting_phase: Participants voting phase
|
||||
participants_total: Total Participants
|
||||
percent_total_participants_html: "% <br>Total<br>Participants"
|
||||
percent_heading_census_html: "% <br>Heading<br>Census"
|
||||
no_demographic_data: "* There is no demographic data for %{total} participants."
|
||||
participatory_disclaimer: "** The numbers of total participation refer to persons that created, supported or voted investment proposals."
|
||||
heading_disclaimer: "*** Data about headings refer to the heading where each user voted, not necessarily the one that person is registered on."
|
||||
|
||||
@@ -650,15 +650,14 @@ en:
|
||||
stats_menu: "Participation statistics"
|
||||
results_menu: "Poll results"
|
||||
stats:
|
||||
title: "Participation data"
|
||||
total_participation: "Total participation"
|
||||
total_votes: "Total amount of given votes"
|
||||
votes: "VOTES"
|
||||
web: "WEB"
|
||||
mail: "MAIL"
|
||||
booth: "BOOTH"
|
||||
total: "TOTAL"
|
||||
valid: "Valid"
|
||||
white: "White votes"
|
||||
white: "Fully blank"
|
||||
null_votes: "Invalid"
|
||||
results:
|
||||
title: "Questions"
|
||||
|
||||
43
config/locales/en/stats.yml
Normal file
@@ -0,0 +1,43 @@
|
||||
en:
|
||||
stats:
|
||||
title: "Participation data"
|
||||
advanced: "Advanced statistics"
|
||||
total_participants: "Participants"
|
||||
by_gender: "Participants by gender"
|
||||
by_age: "Participants by age"
|
||||
by_geozone: "Participants by district"
|
||||
men_percentage: "%{percentage} Men"
|
||||
women_percentage: "%{percentage} Women"
|
||||
age: "Age"
|
||||
age_more_than: "%{start} years old and older"
|
||||
age_range: "%{start} - %{finish} years old"
|
||||
total: "Total"
|
||||
geozone: "District"
|
||||
no_demographic_data:
|
||||
zero: ""
|
||||
one: "* There is no demographic data for 1 participant."
|
||||
other: "* There is no demographic data for %{count} participants."
|
||||
budgets:
|
||||
link: "Stats"
|
||||
page_title: "%{budget} - Participation stats"
|
||||
total_investments: "Total investment projects"
|
||||
total_selected_investments: "Projects in final voting phase"
|
||||
total_unfeasible_investments: "Unfeasible projects"
|
||||
by_phase: "Participants by phase"
|
||||
by_heading: "Participants by phase and heading"
|
||||
total: "Total"
|
||||
heading: "Heading"
|
||||
investments_sent_html: "Investment proposals sent"
|
||||
participants_support_phase: "Participants support phase"
|
||||
participants_vote_phase: "Participants voting phase"
|
||||
participants_every_phase: "Total participants"
|
||||
percent_total_participants: "% Total participants"
|
||||
percent_heading_census: "% Heading census"
|
||||
participatory_disclaimer: "** The numbers of total participants refer to persons that created, supported or voted investment proposals."
|
||||
heading_disclaimer: "*** Data about headings refer to the heading where each user voted, not necessarily the one that person is registered on."
|
||||
polls:
|
||||
by_channel: "Participants by channel"
|
||||
vote_by_channel: "Vote type by channel"
|
||||
web_percentage: "%{percentage} Web"
|
||||
booth_percentage: "%{percentage} Booths"
|
||||
letter_percentage: "%{percentage} Mail"
|
||||
@@ -191,35 +191,3 @@ es:
|
||||
dates_range_invalid: "La fecha de comienzo no puede ser igual o superior a la de finalización"
|
||||
prev_phase_dates_invalid: "La fecha de inicio debe ser posterior a la fecha de inicio de la anterior fase habilitada (%{phase_name})"
|
||||
next_phase_dates_invalid: "La fecha de fin debe ser anterior a la fecha de fin de la siguiente fase habilitada (%{phase_name})"
|
||||
stats:
|
||||
title: Estadísticas de participación
|
||||
link: Estadísticas
|
||||
page_title: "%{budget} - Estadísticas de participación"
|
||||
total_participants: Total participantes
|
||||
total_budget_investments: Total propuestas enviadas
|
||||
total_selected_investments: Propuestas en la fase final
|
||||
total_unfeasible_investments: Propuestas inviables
|
||||
total_participants_support_phase: Fase de apoyos
|
||||
total_participants_web: Participantes web en votación final
|
||||
total_participants_booths: Participantes en urnas votación final
|
||||
participants: Participantes
|
||||
supports: Apoyos
|
||||
total: Total
|
||||
by_gender: "Participación por género"
|
||||
total_male_participants: Hombres
|
||||
total_female_participants: Mujeres
|
||||
by_age: "Participación por grupos de edad"
|
||||
age: Edad
|
||||
more_than: Más de
|
||||
years: años
|
||||
by_heading: "Participación por distritos"
|
||||
heading: Distrito
|
||||
investments_sent_html: "Propuestas<br>enviadas"
|
||||
participants_support_phase: Participación fase apoyos
|
||||
participants_voting_phase: Participación fase votación
|
||||
participants_total: Participación total
|
||||
percent_total_participants_html: "% <br>Total<br>Participantes"
|
||||
percent_heading_census_html: "% <br>Censo<br>Distrito"
|
||||
no_demographic_data: "* No se dispone de los datos demográficos de %{total} participantes."
|
||||
participatory_disclaimer: "** Las cifras de participación total se refieren a personas que han creado, apoyado o votado propuestas."
|
||||
heading_disclaimer: "*** Los datos de distrito se refieren al distrito en el que el usuario ha votado, no necesariamente en el que está empadronado."
|
||||
|
||||
@@ -650,15 +650,14 @@ es:
|
||||
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"
|
||||
total_votes: "Nº total de votos emitidos"
|
||||
votes: "VOTOS"
|
||||
web: "WEB"
|
||||
booth: "URNAS"
|
||||
mail: "CORREO"
|
||||
total: "TOTAL"
|
||||
valid: "Válidos"
|
||||
white: "En blanco"
|
||||
white: "Totalmente en blanco"
|
||||
null_votes: "Nulos"
|
||||
results:
|
||||
title: "Preguntas"
|
||||
|
||||
43
config/locales/es/stats.yml
Normal file
@@ -0,0 +1,43 @@
|
||||
es:
|
||||
stats:
|
||||
title: "Estadísticas de participación"
|
||||
advanced: "Estadísticas avanzadas"
|
||||
total_participants: "Participantes"
|
||||
by_gender: "Participantes por género"
|
||||
by_age: "Participantes por grupos de edad"
|
||||
by_geozone: "Participantes por distritos"
|
||||
men_percentage: "%{percentage} Hombres"
|
||||
women_percentage: "%{percentage} Mujeres"
|
||||
age: "Edad"
|
||||
age_more_than: "De %{start} y más años"
|
||||
age_range: "De %{start} a %{finish} años"
|
||||
total: "Total"
|
||||
geozone: "Distrito"
|
||||
no_demographic_data:
|
||||
zero: ""
|
||||
one: "* No se dispone de los datos demográficos de 1 participante."
|
||||
other: "* No se dispone de los datos demográficos de %{count} participantes."
|
||||
budgets:
|
||||
link: "Estadísticas"
|
||||
page_title: "%{budget} - Estadísticas de participación"
|
||||
total_investments: "Total proyectos de gasto"
|
||||
total_selected_investments: "Proyectos en la fase final"
|
||||
total_unfeasible_investments: "Proyectos inviables"
|
||||
by_phase: "Participantes por fases"
|
||||
by_heading: "Participantes por distritos y fase"
|
||||
total: "Total"
|
||||
heading: "Distrito"
|
||||
investments_sent_html: "Propuestas<br>enviadas"
|
||||
participants_support_phase: Participantes fase apoyos
|
||||
participants_vote_phase: Participantes fase votación
|
||||
participants_every_phase: Total de participantes
|
||||
percent_total_participants: "% Total participantes"
|
||||
percent_heading_census: "% Censo distrito"
|
||||
participatory_disclaimer: "** Las cifras de total de participantes se refieren a personas que han creado, apoyado o votado propuestas."
|
||||
heading_disclaimer: "*** Los datos de distrito se refieren al distrito en el que el usuario ha votado, no necesariamente en el que está empadronado."
|
||||
polls:
|
||||
by_channel: "Participantes por medio"
|
||||
vote_by_channel: "Votos emitidos por medio"
|
||||
web_percentage: "%{percentage} Web"
|
||||
booth_percentage: "%{percentage} Urnas"
|
||||
letter_percentage: "%{percentage} Correo"
|
||||
@@ -27,6 +27,10 @@ every 1.day, at: "5:00 am" do
|
||||
rake "-s sitemap:refresh"
|
||||
end
|
||||
|
||||
every 2.hours do
|
||||
rake "-s stats:generate"
|
||||
end
|
||||
|
||||
# Temporally not send dashboard's notifications
|
||||
# every 1.day, at: "7:00 am" do
|
||||
# rake "dashboards:send_notifications"
|
||||
|
||||
8
db/migrate/20190408133956_create_stats_versions.rb
Normal file
@@ -0,0 +1,8 @@
|
||||
class CreateStatsVersions < ActiveRecord::Migration[4.2]
|
||||
def change
|
||||
create_table :stats_versions do |t|
|
||||
t.references :process, polymorphic: true, index: true
|
||||
t.timestamps null: false
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1334,6 +1334,14 @@ ActiveRecord::Schema.define(version: 20190411090023) do
|
||||
t.index ["tsv"], name: "index_spending_proposals_on_tsv", using: :gin
|
||||
end
|
||||
|
||||
create_table "stats_versions", force: :cascade do |t|
|
||||
t.string "process_type"
|
||||
t.integer "process_id"
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
t.index ["process_type", "process_id"], name: "index_stats_versions_on_process_type_and_process_id", using: :btree
|
||||
end
|
||||
|
||||
create_table "taggings", force: :cascade do |t|
|
||||
t.integer "tag_id"
|
||||
t.integer "taggable_id"
|
||||
|
||||
24
lib/geozone_stats.rb
Normal file
@@ -0,0 +1,24 @@
|
||||
class GeozoneStats
|
||||
attr_reader :geozone, :participants
|
||||
|
||||
def initialize(geozone, participants)
|
||||
@geozone = geozone
|
||||
@participants = participants
|
||||
end
|
||||
|
||||
def geozone_participants
|
||||
participants.where(geozone: geozone)
|
||||
end
|
||||
|
||||
def name
|
||||
geozone.name
|
||||
end
|
||||
|
||||
def count
|
||||
geozone_participants.count
|
||||
end
|
||||
|
||||
def percentage
|
||||
PercentageCalculator.calculate(count, participants.count)
|
||||
end
|
||||
end
|
||||
7
lib/percentage_calculator.rb
Normal file
@@ -0,0 +1,7 @@
|
||||
module PercentageCalculator
|
||||
def self.calculate(fraction, total)
|
||||
return 0.0 if total.zero?
|
||||
|
||||
(fraction * 100.0 / total).round(3)
|
||||
end
|
||||
end
|
||||
24
lib/tasks/stats.rake
Normal file
@@ -0,0 +1,24 @@
|
||||
namespace :stats do
|
||||
desc "Generates stats which are not cached yet"
|
||||
task generate: :environment do
|
||||
Budget.find_each do |budget|
|
||||
Budget::Stats.new(budget).generate
|
||||
print "."
|
||||
end
|
||||
|
||||
Poll.find_each do |poll|
|
||||
Poll::Stats.new(poll).generate
|
||||
print "."
|
||||
end
|
||||
end
|
||||
|
||||
desc "Expires stats cache"
|
||||
task expire_cache: :environment do
|
||||
[Budget, Poll].each do |model_class|
|
||||
model_class.find_each { |record| record.find_or_create_stats_version.touch }
|
||||
end
|
||||
end
|
||||
|
||||
desc "Deletes stats cache and generates it again"
|
||||
task regenerate: [:expire_cache, :generate]
|
||||
end
|
||||
@@ -74,13 +74,15 @@ FactoryBot.define do
|
||||
end
|
||||
|
||||
factory :budget_heading, class: "Budget::Heading" do
|
||||
association :group, factory: :budget_group
|
||||
sequence(:name) { |n| "Heading #{n}" }
|
||||
price 1000000
|
||||
population 1234
|
||||
latitude "40.416775"
|
||||
longitude "-3.703790"
|
||||
|
||||
transient { budget nil }
|
||||
group { association :budget_group, budget: budget || association(:budget) }
|
||||
|
||||
trait :drafting_budget do
|
||||
association :group, factory: [:budget_group, :drafting_budget]
|
||||
end
|
||||
@@ -88,7 +90,7 @@ FactoryBot.define do
|
||||
|
||||
factory :budget_investment, class: "Budget::Investment" do
|
||||
sequence(:title) { |n| "Budget Investment #{n} title" }
|
||||
association :heading, factory: :budget_heading
|
||||
heading { association :budget_heading, budget: budget }
|
||||
association :author, factory: :user
|
||||
description "Spend money on this"
|
||||
price 10
|
||||
@@ -189,8 +191,13 @@ FactoryBot.define do
|
||||
end
|
||||
|
||||
factory :budget_ballot_line, class: "Budget::Ballot::Line" do
|
||||
association :ballot, factory: :budget_ballot
|
||||
association :investment, factory: :budget_investment
|
||||
|
||||
transient { user nil }
|
||||
|
||||
ballot do
|
||||
association :budget_ballot, budget: investment.budget.reload, user: user || association(:user)
|
||||
end
|
||||
end
|
||||
|
||||
factory :budget_reclassified_vote, class: "Budget::ReclassifiedVote" do
|
||||
|
||||
@@ -88,10 +88,13 @@ FactoryBot.define do
|
||||
end
|
||||
|
||||
factory :poll_voter, class: "Poll::Voter" do
|
||||
poll
|
||||
association :user, :level_two
|
||||
from_web
|
||||
|
||||
transient { budget nil }
|
||||
|
||||
poll { budget&.poll || association(:poll, budget: budget) }
|
||||
|
||||
trait :from_web do
|
||||
origin "web"
|
||||
token SecureRandom.hex(32)
|
||||
|
||||
@@ -8,25 +8,16 @@ feature "Stats" do
|
||||
|
||||
describe "Show" do
|
||||
|
||||
it "is not accessible to normal users if phase is not 'finished'" do
|
||||
budget.update(phase: "reviewing_ballots")
|
||||
it "is not accessible if supports phase is not finished" do
|
||||
budget.update(phase: "selecting")
|
||||
|
||||
visit budget_stats_path(budget.id)
|
||||
expect(page).to have_content "You do not have permission to carry out the action "\
|
||||
"'read_stats' on budget."
|
||||
end
|
||||
|
||||
it "is accessible to normal users if phase is 'finished'" do
|
||||
budget.update(phase: "finished")
|
||||
|
||||
visit budget_stats_path(budget.id)
|
||||
expect(page).to have_content "Stats"
|
||||
end
|
||||
|
||||
it "is accessible to administrators when budget has phase 'reviewing_ballots'" do
|
||||
budget.update(phase: "reviewing_ballots")
|
||||
|
||||
login_as(create(:administrator).user)
|
||||
it "is accessible if supports phase is finished" do
|
||||
budget.update(phase: "valuating")
|
||||
|
||||
visit budget_stats_path(budget.id)
|
||||
expect(page).to have_content "Stats"
|
||||
|
||||
44
spec/lib/geozone_stats_spec.rb
Normal file
@@ -0,0 +1,44 @@
|
||||
require "rails_helper"
|
||||
|
||||
describe GeozoneStats do
|
||||
let(:winterfell) { create(:geozone, name: "Winterfell") }
|
||||
let(:riverlands) { create(:geozone, name: "Riverlands") }
|
||||
|
||||
describe "#name" do
|
||||
let(:stats) { GeozoneStats.new(winterfell, []) }
|
||||
|
||||
it "returns the geozone name" do
|
||||
expect(stats.name).to eq "Winterfell"
|
||||
end
|
||||
end
|
||||
|
||||
describe "#count" do
|
||||
before do
|
||||
2.times { create(:user, geozone: winterfell) }
|
||||
1.times { create(:user, geozone: riverlands) }
|
||||
end
|
||||
|
||||
let(:winterfell_stats) { GeozoneStats.new(winterfell, User.all) }
|
||||
let(:riverlands_stats) { GeozoneStats.new(riverlands, User.all) }
|
||||
|
||||
it "counts participants from the geozone" do
|
||||
expect(winterfell_stats.count).to eq 2
|
||||
expect(riverlands_stats.count).to eq 1
|
||||
end
|
||||
end
|
||||
|
||||
describe "#percentage" do
|
||||
before do
|
||||
2.times { create(:user, geozone: winterfell) }
|
||||
1.times { create(:user, geozone: riverlands) }
|
||||
end
|
||||
|
||||
let(:winterfell_stats) { GeozoneStats.new(winterfell, User.all) }
|
||||
let(:riverlands_stats) { GeozoneStats.new(riverlands, User.all) }
|
||||
|
||||
it "calculates percentage relative to the amount of participants" do
|
||||
expect(winterfell_stats.percentage).to eq 66.667
|
||||
expect(riverlands_stats.percentage).to eq 33.333
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -7,17 +7,8 @@ describe Budget::Phase do
|
||||
let(:second_phase) { budget.phases.informing }
|
||||
let(:third_phase) { budget.phases.accepting }
|
||||
let(:fourth_phase) { budget.phases.reviewing }
|
||||
let(:fifth_phase) { budget.phases.selecting }
|
||||
let(:final_phase) { budget.phases.finished}
|
||||
|
||||
before do
|
||||
first_phase.update_attributes(starts_at: Date.current - 3.days, ends_at: Date.current - 1.day)
|
||||
second_phase.update_attributes(starts_at: Date.current - 1.days, ends_at: Date.current + 1.day)
|
||||
third_phase.update_attributes(starts_at: Date.current + 1.days, ends_at: Date.current + 3.day)
|
||||
fourth_phase.update_attributes(starts_at: Date.current + 3.days, ends_at: Date.current + 5.day)
|
||||
fifth_phase.update_attributes(starts_at: Date.current + 5.days, ends_at: Date.current + 7.day)
|
||||
end
|
||||
|
||||
describe "validates" do
|
||||
it "is not valid without a budget" do
|
||||
expect(build(:budget_phase, budget: nil)).not_to be_valid
|
||||
|
||||
@@ -1,212 +1,287 @@
|
||||
require "rails_helper"
|
||||
|
||||
describe Budget::Stats do
|
||||
let(:budget) { create(:budget, :finished) }
|
||||
let(:stats) { Budget::Stats.new(budget) }
|
||||
let(:investment) { create(:budget_investment, :selected, budget: budget) }
|
||||
|
||||
before(:each) do
|
||||
@budget = create(:budget)
|
||||
@group = create(:budget_group, budget: @budget)
|
||||
@heading = create(:budget_heading, group: @group, price: 1000)
|
||||
describe "#participants" do
|
||||
let(:author) { investment.author }
|
||||
let(:author_and_voter) { create(:user, hidden_at: Time.current) }
|
||||
let(:voter) { create(:user) }
|
||||
let(:voter_and_balloter) { create(:user) }
|
||||
let(:balloter) { create(:user, hidden_at: Time.current) }
|
||||
let(:poll_balloter) { create(:user, :level_two) }
|
||||
let(:non_participant) { create(:user, :level_two) }
|
||||
|
||||
@investment1 = create(:budget_investment, :selected, author: create(:user, gender: "female"),
|
||||
heading: @heading, price: 200, ballot_lines_count: 900, winner: true)
|
||||
@investment2 = create(:budget_investment, :selected, author: create(:user, gender: "female"),
|
||||
heading: @heading, price: 300, ballot_lines_count: 800, winner: true)
|
||||
@investment3 = create(:budget_investment, :selected, author: create(:user, gender: "female",
|
||||
date_of_birth: 40.years.ago), heading: @heading, price: 400,
|
||||
ballot_lines_count: 880, winner: true)
|
||||
@investment4 = create(:budget_investment, :selected, author: create(:user, gender: "male"),
|
||||
heading: @heading, price: 100, ballot_lines_count: 915, winner: true)
|
||||
@investment5 = create(:budget_investment, :unfeasible, author: create(:user, gender: "male",
|
||||
date_of_birth: 25.years.ago), heading: @heading)
|
||||
before do
|
||||
create(:budget_investment, :selected, budget: budget, author: author_and_voter)
|
||||
|
||||
@support1 = create(:vote, votable: @investment1, voter: create(:user, gender: "male"))
|
||||
@support2 = create(:vote, votable: @investment2, voter: create(:user))
|
||||
create(:vote, votable: investment, voter: author_and_voter)
|
||||
create(:vote, votable: investment, voter: voter)
|
||||
create(:vote, votable: investment, voter: voter_and_balloter)
|
||||
|
||||
@budget_ballot1 = create(:budget_ballot, budget: @budget, user: create(:user, gender: "female",
|
||||
date_of_birth: 54.years.ago))
|
||||
@budget_ballot2 = create(:budget_ballot, budget: @budget, user: create(:user, gender: "female"))
|
||||
@budget_ballot3 = create(:budget_ballot, budget: @budget, user: create(:user, gender: "male"))
|
||||
create(:budget_ballot_line, investment: investment, user: balloter)
|
||||
create(:budget_ballot_line, investment: investment, user: voter_and_balloter)
|
||||
|
||||
@budget_ballot_line1 = create(:budget_ballot_line, ballot: @budget_ballot1,
|
||||
investment: @investment1)
|
||||
@budget_ballot_line2 = create(:budget_ballot_line, ballot: @budget_ballot2,
|
||||
investment: @investment2)
|
||||
@budget_ballot_line3 = create(:budget_ballot_line, ballot: @budget_ballot3,
|
||||
investment: @investment3)
|
||||
create(:poll_voter, :from_booth, user: poll_balloter, budget: budget)
|
||||
|
||||
@poll = create(:poll, budget: @budget)
|
||||
@poll_voter = create(:poll_voter, :from_booth, poll: @poll)
|
||||
|
||||
@budget_ballot4 = create(:budget_ballot, budget: @budget, physical: true, user: nil)
|
||||
@budget_ballot_line4 = create(:budget_ballot_line, ballot: @budget_ballot4,
|
||||
investment: @investment4)
|
||||
|
||||
@stats = Budget::Stats.new(@budget).generate
|
||||
end
|
||||
|
||||
context "#total_participants" do
|
||||
|
||||
it "returns the number of total participants" do
|
||||
expect(@stats[:total_participants]).to be 11
|
||||
create(:poll_voter, :from_booth, user: non_participant, budget: create(:budget))
|
||||
end
|
||||
|
||||
it "returns unique participants, including authors and hidden users" do
|
||||
expect(stats.participants).to match_array(
|
||||
[author, author_and_voter, voter, voter_and_balloter, balloter, poll_balloter]
|
||||
)
|
||||
expect(stats.total_participants).to be 6
|
||||
end
|
||||
end
|
||||
|
||||
context "#total_participants_support_phase" do
|
||||
|
||||
describe "#total_participants_support_phase" do
|
||||
it "returns the number of total participants in the support phase" do
|
||||
expect(@stats[:total_participants_support_phase]).to be 2
|
||||
2.times { create(:vote, votable: investment) }
|
||||
create(:budget_ballot_line, investment: investment)
|
||||
|
||||
expect(stats.total_participants_support_phase).to be 2
|
||||
end
|
||||
|
||||
it "counts a user who is voter and balloter" do
|
||||
voter_and_balloter = create(:user)
|
||||
create(:vote, votable: investment, voter: voter_and_balloter)
|
||||
create(:budget_ballot_line, investment: investment, user: voter_and_balloter)
|
||||
|
||||
expect(stats.total_participants_support_phase).to be 1
|
||||
end
|
||||
end
|
||||
|
||||
context "#total_participants_vote_phase" do
|
||||
|
||||
describe "#total_participants_vote_phase" do
|
||||
it "returns the number of total participants in the votes phase" do
|
||||
expect(@stats[:total_participants_vote_phase]).to be 4
|
||||
2.times { create(:budget_ballot_line, investment: investment) }
|
||||
create(:vote, votable: investment)
|
||||
|
||||
expect(stats.total_participants_vote_phase).to be 2
|
||||
end
|
||||
|
||||
end
|
||||
it "counts a user who is voter and balloter" do
|
||||
voter_and_balloter = create(:user)
|
||||
create(:vote, votable: investment, voter: voter_and_balloter)
|
||||
create(:budget_ballot_line, investment: investment, user: voter_and_balloter)
|
||||
|
||||
context "#total_participants_web" do
|
||||
|
||||
it "returns the number of total participants in the votes phase via web" do
|
||||
expect(@stats[:total_participants_web]).to be 3
|
||||
expect(stats.total_participants_vote_phase).to be 1
|
||||
end
|
||||
|
||||
end
|
||||
it "includes balloters and poll balloters" do
|
||||
create(:budget_ballot_line, investment: investment)
|
||||
create(:poll_voter, :from_booth, budget: budget)
|
||||
|
||||
context "#total_participants_booths" do
|
||||
|
||||
it "returns the number of total participants in the votes phase in booths" do
|
||||
expect(@stats[:total_participants_booths]).to be 1
|
||||
expect(stats.total_participants_vote_phase).to be 2
|
||||
end
|
||||
|
||||
it "counts once a user who is balloter and poll balloter" do
|
||||
poller_and_balloter = create(:user, :level_two)
|
||||
create(:budget_ballot_line, investment: investment, user: poller_and_balloter)
|
||||
create(:poll_voter, :from_booth, user: poller_and_balloter, budget: budget)
|
||||
|
||||
expect(stats.total_participants_vote_phase).to be 1
|
||||
end
|
||||
|
||||
it "doesn't count nil user ids" do
|
||||
create(:budget_ballot_line, investment: investment,
|
||||
ballot: create(:budget_ballot, budget: budget, user: nil, physical: true)
|
||||
)
|
||||
|
||||
expect(stats.total_participants_vote_phase).to be 0
|
||||
end
|
||||
end
|
||||
|
||||
context "#total_budget_investments" do
|
||||
|
||||
describe "#total_budget_investments" do
|
||||
it "returns the number of total budget investments" do
|
||||
expect(@stats[:total_budget_investments]).to be 5
|
||||
end
|
||||
2.times { create(:budget_investment, budget: budget) }
|
||||
create(:budget_investment, budget: create(:budget))
|
||||
|
||||
expect(stats.total_budget_investments).to be 2
|
||||
end
|
||||
end
|
||||
|
||||
context "#total_votes" do
|
||||
|
||||
describe "#total_votes" do
|
||||
it "returns the number of total votes" do
|
||||
expect(@stats[:total_votes]).to be 4
|
||||
end
|
||||
create(:budget_ballot_line, investment: investment)
|
||||
create(:budget_ballot_line, investment: create(:budget_investment, :selected, budget: budget))
|
||||
|
||||
expect(stats.total_votes).to be 2
|
||||
end
|
||||
end
|
||||
|
||||
context "#total_selected_investments" do
|
||||
|
||||
describe "#total_selected_investments" do
|
||||
it "returns the number of total selected investments" do
|
||||
expect(@stats[:total_selected_investments]).to be 4
|
||||
end
|
||||
3.times { create(:budget_investment, :selected, budget: budget) }
|
||||
create(:budget_investment, :selected, budget: create(:budget))
|
||||
create(:budget_investment, :unfeasible, budget: budget)
|
||||
|
||||
expect(stats.total_selected_investments).to be 3
|
||||
end
|
||||
end
|
||||
|
||||
context "#total_unfeasible_investments" do
|
||||
|
||||
describe "#total_unfeasible_investments" do
|
||||
it "returns the number of total unfeasible investments" do
|
||||
expect(@stats[:total_unfeasible_investments]).to be 1
|
||||
end
|
||||
3.times { create(:budget_investment, :unfeasible, budget: budget) }
|
||||
create(:budget_investment, :unfeasible, budget: create(:budget))
|
||||
create(:budget_investment, :selected, budget: budget)
|
||||
|
||||
expect(stats.total_unfeasible_investments).to be 3
|
||||
end
|
||||
end
|
||||
|
||||
context "#total_male_participants" do
|
||||
describe "Participants by gender" do
|
||||
before do
|
||||
3.times { create(:user, gender: "male") }
|
||||
2.times { create(:user, gender: "female") }
|
||||
create(:user, gender: nil)
|
||||
|
||||
it "returns the number of total male participants" do
|
||||
expect(@stats[:total_male_participants]).to be 4
|
||||
allow(stats).to receive(:participants).and_return(User.all)
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
context "#total_female_participants" do
|
||||
|
||||
it "returns the number of total female participants" do
|
||||
expect(@stats[:total_female_participants]).to be 6
|
||||
describe "#total_male_participants" do
|
||||
it "returns the number of total male participants" do
|
||||
expect(stats.total_male_participants).to be 3
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
context "#total_supports" do
|
||||
|
||||
it "returns the number of total supports" do
|
||||
expect(@stats[:total_supports]).to be 2
|
||||
describe "#total_female_participants" do
|
||||
it "returns the number of total female participants" do
|
||||
expect(stats.total_female_participants).to be 2
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
context "#total_unknown_gender_or_age" do
|
||||
|
||||
it "returns the number of total unknown participants' gender or age" do
|
||||
expect(@stats[:total_unknown_gender_or_age]).to be 1
|
||||
describe "#male_percentage" do
|
||||
it "returns the percentage of male participants" do
|
||||
expect(stats.male_percentage).to be 60.0
|
||||
end
|
||||
end
|
||||
|
||||
describe "#female_percentage" do
|
||||
it "returns the percentage of female participants" do
|
||||
expect(stats.female_percentage).to be 40.0
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "#age_groups" do
|
||||
describe "#participants_by_age" do
|
||||
before do
|
||||
[21, 22, 23, 23, 34, 42, 43, 44, 50, 51].each do |age|
|
||||
create(:user, date_of_birth: age.years.ago)
|
||||
end
|
||||
|
||||
allow(stats).to receive(:participants).and_return(User.all)
|
||||
end
|
||||
|
||||
it "returns the age groups hash" do
|
||||
expect(@stats[:age_groups]["16 - 19"]).to be 0
|
||||
expect(@stats[:age_groups]["20 - 24"]).to be 7
|
||||
expect(@stats[:age_groups]["25 - 29"]).to be 1
|
||||
expect(@stats[:age_groups]["30 - 34"]).to be 0
|
||||
expect(@stats[:age_groups]["35 - 39"]).to be 1
|
||||
expect(@stats[:age_groups]["40 - 44"]).to be 1
|
||||
expect(@stats[:age_groups]["45 - 49"]).to be 0
|
||||
expect(@stats[:age_groups]["50 - 54"]).to be 1
|
||||
expect(@stats[:age_groups]["55 - 59"]).to be 0
|
||||
expect(@stats[:age_groups]["60 - 64"]).to be 0
|
||||
expect(@stats[:age_groups]["65 - 69"]).to be 0
|
||||
expect(@stats[:age_groups]["70 - 140"]).to be 0
|
||||
expect(stats.participants_by_age["16 - 19"][:count]).to be 0
|
||||
expect(stats.participants_by_age["20 - 24"][:count]).to be 4
|
||||
expect(stats.participants_by_age["25 - 29"][:count]).to be 0
|
||||
expect(stats.participants_by_age["30 - 34"][:count]).to be 1
|
||||
expect(stats.participants_by_age["35 - 39"][:count]).to be 0
|
||||
expect(stats.participants_by_age["40 - 44"][:count]).to be 3
|
||||
expect(stats.participants_by_age["45 - 49"][:count]).to be 0
|
||||
expect(stats.participants_by_age["50 - 54"][:count]).to be 2
|
||||
expect(stats.participants_by_age["55 - 59"][:count]).to be 0
|
||||
expect(stats.participants_by_age["60 - 64"][:count]).to be 0
|
||||
expect(stats.participants_by_age["65 - 69"][:count]).to be 0
|
||||
expect(stats.participants_by_age["70 - 74"][:count]).to be 0
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
context "#male_percentage" do
|
||||
|
||||
it "returns the percentage of male participants" do
|
||||
expect(@stats[:male_percentage]).to be 40.0
|
||||
describe "#headings" do
|
||||
before do
|
||||
investment.heading.update_column(:population, 1234)
|
||||
create(:budget_investment, heading: investment.heading)
|
||||
2.times { create(:vote, votable: investment) }
|
||||
create(:budget_ballot_line, investment: investment)
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
context "#female_percentage" do
|
||||
|
||||
it "returns the percentage of female participants" do
|
||||
expect(@stats[:female_percentage]).to be 60.0
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
context "#headings" do
|
||||
|
||||
it "returns headings data" do
|
||||
heading_stats = @stats[:headings][@heading.id]
|
||||
expect(heading_stats[:total_investments_count]).to be 5
|
||||
heading_stats = stats.headings[investment.heading.id]
|
||||
expect(heading_stats[:total_investments_count]).to be 2
|
||||
expect(heading_stats[:total_participants_support_phase]).to be 2
|
||||
expect(heading_stats[:total_participants_vote_phase]).to be 4
|
||||
expect(heading_stats[:total_participants_all_phase]).to be 6
|
||||
expect(heading_stats[:total_participants_vote_phase]).to be 1
|
||||
expect(heading_stats[:total_participants_every_phase]).to be 3
|
||||
expect(heading_stats[:percentage_participants_support_phase]).to be 100.0
|
||||
expect(heading_stats[:percentage_district_population_support_phase]).to be 0.162
|
||||
expect(heading_stats[:percentage_participants_vote_phase]).to be 100.0
|
||||
expect(heading_stats[:percentage_district_population_vote_phase]).to be 0.324
|
||||
expect(heading_stats[:percentage_participants_all_phase]).to be 100.0
|
||||
expect(heading_stats[:percentage_district_population_all_phase]).to be 0.486
|
||||
|
||||
expect(heading_stats[:total_investments_count]).to be 5
|
||||
expect(heading_stats[:total_participants_support_phase]).to be 2
|
||||
expect(heading_stats[:total_participants_vote_phase]).to be 4
|
||||
expect(heading_stats[:total_participants_all_phase]).to be 6
|
||||
expect(heading_stats[:percentage_participants_support_phase]).to be 100.0
|
||||
expect(heading_stats[:percentage_participants_vote_phase]).to be 100.0
|
||||
expect(heading_stats[:percentage_participants_all_phase]).to be 100.0
|
||||
expect(heading_stats[:percentage_district_population_vote_phase]).to be 0.081
|
||||
expect(heading_stats[:percentage_participants_every_phase]).to be 100.0
|
||||
expect(heading_stats[:percentage_district_population_every_phase]).to be 0.243
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
describe "#support_phase_finished?" do
|
||||
context "support phase isn't finished" do
|
||||
before { budget.phase = "selecting" }
|
||||
|
||||
it "is false" do
|
||||
expect(stats.support_phase_finished?).to be false
|
||||
end
|
||||
end
|
||||
|
||||
context "support phase is finished" do
|
||||
before { budget.phase = "valuating" }
|
||||
|
||||
it "is false" do
|
||||
expect(stats.support_phase_finished?).to be true
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "#vote_phase_finished" do
|
||||
context "support phase isn't finished" do
|
||||
before { budget.phase = "reviewing_ballots" }
|
||||
|
||||
it "is false" do
|
||||
expect(stats.vote_phase_finished?).to be false
|
||||
end
|
||||
end
|
||||
|
||||
context "vote phase is finished" do
|
||||
before { budget.phase = "finished" }
|
||||
|
||||
it "is false" do
|
||||
expect(stats.vote_phase_finished?).to be true
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "#all_phases" do
|
||||
context "no phases are finished" do
|
||||
before do
|
||||
allow(stats).to receive(:support_phase_finished?).and_return(false)
|
||||
allow(stats).to receive(:vote_phase_finished?).and_return(false)
|
||||
end
|
||||
|
||||
it "returns an empty array" do
|
||||
expect(stats.all_phases).to eq []
|
||||
end
|
||||
end
|
||||
|
||||
context "one phase is finished" do
|
||||
before do
|
||||
allow(stats).to receive(:support_phase_finished?).and_return(true)
|
||||
allow(stats).to receive(:vote_phase_finished?).and_return(false)
|
||||
end
|
||||
|
||||
it "returns the finished phase" do
|
||||
expect(stats.all_phases).to eq ["support"]
|
||||
end
|
||||
end
|
||||
|
||||
context "all phases are finished" do
|
||||
before do
|
||||
allow(stats).to receive(:support_phase_finished?).and_return(true)
|
||||
allow(stats).to receive(:vote_phase_finished?).and_return(true)
|
||||
end
|
||||
|
||||
it "returns the finished phases and a total phase" do
|
||||
expect(stats.all_phases).to eq ["support", "vote", "every"]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -97,33 +97,55 @@ describe Budget do
|
||||
expect(budget).to be_finished
|
||||
end
|
||||
|
||||
it "balloting_or_later?" do
|
||||
budget.phase = "drafting"
|
||||
expect(budget).not_to be_balloting_or_later
|
||||
describe "#valuating_or_later?" do
|
||||
it "returns false before valuating" do
|
||||
budget.phase = "selecting"
|
||||
expect(budget).not_to be_valuating_or_later
|
||||
end
|
||||
|
||||
budget.phase = "accepting"
|
||||
expect(budget).not_to be_balloting_or_later
|
||||
it "returns true while valuating" do
|
||||
budget.phase = "valuating"
|
||||
expect(budget).to be_valuating_or_later
|
||||
end
|
||||
|
||||
budget.phase = "reviewing"
|
||||
expect(budget).not_to be_balloting_or_later
|
||||
it "returns true after valuating" do
|
||||
budget.phase = "publishing_prices"
|
||||
expect(budget).to be_valuating_or_later
|
||||
end
|
||||
end
|
||||
|
||||
budget.phase = "selecting"
|
||||
expect(budget).not_to be_balloting_or_later
|
||||
describe "#publishing_prices_or_later?" do
|
||||
it "returns false before publishing prices" do
|
||||
budget.phase = "valuating"
|
||||
expect(budget).not_to be_publishing_prices_or_later
|
||||
end
|
||||
|
||||
budget.phase = "valuating"
|
||||
expect(budget).not_to be_balloting_or_later
|
||||
it "returns true while publishing prices" do
|
||||
budget.phase = "publishing_prices"
|
||||
expect(budget).to be_publishing_prices_or_later
|
||||
end
|
||||
|
||||
budget.phase = "publishing_prices"
|
||||
expect(budget).not_to be_balloting_or_later
|
||||
it "returns true after publishing prices" do
|
||||
budget.phase = "balloting"
|
||||
expect(budget).to be_publishing_prices_or_later
|
||||
end
|
||||
end
|
||||
|
||||
budget.phase = "balloting"
|
||||
expect(budget).to be_balloting_or_later
|
||||
describe "#balloting_or_later?" do
|
||||
it "returns false before balloting" do
|
||||
budget.phase = "publishing_prices"
|
||||
expect(budget).not_to be_balloting_or_later
|
||||
end
|
||||
|
||||
budget.phase = "reviewing_ballots"
|
||||
expect(budget).to be_balloting_or_later
|
||||
it "returns true while balloting" do
|
||||
budget.phase = "balloting"
|
||||
expect(budget).to be_balloting_or_later
|
||||
end
|
||||
|
||||
budget.phase = "finished"
|
||||
expect(budget).to be_balloting_or_later
|
||||
it "returns true after balloting" do
|
||||
budget.phase = "finished"
|
||||
expect(budget).to be_balloting_or_later
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -1,44 +1,288 @@
|
||||
require "rails_helper"
|
||||
|
||||
describe Poll::Stats do
|
||||
let(:poll) { create(:poll) }
|
||||
let(:stats) { Poll::Stats.new(poll) }
|
||||
|
||||
describe "#generate" do
|
||||
it "generates the correct stats" do
|
||||
poll = create(:poll)
|
||||
2.times { create(:poll_voter, :from_web, poll: poll) }
|
||||
3.times { create(:poll_voter, :from_booth, poll: poll) }
|
||||
create(:poll_recount, :from_booth, poll: poll,
|
||||
white_amount: 1, null_amount: 0, total_amount: 2)
|
||||
describe "#participants" do
|
||||
it "includes hidden users" do
|
||||
create(:poll_voter, poll: poll)
|
||||
create(:poll_voter, poll: poll, user: create(:user, :level_two, hidden_at: Time.current))
|
||||
|
||||
stats = Poll::Stats.new(poll).generate
|
||||
|
||||
expect(stats[:total_participants]).to eq(5)
|
||||
expect(stats[:total_participants_web]).to eq(2)
|
||||
expect(stats[:total_participants_booth]).to eq(3)
|
||||
expect(stats[:total_valid_votes]).to eq(4)
|
||||
expect(stats[:total_white_votes]).to eq(1)
|
||||
expect(stats[:total_null_votes]).to eq(0)
|
||||
|
||||
expect(stats[:total_web_valid]).to eq(2)
|
||||
expect(stats[:total_web_white]).to eq(0)
|
||||
expect(stats[:total_web_null]).to eq(0)
|
||||
|
||||
expect(stats[:total_booth_valid]).to eq(2)
|
||||
expect(stats[:total_booth_white]).to eq(1)
|
||||
expect(stats[:total_booth_null]).to eq(0)
|
||||
|
||||
expect(stats[:total_participants_web_percentage]).to eq(40)
|
||||
expect(stats[:total_participants_booth_percentage]).to eq(60)
|
||||
expect(stats[:valid_percentage_web]).to eq(50)
|
||||
expect(stats[:white_percentage_web]).to eq(0)
|
||||
expect(stats[:null_percentage_web]).to eq(0)
|
||||
expect(stats[:valid_percentage_booth]).to eq(50)
|
||||
expect(stats[:white_percentage_booth]).to eq(100)
|
||||
expect(stats[:null_percentage_booth]).to eq(0)
|
||||
expect(stats[:total_valid_percentage]).to eq(80)
|
||||
expect(stats[:total_white_percentage]).to eq(20)
|
||||
expect(stats[:total_null_percentage]).to eq(0)
|
||||
expect(stats.participants.count).to eq(2)
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
describe "total participants" do
|
||||
before { allow(stats).to receive(:total_web_white).and_return(1) }
|
||||
|
||||
it "supports every channel" do
|
||||
3.times { create(:poll_voter, :from_web, poll: poll) }
|
||||
create(:poll_recount, :from_booth, poll: poll,
|
||||
total_amount: 8, white_amount: 4, null_amount: 1)
|
||||
|
||||
expect(stats.total_participants_web).to eq(3)
|
||||
expect(stats.total_participants_booth).to eq(13)
|
||||
expect(stats.total_participants).to eq(16)
|
||||
end
|
||||
end
|
||||
|
||||
describe "#total_participants_booth" do
|
||||
it "uses recounts even if there are discrepancies when recounting" do
|
||||
create(:poll_recount, :from_booth, poll: poll, total_amount: 1)
|
||||
2.times { create(:poll_voter, :from_booth, poll: poll) }
|
||||
|
||||
expect(stats.total_participants_booth).to eq(1)
|
||||
end
|
||||
end
|
||||
|
||||
describe "total participants percentage by channel" do
|
||||
it "is relative to the total amount of participants" do
|
||||
create(:poll_voter, :from_web, poll: poll)
|
||||
create(:poll_recount, :from_booth, poll: poll, total_amount: 5)
|
||||
|
||||
expect(stats.total_participants_web_percentage).to eq(16.667)
|
||||
expect(stats.total_participants_booth_percentage).to eq(83.333)
|
||||
end
|
||||
end
|
||||
|
||||
describe "#total_web_valid" do
|
||||
before { allow(stats).to receive(:total_web_white).and_return(1) }
|
||||
|
||||
it "returns only valid votes" do
|
||||
3.times { create(:poll_voter, :from_web, poll: poll) }
|
||||
|
||||
expect(stats.total_web_valid).to eq(2)
|
||||
end
|
||||
end
|
||||
|
||||
describe "#total_web_white" do
|
||||
pending "Too complex to test"
|
||||
end
|
||||
|
||||
describe "#total_web_null" do
|
||||
it "returns 0" do
|
||||
expect(stats.total_web_null).to eq(0)
|
||||
end
|
||||
end
|
||||
|
||||
describe "#total_booth_valid" do
|
||||
it "sums the total amounts in the recounts" do
|
||||
create(:poll_recount, :from_booth, poll: poll, total_amount: 3, white_amount: 1)
|
||||
create(:poll_recount, :from_booth, poll: poll, total_amount: 4, null_amount: 2)
|
||||
|
||||
expect(stats.total_booth_valid).to eq(7)
|
||||
end
|
||||
end
|
||||
|
||||
describe "#total_booth_white" do
|
||||
it "sums the white amounts in the recounts" do
|
||||
create(:poll_recount, :from_booth, poll: poll, white_amount: 120, total_amount: 3)
|
||||
create(:poll_recount, :from_booth, poll: poll, white_amount: 203, null_amount: 5)
|
||||
|
||||
expect(stats.total_booth_white).to eq(323)
|
||||
end
|
||||
end
|
||||
|
||||
describe "#total_booth_null" do
|
||||
it "sums the null amounts in the recounts" do
|
||||
create(:poll_recount, :from_booth, poll: poll, null_amount: 125, total_amount: 3)
|
||||
create(:poll_recount, :from_booth, poll: poll, null_amount: 34, white_amount: 5)
|
||||
|
||||
expect(stats.total_booth_null).to eq(159)
|
||||
end
|
||||
end
|
||||
|
||||
describe "valid percentage by channel" do
|
||||
it "is relative to the total amount of valid votes" do
|
||||
create(:poll_recount, :from_booth, poll: poll, total_amount: 2)
|
||||
create(:poll_voter, :from_web, poll: poll)
|
||||
|
||||
expect(stats.valid_percentage_web).to eq(33.333)
|
||||
expect(stats.valid_percentage_booth).to eq(66.667)
|
||||
end
|
||||
end
|
||||
|
||||
describe "white percentage by channel" do
|
||||
before { allow(stats).to receive(:total_web_white).and_return(10) }
|
||||
|
||||
it "is relative to the total amount of white votes" do
|
||||
create(:poll_recount, :from_booth, poll: poll, white_amount: 70)
|
||||
|
||||
expect(stats.white_percentage_web).to eq(12.5)
|
||||
expect(stats.white_percentage_booth).to eq(87.5)
|
||||
end
|
||||
end
|
||||
|
||||
describe "null percentage by channel" do
|
||||
it "only accepts null votes from booth" do
|
||||
create(:poll_recount, :from_booth, poll: poll, null_amount: 70)
|
||||
|
||||
expect(stats.null_percentage_web).to eq(0)
|
||||
expect(stats.null_percentage_booth).to eq(100)
|
||||
end
|
||||
end
|
||||
|
||||
describe "#total_valid_votes" do
|
||||
it "counts valid votes from every channel" do
|
||||
2.times { create(:poll_voter, :from_web, poll: poll) }
|
||||
create(:poll_recount, :from_booth, poll: poll, total_amount: 3, white_amount: 10)
|
||||
create(:poll_recount, :from_booth, poll: poll, total_amount: 4, null_amount: 20)
|
||||
|
||||
expect(stats.total_valid_votes).to eq(9)
|
||||
end
|
||||
end
|
||||
|
||||
describe "#total_white_votes" do
|
||||
before { allow(stats).to receive(:total_web_white).and_return(9) }
|
||||
|
||||
it "counts white votes on every channel" do
|
||||
create(:poll_recount, :from_booth, poll: poll, white_amount: 12)
|
||||
|
||||
expect(stats.total_white_votes).to eq(21)
|
||||
end
|
||||
end
|
||||
|
||||
describe "#total_null_votes" do
|
||||
it "only accepts null votes from booth" do
|
||||
create(:poll_recount, :from_booth, poll: poll, null_amount: 32)
|
||||
|
||||
expect(stats.total_null_votes).to eq(32)
|
||||
end
|
||||
end
|
||||
|
||||
describe "total percentage by type" do
|
||||
before { allow(stats).to receive(:total_web_white).and_return(1) }
|
||||
|
||||
it "is relative to the total amount of votes" do
|
||||
3.times { create(:poll_voter, :from_web, poll: poll) }
|
||||
create(:poll_recount, :from_booth, poll: poll,
|
||||
total_amount: 8, white_amount: 5, null_amount: 4)
|
||||
|
||||
expect(stats.total_valid_percentage).to eq(50)
|
||||
expect(stats.total_white_percentage).to eq(30)
|
||||
expect(stats.total_null_percentage).to eq(20)
|
||||
end
|
||||
end
|
||||
|
||||
describe "#participants_by_geozone" do
|
||||
it "groups by geozones in alphabetic order" do
|
||||
%w[Oceania Eurasia Eastasia].each { |name| create(:geozone, name: name) }
|
||||
|
||||
expect(stats.participants_by_geozone.keys).to eq %w[Eastasia Eurasia Oceania]
|
||||
end
|
||||
|
||||
it "calculates percentage relative to total participants" do
|
||||
hobbiton = create(:geozone, name: "Hobbiton")
|
||||
rivendel = create(:geozone, name: "Rivendel")
|
||||
|
||||
3.times { create :poll_voter, poll: poll, user: create(:user, :level_two, geozone: hobbiton) }
|
||||
2.times { create :poll_voter, poll: poll, user: create(:user, :level_two, geozone: rivendel) }
|
||||
|
||||
expect(stats.participants_by_geozone["Hobbiton"][:count]).to eq 3
|
||||
expect(stats.participants_by_geozone["Hobbiton"][:percentage]).to eq 60.0
|
||||
expect(stats.participants_by_geozone["Rivendel"][:count]).to eq 2
|
||||
expect(stats.participants_by_geozone["Rivendel"][:percentage]).to eq 40.0
|
||||
end
|
||||
end
|
||||
|
||||
describe "#total_no_demographic_data" do
|
||||
before do
|
||||
create(:poll_voter, :from_web, poll: poll, user: create(:user, :level_two, gender: nil))
|
||||
end
|
||||
|
||||
context "more registered participants than participants in recounts" do
|
||||
before do
|
||||
create(:poll_recount, :from_booth, poll: poll, total_amount: 1)
|
||||
2.times { create(:poll_voter, :from_booth, poll: poll) }
|
||||
end
|
||||
|
||||
it "returns registered users with no demographic data" do
|
||||
expect(stats.total_no_demographic_data).to eq 1
|
||||
end
|
||||
end
|
||||
|
||||
context "more participants in recounts than registered participants" do
|
||||
before do
|
||||
create(:poll_recount, :from_booth, poll: poll, total_amount: 3)
|
||||
2.times { create(:poll_voter, :from_booth, poll: poll) }
|
||||
end
|
||||
|
||||
it "returns registered users with no demographic data plus users not registered" do
|
||||
expect(stats.total_no_demographic_data).to eq 2
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "#channels" do
|
||||
context "no participants" do
|
||||
it "returns no channels" do
|
||||
expect(stats.channels).to eq []
|
||||
end
|
||||
end
|
||||
|
||||
context "only participants from web" do
|
||||
before { create(:poll_voter, :from_web, poll: poll) }
|
||||
|
||||
it "returns the web channel" do
|
||||
expect(stats.channels).to eq ["web"]
|
||||
end
|
||||
end
|
||||
|
||||
context "only participants from booth" do
|
||||
before do
|
||||
create(:poll_recount, :from_booth, poll: poll, total_amount: 1)
|
||||
end
|
||||
|
||||
it "returns the booth channel" do
|
||||
expect(stats.channels).to eq ["booth"]
|
||||
end
|
||||
end
|
||||
|
||||
context "only participants from letter" do
|
||||
before { create(:poll_voter, origin: "letter", poll: poll) }
|
||||
|
||||
it "returns the web channel" do
|
||||
expect(stats.channels).to eq ["letter"]
|
||||
end
|
||||
end
|
||||
|
||||
context "participants from all channels" do
|
||||
before do
|
||||
create(:poll_voter, :from_web, poll: poll)
|
||||
create(:poll_recount, :from_booth, poll: poll, total_amount: 1)
|
||||
create(:poll_voter, origin: "letter", poll: poll)
|
||||
end
|
||||
|
||||
it "returns all channels" do
|
||||
expect(stats.channels).to eq %w[web booth letter]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "#version", :with_frozen_time do
|
||||
context "record with no stats" do
|
||||
it "returns a string based on the current time" do
|
||||
expect(stats.version).to eq "v#{Time.current.to_i}"
|
||||
end
|
||||
|
||||
it "doesn't overwrite the timestamp when called multiple times" do
|
||||
time = Time.current
|
||||
|
||||
expect(stats.version).to eq "v#{time.to_i}"
|
||||
|
||||
travel_to 2.seconds.from_now do
|
||||
expect(stats.version).to eq "v#{time.to_i}"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "record with stats" do
|
||||
before { poll.create_stats_version(updated_at: 1.day.ago) }
|
||||
|
||||
it "returns the version of the existing stats" do
|
||||
expect(stats.version).to eq "v#{1.day.ago.to_i}"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
180
spec/models/statisticable_spec.rb
Normal file
@@ -0,0 +1,180 @@
|
||||
require "rails_helper"
|
||||
|
||||
describe Statisticable do
|
||||
class DummyStats
|
||||
include Statisticable
|
||||
|
||||
def participants
|
||||
User.all
|
||||
end
|
||||
end
|
||||
|
||||
let(:stats) { DummyStats.new(nil) }
|
||||
|
||||
describe "#gender?" do
|
||||
context "No participants" do
|
||||
it "is false" do
|
||||
expect(stats.gender?).to be false
|
||||
end
|
||||
end
|
||||
|
||||
context "All participants have no defined gender" do
|
||||
before { create(:user, gender: nil) }
|
||||
|
||||
it "is false" do
|
||||
expect(stats.gender?).to be false
|
||||
end
|
||||
end
|
||||
|
||||
context "There's a male participant" do
|
||||
before { create(:user, gender: "male") }
|
||||
|
||||
it "is true" do
|
||||
expect(stats.gender?).to be true
|
||||
end
|
||||
end
|
||||
|
||||
context "There's a female participant" do
|
||||
before { create(:user, gender: "female") }
|
||||
|
||||
it "is true" do
|
||||
expect(stats.gender?).to be true
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "#age?" do
|
||||
context "No participants" do
|
||||
it "is false" do
|
||||
expect(stats.age?).to be false
|
||||
end
|
||||
end
|
||||
|
||||
context "All participants have no defined age" do
|
||||
before { create(:user, date_of_birth: nil) }
|
||||
|
||||
it "is false" do
|
||||
expect(stats.age?).to be false
|
||||
end
|
||||
end
|
||||
|
||||
context "All participants have impossible ages" do
|
||||
before do
|
||||
create(:user, date_of_birth: 3.seconds.ago)
|
||||
create(:user, date_of_birth: 3000.years.ago)
|
||||
end
|
||||
|
||||
it "is false" do
|
||||
expect(stats.age?).to be false
|
||||
end
|
||||
end
|
||||
|
||||
context "There's a participant with a defined age" do
|
||||
before { create(:user, date_of_birth: 30.years.ago) }
|
||||
|
||||
it "is true" do
|
||||
expect(stats.age?).to be true
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "#geozone?" do
|
||||
context "No participants" do
|
||||
it "is false" do
|
||||
expect(stats.geozone?).to be false
|
||||
end
|
||||
end
|
||||
|
||||
context "All participants have no defined geozone" do
|
||||
before { create(:user, geozone: nil) }
|
||||
|
||||
it "is false" do
|
||||
expect(stats.geozone?).to be false
|
||||
end
|
||||
end
|
||||
|
||||
context "There's a participant with a defined geozone" do
|
||||
before { create(:user, geozone: create(:geozone)) }
|
||||
|
||||
it "is true" do
|
||||
expect(stats.geozone?).to be true
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "#total_no_demographic_data" do
|
||||
it "returns users with no defined gender" do
|
||||
create(:user, gender: nil)
|
||||
|
||||
expect(stats.total_no_demographic_data).to be 1
|
||||
end
|
||||
|
||||
it "returns users with no defined age" do
|
||||
create(:user, gender: "female", date_of_birth: nil)
|
||||
|
||||
expect(stats.total_no_demographic_data).to be 1
|
||||
end
|
||||
|
||||
it "returns users with no defined geozone" do
|
||||
create(:user, gender: "female", geozone: nil)
|
||||
|
||||
expect(stats.total_no_demographic_data).to be 1
|
||||
end
|
||||
|
||||
it "returns users with no defined gender, age nor geozone" do
|
||||
create(:user, gender: nil, date_of_birth: nil, geozone: nil)
|
||||
|
||||
expect(stats.total_no_demographic_data).to be 1
|
||||
end
|
||||
|
||||
it "doesn't return users with defined gender, age and geozone" do
|
||||
create(:user, gender: "male", date_of_birth: 20.years.ago, geozone: create(:geozone))
|
||||
|
||||
expect(stats.total_no_demographic_data).to be 0
|
||||
end
|
||||
end
|
||||
|
||||
describe "#stats_methods" do
|
||||
it "includes total participants" do
|
||||
expect(stats.stats_methods).to include(:total_participants)
|
||||
end
|
||||
|
||||
context "no gender stats" do
|
||||
before { allow(stats).to receive(:gender?).and_return(false) }
|
||||
|
||||
it "doesn't include gender methods" do
|
||||
expect(stats.stats_methods).not_to include(:total_male_participants)
|
||||
end
|
||||
end
|
||||
|
||||
context "no age stats" do
|
||||
before { allow(stats).to receive(:age?).and_return(false) }
|
||||
|
||||
it "doesn't include age methods" do
|
||||
expect(stats.stats_methods).not_to include(:participants_by_age)
|
||||
end
|
||||
end
|
||||
|
||||
context "no geozone stats" do
|
||||
before { allow(stats).to receive(:geozone?).and_return(false) }
|
||||
|
||||
it "doesn't include age methods" do
|
||||
expect(stats.stats_methods).not_to include(:participants_by_geozone)
|
||||
end
|
||||
end
|
||||
|
||||
context "all gender, age and geozone stats" do
|
||||
before do
|
||||
allow(stats).to receive(:gender?).and_return(true)
|
||||
allow(stats).to receive(:age?).and_return(true)
|
||||
allow(stats).to receive(:geozone?).and_return(true)
|
||||
end
|
||||
|
||||
it "includes all stats methods" do
|
||||
expect(stats.stats_methods).to include(:total_male_participants)
|
||||
expect(stats.stats_methods).to include(:participants_by_age)
|
||||
expect(stats.stats_methods).to include(:participants_by_geozone)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
13
spec/models/stats_version_spec.rb
Normal file
@@ -0,0 +1,13 @@
|
||||
require "rails_helper"
|
||||
|
||||
describe StatsVersion do
|
||||
describe "validations" do
|
||||
it "is valid with a process" do
|
||||
expect(StatsVersion.new(process: Budget.new)).to be_valid
|
||||
end
|
||||
|
||||
it "is not valid without a process" do
|
||||
expect(StatsVersion.new(process: nil)).not_to be_valid
|
||||
end
|
||||
end
|
||||
end
|
||||