Files
nairobi/app/models/budget/stats.rb
Javi Martín a4461a1a56 Expire the stats cache once per day
When we first started caching the stats, generating them was a process
that took several minutes, so we never expired the cache.

However, there have been cases where we run into issues where the stats
shown on the screen were outdated. That's why we introduced a task to
manually expire the cache.

But now, generating the stats only takes a few seconds, so we can
automatically expire them every day, remove all the logic needed to
manually expire them, and get rid of most of the issues related to the
cache being outdated.

We're expiring them every day because it's the same day we were doing in
public stats (which we removed in commit 631b48f58), only we're using
`expires_at:` to set the expiration time, in order to simplify the code.

Note that, in the test, we're using `travel_to(time)` so the test passes
even when it starts an instant before midnight. We aren't using
`:with_frozen_time` because, in similar cases (although not in this
case, but I'm not sure whether that's intentional), `travel_to` shows
this error:

> Calling `travel_to` with a block, when we have previously already made
> a call to `travel_to`, can lead to confusing time stubbing.
2024-05-17 20:11:16 +02:00

199 lines
5.9 KiB
Ruby

class Budget::Stats
include Statisticable
alias_method :budget, :resource
def self.stats_methods
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 participation_date
send("#{phases.last}_phase_participation_date")
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_supports
supports(budget).count
end
def total_votes
budget.ballots.pluck(:ballot_lines_count).sum
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.sum { |_k, v| v[:total_investments_count] }
groups[:total][:total_participants_support_phase] = groups.sum { |_k, v| v[:total_participants_support_phase] }
groups[:total][:total_participants_vote_phase] = groups.sum { |_k, v| v[:total_participants_vote_phase] }
groups[:total][:total_participants_every_phase] = groups.sum { |_k, v| v[:total_participants_every_phase] }
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.sum { |_k, v| v[:percentage_participants_support_phase] }
groups[:total][:percentage_participants_vote_phase] = groups.sum { |_k, v| v[:percentage_participants_vote_phase] }
groups[:total][:percentage_participants_every_phase] = groups.sum { |_k, v| v[:percentage_participants_every_phase] }
groups
end
private
def phase_methods
phases.map { |phase| self.class.send("#{phase}_phase_methods") }.flatten
end
def support_phase_participation_date
budget.phases.selecting.ends_at
end
def vote_phase_participation_date
budget.phases.balloting.ends_at
end
def participant_ids
phases.map { |phase| send("participant_ids_#{phase}_phase") }.flatten.uniq
end
def participant_ids_support_phase
(authors + voters).uniq
end
def participant_ids_vote_phase
(balloters + poll_ballot_voters).uniq
end
def authors
@authors ||= budget.investments.pluck(:author_id)
end
def voters
@voters ||= supports(budget).distinct.pluck(:voter_id)
end
def balloters
@balloters ||= budget.ballots.where("ballot_lines_count > ?", 0).distinct.pluck(:user_id).compact
end
def poll_ballot_voters
@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 })
.distinct.pluck(:user_id)
end
end
def voters_by_heading(heading)
stats_cache("voters_by_heading_#{heading.id}") do
supports(heading).distinct.pluck(:voter_id)
end
end
def calculate_heading_totals(heading)
{
total_investments_count: heading.investments.count,
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
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_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
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 supports(supportable)
Vote.where(votable: supportable.investments)
end
stats_cache(*stats_methods)
def full_cache_key_for(key)
"budgets_stats/#{budget.id}/#{phases.join}/#{key}"
end
end