Merge pull request #3512 from consul/backport-stats

Change stats layout
This commit is contained in:
Javier Martín
2019-05-21 17:18:47 +02:00
committed by GitHub
61 changed files with 2004 additions and 925 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -27,7 +27,7 @@
//= require app
//= require check_all_none
//= require comments
//= require dropdown
//= require foundation_extras
//= require ie_alert
//= require location_changer
//= require moderator_comment
@@ -93,7 +93,7 @@ var initialize_modules = function() {
App.Votes.initialize();
App.AllowParticipation.initialize();
App.Tags.initialize();
App.Dropdown.initialize();
App.FoundationExtras.initialize();
App.LocationChanger.initialize();
App.CheckAllNone.initialize();
App.PreventDoubleSubmission.initialize();

View File

@@ -1,4 +0,0 @@
App.Dropdown =
initialize: ->
$(document).foundation()

View File

@@ -0,0 +1,5 @@
App.FoundationExtras =
initialize: ->
$(document).foundation()
$(window).trigger "load.zf.sticky"

View File

@@ -119,3 +119,6 @@ $tab-content-border: $border;
$orbit-bullet-diameter: 0.8rem;
$pagination-radius: $global-radius;
$show-header-for-stacked: true;

View File

@@ -12,6 +12,7 @@
@import "legislation";
@import "legislation_process";
@import "community";
@import "stats";
@import "custom";
@import "c3";
@import "annotator.min";

View File

@@ -1988,15 +1988,8 @@
.polls-results-stats {
.sidebar {
border-bottom: 1px solid $border;
margin-bottom: $line-height;
@include breakpoint(medium) {
border-bottom: 0;
border-right: 1px solid $border;
}
.menu {
.menu:not(.vertical) {
padding: 0;
li a {
@@ -2030,10 +2023,4 @@
}
}
}
.number {
font-size: rem-calc(60);
font-weight: bold;
line-height: rem-calc(60);
}
}

View 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;
}
}
}

View File

@@ -6,16 +6,12 @@ module Budgets
def show
authorize! :read_stats, @budget
@stats = load_stats
@stats = Budget::Stats.new(@budget)
@headings = @budget.headings.sort_by_name
end
private
def load_stats
Budget::Stats.new(@budget).generate
end
def load_budget
@budget = Budget.find_by(slug: params[:budget_id]) || Budget.find_by(id: params[:budget_id])
end

View File

@@ -33,7 +33,7 @@ class PollsController < ApplicationController
end
def stats
@stats = Poll::Stats.new(@poll).generate
@stats = Poll::Stats.new(@poll)
end
def results

View File

@@ -30,4 +30,16 @@ module StatsHelper
opt[:data][:graph] = admin_api_stats_path(budget_investments: true)
content_tag :div, "", opt
end
def number_to_stats_percentage(number, options = {})
number_to_percentage(number, { strip_insignificant_zeros: true, precision: 2 }.merge(options))
end
def number_with_info_tags(number, text, html_class: "")
content_tag :p, class: "number-with-info #{html_class}".strip do
content_tag :span, class: "content" do
content_tag(:span, number, class: "number") + content_tag(:span, text, class: "info")
end
end
end
end

View File

@@ -66,7 +66,6 @@ module Abilities
can [:hide, :update, :toggle_selection], Budget::Investment
can [:valuate, :comment_valuation], Budget::Investment
can :create, Budget::ValuatorAssignment
can :read_stats, Budget, phase: "reviewing_ballots"
can [:search, :edit, :update, :create, :index, :destroy], Banner

View File

@@ -21,7 +21,7 @@ module Abilities
can [:read], Budget::Group
can [:read, :print, :json_data], Budget::Investment
can [:read_results, :read_executions], Budget, phase: "finished"
can :read_stats, Budget, phase: "finished"
can(:read_stats, Budget) { |budget| budget.valuating_or_later? }
can :new, DirectMessage
can [:read, :debate, :draft_publication, :allegations, :result_publication,
:proposals, :milestones], Legislation::Process, published: true

View File

@@ -2,6 +2,7 @@ class Budget < ApplicationRecord
include Measurable
include Sluggable
include StatsVersionable
translates :name, touch: true
include Globalizable
@@ -125,8 +126,12 @@ class Budget < ApplicationRecord
Budget::Phase::PUBLISHED_PRICES_PHASES.include?(phase)
end
def valuating_or_later?
current_phase&.valuating_or_later?
end
def publishing_prices_or_later?
publishing_prices? || balloting_or_later?
current_phase&.publishing_prices_or_later?
end
def balloting_process?
@@ -134,7 +139,7 @@ class Budget < ApplicationRecord
end
def balloting_or_later?
balloting_process? || finished?
current_phase&.balloting_or_later?
end
def heading_price(heading)

View File

@@ -28,16 +28,10 @@ class Budget
scope :enabled, -> { where(enabled: true) }
scope :published, -> { enabled.where.not(kind: "drafting") }
scope :drafting, -> { find_by_kind("drafting") }
scope :informing, -> { find_by_kind("informing") }
scope :accepting, -> { find_by_kind("accepting")}
scope :reviewing, -> { find_by_kind("reviewing")}
scope :selecting, -> { find_by_kind("selecting")}
scope :valuating, -> { find_by_kind("valuating")}
scope :publishing_prices, -> { find_by_kind("publishing_prices")}
scope :balloting, -> { find_by_kind("balloting")}
scope :reviewing_ballots, -> { find_by_kind("reviewing_ballots")}
scope :finished, -> { find_by_kind("finished")}
PHASE_KINDS.each do |phase|
define_singleton_method(phase) { find_by_kind(phase) }
end
def next_enabled_phase
next_phase&.enabled? ? next_phase : next_phase&.next_enabled_phase
@@ -53,42 +47,58 @@ class Budget
end
end
def valuating_or_later?
in_phase_or_later?("valuating")
end
def publishing_prices_or_later?
in_phase_or_later?("publishing_prices")
end
def balloting_or_later?
in_phase_or_later?("balloting")
end
private
def adjust_date_ranges
if enabled?
next_enabled_phase&.update_column(:starts_at, ends_at)
prev_enabled_phase&.update_column(:ends_at, starts_at)
elsif enabled_changed?
next_enabled_phase&.update_column(:starts_at, starts_at)
end
end
def touch_budget
budget.touch
end
def prev_phase_dates_valid?
if enabled? && starts_at.present? && prev_enabled_phase.present?
prev_enabled_phase.assign_attributes(ends_at: starts_at)
if prev_enabled_phase.invalid_dates_range?
phase_name = I18n.t("budgets.phase.#{prev_enabled_phase.kind}")
error = I18n.t("budgets.phases.errors.prev_phase_dates_invalid", phase_name: phase_name)
errors.add(:starts_at, error)
def adjust_date_ranges
if enabled?
next_enabled_phase&.update_column(:starts_at, ends_at)
prev_enabled_phase&.update_column(:ends_at, starts_at)
elsif enabled_changed?
next_enabled_phase&.update_column(:starts_at, starts_at)
end
end
end
def next_phase_dates_valid?
if enabled? && ends_at.present? && next_enabled_phase.present?
next_enabled_phase.assign_attributes(starts_at: ends_at)
if next_enabled_phase.invalid_dates_range?
phase_name = I18n.t("budgets.phase.#{next_enabled_phase.kind}")
error = I18n.t("budgets.phases.errors.next_phase_dates_invalid", phase_name: phase_name)
errors.add(:ends_at, error)
def touch_budget
budget.touch
end
def prev_phase_dates_valid?
if enabled? && starts_at.present? && prev_enabled_phase.present?
prev_enabled_phase.assign_attributes(ends_at: starts_at)
if prev_enabled_phase.invalid_dates_range?
phase_name = I18n.t("budgets.phase.#{prev_enabled_phase.kind}")
error = I18n.t("budgets.phases.errors.prev_phase_dates_invalid", phase_name: phase_name)
errors.add(:starts_at, error)
end
end
end
end
def next_phase_dates_valid?
if enabled? && ends_at.present? && next_enabled_phase.present?
next_enabled_phase.assign_attributes(starts_at: ends_at)
if next_enabled_phase.invalid_dates_range?
phase_name = I18n.t("budgets.phase.#{next_enabled_phase.kind}")
error = I18n.t("budgets.phases.errors.next_phase_dates_invalid", phase_name: phase_name)
errors.add(:ends_at, error)
end
end
end
def in_phase_or_later?(phase)
PHASE_KINDS.index(kind) >= PHASE_KINDS.index(phase)
end
end
end

View File

