Use stats objects instead of hashes

It will make it far easier to call other methods on the stats object,
and we're already caching the methods.

We had to remove the view fragment caching because the stats object
isn't as easy to cache. The good thing about it is the view will
automatically be updated when we change logic regarding which stats to
show, and the methods taking long to execute are cached in the model.
This commit is contained in:
Javi Martín
2019-03-18 15:38:53 +01:00
parent eba30d1585
commit 76c7827cf4
9 changed files with 244 additions and 290 deletions

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

@@ -13,8 +13,6 @@ class Budget::Stats
User.where(id: (authors + voters + balloters + poll_ballot_voters).uniq.compact)
end
private
def total_participants
participants.distinct.count
end
@@ -43,6 +41,31 @@ class Budget::Stats
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_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
private
def authors
budget.investments.pluck(:author_id)
end
@@ -71,29 +94,6 @@ class Budget::Stats
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,

View File

@@ -9,7 +9,7 @@ module Statisticable
end
def generate
self.class.stats_methods.map { |stat_name| [stat_name, send(stat_name)] }.to_h
self.class.stats_methods.each { |stat_name| send(stat_name) }
end
def total_male_participants

View File

@@ -8,7 +8,6 @@
social_description: @budget.description_finished %>
<% end %>
<% cache [@stats] do %>
<div class="participation-stats budgets-stats">
<div class="expanded no-margin-top padding header">
<div class="row">
@@ -67,23 +66,23 @@
<h4><%= t("stats.budgets.total_investments") %></h4>
<%= number_with_info_tags(
@stats[:total_budget_investments],
@stats.total_budget_investments,
t("stats.budgets.total_investments"),
html_class: "total-investments"
) %>
<%= number_with_info_tags(@stats[:total_unfeasible_investments],
<%= number_with_info_tags(@stats.total_unfeasible_investments,
t("stats.budgets.total_unfeasible_investments")) %>
<%= number_with_info_tags(@stats[:total_selected_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>
<%= number_with_info_tags(@stats[:total_participants_support_phase],
<%= number_with_info_tags(@stats.total_participants_support_phase,
t("stats.budgets.participants_support_phase")) %>
<%= number_with_info_tags(@stats[:total_participants_vote_phase],
<%= number_with_info_tags(@stats.total_participants_vote_phase,
t("stats.budgets.participants_voting_phase")) %>
</div>
@@ -120,21 +119,21 @@
</td>
<td id="total_spending_proposals_heading_<%= heading.id %>"
class="text-center border-left border-right">
<%= @stats[:headings][heading.id][:total_investments_count] %>
<%= @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] %>
<%= @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]) %>
<%= 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]) %>
<%= number_to_stats_percentage(@stats.headings[heading.id]["percentage_district_population_#{phase}_phase".to_sym]) %>
</td>
<% end %>
</tr>
@@ -148,7 +147,7 @@
<div class="small-12 column">
<div id="total_unknown_gender_or_age">
<p class="help-text">
<%= t("stats.budgets.no_demographic_data", total: @stats[:total_unknown_gender_or_age]) %>
<%= t("stats.budgets.no_demographic_data", total: @stats.total_unknown_gender_or_age) %>
</p>
<p class="help-text">
<%= t("stats.budgets.participatory_disclaimer") %>
@@ -162,4 +161,3 @@
</div>
</div>
</div>
<% end %>

View File

