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 app
|
||||||
//= require check_all_none
|
//= require check_all_none
|
||||||
//= require comments
|
//= require comments
|
||||||
//= require dropdown
|
//= require foundation_extras
|
||||||
//= require ie_alert
|
//= require ie_alert
|
||||||
//= require location_changer
|
//= require location_changer
|
||||||
//= require moderator_comment
|
//= require moderator_comment
|
||||||
@@ -93,7 +93,7 @@ var initialize_modules = function() {
|
|||||||
App.Votes.initialize();
|
App.Votes.initialize();
|
||||||
App.AllowParticipation.initialize();
|
App.AllowParticipation.initialize();
|
||||||
App.Tags.initialize();
|
App.Tags.initialize();
|
||||||
App.Dropdown.initialize();
|
App.FoundationExtras.initialize();
|
||||||
App.LocationChanger.initialize();
|
App.LocationChanger.initialize();
|
||||||
App.CheckAllNone.initialize();
|
App.CheckAllNone.initialize();
|
||||||
App.PreventDoubleSubmission.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;
|
$orbit-bullet-diameter: 0.8rem;
|
||||||
|
|
||||||
$pagination-radius: $global-radius;
|
$pagination-radius: $global-radius;
|
||||||
|
|
||||||
|
$show-header-for-stacked: true;
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
@import "legislation";
|
@import "legislation";
|
||||||
@import "legislation_process";
|
@import "legislation_process";
|
||||||
@import "community";
|
@import "community";
|
||||||
|
@import "stats";
|
||||||
@import "custom";
|
@import "custom";
|
||||||
@import "c3";
|
@import "c3";
|
||||||
@import "annotator.min";
|
@import "annotator.min";
|
||||||
|
|||||||
@@ -1988,15 +1988,8 @@
|
|||||||
.polls-results-stats {
|
.polls-results-stats {
|
||||||
|
|
||||||
.sidebar {
|
.sidebar {
|
||||||
border-bottom: 1px solid $border;
|
|
||||||
margin-bottom: $line-height;
|
|
||||||
|
|
||||||
@include breakpoint(medium) {
|
.menu:not(.vertical) {
|
||||||
border-bottom: 0;
|
|
||||||
border-right: 1px solid $border;
|
|
||||||
}
|
|
||||||
|
|
||||||
.menu {
|
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
|
||||||
li a {
|
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
|
def show
|
||||||
authorize! :read_stats, @budget
|
authorize! :read_stats, @budget
|
||||||
@stats = load_stats
|
@stats = Budget::Stats.new(@budget)
|
||||||
@headings = @budget.headings.sort_by_name
|
@headings = @budget.headings.sort_by_name
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def load_stats
|
|
||||||
Budget::Stats.new(@budget).generate
|
|
||||||
end
|
|
||||||
|
|
||||||
def load_budget
|
def load_budget
|
||||||
@budget = Budget.find_by(slug: params[:budget_id]) || Budget.find_by(id: params[:budget_id])
|
@budget = Budget.find_by(slug: params[:budget_id]) || Budget.find_by(id: params[:budget_id])
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ class PollsController < ApplicationController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def stats
|
def stats
|
||||||
@stats = Poll::Stats.new(@poll).generate
|
@stats = Poll::Stats.new(@poll)
|
||||||
end
|
end
|
||||||
|
|
||||||
def results
|
def results
|
||||||
|
|||||||
@@ -30,4 +30,16 @@ module StatsHelper
|
|||||||
opt[:data][:graph] = admin_api_stats_path(budget_investments: true)
|
opt[:data][:graph] = admin_api_stats_path(budget_investments: true)
|
||||||
content_tag :div, "", opt
|
content_tag :div, "", opt
|
||||||
end
|
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
|
end
|
||||||
|
|||||||
@@ -66,7 +66,6 @@ module Abilities
|
|||||||
can [:hide, :update, :toggle_selection], Budget::Investment
|
can [:hide, :update, :toggle_selection], Budget::Investment
|
||||||
can [:valuate, :comment_valuation], Budget::Investment
|
can [:valuate, :comment_valuation], Budget::Investment
|
||||||
can :create, Budget::ValuatorAssignment
|
can :create, Budget::ValuatorAssignment
|
||||||
can :read_stats, Budget, phase: "reviewing_ballots"
|
|
||||||
|
|
||||||
can [:search, :edit, :update, :create, :index, :destroy], Banner
|
can [:search, :edit, :update, :create, :index, :destroy], Banner
|
||||||
|
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ module Abilities
|
|||||||
can [:read], Budget::Group
|
can [:read], Budget::Group
|
||||||
can [:read, :print, :json_data], Budget::Investment
|
can [:read, :print, :json_data], Budget::Investment
|
||||||
can [:read_results, :read_executions], Budget, phase: "finished"
|
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 :new, DirectMessage
|
||||||
can [:read, :debate, :draft_publication, :allegations, :result_publication,
|
can [:read, :debate, :draft_publication, :allegations, :result_publication,
|
||||||
:proposals, :milestones], Legislation::Process, published: true
|
:proposals, :milestones], Legislation::Process, published: true
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ class Budget < ApplicationRecord
|
|||||||
|
|
||||||
include Measurable
|
include Measurable
|
||||||
include Sluggable
|
include Sluggable
|
||||||
|
include StatsVersionable
|
||||||
|
|
||||||
translates :name, touch: true
|
translates :name, touch: true
|
||||||
include Globalizable
|
include Globalizable
|
||||||
@@ -125,8 +126,12 @@ class Budget < ApplicationRecord
|
|||||||
Budget::Phase::PUBLISHED_PRICES_PHASES.include?(phase)
|
Budget::Phase::PUBLISHED_PRICES_PHASES.include?(phase)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def valuating_or_later?
|
||||||
|
current_phase&.valuating_or_later?
|
||||||
|
end
|
||||||
|
|
||||||
def publishing_prices_or_later?
|
def publishing_prices_or_later?
|
||||||
publishing_prices? || balloting_or_later?
|
current_phase&.publishing_prices_or_later?
|
||||||
end
|
end
|
||||||
|
|
||||||
def balloting_process?
|
def balloting_process?
|
||||||
@@ -134,7 +139,7 @@ class Budget < ApplicationRecord
|
|||||||
end
|
end
|
||||||
|
|
||||||
def balloting_or_later?
|
def balloting_or_later?
|
||||||
balloting_process? || finished?
|
current_phase&.balloting_or_later?
|
||||||
end
|
end
|
||||||
|
|
||||||
def heading_price(heading)
|
def heading_price(heading)
|
||||||
|
|||||||
@@ -28,16 +28,10 @@ class Budget
|
|||||||
|
|
||||||
scope :enabled, -> { where(enabled: true) }
|
scope :enabled, -> { where(enabled: true) }
|
||||||
scope :published, -> { enabled.where.not(kind: "drafting") }
|
scope :published, -> { enabled.where.not(kind: "drafting") }
|
||||||
scope :drafting, -> { find_by_kind("drafting") }
|
|
||||||
scope :informing, -> { find_by_kind("informing") }
|
PHASE_KINDS.each do |phase|
|
||||||
scope :accepting, -> { find_by_kind("accepting")}
|
define_singleton_method(phase) { find_by_kind(phase) }
|
||||||
scope :reviewing, -> { find_by_kind("reviewing")}
|
end
|
||||||
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")}
|
|
||||||
|
|
||||||
def next_enabled_phase
|
def next_enabled_phase
|
||||||
next_phase&.enabled? ? next_phase : next_phase&.next_enabled_phase
|
next_phase&.enabled? ? next_phase : next_phase&.next_enabled_phase
|
||||||
@@ -53,42 +47,58 @@ class Budget
|
|||||||
end
|
end
|
||||||
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
|
private
|
||||||
|
|
||||||
def adjust_date_ranges
|
def adjust_date_ranges
|
||||||
if enabled?
|
if enabled?
|
||||||
next_enabled_phase&.update_column(:starts_at, ends_at)
|
next_enabled_phase&.update_column(:starts_at, ends_at)
|
||||||
prev_enabled_phase&.update_column(:ends_at, starts_at)
|
prev_enabled_phase&.update_column(:ends_at, starts_at)
|
||||||
elsif enabled_changed?
|
elsif enabled_changed?
|
||||||
next_enabled_phase&.update_column(:starts_at, starts_at)
|
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)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
|
||||||
|
|
||||||
def next_phase_dates_valid?
|
def touch_budget
|
||||||
if enabled? && ends_at.present? && next_enabled_phase.present?
|
budget.touch
|
||||||
next_enabled_phase.assign_attributes(starts_at: ends_at)
|
end
|
||||||
if next_enabled_phase.invalid_dates_range?
|
|
||||||
phase_name = I18n.t("budgets.phase.#{next_enabled_phase.kind}")
|
def prev_phase_dates_valid?
|
||||||
error = I18n.t("budgets.phases.errors.next_phase_dates_invalid", phase_name: phase_name)
|
if enabled? && starts_at.present? && prev_enabled_phase.present?
|
||||||
errors.add(:ends_at, error)
|
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
|
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
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -3,116 +3,145 @@ class Budget::Stats
|
|||||||
alias_method :budget, :resource
|
alias_method :budget, :resource
|
||||||
|
|
||||||
def self.stats_methods
|
def self.stats_methods
|
||||||
super +
|
super + support_phase_methods + vote_phase_methods
|
||||||
%i[total_participants_support_phase total_participants_vote_phase
|
end
|
||||||
total_budget_investments total_votes total_selected_investments
|
|
||||||
total_unfeasible_investments total_supports headings
|
def self.support_phase_methods
|
||||||
total_participants_web total_participants_booths]
|
%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
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def total_participants
|
def phase_methods
|
||||||
participants.distinct.count
|
phases.map { |phase| self.class.send("#{phase}_phase_methods") }.flatten
|
||||||
end
|
end
|
||||||
|
|
||||||
def total_participants_support_phase
|
def participant_ids
|
||||||
voters.uniq.count
|
phases.map { |phase| send("participant_ids_#{phase}_phase") }.flatten.uniq
|
||||||
end
|
end
|
||||||
|
|
||||||
def total_participants_web
|
def participant_ids_support_phase
|
||||||
(balloters - poll_ballot_voters).uniq.compact.count
|
(authors + voters).uniq
|
||||||
end
|
end
|
||||||
|
|
||||||
def total_participants_booths
|
def participant_ids_vote_phase
|
||||||
poll_ballot_voters.uniq.count
|
(balloters + poll_ballot_voters).uniq
|
||||||
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)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def authors
|
def authors
|
||||||
budget.investments.pluck(:author_id)
|
@authors ||= budget.investments.pluck(:author_id)
|
||||||
end
|
end
|
||||||
|
|
||||||
def voters
|
def voters
|
||||||
supports(budget).pluck(:voter_id)
|
@voters ||= supports(budget).distinct.pluck(:voter_id)
|
||||||
end
|
end
|
||||||
|
|
||||||
def balloters
|
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
|
end
|
||||||
|
|
||||||
def poll_ballot_voters
|
def poll_ballot_voters
|
||||||
budget&.poll ? budget.poll.voters.pluck(:user_id) : []
|
@poll_ballot_voters ||= budget.poll ? budget.poll.voters.pluck(:user_id) : []
|
||||||
end
|
end
|
||||||
|
|
||||||
def balloters_by_heading(heading_id)
|
def balloters_by_heading(heading_id)
|
||||||
stats_cache("balloters_by_heading_#{heading_id}") do
|
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
|
||||||
end
|
end
|
||||||
|
|
||||||
def voters_by_heading(heading)
|
def voters_by_heading(heading)
|
||||||
stats_cache("voters_by_heading_#{heading.id}") do
|
stats_cache("voters_by_heading_#{heading.id}") do
|
||||||
supports(heading).pluck(:voter_id)
|
supports(heading).distinct.pluck(:voter_id)
|
||||||
end
|
end
|
||||||
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)
|
def calculate_heading_totals(heading)
|
||||||
{
|
{
|
||||||
total_investments_count: heading.investments.count,
|
total_investments_count: heading.investments.count,
|
||||||
total_participants_support_phase: voters_by_heading(heading).uniq.count,
|
total_participants_support_phase: voters_by_heading(heading).count,
|
||||||
total_participants_vote_phase: balloters_by_heading(heading.id).uniq.count,
|
total_participants_vote_phase: balloters_by_heading(heading.id).count,
|
||||||
total_participants_all_phase: voters_and_balloters_by_heading(heading)
|
total_participants_every_phase: voters_and_balloters_by_heading(heading)
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -122,8 +151,8 @@ class Budget::Stats
|
|||||||
percentage_district_population_support_phase: population_percent(population, heading_totals[:total_participants_support_phase]),
|
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_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_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_participants_every_phase: participants_percent(heading_totals, groups_totals, :total_participants_every_phase),
|
||||||
percentage_district_population_all_phase: population_percent(population, heading_totals[:total_participants_all_phase])
|
percentage_district_population_every_phase: population_percent(population, heading_totals[:total_participants_every_phase])
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -141,14 +170,12 @@ class Budget::Stats
|
|||||||
end
|
end
|
||||||
|
|
||||||
def supports(supportable)
|
def supports(supportable)
|
||||||
ActsAsVotable::Vote.where(votable_type: "Budget::Investment", votable_id: supportable.investments.pluck(:id))
|
Vote.where(votable: supportable.investments)
|
||||||
end
|
end
|
||||||
|
|
||||||
stats_cache(*stats_methods)
|
stats_cache(*stats_methods)
|
||||||
stats_cache :total_participants_with_gender
|
|
||||||
stats_cache :voters, :participants, :authors, :balloters, :poll_ballot_voters
|
|
||||||
|
|
||||||
def stats_cache(key, &block)
|
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
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,79 +1,175 @@
|
|||||||
module Statisticable
|
module Statisticable
|
||||||
extend ActiveSupport::Concern
|
extend ActiveSupport::Concern
|
||||||
|
PARTICIPATIONS = %w[gender age geozone]
|
||||||
|
|
||||||
included do
|
included do
|
||||||
attr_reader :resource
|
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
|
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
|
class_methods do
|
||||||
def stats_methods
|
def stats_methods
|
||||||
%i[total_participants total_male_participants
|
base_stats_methods + gender_methods + age_methods + geozone_methods
|
||||||
total_female_participants total_unknown_gender_or_age
|
end
|
||||||
male_percentage female_percentage age_groups]
|
|
||||||
|
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
|
end
|
||||||
|
|
||||||
def stats_cache(*method_names)
|
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 ActsAsParanoidAliases
|
||||||
include Notifiable
|
include Notifiable
|
||||||
include Sluggable
|
include Sluggable
|
||||||
|
include StatsVersionable
|
||||||
|
|
||||||
translates :name, touch: true
|
translates :name, touch: true
|
||||||
translates :summary, touch: true
|
translates :summary, touch: true
|
||||||
|
|||||||
@@ -2,122 +2,123 @@ class Poll::Stats
|
|||||||
include Statisticable
|
include Statisticable
|
||||||
alias_method :poll, :resource
|
alias_method :poll, :resource
|
||||||
|
|
||||||
|
CHANNELS = Poll::Voter::VALID_ORIGINS
|
||||||
|
|
||||||
def self.stats_methods
|
def self.stats_methods
|
||||||
%i[total_participants total_participants_web total_web_valid total_web_white total_web_null
|
super +
|
||||||
total_participants_booth total_booth_valid total_booth_white total_booth_null
|
%i[total_valid_votes total_white_votes total_null_votes
|
||||||
total_valid_votes total_white_votes total_null_votes valid_percentage_web valid_percentage_booth
|
total_participants_web total_web_valid total_web_white total_web_null
|
||||||
total_valid_percentage white_percentage_web white_percentage_booth total_white_percentage
|
total_participants_booth total_booth_valid total_booth_white total_booth_null
|
||||||
null_percentage_web null_percentage_booth total_null_percentage total_participants_web_percentage
|
total_participants_letter total_letter_valid total_letter_white total_letter_null
|
||||||
total_participants_booth_percentage]
|
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
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def total_participants
|
def participant_ids
|
||||||
total_participants_web + total_participants_booth
|
voters
|
||||||
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)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def voters
|
def voters
|
||||||
poll.voters
|
@voters ||= poll.voters.select(:user_id)
|
||||||
end
|
end
|
||||||
|
|
||||||
def recounts
|
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
|
end
|
||||||
|
|
||||||
stats_cache(*stats_methods)
|
stats_cache(*stats_methods)
|
||||||
stats_cache :voters, :recounts
|
|
||||||
|
|
||||||
def stats_cache(key, &block)
|
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
|
||||||
|
|
||||||
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 :moderators, -> { joins(:moderator) }
|
||||||
scope :organizations, -> { joins(:organization) }
|
scope :organizations, -> { joins(:organization) }
|
||||||
scope :officials, -> { where("official_level > 0") }
|
scope :officials, -> { where("official_level > 0") }
|
||||||
|
scope :male, -> { where(gender: "male") }
|
||||||
|
scope :female, -> { where(gender: "female") }
|
||||||
scope :newsletter, -> { where(newsletter: true) }
|
scope :newsletter, -> { where(newsletter: true) }
|
||||||
scope :for_render, -> { includes(:organization) }
|
scope :for_render, -> { includes(:organization) }
|
||||||
scope :by_document, ->(document_type, document_number) do
|
scope :by_document, ->(document_type, document_number) do
|
||||||
@@ -70,6 +72,13 @@ class User < ApplicationRecord
|
|||||||
string = "%#{search_string}%"
|
string = "%#{search_string}%"
|
||||||
where("username ILIKE ? OR email ILIKE ? OR document_number ILIKE ?", string, string, string)
|
where("username ILIKE ? OR email ILIKE ? OR document_number ILIKE ?", string, string, string)
|
||||||
end
|
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
|
before_validation :clean_document_number
|
||||||
|
|
||||||
|
|||||||
@@ -32,7 +32,7 @@
|
|||||||
<%= link_to t("budgets.results.link"), budget_results_path(@budget) %>
|
<%= link_to t("budgets.results.link"), budget_results_path(@budget) %>
|
||||||
</li>
|
</li>
|
||||||
<li class="tabs-title">
|
<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>
|
||||||
<li class="tabs-title is-active">
|
<li class="tabs-title is-active">
|
||||||
<%= link_to t("budgets.executions.link"), budget_executions_path(@budget), class: "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" %>
|
<%= link_to t("budgets.results.link"), budget_results_path(@budget), class: "is-active" %>
|
||||||
</li>
|
</li>
|
||||||
<li class="tabs-title">
|
<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>
|
||||||
<li class="tabs-title">
|
<li class="tabs-title">
|
||||||
<%= link_to t("budgets.executions.link"), budget_executions_path(@budget) %>
|
<%= link_to t("budgets.executions.link"), budget_executions_path(@budget) %>
|
||||||
|
|||||||
@@ -1,254 +1,163 @@
|
|||||||
<% provide :title do %>
|
<% provide :title do %>
|
||||||
<%= t("budgets.stats.page_title", budget: @budget.name) %>
|
<%= t("stats.budgets.page_title", budget: @budget.name) %>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% provide :social_media_meta_tags do %>
|
<% provide :social_media_meta_tags do %>
|
||||||
<%= render "shared/social_media_meta_tags",
|
<%= render "shared/social_media_meta_tags",
|
||||||
social_url: budget_url(@budget),
|
social_url: budget_stats_url(@budget),
|
||||||
social_title: @budget.name,
|
social_title: @budget.name,
|
||||||
social_description: @budget.description_finished %>
|
social_description: @budget.description_for_phase("finished") %>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
<% cache [@stats] do %>
|
<div class="participation-stats budgets-stats">
|
||||||
<div class="budgets-stats">
|
<div class="expanded no-margin-top padding header">
|
||||||
<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="row">
|
<div class="row">
|
||||||
<div class="small-12 column">
|
<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">
|
<div class="row margin-top">
|
||||||
<thead>
|
<div class="small-12 column">
|
||||||
<tr>
|
<ul class="tabs">
|
||||||
<th scope="col" rowspan="2"><%= t("budgets.stats.heading") %></th>
|
<li class="tabs-title">
|
||||||
<th scope="col" rowspan="2"><%= t("budgets.stats.investments_sent_html") %></th>
|
<span class="show-for-sr"><%= t("shared.you_are_in") %></span>
|
||||||
<th scope="col" colspan="3"><%= t("budgets.stats.participants_support_phase") %></th>
|
<%= link_to t("budgets.results.link"), budget_results_path(@budget) %>
|
||||||
<th scope="col" colspan="3"><%= t("budgets.stats.participants_voting_phase") %></th>
|
</li>
|
||||||
<th scope="col" colspan="3"><%= t("budgets.stats.participants_total") %></th>
|
<li class="tabs-title is-active">
|
||||||
</tr>
|
<%= link_to t("stats.budgets.link"), budget_stats_path(@budget), class: "is-active" %>
|
||||||
<tr>
|
</li>
|
||||||
<th scope="col" class="tiny"><%= t("budgets.stats.total") %></th>
|
<li class="tabs-title">
|
||||||
<th scope="col" class="tiny"><%= t("budgets.stats.percent_total_participants_html") %></th>
|
<%= link_to t("budgets.executions.link"), budget_executions_path(@budget) %>
|
||||||
<th scope="col" class="tiny"><%= t("budgets.stats.percent_heading_census_html") %></th>
|
</li>
|
||||||
<th scope="col" class="tiny"><%= t("budgets.stats.total") %></th>
|
</ul>
|
||||||
<th scope="col" class="tiny"><%= t("budgets.stats.percent_total_participants_html") %></th>
|
</div>
|
||||||
<th scope="col" class="tiny"><%= t("budgets.stats.percent_heading_census_html") %></th>
|
</div>
|
||||||
<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>
|
|
||||||
|
|
||||||
<% ["support", "vote", "all"].each do |phase| %>
|
<div class="row margin">
|
||||||
<td id="total_participants_<%= phase %>_phase_heading_<%= heading.id %>"
|
<div class="small-12 medium-3 column sidebar">
|
||||||
class="border-left text-center">
|
<%= render "shared/stats/links", stats: @stats %>
|
||||||
<%= @stats[:headings][heading.id]["total_participants_#{phase}_phase".to_sym] %>
|
|
||||||
</td>
|
<p><strong><%= link_to t("stats.advanced"), "#advanced_statistics" %></strong></p>
|
||||||
<td id="percentage_participants_<%= phase %>_phase_heading_<%= heading.id %>"
|
<ul class="menu vertical">
|
||||||
class="border-left border-right text-center">
|
<li>
|
||||||
<%= number_to_percentage(@stats[:headings][heading.id]["percentage_participants_#{phase}_phase".to_sym],
|
<%= link_to t("stats.budgets.total_investments"), "#total_investments" %>
|
||||||
strip_insignificant_zeros: true,
|
</li>
|
||||||
precision: 2) %>
|
<li>
|
||||||
</td>
|
<%= link_to t("stats.budgets.by_phase"), "#stats_by_phase" %>
|
||||||
<td id="percentage_district_population_<%= phase %>_phase_heading_<%= heading.id %>"
|
</li>
|
||||||
class="text-center border-right">
|
<li>
|
||||||
<%= number_to_percentage(@stats[:headings][heading.id]["percentage_district_population_#{phase}_phase".to_sym],
|
<%= link_to t("stats.budgets.by_heading"), "#stats_by_heading" %>
|
||||||
strip_insignificant_zeros: true,
|
</li>
|
||||||
precision: 2) %>
|
</ul>
|
||||||
</td>
|
</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 %>
|
<% end %>
|
||||||
</tr>
|
</tr>
|
||||||
<% end %>
|
<tr>
|
||||||
</tbody>
|
<% @stats.all_phases.each do %>
|
||||||
</table>
|
<th scope="col" class="phase-subheader"><%= t("stats.budgets.total") %></th>
|
||||||
</div>
|
<th scope="col" class="phase-subheader"><%= t("stats.budgets.percent_total_participants") %></th>
|
||||||
</div>
|
<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">
|
<% @stats.all_phases.each do |phase| %>
|
||||||
<div class="small-12 column">
|
<td id="total_participants_<%= phase %>_phase_heading_<%= heading.id %>"
|
||||||
<div id="total_unknown_gender_or_age">
|
class="border-left text-center">
|
||||||
<p class="help-text">
|
<%= @stats.headings[heading.id]["total_participants_#{phase}_phase".to_sym] %>
|
||||||
<%= t("budgets.stats.no_demographic_data", total: @stats[:total_unknown_gender_or_age]) %>
|
</td>
|
||||||
</p>
|
<td id="percentage_participants_<%= phase %>_phase_heading_<%= heading.id %>"
|
||||||
<p class="help-text">
|
class="border-left border-right text-center">
|
||||||
<%= t("budgets.stats.participatory_disclaimer") %>
|
<%= number_to_stats_percentage(@stats.headings[heading.id]["percentage_participants_#{phase}_phase".to_sym]) %>
|
||||||
</p>
|
</td>
|
||||||
<p class="help-text">
|
<td id="percentage_district_population_<%= phase %>_phase_heading_<%= heading.id %>"
|
||||||
<%= t("budgets.stats.heading_disclaimer") %>
|
class="text-center border-right">
|
||||||
</p>
|
<%= 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>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
</div>
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
<%= link_to t("polls.poll_header.back_to_proposal"), [@poll.related] %>
|
<%= link_to t("polls.poll_header.back_to_proposal"), [@poll.related] %>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
<h2><%= @poll.name %></h2>
|
<h1><%= @poll.name %></h1>
|
||||||
|
|
||||||
<%= safe_html_with_links simple_format(@poll.summary) %>
|
<%= safe_html_with_links simple_format(@poll.summary) %>
|
||||||
|
|
||||||
|
|||||||
@@ -1,95 +1,126 @@
|
|||||||
<% provide :title do %><%= @poll.name %><% end %>
|
<% provide :title do %><%= @poll.name %><% end %>
|
||||||
|
|
||||||
<div class="polls-results-stats">
|
<div class="participation-stats polls-results-stats">
|
||||||
<%= render "poll_header" %>
|
<%= render "poll_header" %>
|
||||||
|
|
||||||
<%= render "poll_subnav" %>
|
<%= render "poll_subnav" %>
|
||||||
|
|
||||||
<div class="row margin" data-equalizer data-equalize-on="medium">
|
<div class="row margin">
|
||||||
<div class="small-12 medium-3 column sidebar" data-equalizer-watch>
|
<div class="small-12 medium-3 column sidebar">
|
||||||
<p><strong><%= t("polls.show.stats.title") %></strong></p>
|
<%= render "shared/stats/links", stats: @stats %>
|
||||||
|
|
||||||
<ul class="menu vertical margin-top">
|
<p><strong><%= link_to t("stats.advanced"), "#advanced_statistics" %></strong></p>
|
||||||
<li><a href="#total"><%= t("polls.show.stats.total_participation") %></a></li>
|
<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>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="small-12 medium-9 column" data-equalizer-watch>
|
<div class="small-12 medium-9 column stats-content">
|
||||||
<h3 id="total"><%= t("polls.show.stats.total_participation") %></h3>
|
<%= render "shared/stats/participation", stats: @stats %>
|
||||||
|
|
||||||
<p class="margin-top uppercase">
|
<div id="advanced_statistics">
|
||||||
<%= t("polls.show.stats.total_votes") %><br>
|
<h3 class="section-title"><%= t("stats.advanced") %></h3>
|
||||||
<span class="number"><%= @stats[:total_participants] %></span>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<table>
|
<div id="stats_by_channel" class="stats-group">
|
||||||
<thead>
|
<h4><%= t("stats.polls.by_channel") %></h4>
|
||||||
<tr>
|
|
||||||
<th scope="col"><%= t("polls.show.stats.votes") %></th>
|
<% @stats.channels.each do |channel| %>
|
||||||
<th scope="col"><%= t("polls.show.stats.web") %></th>
|
<%= number_with_info_tags(
|
||||||
<th scope="col"><%= t("polls.show.stats.booth") %></th>
|
@stats.send("total_participants_#{channel}"),
|
||||||
<th scope="col"><%= t("polls.show.stats.total") %></th>
|
t("stats.polls.#{channel}_percentage",
|
||||||
</tr>
|
percentage: number_to_stats_percentage(@stats.send(:"total_participants_#{channel}_percentage"))
|
||||||
</thead>
|
),
|
||||||
<tbody>
|
html_class: channel
|
||||||
<tr>
|
) %>
|
||||||
<th scope="row"><%= t("polls.show.stats.valid") %></th>
|
<% end %>
|
||||||
<td>
|
</div>
|
||||||
<%= @stats[:total_web_valid] %>
|
|
||||||
<small><em>(<%= @stats[:valid_percentage_web].round(2) %>%)</em></small>
|
<div id="vote_stats_by_channel" class="stats-group">
|
||||||
</td>
|
<h4><%= t("stats.polls.vote_by_channel") %></h4>
|
||||||
<td>
|
|
||||||
<%= @stats[:total_booth_valid] %>
|
<table class="stack">
|
||||||
<small><em>(<%= @stats[:valid_percentage_booth].round(2) %>%)</em></small>
|
<thead>
|
||||||
</td>
|
<tr>
|
||||||
<td>
|
<th scope="col"><%= t("polls.show.stats.votes") %></th>
|
||||||
<%= @stats[:total_valid_votes] %>
|
<% @stats.channels.each do |channel| %>
|
||||||
<small><em>(<%= @stats[:total_valid_percentage].round(2) %>%)</em></small>
|
<th scope="col"><%= t("polls.show.stats.#{channel}") %></th>
|
||||||
</td>
|
<% end %>
|
||||||
</tr>
|
<th scope="col"><%= t("polls.show.stats.total") %></th>
|
||||||
<tr>
|
</tr>
|
||||||
<th scope="row"><%= t("polls.show.stats.white") %></th>
|
</thead>
|
||||||
<td>
|
<tbody>
|
||||||
<%= @stats[:total_web_white] %>
|
<tr>
|
||||||
<small><em>(<%= @stats[:white_percentage_web].round(2) %>%)</em></small>
|
<th scope="row"><%= t("polls.show.stats.valid") %></th>
|
||||||
</td>
|
|
||||||
<td>
|
<% @stats.channels.each do |channel| %>
|
||||||
<%= @stats[:total_booth_white] %>
|
<td>
|
||||||
<small><em>(<%= @stats[:white_percentage_booth].round(2) %>%)</em></small>
|
<%= @stats.send(:"total_#{channel}_valid") %>
|
||||||
</td>
|
<small><em>(<%= @stats.send(:"valid_percentage_#{channel}").round(2) %>%)</em></small>
|
||||||
<td><%= @stats[:total_white_votes] %>
|
</td>
|
||||||
<small><em>(<%= @stats[:total_white_percentage].round(2) %>%)</em></small>
|
<% end %>
|
||||||
</td>
|
|
||||||
</tr>
|
<td>
|
||||||
<tr>
|
<%= @stats.total_valid_votes %>
|
||||||
<th scope="row"><%= t("polls.show.stats.null_votes") %></th>
|
<small><em>(<%= @stats.total_valid_percentage.round(2) %>%)</em></small>
|
||||||
<td>
|
</td>
|
||||||
<%= @stats[:total_web_null] %>
|
|
||||||
<small><em>(<%= @stats[:null_percentage_web].round(2) %>%)</em></small>
|
</tr>
|
||||||
</td>
|
<tr>
|
||||||
<td>
|
<th scope="row"><%= t("polls.show.stats.white") %></th>
|
||||||
<%= @stats[:total_booth_null] %>
|
|
||||||
<small><em>(<%= @stats[:null_percentage_booth].round(2) %>%)</em></small>
|
<% @stats.channels.each do |channel| %>
|
||||||
</td>
|
<td>
|
||||||
<td>
|
<%= @stats.send(:"total_#{channel}_white") %>
|
||||||
<%= @stats[:total_null_votes] %>
|
<small><em>(<%= @stats.send(:"white_percentage_#{channel}").round(2) %>%)</em></small>
|
||||||
<small><em>(<%= @stats[:total_null_percentage].round(2) %>%)</em></small>
|
</td>
|
||||||
</td>
|
<% end %>
|
||||||
</tr>
|
|
||||||
<tr>
|
<td><%= @stats.total_white_votes %>
|
||||||
<th scope="row"><%= t("polls.show.stats.total") %></th>
|
<small><em>(<%= @stats.total_white_percentage.round(2) %>%)</em></small>
|
||||||
<td>
|
</td>
|
||||||
<%= @stats[:total_participants_web] %>
|
</tr>
|
||||||
<small><em>(<%= @stats[:total_participants_web_percentage].round(2) %>%)</em></small>
|
<tr>
|
||||||
</td>
|
<th scope="row"><%= t("polls.show.stats.null_votes") %></th>
|
||||||
<td>
|
|
||||||
<%= @stats[:total_participants_booth] %>
|
<% @stats.channels.each do |channel| %>
|
||||||
<small><em>(<%= @stats[:total_participants_booth_percentage].round(2) %>%)</em></small>
|
<td>
|
||||||
</td>
|
<%= @stats.send(:"total_#{channel}_null") %>
|
||||||
<td><%= @stats[:total_participants] %></td>
|
<small><em>(<%= @stats.send(:"null_percentage_#{channel}").round(2) %>%)</em></small>
|
||||||
</tr>
|
</td>
|
||||||
</tbody>
|
<% end %>
|
||||||
</table>
|
|
||||||
|
<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>
|
||||||
</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}/user_groups.yml
|
||||||
- config/locales/%{locale}/i18n.yml
|
- config/locales/%{locale}/i18n.yml
|
||||||
- config/locales/%{locale}/milestones.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:
|
# 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
|
# `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.site_customization.pages.page.status_*"
|
||||||
- "admin.legislation.processes.process.*"
|
- "admin.legislation.processes.process.*"
|
||||||
- "legislation.processes.index.*"
|
- "legislation.processes.index.*"
|
||||||
|
- "stats.budgets.participants_*_phase"
|
||||||
- "votes.budget_investments.different_heading_assigned*"
|
- "votes.budget_investments.different_heading_assigned*"
|
||||||
- "*.form.map_skip_checkbox"
|
- "*.form.map_skip_checkbox"
|
||||||
# - "{devise,kaminari,will_paginate}.*"
|
# - "{devise,kaminari,will_paginate}.*"
|
||||||
|
|||||||
@@ -191,35 +191,3 @@ en:
|
|||||||
dates_range_invalid: "Start date can't be equal or later than End date"
|
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})"
|
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})"
|
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"
|
stats_menu: "Participation statistics"
|
||||||
results_menu: "Poll results"
|
results_menu: "Poll results"
|
||||||
stats:
|
stats:
|
||||||
title: "Participation data"
|
|
||||||
total_participation: "Total participation"
|
|
||||||
total_votes: "Total amount of given votes"
|
total_votes: "Total amount of given votes"
|
||||||
votes: "VOTES"
|
votes: "VOTES"
|
||||||
web: "WEB"
|
web: "WEB"
|
||||||
|
mail: "MAIL"
|
||||||
booth: "BOOTH"
|
booth: "BOOTH"
|
||||||
total: "TOTAL"
|
total: "TOTAL"
|
||||||
valid: "Valid"
|
valid: "Valid"
|
||||||
white: "White votes"
|
white: "Fully blank"
|
||||||
null_votes: "Invalid"
|
null_votes: "Invalid"
|
||||||
results:
|
results:
|
||||||
title: "Questions"
|
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"
|
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})"
|
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})"
|
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"
|
stats_menu: "Estadísticas de participación"
|
||||||
results_menu: "Resultados de la votación"
|
results_menu: "Resultados de la votación"
|
||||||
stats:
|
stats:
|
||||||
title: "Datos de participación"
|
|
||||||
total_participation: "Participación total"
|
|
||||||
total_votes: "Nº total de votos emitidos"
|
total_votes: "Nº total de votos emitidos"
|
||||||
votes: "VOTOS"
|
votes: "VOTOS"
|
||||||
web: "WEB"
|
web: "WEB"
|
||||||
booth: "URNAS"
|
booth: "URNAS"
|
||||||
|
mail: "CORREO"
|
||||||
total: "TOTAL"
|
total: "TOTAL"
|
||||||
valid: "Válidos"
|
valid: "Válidos"
|
||||||
white: "En blanco"
|
white: "Totalmente en blanco"
|
||||||
null_votes: "Nulos"
|
null_votes: "Nulos"
|
||||||
results:
|
results:
|
||||||
title: "Preguntas"
|
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"
|
rake "-s sitemap:refresh"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
every 2.hours do
|
||||||
|
rake "-s stats:generate"
|
||||||
|
end
|
||||||
|
|
||||||
# Temporally not send dashboard's notifications
|
# Temporally not send dashboard's notifications
|
||||||
# every 1.day, at: "7:00 am" do
|
# every 1.day, at: "7:00 am" do
|
||||||
# rake "dashboards:send_notifications"
|
# 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
|
t.index ["tsv"], name: "index_spending_proposals_on_tsv", using: :gin
|
||||||
end
|
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|
|
create_table "taggings", force: :cascade do |t|
|
||||||
t.integer "tag_id"
|
t.integer "tag_id"
|
||||||
t.integer "taggable_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
|
end
|
||||||
|
|
||||||
factory :budget_heading, class: "Budget::Heading" do
|
factory :budget_heading, class: "Budget::Heading" do
|
||||||
association :group, factory: :budget_group
|
|
||||||
sequence(:name) { |n| "Heading #{n}" }
|
sequence(:name) { |n| "Heading #{n}" }
|
||||||
price 1000000
|
price 1000000
|
||||||
population 1234
|
population 1234
|
||||||
latitude "40.416775"
|
latitude "40.416775"
|
||||||
longitude "-3.703790"
|
longitude "-3.703790"
|
||||||
|
|
||||||
|
transient { budget nil }
|
||||||
|
group { association :budget_group, budget: budget || association(:budget) }
|
||||||
|
|
||||||
trait :drafting_budget do
|
trait :drafting_budget do
|
||||||
association :group, factory: [:budget_group, :drafting_budget]
|
association :group, factory: [:budget_group, :drafting_budget]
|
||||||
end
|
end
|
||||||
@@ -88,7 +90,7 @@ FactoryBot.define do
|
|||||||
|
|
||||||
factory :budget_investment, class: "Budget::Investment" do
|
factory :budget_investment, class: "Budget::Investment" do
|
||||||
sequence(:title) { |n| "Budget Investment #{n} title" }
|
sequence(:title) { |n| "Budget Investment #{n} title" }
|
||||||
association :heading, factory: :budget_heading
|
heading { association :budget_heading, budget: budget }
|
||||||
association :author, factory: :user
|
association :author, factory: :user
|
||||||
description "Spend money on this"
|
description "Spend money on this"
|
||||||
price 10
|
price 10
|
||||||
@@ -189,8 +191,13 @@ FactoryBot.define do
|
|||||||
end
|
end
|
||||||
|
|
||||||
factory :budget_ballot_line, class: "Budget::Ballot::Line" do
|
factory :budget_ballot_line, class: "Budget::Ballot::Line" do
|
||||||
association :ballot, factory: :budget_ballot
|
|
||||||
association :investment, factory: :budget_investment
|
association :investment, factory: :budget_investment
|
||||||
|
|
||||||
|
transient { user nil }
|
||||||
|
|
||||||
|
ballot do
|
||||||
|
association :budget_ballot, budget: investment.budget.reload, user: user || association(:user)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
factory :budget_reclassified_vote, class: "Budget::ReclassifiedVote" do
|
factory :budget_reclassified_vote, class: "Budget::ReclassifiedVote" do
|
||||||
|
|||||||
@@ -88,10 +88,13 @@ FactoryBot.define do
|
|||||||
end
|
end
|
||||||
|
|
||||||
factory :poll_voter, class: "Poll::Voter" do
|
factory :poll_voter, class: "Poll::Voter" do
|
||||||
poll
|
|
||||||
association :user, :level_two
|
association :user, :level_two
|
||||||
from_web
|
from_web
|
||||||
|
|
||||||
|
transient { budget nil }
|
||||||
|
|
||||||
|
poll { budget&.poll || association(:poll, budget: budget) }
|
||||||
|
|
||||||
trait :from_web do
|
trait :from_web do
|
||||||
origin "web"
|
origin "web"
|
||||||
token SecureRandom.hex(32)
|
token SecureRandom.hex(32)
|
||||||
|
|||||||
@@ -8,25 +8,16 @@ feature "Stats" do
|
|||||||
|
|
||||||
describe "Show" do
|
describe "Show" do
|
||||||
|
|
||||||
it "is not accessible to normal users if phase is not 'finished'" do
|
it "is not accessible if supports phase is not finished" do
|
||||||
budget.update(phase: "reviewing_ballots")
|
budget.update(phase: "selecting")
|
||||||
|
|
||||||
visit budget_stats_path(budget.id)
|
visit budget_stats_path(budget.id)
|
||||||
expect(page).to have_content "You do not have permission to carry out the action "\
|
expect(page).to have_content "You do not have permission to carry out the action "\
|
||||||
"'read_stats' on budget."
|
"'read_stats' on budget."
|
||||||
end
|
end
|
||||||
|
|
||||||
it "is accessible to normal users if phase is 'finished'" do
|
it "is accessible if supports phase is finished" do
|
||||||
budget.update(phase: "finished")
|
budget.update(phase: "valuating")
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
visit budget_stats_path(budget.id)
|
visit budget_stats_path(budget.id)
|
||||||
expect(page).to have_content "Stats"
|
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(:second_phase) { budget.phases.informing }
|
||||||
let(:third_phase) { budget.phases.accepting }
|
let(:third_phase) { budget.phases.accepting }
|
||||||
let(:fourth_phase) { budget.phases.reviewing }
|
let(:fourth_phase) { budget.phases.reviewing }
|
||||||
let(:fifth_phase) { budget.phases.selecting }
|
|
||||||
let(:final_phase) { budget.phases.finished}
|
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
|
describe "validates" do
|
||||||
it "is not valid without a budget" do
|
it "is not valid without a budget" do
|
||||||
expect(build(:budget_phase, budget: nil)).not_to be_valid
|
expect(build(:budget_phase, budget: nil)).not_to be_valid
|
||||||
|
|||||||
@@ -1,212 +1,287 @@
|
|||||||
require "rails_helper"
|
require "rails_helper"
|
||||||
|
|
||||||
describe Budget::Stats do
|
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
|
describe "#participants" do
|
||||||
@budget = create(:budget)
|
let(:author) { investment.author }
|
||||||
@group = create(:budget_group, budget: @budget)
|
let(:author_and_voter) { create(:user, hidden_at: Time.current) }
|
||||||
@heading = create(:budget_heading, group: @group, price: 1000)
|
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"),
|
before do
|
||||||
heading: @heading, price: 200, ballot_lines_count: 900, winner: true)
|
create(:budget_investment, :selected, budget: budget, author: author_and_voter)
|
||||||
@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)
|
|
||||||
|
|
||||||
@support1 = create(:vote, votable: @investment1, voter: create(:user, gender: "male"))
|
create(:vote, votable: investment, voter: author_and_voter)
|
||||||
@support2 = create(:vote, votable: @investment2, voter: create(:user))
|
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",
|
create(:budget_ballot_line, investment: investment, user: balloter)
|
||||||
date_of_birth: 54.years.ago))
|
create(:budget_ballot_line, investment: investment, user: voter_and_balloter)
|
||||||
@budget_ballot2 = create(:budget_ballot, budget: @budget, user: create(:user, gender: "female"))
|
|
||||||
@budget_ballot3 = create(:budget_ballot, budget: @budget, user: create(:user, gender: "male"))
|
|
||||||
|
|
||||||
@budget_ballot_line1 = create(:budget_ballot_line, ballot: @budget_ballot1,
|
create(:poll_voter, :from_booth, user: poll_balloter, budget: budget)
|
||||||
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)
|
|
||||||
|
|
||||||
@poll = create(:poll, budget: @budget)
|
create(:poll_voter, :from_booth, user: non_participant, budget: create(: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
|
|
||||||
end
|
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
|
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
|
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
|
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
|
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
|
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
|
||||||
|
|
||||||
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
|
expect(stats.total_participants_vote_phase).to be 1
|
||||||
|
|
||||||
it "returns the number of total participants in the votes phase via web" do
|
|
||||||
expect(@stats[:total_participants_web]).to be 3
|
|
||||||
end
|
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
|
expect(stats.total_participants_vote_phase).to be 2
|
||||||
|
|
||||||
it "returns the number of total participants in the votes phase in booths" do
|
|
||||||
expect(@stats[:total_participants_booths]).to be 1
|
|
||||||
end
|
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
|
end
|
||||||
|
|
||||||
context "#total_budget_investments" do
|
describe "#total_budget_investments" do
|
||||||
|
|
||||||
it "returns the number of total budget investments" do
|
it "returns the number of total budget investments" do
|
||||||
expect(@stats[:total_budget_investments]).to be 5
|
2.times { create(:budget_investment, budget: budget) }
|
||||||
end
|
create(:budget_investment, budget: create(:budget))
|
||||||
|
|
||||||
|
expect(stats.total_budget_investments).to be 2
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context "#total_votes" do
|
describe "#total_votes" do
|
||||||
|
|
||||||
it "returns the number of total votes" do
|
it "returns the number of total votes" do
|
||||||
expect(@stats[:total_votes]).to be 4
|
create(:budget_ballot_line, investment: investment)
|
||||||
end
|
create(:budget_ballot_line, investment: create(:budget_investment, :selected, budget: budget))
|
||||||
|
|
||||||
|
expect(stats.total_votes).to be 2
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context "#total_selected_investments" do
|
describe "#total_selected_investments" do
|
||||||
|
|
||||||
it "returns the number of total selected investments" do
|
it "returns the number of total selected investments" do
|
||||||
expect(@stats[:total_selected_investments]).to be 4
|
3.times { create(:budget_investment, :selected, budget: budget) }
|
||||||
end
|
create(:budget_investment, :selected, budget: create(:budget))
|
||||||
|
create(:budget_investment, :unfeasible, budget: budget)
|
||||||
|
|
||||||
|
expect(stats.total_selected_investments).to be 3
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context "#total_unfeasible_investments" do
|
describe "#total_unfeasible_investments" do
|
||||||
|
|
||||||
it "returns the number of total unfeasible investments" do
|
it "returns the number of total unfeasible investments" do
|
||||||
expect(@stats[:total_unfeasible_investments]).to be 1
|
3.times { create(:budget_investment, :unfeasible, budget: budget) }
|
||||||
end
|
create(:budget_investment, :unfeasible, budget: create(:budget))
|
||||||
|
create(:budget_investment, :selected, budget: budget)
|
||||||
|
|
||||||
|
expect(stats.total_unfeasible_investments).to be 3
|
||||||
|
end
|
||||||
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
|
allow(stats).to receive(:participants).and_return(User.all)
|
||||||
expect(@stats[:total_male_participants]).to be 4
|
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
describe "#total_male_participants" do
|
||||||
|
it "returns the number of total male participants" do
|
||||||
context "#total_female_participants" do
|
expect(stats.total_male_participants).to be 3
|
||||||
|
end
|
||||||
it "returns the number of total female participants" do
|
|
||||||
expect(@stats[:total_female_participants]).to be 6
|
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
describe "#total_female_participants" do
|
||||||
|
it "returns the number of total female participants" do
|
||||||
context "#total_supports" do
|
expect(stats.total_female_participants).to be 2
|
||||||
|
end
|
||||||
it "returns the number of total supports" do
|
|
||||||
expect(@stats[:total_supports]).to be 2
|
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
describe "#male_percentage" do
|
||||||
|
it "returns the percentage of male participants" do
|
||||||
context "#total_unknown_gender_or_age" do
|
expect(stats.male_percentage).to be 60.0
|
||||||
|
end
|
||||||
it "returns the number of total unknown participants' gender or age" do
|
|
||||||
expect(@stats[:total_unknown_gender_or_age]).to be 1
|
|
||||||
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
|
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
|
it "returns the age groups hash" do
|
||||||
expect(@stats[:age_groups]["16 - 19"]).to be 0
|
expect(stats.participants_by_age["16 - 19"][:count]).to be 0
|
||||||
expect(@stats[:age_groups]["20 - 24"]).to be 7
|
expect(stats.participants_by_age["20 - 24"][:count]).to be 4
|
||||||
expect(@stats[:age_groups]["25 - 29"]).to be 1
|
expect(stats.participants_by_age["25 - 29"][:count]).to be 0
|
||||||
expect(@stats[:age_groups]["30 - 34"]).to be 0
|
expect(stats.participants_by_age["30 - 34"][:count]).to be 1
|
||||||
expect(@stats[:age_groups]["35 - 39"]).to be 1
|
expect(stats.participants_by_age["35 - 39"][:count]).to be 0
|
||||||
expect(@stats[:age_groups]["40 - 44"]).to be 1
|
expect(stats.participants_by_age["40 - 44"][:count]).to be 3
|
||||||
expect(@stats[:age_groups]["45 - 49"]).to be 0
|
expect(stats.participants_by_age["45 - 49"][:count]).to be 0
|
||||||
expect(@stats[:age_groups]["50 - 54"]).to be 1
|
expect(stats.participants_by_age["50 - 54"][:count]).to be 2
|
||||||
expect(@stats[:age_groups]["55 - 59"]).to be 0
|
expect(stats.participants_by_age["55 - 59"][:count]).to be 0
|
||||||
expect(@stats[:age_groups]["60 - 64"]).to be 0
|
expect(stats.participants_by_age["60 - 64"][:count]).to be 0
|
||||||
expect(@stats[:age_groups]["65 - 69"]).to be 0
|
expect(stats.participants_by_age["65 - 69"][:count]).to be 0
|
||||||
expect(@stats[:age_groups]["70 - 140"]).to be 0
|
expect(stats.participants_by_age["70 - 74"][:count]).to be 0
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
||||||
context "#male_percentage" do
|
describe "#headings" do
|
||||||
|
before do
|
||||||
it "returns the percentage of male participants" do
|
investment.heading.update_column(:population, 1234)
|
||||||
expect(@stats[:male_percentage]).to be 40.0
|
create(:budget_investment, heading: investment.heading)
|
||||||
|
2.times { create(:vote, votable: investment) }
|
||||||
|
create(:budget_ballot_line, investment: investment)
|
||||||
end
|
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
|
it "returns headings data" do
|
||||||
heading_stats = @stats[:headings][@heading.id]
|
heading_stats = stats.headings[investment.heading.id]
|
||||||
expect(heading_stats[:total_investments_count]).to be 5
|
expect(heading_stats[:total_investments_count]).to be 2
|
||||||
expect(heading_stats[:total_participants_support_phase]).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_vote_phase]).to be 1
|
||||||
expect(heading_stats[:total_participants_all_phase]).to be 6
|
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_participants_support_phase]).to be 100.0
|
||||||
expect(heading_stats[:percentage_district_population_support_phase]).to be 0.162
|
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_participants_vote_phase]).to be 100.0
|
||||||
expect(heading_stats[:percentage_district_population_vote_phase]).to be 0.324
|
expect(heading_stats[:percentage_district_population_vote_phase]).to be 0.081
|
||||||
expect(heading_stats[:percentage_participants_all_phase]).to be 100.0
|
expect(heading_stats[:percentage_participants_every_phase]).to be 100.0
|
||||||
expect(heading_stats[:percentage_district_population_all_phase]).to be 0.486
|
expect(heading_stats[:percentage_district_population_every_phase]).to be 0.243
|
||||||
|
|
||||||
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
|
|
||||||
end
|
end
|
||||||
|
|
||||||
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
|
end
|
||||||
|
|||||||
@@ -97,33 +97,55 @@ describe Budget do
|
|||||||
expect(budget).to be_finished
|
expect(budget).to be_finished
|
||||||
end
|
end
|
||||||
|
|
||||||
it "balloting_or_later?" do
|
describe "#valuating_or_later?" do
|
||||||
budget.phase = "drafting"
|
it "returns false before valuating" do
|
||||||
expect(budget).not_to be_balloting_or_later
|
budget.phase = "selecting"
|
||||||
|
expect(budget).not_to be_valuating_or_later
|
||||||
|
end
|
||||||
|
|
||||||
budget.phase = "accepting"
|
it "returns true while valuating" do
|
||||||
expect(budget).not_to be_balloting_or_later
|
budget.phase = "valuating"
|
||||||
|
expect(budget).to be_valuating_or_later
|
||||||
|
end
|
||||||
|
|
||||||
budget.phase = "reviewing"
|
it "returns true after valuating" do
|
||||||
expect(budget).not_to be_balloting_or_later
|
budget.phase = "publishing_prices"
|
||||||
|
expect(budget).to be_valuating_or_later
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
budget.phase = "selecting"
|
describe "#publishing_prices_or_later?" do
|
||||||
expect(budget).not_to be_balloting_or_later
|
it "returns false before publishing prices" do
|
||||||
|
budget.phase = "valuating"
|
||||||
|
expect(budget).not_to be_publishing_prices_or_later
|
||||||
|
end
|
||||||
|
|
||||||
budget.phase = "valuating"
|
it "returns true while publishing prices" do
|
||||||
expect(budget).not_to be_balloting_or_later
|
budget.phase = "publishing_prices"
|
||||||
|
expect(budget).to be_publishing_prices_or_later
|
||||||
|
end
|
||||||
|
|
||||||
budget.phase = "publishing_prices"
|
it "returns true after publishing prices" do
|
||||||
expect(budget).not_to be_balloting_or_later
|
budget.phase = "balloting"
|
||||||
|
expect(budget).to be_publishing_prices_or_later
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
budget.phase = "balloting"
|
describe "#balloting_or_later?" do
|
||||||
expect(budget).to be_balloting_or_later
|
it "returns false before balloting" do
|
||||||
|
budget.phase = "publishing_prices"
|
||||||
|
expect(budget).not_to be_balloting_or_later
|
||||||
|
end
|
||||||
|
|
||||||
budget.phase = "reviewing_ballots"
|
it "returns true while balloting" do
|
||||||
expect(budget).to be_balloting_or_later
|
budget.phase = "balloting"
|
||||||
|
expect(budget).to be_balloting_or_later
|
||||||
|
end
|
||||||
|
|
||||||
budget.phase = "finished"
|
it "returns true after balloting" do
|
||||||
expect(budget).to be_balloting_or_later
|
budget.phase = "finished"
|
||||||
|
expect(budget).to be_balloting_or_later
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -1,44 +1,288 @@
|
|||||||
require "rails_helper"
|
require "rails_helper"
|
||||||
|
|
||||||
describe Poll::Stats do
|
describe Poll::Stats do
|
||||||
|
let(:poll) { create(:poll) }
|
||||||
|
let(:stats) { Poll::Stats.new(poll) }
|
||||||
|
|
||||||
describe "#generate" do
|
describe "#participants" do
|
||||||
it "generates the correct stats" do
|
it "includes hidden users" do
|
||||||
poll = create(:poll)
|
create(:poll_voter, poll: poll)
|
||||||
2.times { create(:poll_voter, :from_web, poll: poll) }
|
create(:poll_voter, poll: poll, user: create(:user, :level_two, hidden_at: Time.current))
|
||||||
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)
|
|
||||||
|
|
||||||
stats = Poll::Stats.new(poll).generate
|
expect(stats.participants.count).to eq(2)
|
||||||
|
|
||||||
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)
|
|
||||||
end
|
end
|
||||||
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
|
||||||