@@ -3,116 +3,145 @@ class Budget::Stats
alias_method :budget, :resource
def self.stats_methods
super +
%i[total_participants_support_phase total_participants_vote_phase
total_budget_investments total_votes total_selected_investments
total_unfeasible_investments total_supports headings
total_participants_web total_participants_booths]
super + support_phase_methods + vote_phase_methods
end
def self.support_phase_methods
%i[total_participants_support_phase total_budget_investments
total_selected_investments total_unfeasible_investments headings]
end
def self.vote_phase_methods
%i[total_votes total_participants_vote_phase]
end
def stats_methods
base_stats_methods + participation_methods + phase_methods
end
def phases
%w[support vote].select { |phase| send("#{phase}_phase_finished?") }
end
def all_phases
return phases unless phases.many?
[*phases, "every"]
end
def support_phase_finished?
budget.valuating_or_later?
end
def vote_phase_finished?
budget.finished?
end
def total_participants
participants.distinct.count
end
def total_participants_support_phase
voters.count
end
def total_participants_vote_phase
(balloters + poll_ballot_voters).uniq.count
end
def total_budget_investments
budget.investments.count
end
def total_votes
budget.ballots.pluck(:ballot_lines_count).inject(0) { |sum, x| sum + x }
end
def total_selected_investments
budget.investments.selected.count
end
def total_unfeasible_investments
budget.investments.unfeasible.count
end
def headings
groups = Hash.new(0)
budget.headings.order("id ASC").each do |heading|
groups[heading.id] = Hash.new(0).merge(calculate_heading_totals(heading))
end
groups[:total] = Hash.new(0)
groups[:total][:total_investments_count] = groups.collect {|_k, v| v[:total_investments_count]}.sum
groups[:total][:total_participants_support_phase] = groups.collect {|_k, v| v[:total_participants_support_phase]}.sum
groups[:total][:total_participants_vote_phase] = groups.collect {|_k, v| v[:total_participants_vote_phase]}.sum
groups[:total][:total_participants_every_phase] = groups.collect {|_k, v| v[:total_participants_every_phase]}.sum
budget.headings.each do |heading|
groups[heading.id].merge!(calculate_heading_stats_with_totals(groups[heading.id], groups[:total], heading.population))
end
groups[:total][:percentage_participants_support_phase] = groups.collect {|_k, v| v[:percentage_participants_support_phase]}.sum
groups[:total][:percentage_participants_vote_phase] = groups.collect {|_k, v| v[:percentage_participants_vote_phase]}.sum
groups[:total][:percentage_participants_every_phase] = groups.collect {|_k, v| v[:percentage_participants_every_phase]}.sum
groups
end
private
def total_participants
participants.distinct.count
def phase_methods
phases.map { |phase| self.class.send("#{phase}_phase_methods") }.flatten
end
def total_participants_support_phase
voters.uniq.count
def participant_ids
phases.map { |phase| send("participant_ids_#{phase}_phase") }.flatten.uniq
end
def total_participants_web
(balloters - poll_ballot_voters).uniq.compact.count
def participant_ids_support_phase
(authors + voters).uniq
end
def total_participants_booths
poll_ballot_voters.uniq.count
end
def total_participants_vote_phase
balloters.uniq.count
end
def total_budget_investments
budget.investments.count
end
def total_votes
budget.ballots.pluck(:ballot_lines_count).inject(0) { |sum, x| sum + x }
end
def total_selected_investments
budget.investments.selected.count
end
def total_unfeasible_investments
budget.investments.unfeasible.count
end
def total_supports
supports(budget).count
end
def participants
User.where(id: (authors + voters + balloters + poll_ballot_voters).uniq.compact)
def participant_ids_vote_phase
(balloters + poll_ballot_voters).uniq
end
def authors
budget.investments.pluck(:author_id)
@authors ||= budget.investments.pluck(:author_id)
end
def voters
supports(budget).pluck(:voter_id)
@voters ||= supports(budget).distinct.pluck(:voter_id)
end
def balloters
budget.ballots.where("ballot_lines_count > ?", 0).pluck(:user_id)
@balloters ||= budget.ballots.where("ballot_lines_count > ?", 0).distinct.pluck(:user_id).compact
end
def poll_ballot_voters
budget&.poll ? budget.poll.voters.pluck(:user_id) : []
@poll_ballot_voters ||= budget.poll ? budget.poll.voters.pluck(:user_id) : []
end
def balloters_by_heading(heading_id)
stats_cache("balloters_by_heading_#{heading_id}") do
budget.ballots.joins(:lines).where(budget_ballot_lines: {heading_id: heading_id}).pluck(:user_id)
budget.ballots.joins(:lines)
.where(budget_ballot_lines: { heading_id: heading_id} )
.distinct.pluck(:user_id)
end
end
def voters_by_heading(heading)
stats_cache("voters_by_heading_#{heading.id}") do
supports(heading).pluck(:voter_id)
supports(heading).distinct.pluck(:voter_id)
end
end
def headings
groups = Hash.new(0)
budget.headings.order("id ASC").each do |heading|
groups[heading.id] = Hash.new(0).merge(calculate_heading_totals(heading))
end
groups[:total] = Hash.new(0)
groups[:total][:total_investments_count] = groups.collect {|_k, v| v[:total_investments_count]}.sum
groups[:total][:total_participants_support_phase] = groups.collect {|_k, v| v[:total_participants_support_phase]}.sum
groups[:total][:total_participants_vote_phase] = groups.collect {|_k, v| v[:total_participants_vote_phase]}.sum
groups[:total][:total_participants_all_phase] = groups.collect {|_k, v| v[:total_participants_all_phase]}.sum
budget.headings.each do |heading|
groups[heading.id].merge!(calculate_heading_stats_with_totals(groups[heading.id], groups[:total], heading.population))
end
groups[:total][:percentage_participants_support_phase] = groups.collect {|_k, v| v[:percentage_participants_support_phase]}.sum
groups[:total][:percentage_participants_vote_phase] = groups.collect {|_k, v| v[:percentage_participants_vote_phase]}.sum
groups[:total][:percentage_participants_all_phase] = groups.collect {|_k, v| v[:percentage_participants_all_phase]}.sum
groups
end
def calculate_heading_totals(heading)
{
total_investments_count: heading.investments.count,
total_participants_support_phase: voters_by_heading(heading).uniq.count,
total_participants_vote_phase: balloters_by_heading(heading.id).uniq.count,
total_participants_all_phase: voters_and_balloters_by_heading(heading)
total_participants_support_phase: voters_by_heading(heading).count,
total_participants_vote_phase: balloters_by_heading(heading.id).count,
total_participants_every_phase: voters_and_balloters_by_heading(heading)
}
end
@@ -122,8 +151,8 @@ class Budget::Stats
percentage_district_population_support_phase: population_percent(population, heading_totals[:total_participants_support_phase]),
percentage_participants_vote_phase: participants_percent(heading_totals, groups_totals, :total_participants_vote_phase),
percentage_district_population_vote_phase: population_percent(population, heading_totals[:total_participants_vote_phase]),
percentage_participants_all_phase: participants_percent(heading_totals, groups_totals, :total_participants_all_phase),
percentage_district_population_all_phase: population_percent(population, heading_totals[:total_participants_all_phase])
percentage_participants_every_phase: participants_percent(heading_totals, groups_totals, :total_participants_every_phase),
percentage_district_population_every_phase: population_percent(population, heading_totals[:total_participants_every_phase])
}
end
@@ -141,14 +170,12 @@ class Budget::Stats
end
def supports(supportable)
ActsAsVotable::Vote.where(votable_type: "Budget::Investment", votable_id: supportable.investments.pluck(:id))
Vote.where(votable: supportable.investments)
end
stats_cache(*stats_methods)
stats_cache :total_participants_with_gender
stats_cache :voters, :participants, :authors, :balloters, :poll_ballot_voters
def stats_cache(key, &block)
Rails.cache.fetch("budgets_stats/#{budget.id}/#{key}/v10", &block)
Rails.cache.fetch("budgets_stats/#{budget.id}/#{phases.join}/#{key}/#{version}", &block)
end
end

View File

@@ -1,79 +1,175 @@
module Statisticable
extend ActiveSupport::Concern
PARTICIPATIONS = %w[gender age geozone]
included do
attr_reader :resource
def initialize(resource)
@resource = resource
end
def generate
self.class.stats_methods.map { |stat_name| [stat_name, send(stat_name)] }.to_h
end
private
def total_participants_with_gender
participants.where.not(gender: nil).distinct.count
end
def total_male_participants
participants.where(gender: "male").count
end
def total_female_participants
participants.where(gender: "female").count
end
def total_unknown_gender_or_age
participants.where("gender IS NULL OR date_of_birth is NULL").uniq.count
end
def male_percentage
calculate_percentage(total_male_participants, total_participants_with_gender)
end
def female_percentage
calculate_percentage(total_female_participants, total_participants_with_gender)
end
def age_groups
groups = Hash.new(0)
["16 - 19",
"20 - 24",
"25 - 29",
"30 - 34",
"35 - 39",
"40 - 44",
"45 - 49",
"50 - 54",
"55 - 59",
"60 - 64",
"65 - 69",
"70 - 140"].each do |group|
start, finish = group.split(" - ")
group_name = (group == "70 - 140" ? "+ 70" : group)
groups[group_name] = User.where(id: participants)
.where("date_of_birth > ? AND date_of_birth < ?",
finish.to_i.years.ago.beginning_of_year,
start.to_i.years.ago.end_of_year).count
end
groups
end
def calculate_percentage(fraction, total)
return 0.0 if total.zero?
(fraction * 100.0 / total).round(3)
end
end
def initialize(resource)
@resource = resource
end
def generate
stats_methods.each { |stat_name| send(stat_name) }
end
def stats_methods
base_stats_methods + participation_methods
end
def participations
PARTICIPATIONS.select { |participation| send("#{participation}?") }
end
def gender?
participants.male.any? || participants.female.any?
end
def age?
participants.between_ages(age_groups.flatten.min, age_groups.flatten.max).any?
end
def geozone?
participants.where(geozone: geozones).any?
end
def participants
@participants ||= User.unscoped.where(id: participant_ids)
end
def total_male_participants
participants.male.count
end
def total_female_participants
participants.female.count
end
def total_no_demographic_data
participants.where("gender IS NULL OR date_of_birth IS NULL OR geozone_id IS NULL").count
end
def male_percentage
calculate_percentage(total_male_participants, total_participants_with_gender)
end
def female_percentage
calculate_percentage(total_female_participants, total_participants_with_gender)
end
def participants_by_age
age_groups.map do |start, finish|
count = participants.between_ages(start, finish).count
[
"#{start} - #{finish}",
{
range: range_description(start, finish),
count: count,
percentage: calculate_percentage(count, total_participants)
}
]
end.to_h
end
def participants_by_geozone
geozone_stats.map do |stats|
[
stats.name,
{
count: stats.count,
percentage: stats.percentage
}
]
end.to_h
end
def calculate_percentage(fraction, total)
PercentageCalculator.calculate(fraction, total)
end
def version
"v#{resource.find_or_create_stats_version.updated_at.to_i}"
end
private
def base_stats_methods
self.class.base_stats_methods
end
def participation_methods
participations.map { |participation| self.class.send("#{participation}_methods") }.flatten
end
def total_participants_with_gender
@total_participants_with_gender ||= participants.where.not(gender: nil).distinct.count
end
def age_groups
[[16, 19],
[20, 24],
[25, 29],
[30, 34],
[35, 39],
[40, 44],
[45, 49],
[50, 54],
[55, 59],
[60, 64],
[65, 69],
[70, 74],
[75, 79],
[80, 84],
[85, 89],
[90, 300]
]
end
def participants_between_ages(from, to)
participants.between_ages(from, to)
end
def geozones
Geozone.all.order("name")
end
def geozone_stats
geozones.map { |geozone| GeozoneStats.new(geozone, participants) }
end
def range_description(start, finish)
if finish > 200
I18n.t("stats.age_more_than", start: start)
else
I18n.t("stats.age_range", start: start, finish: finish)
end
end
class_methods do
def stats_methods
%i[total_participants total_male_participants
total_female_participants total_unknown_gender_or_age
male_percentage female_percentage age_groups]
base_stats_methods + gender_methods + age_methods + geozone_methods
end
def base_stats_methods
%i[total_participants participations] + participation_check_methods
end
def participation_check_methods
PARTICIPATIONS.map { |participation| :"#{participation}?" }
end
def gender_methods
%i[total_male_participants total_female_participants male_percentage female_percentage]
end
def age_methods
[:participants_by_age]
end
def geozone_methods
%i[participants_by_geozone total_no_demographic_data]
end
def stats_cache(*method_names)