@@ -31,9 +31,9 @@
<% Poll::Stats::CHANNELS.each do |channel| %>
<%= number_with_info_tags(
@stats[:"total_participants_#{channel}"],
@stats.send("total_participants_#{channel}"),
t("stats.polls.#{channel}_percentage",
percentage: number_to_stats_percentage(@stats[:"total_participants_#{channel}_percentage"])
percentage: number_to_stats_percentage(@stats.send(:"total_participants_#{channel}_percentage"))
),
html_class: channel
) %>
@@ -59,14 +59,14 @@
<% Poll::Stats::CHANNELS.each do |channel| %>
<td>
<%= @stats[:"total_#{channel}_valid"] %>
<small><em>(<%= @stats[:"valid_percentage_#{channel}"].round(2) %>%)</em></small>
<%= @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>
<%= @stats.total_valid_votes %>
<small><em>(<%= @stats.total_valid_percentage.round(2) %>%)</em></small>
</td>
</tr>
@@ -75,13 +75,13 @@
<% Poll::Stats::CHANNELS.each do |channel| %>
<td>
<%= @stats[:"total_#{channel}_white"] %>
<small><em>(<%= @stats[:"white_percentage_#{channel}"].round(2) %>%)</em></small>
<%= @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><%= @stats.total_white_votes %>
<small><em>(<%= @stats.total_white_percentage.round(2) %>%)</em></small>
</td>
</tr>
<tr>
@@ -89,14 +89,14 @@
<% Poll::Stats::CHANNELS.each do |channel| %>
<td>
<%= @stats[:"total_#{channel}_null"] %>
<small><em>(<%= @stats[:"null_percentage_#{channel}"].round(2) %>%)</em></small>
<%= @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>
<%= @stats.total_null_votes %>
<small><em>(<%= @stats.total_null_percentage.round(2) %>%)</em></small>
</td>
</tr>
<tr>
@@ -104,12 +104,12 @@
<% Poll::Stats::CHANNELS.each do |channel| %>
<td>
<%= @stats[:"total_participants_#{channel}"] %>
<small><em>(<%= @stats[:"total_participants_#{channel}_percentage"].round(2) %>%)</em></small>
<%= @stats.send(:"total_participants_#{channel}") %>
<small><em>(<%= @stats.send(:"total_participants_#{channel}_percentage").round(2) %>%)</em></small>
</td>
<% end %>
<td><%= @stats[:total_participants] %></td>
<td><%= @stats.total_participants %></td>
</tr>
</tbody>
</table>

View File

@@ -5,7 +5,7 @@
<h4><%= t("stats.total_participants") %></h4>
<%= number_with_info_tags(
stats[:total_participants],
stats.total_participants,
"",
html_class: "participants total-participants"
) %>
@@ -15,14 +15,14 @@
<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])),
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])),
stats.total_female_participants,
t("stats.women_percentage", percentage: number_to_stats_percentage(stats.female_percentage)),
html_class: "participants female"
) %>
</div>
@@ -39,7 +39,7 @@
</thead>
<tbody>
<% stats[:participants_by_age].values.each do |group| %>
<% stats.participants_by_age.values.each do |group| %>
<tr>
<td><%= group[:range] %></td>
<td>
@@ -70,7 +70,7 @@
</thead>
<tbody>
<% stats[:participants_by_geozone].each do |geozone, participants| %>
<% stats.participants_by_geozone.each do |geozone, participants| %>
<tr>
<td><%= geozone %></td>
<td><%= "#{participants[:total][:count]} (#{number_to_stats_percentage(participants[:total][:percentage])})" %></td>

View File

