BIN
app/assets/images/budgets/stats/gender.png
Normal file
BIN
app/assets/images/budgets/stats/gender.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.0 KiB |
@@ -8,6 +8,7 @@ module Budgets
|
||||
def show
|
||||
authorize! :read_results, @budget
|
||||
@investments = Budget::Result.new(@budget, @heading).investments
|
||||
@headings = @budget.headings.sort_by_name
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
24
app/controllers/budgets/stats_controller.rb
Normal file
24
app/controllers/budgets/stats_controller.rb
Normal file
@@ -0,0 +1,24 @@
|
||||
module Budgets
|
||||
class StatsController < ApplicationController
|
||||
|
||||
before_action :load_budget
|
||||
load_and_authorize_resource :budget
|
||||
|
||||
def show
|
||||
authorize! :read_stats, @budget
|
||||
@stats = load_stats
|
||||
@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
|
||||
|
||||
end
|
||||
end
|
||||
@@ -66,6 +66,7 @@ module Abilities
|
||||
can [:hide, :update, :toggle_selection], Budget::Investment
|
||||
can [:valuate, :comment_valuation], Budget::Investment
|
||||
can :create, Budget::ValuatorAssignment
|
||||
can :read_stats, Budget, phase: "reviewing_ballots"
|
||||
|
||||
can [:search, :edit, :update, :create, :index, :destroy], Banner
|
||||
|
||||
|
||||
@@ -21,6 +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 :new, DirectMessage
|
||||
can [:read, :debate, :draft_publication, :allegations, :result_publication,
|
||||
:proposals, :milestones], Legislation::Process, published: true
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
class Budget
|
||||
class Ballot
|
||||
class Line < ApplicationRecord
|
||||
belongs_to :ballot
|
||||
belongs_to :ballot, counter_cache: :ballot_lines_count
|
||||
belongs_to :investment, counter_cache: :ballot_lines_count
|
||||
belongs_to :heading
|
||||
belongs_to :group
|
||||
|
||||
220
app/models/budget/stats.rb
Normal file
220
app/models/budget/stats.rb
Normal file
@@ -0,0 +1,220 @@
|
||||
class Budget
|
||||
class Stats
|
||||
|
||||
def initialize(budget)
|
||||
@budget = budget
|
||||
end
|
||||
|
||||
def generate
|
||||
stats = %w[total_participants total_participants_support_phase total_participants_vote_phase
|
||||
total_budget_investments total_votes total_selected_investments
|
||||
total_unfeasible_investments total_male_participants total_female_participants
|
||||
total_supports total_unknown_gender_or_age age_groups male_percentage
|
||||
female_percentage headings total_participants_web total_participants_booths]
|
||||
stats.map { |stat_name| [stat_name.to_sym, send(stat_name)] }.to_h
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def total_participants
|
||||
stats_cache("total_participants") { participants.distinct.count }
|
||||
end
|
||||
|
||||
def total_participants_support_phase
|
||||
stats_cache("total_participants_support_phase") { voters.uniq.count }
|
||||
end
|
||||
|
||||
def total_participants_web
|
||||
stats_cache("total_participants_web") do
|
||||
(balloters - poll_ballot_voters).uniq.compact.count
|
||||
end
|
||||
end
|
||||
|
||||
def total_participants_booths
|
||||
stats_cache("total_participants_booths") { poll_ballot_voters.uniq.count }
|
||||
end
|
||||
|
||||
def total_participants_vote_phase
|
||||
stats_cache("total_participants_vote_phase") { balloters.uniq.count }
|
||||
end
|
||||
|
||||
def total_budget_investments
|
||||
stats_cache("total_budget_investments") { @budget.investments.count }
|
||||
end
|
||||
|
||||
def total_votes
|
||||
stats_cache("total_votes") { @budget.ballots.pluck(:ballot_lines_count).inject(0) { |sum, x| sum + x } }
|
||||
end
|
||||
|
||||
def total_selected_investments
|
||||
stats_cache("total_selected_investments") { @budget.investments.selected.count }
|
||||
end
|
||||
|
||||
def total_unfeasible_investments
|
||||
stats_cache("total_unfeasible_investments") { @budget.investments.unfeasible.count }
|
||||
end
|
||||
|
||||
def total_male_participants
|
||||
stats_cache("total_male_participants") { participants.where(gender: "male").count }
|
||||
end
|
||||
|
||||
def total_female_participants
|
||||
stats_cache("total_female_participants") { participants.where(gender: "female").count }
|
||||
end
|
||||
|
||||
def total_supports
|
||||
stats_cache("total_supports") { supports(@budget).count }
|
||||
end
|
||||
|
||||
def total_unknown_gender_or_age
|
||||
stats_cache("total_unknown_gender_or_age") do
|
||||
participants.where("gender IS NULL OR date_of_birth is NULL").uniq.count
|
||||
end
|
||||
end
|
||||
|
||||
def age_groups
|
||||
stats_cache("age_groups") do
|
||||
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
|
||||
end
|
||||
|
||||
def male_percentage
|
||||
stats_cache("male_percentage") { total_male_participants / total_participants_with_gender.to_f * 100 }
|
||||
end
|
||||
|
||||
def female_percentage
|
||||
stats_cache("female_percentage") { total_female_participants / total_participants_with_gender.to_f * 100 }
|
||||
end
|
||||
|
||||
def participants
|
||||
stats_cache("participants") do
|
||||
User.where(id: (authors + voters + balloters + poll_ballot_voters).uniq.compact)
|
||||
end
|
||||
end
|
||||
|
||||
def authors
|
||||
stats_cache("authors") { @budget.investments.pluck(:author_id) }
|
||||
end
|
||||
|
||||
def voters
|
||||
stats_cache("voters") { supports(@budget).pluck(:voter_id) }
|
||||
end
|
||||
|
||||
def balloters
|
||||
stats_cache("balloters") { @budget.ballots.where("ballot_lines_count > ?", 0).pluck(:user_id) }
|
||||
end
|
||||
|
||||
def poll_ballot_voters
|
||||
stats_cache("poll_ballot_voters") do
|
||||
@budget&.poll ? @budget.poll.voters.pluck(:user_id) : []
|
||||
end
|
||||
end
|
||||
|
||||
def total_participants_with_gender
|
||||
stats_cache("total_participants_with_gender") { participants.where.not(gender: nil).distinct.count }
|
||||
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)
|
||||
end
|
||||
end
|
||||
|
||||
def voters_by_heading(heading)
|
||||
stats_cache("voters_by_heading_#{heading.id}") do
|
||||
supports(heading).pluck(:voter_id)
|
||||
end
|
||||
end
|
||||
|
||||
def headings
|
||||
stats_cache("headings") do
|
||||
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
|
||||
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)
|
||||
}
|
||||
end
|
||||
|
||||
def calculate_heading_stats_with_totals(heading_totals, groups_totals, population)
|
||||
{
|
||||
percentage_participants_support_phase: participants_percent(heading_totals, groups_totals, :total_participants_support_phase),
|
||||
percentage_district_population_support_phase: population_percent(population, heading_totals[:total_participants_support_phase]),
|
||||
percentage_participants_vote_phase: participants_percent(heading_totals, groups_totals, :total_participants_vote_phase),
|
||||
percentage_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])
|
||||
}
|
||||
end
|
||||
|
||||
def voters_and_balloters_by_heading(heading)
|
||||
(voters_by_heading(heading) + balloters_by_heading(heading.id)).uniq.count
|
||||
end
|
||||
|
||||
def participants_percent(heading_totals, groups_totals, phase)
|
||||
calculate_percentage(heading_totals[phase], groups_totals[phase])
|
||||
end
|
||||
|
||||
def population_percent(population, participants)
|
||||
return "N/A" unless population.to_f.positive?
|
||||
calculate_percentage(participants, population)
|
||||
end
|
||||
|
||||
def calculate_percentage(fraction, total)
|
||||
percent = fraction / total.to_f
|
||||
percent.nan? ? 0.0 : (percent * 100).round(3)
|
||||
end
|
||||
|
||||
def supports(supportable)
|
||||
ActsAsVotable::Vote.where(votable_type: "Budget::Investment", votable_id: supportable.investments.pluck(:id))
|
||||
end
|
||||
|
||||
def stats_cache(key, &block)
|
||||
Rails.cache.fetch("budgets_stats/#{@budget.id}/#{key}/v10", &block)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -31,6 +31,9 @@
|
||||
<li class="tabs-title">
|
||||
<%= 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) %>
|
||||
</li>
|
||||
<li class="tabs-title is-active">
|
||||
<%= link_to t("budgets.executions.link"), budget_executions_path(@budget), class: "is-active" %>
|
||||
</li>
|
||||
|
||||
@@ -31,6 +31,9 @@
|
||||
<span class="show-for-sr"><%= t("shared.you_are_in") %></span>
|
||||
<%= 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) %>
|
||||
</li>
|
||||
<li class="tabs-title">
|
||||
<%= link_to t("budgets.executions.link"), budget_executions_path(@budget) %>
|
||||
</li>
|
||||
@@ -43,10 +46,8 @@
|
||||
<h3 class="margin-bottom">
|
||||
<%= t("budgets.results.heading_selection_title") %>
|
||||
</h3>
|
||||
<ul class="menu vertical no-margin-top no-padding-top">
|
||||
|
||||
|
||||
<% @budget.headings.order("id ASC").each do |heading| %>
|
||||
<ul id="headings" class="menu vertical no-margin-top no-padding-top">
|
||||
<% @headings.each do |heading| %>
|
||||
<li>
|
||||
<%= link_to heading.name,
|
||||
budget_results_path(@budget, heading_id: heading.to_param),
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
|
||||
<% if @budget.finished? %>
|
||||
<%= link_to t("budgets.show.see_results"),
|
||||
budget_results_path(@budget, heading_id: @budget.headings.first),
|
||||
budget_results_path(@budget),
|
||||
class: "button margin-top expanded" %>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
254
app/views/budgets/stats/show.html.erb
Normal file
254
app/views/budgets/stats/show.html.erb
Normal file
@@ -0,0 +1,254 @@
|
||||
<% provide :title do %>
|
||||
<%= t("budgets.stats.page_title", budget: @budget.name) %>
|
||||
<% end %>
|
||||
<% provide :social_media_meta_tags do %>
|
||||
<%= render "shared/social_media_meta_tags",
|
||||
social_url: budget_url(@budget),
|
||||
social_title: @budget.name,
|
||||
social_description: @budget.description_finished %>
|
||||
<% end %>
|
||||
|
||||
<% cache [@stats] do %>
|
||||
<div class="budgets-stats">
|
||||
<div class="expanded no-margin-top padding header">
|
||||
<div class="row">
|
||||
<div class="small-12 column">
|
||||
<%= back_link_to budgets_path %>
|
||||
<h2 class="margin-top">
|
||||
<%= t("budgets.stats.title") %><br>
|
||||
<span><%= @budget.name %></span>
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row margin-top">
|
||||
<div class="small-12 column">
|
||||
<ul class="tabs">
|
||||
<li class="tabs-title">
|
||||
<span class="show-for-sr"><%= t("shared.you_are_in") %></span>
|
||||
<%= link_to t("budgets.results.link"), budget_results_path(@budget) %>
|
||||
</li>
|
||||
<li class="tabs-title is-active">
|
||||
<%= link_to t("budgets.stats.link"), budget_stats_path(@budget), class: "is-active" %>
|
||||
</li>
|
||||
<li class="tabs-title">
|
||||
<%= link_to t("budgets.executions.link"), budget_executions_path(@budget) %>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="small-12 medium-6 column">
|
||||
<div class="callout">
|
||||
<span class="uppercase"><%= t("budgets.stats.total_participants") %></span>
|
||||
<p id="total_participants" class="big-number-stat budget">
|
||||
<%= @stats[:total_participants] %>
|
||||
</p>
|
||||
|
||||
<span class="uppercase"><%= t("budgets.stats.total_budget_investments") %></span>
|
||||
<p class="big-number-stat budget">
|
||||
<%= @stats[:total_budget_investments] %>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="small-12 medium-6 column">
|
||||
<p>
|
||||
<span class="label feasible"></span>
|
||||
<span id="total_selected_investments">
|
||||
<span class="uppercase"><strong><%= t("budgets.stats.total_selected_investments") %></strong></span>:
|
||||
<%= @stats[:total_selected_investments] %><br>
|
||||
</span>
|
||||
|
||||
<span class="label unfeasible"></span>
|
||||
<span id="total_unfeasible_investments">
|
||||
<span class="uppercase"><strong><%= t("budgets.stats.total_unfeasible_investments") %></strong></span>:
|
||||
<%= @stats[:total_unfeasible_investments] %><br>
|
||||
</span>
|
||||
|
||||
<br>
|
||||
|
||||
<span class="label supports"></span>
|
||||
<span class="uppercase"><strong><%= t("budgets.stats.total_participants_support_phase") %></strong></span>:
|
||||
<span id="total_participants_support_phase">
|
||||
<%= @stats[:total_participants_support_phase] %> <em><%= t("budgets.stats.participants") %></em>,
|
||||
<%= @stats[:total_supports] %> <em><%= t("budgets.stats.supports") %></em><br>
|
||||
</span>
|
||||
|
||||
<br>
|
||||
|
||||
<span class="label"></span>
|
||||
<span class="uppercase"><strong><%= t("budgets.stats.total_participants_web") %></strong></span>:
|
||||
<span id="total_participants_web">
|
||||
<%= @stats[:total_participants_web] %><br>
|
||||
</span>
|
||||
|
||||
<span class="label"></span>
|
||||
<span class="uppercase"><strong><%= t("budgets.stats.total_participants_booths") %></strong></span>:
|
||||
<span id="total_participants_booths">
|
||||
<%= @stats[:total_participants_booths] %><br>
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row margin">
|
||||
<div class="small-12 column">
|
||||
<h2 class="margin-bottom"><%= t("budgets.stats.by_gender") %></h2>
|
||||
</div>
|
||||
|
||||
<div class="small-12 medium-6 column text-center">
|
||||
<%= image_tag ("budgets/stats/gender.png") %>
|
||||
</div>
|
||||
|
||||
<div class="small-12 medium-6 column">
|
||||
<div class="small-12 medium-6 column text-right">
|
||||
<p id="female_percentage">
|
||||
<span class="label" style="background: #FF9E01"></span>
|
||||
<%= t("budgets.stats.total_female_participants").upcase %>
|
||||
(<%= number_to_percentage(@stats[:female_percentage],
|
||||
strip_insignificant_zeros: true,
|
||||
precision: 2) %>)
|
||||
</p>
|
||||
<p id="total_female_participants" class="big-number-stat">
|
||||
<%= @stats[:total_female_participants] %>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="small-12 medium-6 column text-left">
|
||||
<p id="male_percentage">
|
||||
<span class="label" style="background: #FF6600"></span>
|
||||
<%= t("budgets.stats.total_male_participants").upcase %>
|
||||
(<%= number_to_percentage(@stats[:male_percentage],
|
||||
strip_insignificant_zeros: true,
|
||||
precision: 2) %>)
|
||||
</p>
|
||||
<p id="total_male_participants" class="big-number-stat">
|
||||
<%= @stats[:total_male_participants] %>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row margin">
|
||||
<div class="small-12 column">
|
||||
<h2 class="margin-bottom"><%= t("budgets.stats.by_age") %></h2>
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col" class="table-width border-right"><%= t("budgets.stats.age").upcase %></th>
|
||||
<th scope="col" class="border-left"><%= t("budgets.stats.total").upcase %></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<% all_ages_count = @stats[:age_groups].values.sum.to_f %>
|
||||
<% @stats[:age_groups].each do |age_group, count| %>
|
||||
<tr id="age_group_<%= age_group.gsub(" - ", "_to_").gsub("+ ", "up_to_") %>">
|
||||
<td class="border-right">
|
||||
<%= age_group.gsub("+", t("budgets.stats.more_than")) + " " + t("budgets.stats.years") %>
|
||||
</td>
|
||||
<td class="border-left">
|
||||
<strong>
|
||||
<%
|
||||
percentage_age_count = all_ages_count == 0 ? 0 : (count / all_ages_count * 100)
|
||||
formatted_percentage_age_count = number_to_percentage(percentage_age_count,
|
||||
strip_insignificant_zeros: true,
|
||||
precision: 2)
|
||||
%>
|
||||
<%= count %>
|
||||
(<%= formatted_percentage_age_count %>)
|
||||
</strong>
|
||||
<div class="progress" role="progressbar" tabindex="0" aria-valuenow="20" aria-valuemin="0" aria-valuetext="<%= percentage_age_count %>" aria-valuemax="100">
|
||||
<span class="progress-meter" style="width: <%= number_to_percentage(percentage_age_count*5,
|
||||
strip_insignificant_zeros: true,
|
||||
precision: 2, locale: :en) %>;"></span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<% end %>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="small-12 column">
|
||||
<h2 class="margin-bottom"><%= t("budgets.stats.by_heading") %></h2>
|
||||
|
||||
<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>
|
||||
|
||||
<% ["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>
|
||||
<% end %>
|
||||
</tr>
|
||||
<% end %>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
@@ -191,3 +191,35 @@ 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."
|
||||
|
||||
@@ -191,3 +191,35 @@ 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."
|
||||
|
||||
@@ -15,6 +15,7 @@ resources :budgets, only: [:show, :index] do
|
||||
end
|
||||
|
||||
resource :results, only: :show, controller: "budgets/results"
|
||||
resource :stats, only: :show, controller: "budgets/stats"
|
||||
resource :executions, only: :show, controller: "budgets/executions"
|
||||
end
|
||||
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
class AddBallotLineCounterCachedToBudgetBallots < ActiveRecord::Migration
|
||||
def change
|
||||
add_column :budget_ballots, :ballot_lines_count, :integer, default: 0
|
||||
end
|
||||
end
|
||||
@@ -131,9 +131,10 @@ ActiveRecord::Schema.define(version: 20190411090023) do
|
||||
create_table "budget_ballots", force: :cascade do |t|
|
||||
t.integer "user_id"
|
||||
t.integer "budget_id"
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
t.boolean "physical", default: false
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
t.integer "ballot_lines_count", default: 0
|
||||
t.boolean "physical", default: false
|
||||
t.integer "poll_ballot_id"
|
||||
end
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ FactoryBot.define do
|
||||
password "judgmentday"
|
||||
terms_of_service "1"
|
||||
confirmed_at { Time.current }
|
||||
date_of_birth { 20.years.ago }
|
||||
public_activity true
|
||||
|
||||
trait :incomplete_verification do
|
||||
|
||||
37
spec/features/budgets/stats_spec.rb
Normal file
37
spec/features/budgets/stats_spec.rb
Normal file
@@ -0,0 +1,37 @@
|
||||
require "rails_helper"
|
||||
|
||||
feature "Stats" do
|
||||
|
||||
let(:budget) { create(:budget) }
|
||||
let(:group) { create(:budget_group, budget: budget) }
|
||||
let(:heading) { create(:budget_heading, group: group, price: 1000) }
|
||||
|
||||
describe "Show" do
|
||||
|
||||
it "is not accessible to normal users if phase is not 'finished'" do
|
||||
budget.update(phase: "reviewing_ballots")
|
||||
|
||||
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)
|
||||
|
||||
visit budget_stats_path(budget.id)
|
||||
expect(page).to have_content "Stats"
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
212
spec/models/budget/stats_spec.rb
Normal file
212
spec/models/budget/stats_spec.rb
Normal file
@@ -0,0 +1,212 @@
|
||||
require "rails_helper"
|
||||
|
||||
describe Budget::Stats do
|
||||
|
||||
before(:each) do
|
||||
@budget = create(:budget)
|
||||
@group = create(:budget_group, budget: @budget)
|
||||
@heading = create(:budget_heading, group: @group, price: 1000)
|
||||
|
||||
@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)
|
||||
|
||||
@support1 = create(:vote, votable: @investment1, voter: create(:user, gender: "male"))
|
||||
@support2 = create(:vote, votable: @investment2, voter: create(:user))
|
||||
|
||||
@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"))
|
||||
|
||||
@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)
|
||||
|
||||
@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
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
context "#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
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
context "#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
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
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
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
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
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
context "#total_budget_investments" do
|
||||
|
||||
it "returns the number of total budget investments" do
|
||||
expect(@stats[:total_budget_investments]).to be 5
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
context "#total_votes" do
|
||||
|
||||
it "returns the number of total votes" do
|
||||
expect(@stats[:total_votes]).to be 4
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
context "#total_selected_investments" do
|
||||
|
||||
it "returns the number of total selected investments" do
|
||||
expect(@stats[:total_selected_investments]).to be 4
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
context "#total_unfeasible_investments" do
|
||||
|
||||
it "returns the number of total unfeasible investments" do
|
||||
expect(@stats[:total_unfeasible_investments]).to be 1
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
context "#total_male_participants" do
|
||||
|
||||
it "returns the number of total male participants" do
|
||||
expect(@stats[:total_male_participants]).to be 4
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
context "#total_female_participants" do
|
||||
|
||||
it "returns the number of total female participants" do
|
||||
expect(@stats[:total_female_participants]).to be 6
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
context "#total_supports" do
|
||||
|
||||
it "returns the number of total supports" do
|
||||
expect(@stats[:total_supports]).to be 2
|
||||
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
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
context "#age_groups" do
|
||||
|
||||
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
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
context "#male_percentage" do
|
||||
|
||||
it "returns the percentage of male participants" do
|
||||
expect(@stats[:male_percentage]).to be 40.0
|
||||
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
|
||||
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_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
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
Reference in New Issue
Block a user