View 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

View File

@@ -6,6 +6,7 @@ class Poll < ApplicationRecord
include ActsAsParanoidAliases
include Notifiable
include Sluggable
include StatsVersionable
translates :name, touch: true
translates :summary, touch: true

View File

@@ -2,122 +2,123 @@ class Poll::Stats
include Statisticable
alias_method :poll, :resource
CHANNELS = Poll::Voter::VALID_ORIGINS
def self.stats_methods
%i[total_participants total_participants_web total_web_valid total_web_white total_web_null
total_participants_booth total_booth_valid total_booth_white total_booth_null
total_valid_votes total_white_votes total_null_votes valid_percentage_web valid_percentage_booth
total_valid_percentage white_percentage_web white_percentage_booth total_white_percentage
null_percentage_web null_percentage_booth total_null_percentage total_participants_web_percentage
total_participants_booth_percentage]
super +
%i[total_valid_votes total_white_votes total_null_votes
total_participants_web total_web_valid total_web_white total_web_null
total_participants_booth total_booth_valid total_booth_white total_booth_null
total_participants_letter total_letter_valid total_letter_white total_letter_null
total_participants_web_percentage total_participants_booth_percentage
total_participants_letter_percentage
valid_percentage_web valid_percentage_booth valid_percentage_letter total_valid_percentage
white_percentage_web white_percentage_booth white_percentage_letter total_white_percentage
null_percentage_web null_percentage_booth null_percentage_letter total_null_percentage]
end
def total_participants
total_participants_web + total_participants_booth
end
def channels
CHANNELS.select { |channel| send(:"total_participants_#{channel}") > 0 }
end
CHANNELS.each do |channel|
define_method :"total_participants_#{channel}" do
send(:"total_#{channel}_valid") +
send(:"total_#{channel}_white") +
send(:"total_#{channel}_null")
end
define_method :"total_participants_#{channel}_percentage" do
calculate_percentage(send(:"total_participants_#{channel}"), total_participants)
end
end
def total_web_valid
voters.where(origin: "web").count - total_web_white
end
def total_web_white
0
end
def total_web_null
0
end
def total_booth_valid
recounts.sum(:total_amount)
end
def total_booth_white
recounts.sum(:white_amount)
end
def total_booth_null
recounts.sum(:null_amount)
end
def total_letter_valid
voters.where(origin: "letter").count # TODO: count only valid votes
end
def total_letter_white
0 # TODO
end
def total_letter_null
0 # TODO
end
%i[valid white null].each do |type|
CHANNELS.each do |channel|
define_method :"#{type}_percentage_#{channel}" do
calculate_percentage(send(:"total_#{channel}_#{type}"), send(:"total_#{type}_votes"))
end
end
define_method :"total_#{type}_votes" do
send(:"total_web_#{type}") + send(:"total_booth_#{type}")
end
define_method :"total_#{type}_percentage" do
calculate_percentage(send(:"total_#{type}_votes"), total_participants)
end
end
def total_no_demographic_data
super + total_unregistered_booth
end
private
def total_participants
total_participants_web + total_participants_booth
end
def total_participants_web
total_web_valid + total_web_white + total_web_null
end
def total_participants_web_percentage
calculate_percentage(total_participants_web, total_participants)
end
def total_participants_booth
total_booth_valid + total_booth_white + total_booth_null
end
def total_participants_booth_percentage
calculate_percentage(total_participants_booth, total_participants)
end
def total_web_valid
voters.where(origin: "web").count - total_web_white
end
def valid_percentage_web
calculate_percentage(total_web_valid, total_valid_votes)
end
def total_web_white
0
end
def white_percentage_web
calculate_percentage(total_web_white, total_white_votes)
end
def total_web_null
0
end
def null_percentage_web
calculate_percentage(total_web_null, total_null_votes)
end
def total_booth_valid
recounts.sum(:total_amount)
end
def valid_percentage_booth
calculate_percentage(total_booth_valid, total_valid_votes)
end
def total_booth_white
recounts.sum(:white_amount)
end
def white_percentage_booth
calculate_percentage(total_booth_white, total_white_votes)
end
def total_booth_null
recounts.sum(:null_amount)
end
def null_percentage_booth
calculate_percentage(total_booth_null, total_null_votes)
end
def total_valid_votes
total_web_valid + total_booth_valid
end
def total_valid_percentage
calculate_percentage(total_valid_votes, total_participants)
end
def total_white_votes
total_web_white + total_booth_white
end
def total_white_percentage
calculate_percentage(total_white_votes, total_participants)
end
def total_null_votes
total_web_null + total_booth_null
end
def total_null_percentage
calculate_percentage(total_null_votes, total_participants)
def participant_ids
voters
end
def voters
poll.voters
@voters ||= poll.voters.select(:user_id)
end
def recounts
poll.recounts
@recounts ||= poll.recounts
end
def total_registered_booth
voters.where(origin: "booth").count
end
def total_unregistered_booth
[total_participants_booth - total_registered_booth, 0].max
end
stats_cache(*stats_methods)
stats_cache :voters, :recounts
def stats_cache(key, &block)
Rails.cache.fetch("polls_stats/#{poll.id}/#{key}/v12", &block)
Rails.cache.fetch("polls_stats/#{poll.id}/#{key}/#{version}", &block)
end
end

View File

@@ -0,0 +1,5 @@
class StatsVersion < ApplicationRecord
validates :process, presence: true
belongs_to :process, polymorphic: true
end

View File

@@ -55,6 +55,8 @@ class User < ApplicationRecord
scope :moderators, -> { joins(:moderator) }
scope :organizations, -> { joins(:organization) }
scope :officials, -> { where("official_level > 0") }
scope :male, -> { where(gender: "male") }
scope :female, -> { where(gender: "female") }
scope :newsletter, -> { where(newsletter: true) }
scope :for_render, -> { includes(:organization) }
scope :by_document, ->(document_type, document_number) do
@@ -70,6 +72,13 @@ class User < ApplicationRecord
string = "%#{search_string}%"
where("username ILIKE ? OR email ILIKE ? OR document_number ILIKE ?", string, string, string)
end
scope :between_ages, -> (from, to) do
where(
"date_of_birth > ? AND date_of_birth < ?",
to.years.ago.beginning_of_year,
from.years.ago.end_of_year
)
end
before_validation :clean_document_number

View File

@@ -32,7 +32,7 @@
<%= link_to t("budgets.results.link"), budget_results_path(@budget) %>
</li>
<li class="tabs-title">
<%= link_to t("budgets.stats.link"), budget_stats_path(@budget) %>
<%= link_to t("stats.budgets.link"), budget_stats_path(@budget) %>
</li>
<li class="tabs-title is-active">
<%= link_to t("budgets.executions.link"), budget_executions_path(@budget), class: "is-active" %>

View File

@@ -32,7 +32,7 @@
<%= link_to t("budgets.results.link"), budget_results_path(@budget), class: "is-active" %>
</li>
<li class="tabs-title">
<%= link_to t("budgets.stats.link"), budget_stats_path(@budget) %>
<%= link_to t("stats.budgets.link"), budget_stats_path(@budget) %>
</li>
<li class="tabs-title">
<%= link_to t("budgets.executions.link"), budget_executions_path(@budget) %>

View File