@@ -33,7 +33,7 @@ describe Budget::Stats do
expect(stats.participants).to match_array(
[author, author_and_voter, voter, voter_and_balloter, balloter, poll_balloter]
)
expect(stats.generate[:total_participants]).to be 6
expect(stats.total_participants).to be 6
end
end
@@ -42,7 +42,7 @@ describe Budget::Stats do
2.times { create(:vote, votable: investment) }
create(:budget_ballot_line, investment: investment)
expect(stats.generate[:total_participants_support_phase]).to be 2
expect(stats.total_participants_support_phase).to be 2
end
it "counts a user who is voter and balloter" do
@@ -50,7 +50,7 @@ describe Budget::Stats do
create(:vote, votable: investment, voter: voter_and_balloter)
create(:budget_ballot_line, investment: investment, user: voter_and_balloter)
expect(stats.generate[:total_participants_support_phase]).to be 1
expect(stats.total_participants_support_phase).to be 1
end
end
@@ -59,7 +59,7 @@ describe Budget::Stats do
2.times { create(:budget_ballot_line, investment: investment) }
create(:vote, votable: investment)
expect(stats.generate[:total_participants_vote_phase]).to be 2
expect(stats.total_participants_vote_phase).to be 2
end
it "counts a user who is voter and balloter" do
@@ -67,14 +67,14 @@ describe Budget::Stats do
create(:vote, votable: investment, voter: voter_and_balloter)
create(:budget_ballot_line, investment: investment, user: voter_and_balloter)
expect(stats.generate[:total_participants_vote_phase]).to be 1
expect(stats.total_participants_vote_phase).to be 1
end
it "includes balloters and poll balloters" do
create(:budget_ballot_line, investment: investment)
create(:poll_voter, :from_booth, budget: budget)
expect(stats.generate[:total_participants_vote_phase]).to be 2
expect(stats.total_participants_vote_phase).to be 2
end
it "counts once a user who is balloter and poll balloter" do
@@ -82,7 +82,7 @@ describe Budget::Stats do
create(:budget_ballot_line, investment: investment, user: poller_and_balloter)
create(:poll_voter, :from_booth, user: poller_and_balloter, budget: budget)
expect(stats.generate[:total_participants_vote_phase]).to be 1
expect(stats.total_participants_vote_phase).to be 1
end
it "doesn't count nil user ids" do
@@ -90,7 +90,7 @@ describe Budget::Stats do
ballot: create(:budget_ballot, budget: budget, user: nil, physical: true)
)
expect(stats.generate[:total_participants_vote_phase]).to be 0
expect(stats.total_participants_vote_phase).to be 0
end
end
@@ -99,7 +99,7 @@ describe Budget::Stats do
2.times { create(:budget_investment, budget: budget) }
create(:budget_investment, budget: create(:budget))
expect(stats.generate[:total_budget_investments]).to be 2
expect(stats.total_budget_investments).to be 2
end
end
@@ -108,7 +108,7 @@ describe Budget::Stats do
create(:budget_ballot_line, investment: investment)
create(:budget_ballot_line, investment: create(:budget_investment, :selected, budget: budget))
expect(stats.generate[:total_votes]).to be 2
expect(stats.total_votes).to be 2
end
end
@@ -118,7 +118,7 @@ describe Budget::Stats do
create(:budget_investment, :selected, budget: create(:budget))
create(:budget_investment, :unfeasible, budget: budget)
expect(stats.generate[:total_selected_investments]).to be 3
expect(stats.total_selected_investments).to be 3
end
end
@@ -128,7 +128,7 @@ describe Budget::Stats do
create(:budget_investment, :unfeasible, budget: create(:budget))
create(:budget_investment, :selected, budget: budget)
expect(stats.generate[:total_unfeasible_investments]).to be 3
expect(stats.total_unfeasible_investments).to be 3
end
end
@@ -143,31 +143,31 @@ describe Budget::Stats do
describe "#total_male_participants" do
it "returns the number of total male participants" do
expect(stats.generate[:total_male_participants]).to be 3
expect(stats.total_male_participants).to be 3
end
end
describe "#total_female_participants" do
it "returns the number of total female participants" do
expect(stats.generate[:total_female_participants]).to be 2
expect(stats.total_female_participants).to be 2
end
end
describe "#total_unknown_gender_or_age" do
it "returns the number of total unknown participants' gender or age" do
expect(stats.generate[:total_unknown_gender_or_age]).to be 1
expect(stats.total_unknown_gender_or_age).to be 1
end
end
describe "#male_percentage" do
it "returns the percentage of male participants" do
expect(stats.generate[:male_percentage]).to be 60.0
expect(stats.male_percentage).to be 60.0
end
end
describe "#female_percentage" do
it "returns the percentage of female participants" do
expect(stats.generate[:female_percentage]).to be 40.0
expect(stats.female_percentage).to be 40.0
end
end
end
@@ -182,18 +182,18 @@ describe Budget::Stats do
end
it "returns the age groups hash" do
expect(stats.generate[:participants_by_age]["16 - 19"][:count]).to be 0
expect(stats.generate[:participants_by_age]["20 - 24"][:count]).to be 4
expect(stats.generate[:participants_by_age]["25 - 29"][:count]).to be 0
expect(stats.generate[:participants_by_age]["30 - 34"][:count]).to be 1
expect(stats.generate[:participants_by_age]["35 - 39"][:count]).to be 0
expect(stats.generate[:participants_by_age]["40 - 44"][:count]).to be 3
expect(stats.generate[:participants_by_age]["45 - 49"][:count]).to be 0
expect(stats.generate[:participants_by_age]["50 - 54"][:count]).to be 2
expect(stats.generate[:participants_by_age]["55 - 59"][:count]).to be 0
expect(stats.generate[:participants_by_age]["60 - 64"][:count]).to be 0
expect(stats.generate[:participants_by_age]["65 - 69"][:count]).to be 0
expect(stats.generate[:participants_by_age]["70 - 74"][:count]).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
@@ -206,7 +206,7 @@ describe Budget::Stats do
end
it "returns headings data" do
heading_stats = stats.generate[:headings][investment.heading.id]
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 1

View File

@@ -183,44 +183,4 @@ describe Poll::Stats do
expect(stats.participants_by_geozone["Midgar"][:percentage]).to eq(33.333)
end
end
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)
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)
end
end
end