@@ -1,254 +1,163 @@
<% provide :title do %>
<%= t("budgets.stats.page_title", budget: @budget.name) %>
<%= t("stats.budgets.page_title", budget: @budget.name) %>
<% end %>
<% provide :social_media_meta_tags do %>
<%= render "shared/social_media_meta_tags",
social_url: budget_url(@budget),
social_url: budget_stats_url(@budget),
social_title: @budget.name,
social_description: @budget.description_finished %>
social_description: @budget.description_for_phase("finished") %>
<% end %>
<% cache [@stats] do %>
<div class="budgets-stats">
<div class="expanded no-margin-top padding header">
<div class="row">
<div class="small-12 column">
<%= back_link_to budgets_path %>
<h2 class="margin-top">
<%= t("budgets.stats.title") %><br>
<span><%= @budget.name %></span>
</h2>
</div>
</div>
</div>
<div class="row margin-top">
<div class="small-12 column">
<ul class="tabs">
<li class="tabs-title">
<span class="show-for-sr"><%= t("shared.you_are_in") %></span>
<%= link_to t("budgets.results.link"), budget_results_path(@budget) %>
</li>
<li class="tabs-title is-active">
<%= link_to t("budgets.stats.link"), budget_stats_path(@budget), class: "is-active" %>
</li>
<li class="tabs-title">
<%= link_to t("budgets.executions.link"), budget_executions_path(@budget) %>
</li>
</ul>
</div>
</div>
<div class="row">
<div class="small-12 medium-6 column">
<div class="callout">
<span class="uppercase"><%= t("budgets.stats.total_participants") %></span>
<p id="total_participants" class="big-number-stat budget">
<%= @stats[:total_participants] %>
</p>
<span class="uppercase"><%= t("budgets.stats.total_budget_investments") %></span>
<p class="big-number-stat budget">
<%= @stats[:total_budget_investments] %>
</p>
</div>
</div>
<div class="small-12 medium-6 column">
<p>
<span class="label feasible"></span>&nbsp;
<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>&nbsp;
<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>&nbsp;
<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>&nbsp;
<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>&nbsp;
<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>
&nbsp;<%= 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>
&nbsp;<%= t("budgets.stats.total_male_participants").upcase %>
(<%= number_to_percentage(@stats[:male_percentage],
strip_insignificant_zeros: true,
precision: 2) %>)
</p>
<p id="total_male_participants" class="big-number-stat">
<%= @stats[:total_male_participants] %>
</p>
</div>
</div>
</div>
<div class="row margin">
<div class="small-12 column">
<h2 class="margin-bottom"><%= t("budgets.stats.by_age") %></h2>
<table>
<thead>
<tr>
<th scope="col" class="table-width border-right"><%= t("budgets.stats.age").upcase %></th>
<th scope="col" class="border-left"><%= t("budgets.stats.total").upcase %></th>
</tr>
</thead>
<tbody>
<% all_ages_count = @stats[:age_groups].values.sum.to_f %>
<% @stats[:age_groups].each do |age_group, count| %>
<tr id="age_group_<%= age_group.gsub(" - ", "_to_").gsub("+ ", "up_to_") %>">
<td class="border-right">
<%= age_group.gsub("+", t("budgets.stats.more_than")) + " " + t("budgets.stats.years") %>
</td>
<td class="border-left">
<strong>
<%
percentage_age_count = all_ages_count == 0 ? 0 : (count / all_ages_count * 100)
formatted_percentage_age_count = number_to_percentage(percentage_age_count,
strip_insignificant_zeros: true,
precision: 2)
%>
<%= count %>
(<%= formatted_percentage_age_count %>)
</strong>
<div class="progress" role="progressbar" tabindex="0" aria-valuenow="20" aria-valuemin="0" aria-valuetext="<%= percentage_age_count %>" aria-valuemax="100">
<span class="progress-meter" style="width: <%= number_to_percentage(percentage_age_count*5,
strip_insignificant_zeros: true,
precision: 2, locale: :en) %>;"></span>
</div>
</td>
</tr>
<% end %>
</tbody>
</table>
</div>
</div>
<div class="participation-stats budgets-stats">
<div class="expanded no-margin-top padding header">
<div class="row">
<div class="small-12 column">
<h2 class="margin-bottom"><%= t("budgets.stats.by_heading") %></h2>
<%= back_link_to budgets_path %>
<h2 class="margin-top">
<%= t("stats.title") %><br>
<span><%= @budget.name %></span>
</h2>
</div>
</div>
</div>
<table class="stats-districts survey-districts">
<thead>
<tr>
<th scope="col" rowspan="2"><%= t("budgets.stats.heading") %></th>
<th scope="col" rowspan="2"><%= t("budgets.stats.investments_sent_html") %></th>
<th scope="col" colspan="3"><%= t("budgets.stats.participants_support_phase") %></th>
<th scope="col" colspan="3"><%= t("budgets.stats.participants_voting_phase") %></th>
<th scope="col" colspan="3"><%= t("budgets.stats.participants_total") %></th>
</tr>
<tr>
<th scope="col" class="tiny"><%= t("budgets.stats.total") %></th>
<th scope="col" class="tiny"><%= t("budgets.stats.percent_total_participants_html") %></th>
<th scope="col" class="tiny"><%= t("budgets.stats.percent_heading_census_html") %></th>
<th scope="col" class="tiny"><%= t("budgets.stats.total") %></th>
<th scope="col" class="tiny"><%= t("budgets.stats.percent_total_participants_html") %></th>
<th scope="col" class="tiny"><%= t("budgets.stats.percent_heading_census_html") %></th>
<th scope="col" class="tiny"><%= t("budgets.stats.total") %></th>
<th scope="col" class="tiny"><%= t("budgets.stats.percent_total_participants_html") %></th>
<th scope="col" class="tiny"><%= t("budgets.stats.percent_heading_census_html") %></th>
</tr>
</thead>
<tbody id="headings">
<% @headings.each do |heading| %>
<tr id="<%= heading.name.parameterize %>">
<td class="border-left">
<strong><%= heading.name %></strong>
</td>
<td id="total_spending_proposals_heading_<%= heading.id %>"
class="text-center border-left border-right">
<%= @stats[:headings][heading.id][:total_investments_count] %>
</td>
<div class="row margin-top">
<div class="small-12 column">
<ul class="tabs">
<li class="tabs-title">
<span class="show-for-sr"><%= t("shared.you_are_in") %></span>
<%= link_to t("budgets.results.link"), budget_results_path(@budget) %>
</li>
<li class="tabs-title is-active">
<%= link_to t("stats.budgets.link"), budget_stats_path(@budget), class: "is-active" %>
</li>
<li class="tabs-title">
<%= link_to t("budgets.executions.link"), budget_executions_path(@budget) %>
</li>
</ul>
</div>
</div>
<% ["support", "vote", "all"].each do |phase| %>
<td id="total_participants_<%= phase %>_phase_heading_<%= heading.id %>"
class="border-left text-center">
<%= @stats[:headings][heading.id]["total_participants_#{phase}_phase".to_sym] %>
</td>
<td id="percentage_participants_<%= phase %>_phase_heading_<%= heading.id %>"
class="border-left border-right text-center">
<%= number_to_percentage(@stats[:headings][heading.id]["percentage_participants_#{phase}_phase".to_sym],
strip_insignificant_zeros: true,
precision: 2) %>
</td>
<td id="percentage_district_population_<%= phase %>_phase_heading_<%= heading.id %>"
class="text-center border-right">
<%= number_to_percentage(@stats[:headings][heading.id]["percentage_district_population_#{phase}_phase".to_sym],
strip_insignificant_zeros: true,
precision: 2) %>
</td>
<div class="row margin">
<div class="small-12 medium-3 column sidebar">
<%= render "shared/stats/links", stats: @stats %>
<p><strong><%= link_to t("stats.advanced"), "#advanced_statistics" %></strong></p>
<ul class="menu vertical">
<li>
<%= link_to t("stats.budgets.total_investments"), "#total_investments" %>
</li>
<li>
<%= link_to t("stats.budgets.by_phase"), "#stats_by_phase" %>
</li>
<li>
<%= link_to t("stats.budgets.by_heading"), "#stats_by_heading" %>
</li>
</ul>
</div>
<div class="small-12 medium-9 column stats-content">
<%= render "shared/stats/participation", stats: @stats %>
<div id="advanced_statistics">
<h3 class="section-title"><%= t("stats.advanced") %></h3>
<div id="total_investments" class="stats-group">
<h4><%= t("stats.budgets.total_investments") %></h4>
<%= number_with_info_tags(
@stats.total_budget_investments,
t("stats.budgets.total_investments"),
html_class: "total-investments"
) %>
<%= number_with_info_tags(@stats.total_unfeasible_investments,
t("stats.budgets.total_unfeasible_investments")) %>
<%= number_with_info_tags(@stats.total_selected_investments,
t("stats.budgets.total_selected_investments")) %>
</div>
<div id="stats_by_phase" class="stats-group">
<h4><%= t("stats.budgets.by_phase") %></h4>
<% @stats.phases.each do |phase| %>
<%= number_with_info_tags(
@stats.send("total_participants_#{phase}_phase"),
t("stats.budgets.participants_#{phase}_phase")
) %>
<% end %>
</div>
<div id="stats_by_heading" class="stats-group">
<h4 class="margin-bottom"><%= t("stats.budgets.by_heading") %></h4>
<table class="stats-districts survey-districts">
<thead>
<tr>
<th scope="col" rowspan="2"><%= t("stats.budgets.heading") %></th>
<th scope="col" rowspan="2"><%= t("stats.budgets.investments_sent_html") %></th>
<% @stats.all_phases.each do |phase| %>
<th scope="col" colspan="3">
<%= t("stats.budgets.participants_#{phase}_phase") %>
</th>
<% end %>
</tr>
<% end %>
</tbody>
</table>
</div>
</div>
<tr>
<% @stats.all_phases.each do %>
<th scope="col" class="phase-subheader"><%= t("stats.budgets.total") %></th>
<th scope="col" class="phase-subheader"><%= t("stats.budgets.percent_total_participants") %></th>
<th scope="col" class="phase-subheader"><%= t("stats.budgets.percent_heading_census") %></th>
<% end %>
</tr>
</thead>
<tbody id="headings">
<% @headings.each do |heading| %>
<tr id="<%= heading.name.parameterize %>">
<td class="border-left">
<strong><%= heading.name %></strong>
</td>
<td id="total_spending_proposals_heading_<%= heading.id %>"
class="text-center border-left border-right">
<%= @stats.headings[heading.id][:total_investments_count] %>
</td>
<div class="row margin">
<div class="small-12 column">
<div id="total_unknown_gender_or_age">
<p class="help-text">
<%= t("budgets.stats.no_demographic_data", total: @stats[:total_unknown_gender_or_age]) %>
</p>
<p class="help-text">
<%= t("budgets.stats.participatory_disclaimer") %>
</p>
<p class="help-text">
<%= t("budgets.stats.heading_disclaimer") %>
</p>
<% @stats.all_phases.each do |phase| %>
<td id="total_participants_<%= phase %>_phase_heading_<%= heading.id %>"
class="border-left text-center">
<%= @stats.headings[heading.id]["total_participants_#{phase}_phase".to_sym] %>
</td>
<td id="percentage_participants_<%= phase %>_phase_heading_<%= heading.id %>"
class="border-left border-right text-center">
<%= number_to_stats_percentage(@stats.headings[heading.id]["percentage_participants_#{phase}_phase".to_sym]) %>
</td>
<td id="percentage_district_population_<%= phase %>_phase_heading_<%= heading.id %>"
class="text-center border-right">
<%= number_to_stats_percentage(@stats.headings[heading.id]["percentage_district_population_#{phase}_phase".to_sym]) %>
</td>
<% end %>
</tr>
<% end %>
</tbody>
</table>
</div>
</div>
<div class="row margin">
<div class="small-12 column">
<div id="total_no_demographic_data">
<p class="help-text">
<%= t("stats.no_demographic_data", count: @stats.total_no_demographic_data) %>
</p>
<p class="help-text">
<%= t("stats.budgets.participatory_disclaimer") %>
</p>
<p class="help-text">
<%= t("stats.budgets.heading_disclaimer") %>
</p>
</div>
</div>
</div>
</div>
</div>
<% end %>
</div>

View File

@@ -7,7 +7,7 @@
<%= link_to t("polls.poll_header.back_to_proposal"), [@poll.related] %>
<% end %>
<h2><%= @poll.name %></h2>
<h1><%= @poll.name %></h1>
<%= safe_html_with_links simple_format(@poll.summary) %>

View File

@@ -1,95 +1,126 @@
<% provide :title do %><%= @poll.name %><% end %>
<div class="polls-results-stats">
<div class="participation-stats polls-results-stats">
<%= render "poll_header" %>
<%= render "poll_subnav" %>
<div class="row margin" data-equalizer data-equalize-on="medium">
<div class="small-12 medium-3 column sidebar" data-equalizer-watch>
<p><strong><%= t("polls.show.stats.title") %></strong></p>
<div class="row margin">
<div class="small-12 medium-3 column sidebar">
<%= render "shared/stats/links", stats: @stats %>
<ul class="menu vertical margin-top">
<li><a href="#total"><%= t("polls.show.stats.total_participation") %></a></li>
<p><strong><%= link_to t("stats.advanced"), "#advanced_statistics" %></strong></p>
<ul class="menu vertical">
<li>
<%= link_to t("stats.polls.by_channel"), "#stats_by_channel" %>
</li>
<li>
<%= link_to t("stats.polls.vote_by_channel"), "#vote_stats_by_channel" %>
</li>
</ul>
</div>
<div class="small-12 medium-9 column" data-equalizer-watch>
<h3 id="total"><%= t("polls.show.stats.total_participation") %></h3>
<div class="small-12 medium-9 column stats-content">
<%= render "shared/stats/participation", stats: @stats %>
<p class="margin-top uppercase">
<%= t("polls.show.stats.total_votes") %><br>
<span class="number"><%= @stats[:total_participants] %></span>
</p>
<div id="advanced_statistics">
<h3 class="section-title"><%= t("stats.advanced") %></h3>
<table>
<thead>
<tr>
<th scope="col"><%= t("polls.show.stats.votes") %></th>
<th scope="col"><%= t("polls.show.stats.web") %></th>
<th scope="col"><%= t("polls.show.stats.booth") %></th>
<th scope="col"><%= t("polls.show.stats.total") %></th>
</tr>
</thead>
<tbody>
<tr>
<th scope="row"><%= t("polls.show.stats.valid") %></th>
<td>
<%= @stats[:total_web_valid] %>
<small><em>(<%= @stats[:valid_percentage_web].round(2) %>%)</em></small>
</td>
<td>
<%= @stats[:total_booth_valid] %>
<small><em>(<%= @stats[:valid_percentage_booth].round(2) %>%)</em></small>
</td>
<td>
<%= @stats[:total_valid_votes] %>
<small><em>(<%= @stats[:total_valid_percentage].round(2) %>%)</em></small>
</td>
</tr>
<tr>
<th scope="row"><%= t("polls.show.stats.white") %></th>
<td>
<%= @stats[:total_web_white] %>
<small><em>(<%= @stats[:white_percentage_web].round(2) %>%)</em></small>
</td>
<td>
<%= @stats[:total_booth_white] %>
<small><em>(<%= @stats[:white_percentage_booth].round(2) %>%)</em></small>
</td>
<td><%= @stats[:total_white_votes] %>
<small><em>(<%= @stats[:total_white_percentage].round(2) %>%)</em></small>
</td>
</tr>
<tr>
<th scope="row"><%= t("polls.show.stats.null_votes") %></th>
<td>
<%= @stats[:total_web_null] %>
<small><em>(<%= @stats[:null_percentage_web].round(2) %>%)</em></small>
</td>
<td>
<%= @stats[:total_booth_null] %>
<small><em>(<%= @stats[:null_percentage_booth].round(2) %>%)</em></small>
</td>
<td>
<%= @stats[:total_null_votes] %>
<small><em>(<%= @stats[:total_null_percentage].round(2) %>%)</em></small>
</td>
</tr>
<tr>
<th scope="row"><%= t("polls.show.stats.total") %></th>
<td>
<%= @stats[:total_participants_web] %>
<small><em>(<%= @stats[:total_participants_web_percentage].round(2) %>%)</em></small>
</td>
<td>
<%= @stats[:total_participants_booth] %>
<small><em>(<%= @stats[:total_participants_booth_percentage].round(2) %>%)</em></small>
</td>
<td><%= @stats[:total_participants] %></td>
</tr>
</tbody>
</table>
<div id="stats_by_channel" class="stats-group">
<h4><%= t("stats.polls.by_channel") %></h4>
<% @stats.channels.each do |channel| %>
<%= number_with_info_tags(
@stats.send("total_participants_#{channel}"),
t("stats.polls.#{channel}_percentage",
percentage: number_to_stats_percentage(@stats.send(:"total_participants_#{channel}_percentage"))
),
html_class: channel
) %>
<% end %>
</div>
<div id="vote_stats_by_channel" class="stats-group">
<h4><%= t("stats.polls.vote_by_channel") %></h4>
<table class="stack">
<thead>
<tr>
<th scope="col"><%= t("polls.show.stats.votes") %></th>
<% @stats.channels.each do |channel| %>
<th scope="col"><%= t("polls.show.stats.#{channel}") %></th>
<% end %>
<th scope="col"><%= t("polls.show.stats.total") %></th>
</tr>
</thead>
<tbody>
<tr>
<th scope="row"><%= t("polls.show.stats.valid") %></th>
<% @stats.channels.each do |channel| %>
<td>
<%= @stats.send(:"total_#{channel}_valid") %>
<small><em>(<%= @stats.send(:"valid_percentage_#{channel}").round(2) %>%)</em></small>
</td>
<% end %>
<td>
<%= @stats.total_valid_votes %>
<small><em>(<%= @stats.total_valid_percentage.round(2) %>%)</em></small>
</td>
</tr>
<tr>
<th scope="row"><%= t("polls.show.stats.white") %></th>
<% @stats.channels.each do |channel| %>
<td>
<%= @stats.send(:"total_#{channel}_white") %>
<small><em>(<%= @stats.send(:"white_percentage_#{channel}").round(2) %>%)</em></small>
</td>
<% end %>
<td><%= @stats.total_white_votes %>
<small><em>(<%= @stats.total_white_percentage.round(2) %>%)</em></small>
</td>
</tr>
<tr>
<th scope="row"><%= t("polls.show.stats.null_votes") %></th>
<% @stats.channels.each do |channel| %>
<td>
<%= @stats.send(:"total_#{channel}_null") %>
<small><em>(<%= @stats.send(:"null_percentage_#{channel}").round(2) %>%)</em></small>
</td>
<% end %>
<td>
<%= @stats.total_null_votes %>
<small><em>(<%= @stats.total_null_percentage.round(2) %>%)</em></small>
</td>
</tr>
<tr>
<th scope="row"><%= t("polls.show.stats.total") %></th>
<% @stats.channels.each do |channel| %>
<td>
<%= @stats.send(:"total_participants_#{channel}") %>
<small><em>(<%= @stats.send(:"total_participants_#{channel}_percentage").round(2) %>%)</em></small>
</td>
<% end %>
<td><%= @stats.total_participants %></td>
</tr>
</tbody>
</table>
</div>
<div id="total_no_demographic_data">
<p class="help-text">
<%= t("stats.no_demographic_data", count: @stats.total_no_demographic_data) %>
</p>
</div>
</div>
</div>
</div>

View 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>

View 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>

View 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>

View 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>

View 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>

View File

@@ -45,6 +45,7 @@ data:
- config/locales/%{locale}/user_groups.yml
- config/locales/%{locale}/i18n.yml
- config/locales/%{locale}/milestones.yml
- config/locales/%{locale}/stats.yml
# Locale files to write new keys to, based on a list of key pattern => file rules. Matched from top to bottom:
# `i18n-tasks normalize -p` will force move the keys according to these rules
@@ -193,6 +194,7 @@ ignore_unused:
- "admin.site_customization.pages.page.status_*"
- "admin.legislation.processes.process.*"
- "legislation.processes.index.*"
- "stats.budgets.participants_*_phase"
- "votes.budget_investments.different_heading_assigned*"
- "*.form.map_skip_checkbox"
# - "{devise,kaminari,will_paginate}.*"

View File

@@ -191,35 +191,3 @@ en:
dates_range_invalid: "Start date can't be equal or later than End date"
prev_phase_dates_invalid: "Start date must be later than the start date of the previous enabled phase (%{phase_name})"
next_phase_dates_invalid: "End date must be earlier than the end date of the next enabled phase (%{phase_name})"
stats:
title: Participation stats
link: Stats
page_title: "%{budget} - Participation stats"
total_participants: Total Participants
total_budget_investments: Total Proposed Investments
total_selected_investments: Proposals on final phase
total_unfeasible_investments: Unfeasible proposals
total_participants_support_phase: Support phase
total_participants_web: Vote phase web participants
total_participants_booths: Vote phase booth participants
participants: Participants
supports: Supports
by_gender: "Participants by gender"
total_male_participants: Mens
total_female_participants: Women
by_age: "Participants by age groups"
age: Age
total: Total
more_than: More than
years: years
by_heading: "Participants by heading"
heading: Heading
investments_sent_html: "Investment proposals sent"
participants_support_phase: Participants support phase
participants_voting_phase: Participants voting phase
participants_total: Total Participants
percent_total_participants_html: "% <br>Total<br>Participants"
percent_heading_census_html: "% <br>Heading<br>Census"
no_demographic_data: "* There is no demographic data for %{total} participants."
participatory_disclaimer: "** The numbers of total participation refer to persons that created, supported or voted investment proposals."
heading_disclaimer: "*** Data about headings refer to the heading where each user voted, not necessarily the one that person is registered on."

View File

@@ -650,15 +650,14 @@ en:
stats_menu: "Participation statistics"
results_menu: "Poll results"
stats:
title: "Participation data"
total_participation: "Total participation"
total_votes: "Total amount of given votes"
votes: "VOTES"
web: "WEB"
mail: "MAIL"
booth: "BOOTH"
total: "TOTAL"
valid: "Valid"
white: "White votes"
white: "Fully blank"
null_votes: "Invalid"
results:
title: "Questions"

View 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"

View File

@@ -191,35 +191,3 @@ es:
dates_range_invalid: "La fecha de comienzo no puede ser igual o superior a la de finalización"
prev_phase_dates_invalid: "La fecha de inicio debe ser posterior a la fecha de inicio de la anterior fase habilitada (%{phase_name})"
next_phase_dates_invalid: "La fecha de fin debe ser anterior a la fecha de fin de la siguiente fase habilitada (%{phase_name})"
stats:
title: Estadísticas de participación
link: Estadísticas
page_title: "%{budget} - Estadísticas de participación"
total_participants: Total participantes
total_budget_investments: Total propuestas enviadas
total_selected_investments: Propuestas en la fase final
total_unfeasible_investments: Propuestas inviables
total_participants_support_phase: Fase de apoyos
total_participants_web: Participantes web en votación final
total_participants_booths: Participantes en urnas votación final
participants: Participantes
supports: Apoyos
total: Total
by_gender: "Participación por género"
total_male_participants: Hombres
total_female_participants: Mujeres
by_age: "Participación por grupos de edad"
age: Edad
more_than: Más de
years: años
by_heading: "Participación por distritos"
heading: Distrito
investments_sent_html: "Propuestas<br>enviadas"
participants_support_phase: Participación fase apoyos
participants_voting_phase: Participación fase votación
participants_total: Participación total
percent_total_participants_html: "% <br>Total<br>Participantes"
percent_heading_census_html: "% <br>Censo<br>Distrito"
no_demographic_data: "* No se dispone de los datos demográficos de %{total} participantes."
participatory_disclaimer: "** Las cifras de participación total se refieren a personas que han creado, apoyado o votado propuestas."
heading_disclaimer: "*** Los datos de distrito se refieren al distrito en el que el usuario ha votado, no necesariamente en el que está empadronado."

View File

@@ -650,15 +650,14 @@ es:
stats_menu: "Estadísticas de participación"
results_menu: "Resultados de la votación"
stats:
title: "Datos de participación"
total_participation: "Participación total"
total_votes: "Nº total de votos emitidos"
votes: "VOTOS"
web: "WEB"
booth: "URNAS"
mail: "CORREO"
total: "TOTAL"
valid: "Válidos"
white: "En blanco"
white: "Totalmente en blanco"
null_votes: "Nulos"
results:
title: "Preguntas"

View 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"

View File

@@ -27,6 +27,10 @@ every 1.day, at: "5:00 am" do
rake "-s sitemap:refresh"
end
every 2.hours do
rake "-s stats:generate"
end
# Temporally not send dashboard's notifications
# every 1.day, at: "7:00 am" do
# rake "dashboards:send_notifications"

View 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

View File

@@ -1334,6 +1334,14 @@ ActiveRecord::Schema.define(version: 20190411090023) do
t.index ["tsv"], name: "index_spending_proposals_on_tsv", using: :gin
end
create_table "stats_versions", force: :cascade do |t|
t.string "process_type"
t.integer "process_id"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["process_type", "process_id"], name: "index_stats_versions_on_process_type_and_process_id", using: :btree
end
create_table "taggings", force: :cascade do |t|
t.integer "tag_id"
t.integer "taggable_id"

24
lib/geozone_stats.rb Normal file
View 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

View 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
View 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

View File

@@ -74,13 +74,15 @@ FactoryBot.define do
end
factory :budget_heading, class: "Budget::Heading" do
association :group, factory: :budget_group
sequence(:name) { |n| "Heading #{n}" }
price 1000000
population 1234
latitude "40.416775"
longitude "-3.703790"
transient { budget nil }
group { association :budget_group, budget: budget || association(:budget) }
trait :drafting_budget do
association :group, factory: [:budget_group, :drafting_budget]
end
@@ -88,7 +90,7 @@ FactoryBot.define do
factory :budget_investment, class: "Budget::Investment" do
sequence(:title) { |n| "Budget Investment #{n} title" }
association :heading, factory: :budget_heading
heading { association :budget_heading, budget: budget }
association :author, factory: :user
description "Spend money on this"
price 10
@@ -189,8 +191,13 @@ FactoryBot.define do
end
factory :budget_ballot_line, class: "Budget::Ballot::Line" do
association :ballot, factory: :budget_ballot
association :investment, factory: :budget_investment
transient { user nil }
ballot do
association :budget_ballot, budget: investment.budget.reload, user: user || association(:user)
end
end
factory :budget_reclassified_vote, class: "Budget::ReclassifiedVote" do

View File

@@ -88,10 +88,13 @@ FactoryBot.define do
end
factory :poll_voter, class: "Poll::Voter" do
poll
association :user, :level_two
from_web
transient { budget nil }
poll { budget&.poll || association(:poll, budget: budget) }
trait :from_web do
origin "web"
token SecureRandom.hex(32)

View File

@@ -8,25 +8,16 @@ feature "Stats" do
describe "Show" do
it "is not accessible to normal users if phase is not 'finished'" do
budget.update(phase: "reviewing_ballots")
it "is not accessible if supports phase is not finished" do
budget.update(phase: "selecting")
visit budget_stats_path(budget.id)
expect(page).to have_content "You do not have permission to carry out the action "\
"'read_stats' on budget."
end
it "is accessible to normal users if phase is 'finished'" do
budget.update(phase: "finished")
visit budget_stats_path(budget.id)
expect(page).to have_content "Stats"
end
it "is accessible to administrators when budget has phase 'reviewing_ballots'" do
budget.update(phase: "reviewing_ballots")
login_as(create(:administrator).user)
it "is accessible if supports phase is finished" do
budget.update(phase: "valuating")
visit budget_stats_path(budget.id)
expect(page).to have_content "Stats"

View 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

View File

@@ -7,17 +7,8 @@ describe Budget::Phase do
let(:second_phase) { budget.phases.informing }
let(:third_phase) { budget.phases.accepting }
let(:fourth_phase) { budget.phases.reviewing }
let(:fifth_phase) { budget.phases.selecting }
let(:final_phase) { budget.phases.finished}
before do
first_phase.update_attributes(starts_at: Date.current - 3.days, ends_at: Date.current - 1.day)
second_phase.update_attributes(starts_at: Date.current - 1.days, ends_at: Date.current + 1.day)
third_phase.update_attributes(starts_at: Date.current + 1.days, ends_at: Date.current + 3.day)
fourth_phase.update_attributes(starts_at: Date.current + 3.days, ends_at: Date.current + 5.day)
fifth_phase.update_attributes(starts_at: Date.current + 5.days, ends_at: Date.current + 7.day)
end
describe "validates" do
it "is not valid without a budget" do
expect(build(:budget_phase, budget: nil)).not_to be_valid

View File

@@ -1,212 +1,287 @@
require "rails_helper"
describe Budget::Stats do
let(:budget) { create(:budget, :finished) }
let(:stats) { Budget::Stats.new(budget) }
let(:investment) { create(:budget_investment, :selected, budget: budget) }
before(:each) do
@budget = create(:budget)
@group = create(:budget_group, budget: @budget)
@heading = create(:budget_heading, group: @group, price: 1000)
describe "#participants" do
let(:author) { investment.author }
let(:author_and_voter) { create(:user, hidden_at: Time.current) }
let(:voter) { create(:user) }
let(:voter_and_balloter) { create(:user) }
let(:balloter) { create(:user, hidden_at: Time.current) }
let(:poll_balloter) { create(:user, :level_two) }
let(:non_participant) { create(:user, :level_two) }
@investment1 = create(:budget_investment, :selected, author: create(:user, gender: "female"),
heading: @heading, price: 200, ballot_lines_count: 900, winner: true)
@investment2 = create(:budget_investment, :selected, author: create(:user, gender: "female"),
heading: @heading, price: 300, ballot_lines_count: 800, winner: true)
@investment3 = create(:budget_investment, :selected, author: create(:user, gender: "female",
date_of_birth: 40.years.ago), heading: @heading, price: 400,
ballot_lines_count: 880, winner: true)
@investment4 = create(:budget_investment, :selected, author: create(:user, gender: "male"),
heading: @heading, price: 100, ballot_lines_count: 915, winner: true)
@investment5 = create(:budget_investment, :unfeasible, author: create(:user, gender: "male",
date_of_birth: 25.years.ago), heading: @heading)
before do
create(:budget_investment, :selected, budget: budget, author: author_and_voter)
@support1 = create(:vote, votable: @investment1, voter: create(:user, gender: "male"))
@support2 = create(:vote, votable: @investment2, voter: create(:user))
create(:vote, votable: investment, voter: author_and_voter)
create(:vote, votable: investment, voter: voter)
create(:vote, votable: investment, voter: voter_and_balloter)
@budget_ballot1 = create(:budget_ballot, budget: @budget, user: create(:user, gender: "female",
date_of_birth: 54.years.ago))
@budget_ballot2 = create(:budget_ballot, budget: @budget, user: create(:user, gender: "female"))
@budget_ballot3 = create(:budget_ballot, budget: @budget, user: create(:user, gender: "male"))
create(:budget_ballot_line, investment: investment, user: balloter)
create(:budget_ballot_line, investment: investment, user: voter_and_balloter)
@budget_ballot_line1 = create(:budget_ballot_line, ballot: @budget_ballot1,
investment: @investment1)
@budget_ballot_line2 = create(:budget_ballot_line, ballot: @budget_ballot2,
investment: @investment2)
@budget_ballot_line3 = create(:budget_ballot_line, ballot: @budget_ballot3,
investment: @investment3)
create(:poll_voter, :from_booth, user: poll_balloter, budget: budget)
@poll = create(:poll, budget: @budget)
@poll_voter = create(:poll_voter, :from_booth, poll: @poll)
@budget_ballot4 = create(:budget_ballot, budget: @budget, physical: true, user: nil)
@budget_ballot_line4 = create(:budget_ballot_line, ballot: @budget_ballot4,
investment: @investment4)
@stats = Budget::Stats.new(@budget).generate
end
context "#total_participants" do
it "returns the number of total participants" do
expect(@stats[:total_participants]).to be 11
create(:poll_voter, :from_booth, user: non_participant, budget: create(:budget))
end
it "returns unique participants, including authors and hidden users" do
expect(stats.participants).to match_array(
[author, author_and_voter, voter, voter_and_balloter, balloter, poll_balloter]
)
expect(stats.total_participants).to be 6
end
end
context "#total_participants_support_phase" do
describe "#total_participants_support_phase" do
it "returns the number of total participants in the support phase" do
expect(@stats[:total_participants_support_phase]).to be 2
2.times { create(:vote, votable: investment) }
create(:budget_ballot_line, investment: investment)
expect(stats.total_participants_support_phase).to be 2
end
it "counts a user who is voter and balloter" do
voter_and_balloter = create(:user)
create(:vote, votable: investment, voter: voter_and_balloter)
create(:budget_ballot_line, investment: investment, user: voter_and_balloter)
expect(stats.total_participants_support_phase).to be 1
end
end
context "#total_participants_vote_phase" do
describe "#total_participants_vote_phase" do
it "returns the number of total participants in the votes phase" do
expect(@stats[:total_participants_vote_phase]).to be 4
2.times { create(:budget_ballot_line, investment: investment) }
create(:vote, votable: investment)
expect(stats.total_participants_vote_phase).to be 2
end
end
it "counts a user who is voter and balloter" do
voter_and_balloter = create(:user)
create(:vote, votable: investment, voter: voter_and_balloter)
create(:budget_ballot_line, investment: investment, user: voter_and_balloter)
context "#total_participants_web" do
it "returns the number of total participants in the votes phase via web" do
expect(@stats[:total_participants_web]).to be 3
expect(stats.total_participants_vote_phase).to be 1
end
end
it "includes balloters and poll balloters" do
create(:budget_ballot_line, investment: investment)
create(:poll_voter, :from_booth, budget: budget)
context "#total_participants_booths" do
it "returns the number of total participants in the votes phase in booths" do
expect(@stats[:total_participants_booths]).to be 1
expect(stats.total_participants_vote_phase).to be 2
end
it "counts once a user who is balloter and poll balloter" do
poller_and_balloter = create(:user, :level_two)
create(:budget_ballot_line, investment: investment, user: poller_and_balloter)
create(:poll_voter, :from_booth, user: poller_and_balloter, budget: budget)
expect(stats.total_participants_vote_phase).to be 1
end
it "doesn't count nil user ids" do
create(:budget_ballot_line, investment: investment,
ballot: create(:budget_ballot, budget: budget, user: nil, physical: true)
)
expect(stats.total_participants_vote_phase).to be 0
end
end
context "#total_budget_investments" do
describe "#total_budget_investments" do
it "returns the number of total budget investments" do
expect(@stats[:total_budget_investments]).to be 5
end
2.times { create(:budget_investment, budget: budget) }
create(:budget_investment, budget: create(:budget))
expect(stats.total_budget_investments).to be 2
end
end
context "#total_votes" do
describe "#total_votes" do
it "returns the number of total votes" do
expect(@stats[:total_votes]).to be 4
end
create(:budget_ballot_line, investment: investment)
create(:budget_ballot_line, investment: create(:budget_investment, :selected, budget: budget))
expect(stats.total_votes).to be 2
end
end
context "#total_selected_investments" do
describe "#total_selected_investments" do
it "returns the number of total selected investments" do
expect(@stats[:total_selected_investments]).to be 4
end
3.times { create(:budget_investment, :selected, budget: budget) }
create(:budget_investment, :selected, budget: create(:budget))
create(:budget_investment, :unfeasible, budget: budget)
expect(stats.total_selected_investments).to be 3
end
end
context "#total_unfeasible_investments" do
describe "#total_unfeasible_investments" do
it "returns the number of total unfeasible investments" do
expect(@stats[:total_unfeasible_investments]).to be 1
end
3.times { create(:budget_investment, :unfeasible, budget: budget) }
create(:budget_investment, :unfeasible, budget: create(:budget))
create(:budget_investment, :selected, budget: budget)
expect(stats.total_unfeasible_investments).to be 3
end
end
context "#total_male_participants" do
describe "Participants by gender" do
before do
3.times { create(:user, gender: "male") }
2.times { create(:user, gender: "female") }
create(:user, gender: nil)
it "returns the number of total male participants" do
expect(@stats[:total_male_participants]).to be 4
allow(stats).to receive(:participants).and_return(User.all)
end
end
context "#total_female_participants" do
it "returns the number of total female participants" do
expect(@stats[:total_female_participants]).to be 6
describe "#total_male_participants" do
it "returns the number of total male participants" do
expect(stats.total_male_participants).to be 3
end
end
end
context "#total_supports" do
it "returns the number of total supports" do
expect(@stats[:total_supports]).to be 2
describe "#total_female_participants" do
it "returns the number of total female participants" do
expect(stats.total_female_participants).to be 2
end
end
end
context "#total_unknown_gender_or_age" do
it "returns the number of total unknown participants' gender or age" do
expect(@stats[:total_unknown_gender_or_age]).to be 1
describe "#male_percentage" do
it "returns the percentage of male participants" do
expect(stats.male_percentage).to be 60.0
end
end
describe "#female_percentage" do
it "returns the percentage of female participants" do
expect(stats.female_percentage).to be 40.0
end
end
end
context "#age_groups" do
describe "#participants_by_age" do
before do
[21, 22, 23, 23, 34, 42, 43, 44, 50, 51].each do |age|
create(:user, date_of_birth: age.years.ago)
end
allow(stats).to receive(:participants).and_return(User.all)
end
it "returns the age groups hash" do
expect(@stats[:age_groups]["16 - 19"]).to be 0
expect(@stats[:age_groups]["20 - 24"]).to be 7
expect(@stats[:age_groups]["25 - 29"]).to be 1
expect(@stats[:age_groups]["30 - 34"]).to be 0
expect(@stats[:age_groups]["35 - 39"]).to be 1
expect(@stats[:age_groups]["40 - 44"]).to be 1
expect(@stats[:age_groups]["45 - 49"]).to be 0
expect(@stats[:age_groups]["50 - 54"]).to be 1
expect(@stats[:age_groups]["55 - 59"]).to be 0
expect(@stats[:age_groups]["60 - 64"]).to be 0
expect(@stats[:age_groups]["65 - 69"]).to be 0
expect(@stats[:age_groups]["70 - 140"]).to be 0
expect(stats.participants_by_age["16 - 19"][:count]).to be 0
expect(stats.participants_by_age["20 - 24"][:count]).to be 4
expect(stats.participants_by_age["25 - 29"][:count]).to be 0
expect(stats.participants_by_age["30 - 34"][:count]).to be 1
expect(stats.participants_by_age["35 - 39"][:count]).to be 0
expect(stats.participants_by_age["40 - 44"][:count]).to be 3
expect(stats.participants_by_age["45 - 49"][:count]).to be 0
expect(stats.participants_by_age["50 - 54"][:count]).to be 2
expect(stats.participants_by_age["55 - 59"][:count]).to be 0
expect(stats.participants_by_age["60 - 64"][:count]).to be 0
expect(stats.participants_by_age["65 - 69"][:count]).to be 0
expect(stats.participants_by_age["70 - 74"][:count]).to be 0
end
end
context "#male_percentage" do
it "returns the percentage of male participants" do
expect(@stats[:male_percentage]).to be 40.0
describe "#headings" do
before do
investment.heading.update_column(:population, 1234)
create(:budget_investment, heading: investment.heading)
2.times { create(:vote, votable: investment) }
create(:budget_ballot_line, investment: investment)
end
end
context "#female_percentage" do
it "returns the percentage of female participants" do
expect(@stats[:female_percentage]).to be 60.0
end
end
context "#headings" do
it "returns headings data" do
heading_stats = @stats[:headings][@heading.id]
expect(heading_stats[:total_investments_count]).to be 5
heading_stats = stats.headings[investment.heading.id]
expect(heading_stats[:total_investments_count]).to be 2
expect(heading_stats[:total_participants_support_phase]).to be 2
expect(heading_stats[:total_participants_vote_phase]).to be 4
expect(heading_stats[:total_participants_all_phase]).to be 6
expect(heading_stats[:total_participants_vote_phase]).to be 1
expect(heading_stats[:total_participants_every_phase]).to be 3
expect(heading_stats[:percentage_participants_support_phase]).to be 100.0
expect(heading_stats[:percentage_district_population_support_phase]).to be 0.162
expect(heading_stats[:percentage_participants_vote_phase]).to be 100.0
expect(heading_stats[:percentage_district_population_vote_phase]).to be 0.324
expect(heading_stats[:percentage_participants_all_phase]).to be 100.0
expect(heading_stats[:percentage_district_population_all_phase]).to be 0.486
expect(heading_stats[:total_investments_count]).to be 5
expect(heading_stats[:total_participants_support_phase]).to be 2
expect(heading_stats[:total_participants_vote_phase]).to be 4
expect(heading_stats[:total_participants_all_phase]).to be 6
expect(heading_stats[:percentage_participants_support_phase]).to be 100.0
expect(heading_stats[:percentage_participants_vote_phase]).to be 100.0
expect(heading_stats[:percentage_participants_all_phase]).to be 100.0
expect(heading_stats[:percentage_district_population_vote_phase]).to be 0.081
expect(heading_stats[:percentage_participants_every_phase]).to be 100.0
expect(heading_stats[:percentage_district_population_every_phase]).to be 0.243
end
end
describe "#support_phase_finished?" do
context "support phase isn't finished" do
before { budget.phase = "selecting" }
it "is false" do
expect(stats.support_phase_finished?).to be false
end
end
context "support phase is finished" do
before { budget.phase = "valuating" }
it "is false" do
expect(stats.support_phase_finished?).to be true
end
end
end
describe "#vote_phase_finished" do
context "support phase isn't finished" do
before { budget.phase = "reviewing_ballots" }
it "is false" do
expect(stats.vote_phase_finished?).to be false
end
end
context "vote phase is finished" do
before { budget.phase = "finished" }
it "is false" do
expect(stats.vote_phase_finished?).to be true
end
end
end
describe "#all_phases" do
context "no phases are finished" do
before do
allow(stats).to receive(:support_phase_finished?).and_return(false)
allow(stats).to receive(:vote_phase_finished?).and_return(false)
end
it "returns an empty array" do
expect(stats.all_phases).to eq []
end
end
context "one phase is finished" do
before do
allow(stats).to receive(:support_phase_finished?).and_return(true)
allow(stats).to receive(:vote_phase_finished?).and_return(false)
end
it "returns the finished phase" do
expect(stats.all_phases).to eq ["support"]
end
end
context "all phases are finished" do
before do
allow(stats).to receive(:support_phase_finished?).and_return(true)
allow(stats).to receive(:vote_phase_finished?).and_return(true)
end
it "returns the finished phases and a total phase" do
expect(stats.all_phases).to eq ["support", "vote", "every"]
end
end
end
end

View File

@@ -97,33 +97,55 @@ describe Budget do
expect(budget).to be_finished
end
it "balloting_or_later?" do
budget.phase = "drafting"
expect(budget).not_to be_balloting_or_later
describe "#valuating_or_later?" do
it "returns false before valuating" do
budget.phase = "selecting"
expect(budget).not_to be_valuating_or_later
end
budget.phase = "accepting"
expect(budget).not_to be_balloting_or_later
it "returns true while valuating" do
budget.phase = "valuating"
expect(budget).to be_valuating_or_later
end
budget.phase = "reviewing"
expect(budget).not_to be_balloting_or_later
it "returns true after valuating" do
budget.phase = "publishing_prices"
expect(budget).to be_valuating_or_later
end
end
budget.phase = "selecting"
expect(budget).not_to be_balloting_or_later
describe "#publishing_prices_or_later?" do
it "returns false before publishing prices" do
budget.phase = "valuating"
expect(budget).not_to be_publishing_prices_or_later
end
budget.phase = "valuating"
expect(budget).not_to be_balloting_or_later
it "returns true while publishing prices" do
budget.phase = "publishing_prices"
expect(budget).to be_publishing_prices_or_later
end
budget.phase = "publishing_prices"
expect(budget).not_to be_balloting_or_later
it "returns true after publishing prices" do
budget.phase = "balloting"
expect(budget).to be_publishing_prices_or_later
end
end
budget.phase = "balloting"
expect(budget).to be_balloting_or_later
describe "#balloting_or_later?" do
it "returns false before balloting" do
budget.phase = "publishing_prices"
expect(budget).not_to be_balloting_or_later
end
budget.phase = "reviewing_ballots"
expect(budget).to be_balloting_or_later
it "returns true while balloting" do
budget.phase = "balloting"
expect(budget).to be_balloting_or_later
end
budget.phase = "finished"
expect(budget).to be_balloting_or_later
it "returns true after balloting" do
budget.phase = "finished"
expect(budget).to be_balloting_or_later
end
end
end

View File

@@ -1,44 +1,288 @@
require "rails_helper"
describe Poll::Stats do
let(:poll) { create(:poll) }
let(:stats) { Poll::Stats.new(poll) }
describe "#generate" do
it "generates the correct stats" do
poll = create(:poll)
2.times { create(:poll_voter, :from_web, poll: poll) }
3.times { create(:poll_voter, :from_booth, poll: poll) }
create(:poll_recount, :from_booth, poll: poll,
white_amount: 1, null_amount: 0, total_amount: 2)
describe "#participants" do
it "includes hidden users" do
create(:poll_voter, poll: poll)
create(:poll_voter, poll: poll, user: create(:user, :level_two, hidden_at: Time.current))
stats = Poll::Stats.new(poll).generate
expect(stats[:total_participants]).to eq(5)
expect(stats[:total_participants_web]).to eq(2)
expect(stats[:total_participants_booth]).to eq(3)
expect(stats[:total_valid_votes]).to eq(4)
expect(stats[:total_white_votes]).to eq(1)
expect(stats[:total_null_votes]).to eq(0)
expect(stats[:total_web_valid]).to eq(2)
expect(stats[:total_web_white]).to eq(0)
expect(stats[:total_web_null]).to eq(0)
expect(stats[:total_booth_valid]).to eq(2)
expect(stats[:total_booth_white]).to eq(1)
expect(stats[:total_booth_null]).to eq(0)
expect(stats[:total_participants_web_percentage]).to eq(40)
expect(stats[:total_participants_booth_percentage]).to eq(60)
expect(stats[:valid_percentage_web]).to eq(50)
expect(stats[:white_percentage_web]).to eq(0)
expect(stats[:null_percentage_web]).to eq(0)
expect(stats[:valid_percentage_booth]).to eq(50)
expect(stats[:white_percentage_booth]).to eq(100)
expect(stats[:null_percentage_booth]).to eq(0)
expect(stats[:total_valid_percentage]).to eq(80)
expect(stats[:total_white_percentage]).to eq(20)
expect(stats[:total_null_percentage]).to eq(0)
expect(stats.participants.count).to eq(2)
end
end
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

View 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

View 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