Files
grecia/app/models/concerns/statisticable.rb
Julian Herrero 16f844595d Don't use the cache in admin budget stats
In commit e51e03446, we started using the same code to show stats in the
public area and in the admin area. However, in doing so we introduced a
bug, since stats in the public area are only shown after a certain part
of the process has finished, meaning the stats appearing on the page
never change (in theory), so it's perfectly fine to cache them. However,
in the admin area stats can be accessed while the process is still
ongoing, so caching the stats will lead to the wrong results being
displayed.

We've thought about expiring the cache when new supports or ballot lines
are added; however, that means the methods calculating the stats for the
supporting phase would expire when supports are added/removed but the
methods calculating the stats for the voting phase would expire when
ballot lines are added/removed. It gets even more complex because the
`headings` method calculates stats for both the supporting and the
voting phases.

So, since loading stats in the admin section is fast even without the
cache because they only load very basic statistics, we're taking the
simple approach of disabling the cache in this case, so everything works
the same way it did before commit e51e03446.

Co-authored-by: Javi Martín <javim@elretirao.net>
2024-05-20 16:19:41 +02:00

238 lines
5.5 KiB
Ruby

module Statisticable
extend ActiveSupport::Concern
PARTICIPATIONS = %w[gender age geozone].freeze
included do
attr_reader :resource, :cache
end
class_methods do
def stats_methods
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)
method_names.each do |method_name|
alias_method :"raw_#{method_name}", method_name
define_method method_name do
stats_cache(method_name) { send(:"raw_#{method_name}") }
end
end
end
end
def initialize(resource, cache: true)
@resource = resource
@cache = cache
end
def generate
User.transaction do
begin
define_singleton_method :participants do
create_participants_table unless participants_table_created?
participants_class.all
end
stats_methods.each { |stat_name| send(stat_name) }
ensure
define_singleton_method :participants do
participants_from_original_table
end
end
drop_participants_table
end
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,
at_time: participation_date
).any?
end
def geozone?
participants.where(geozone: geozones).any?
end
def participants
@participants ||= User.unscoped.where(id: participant_ids)
end
alias_method :participants_from_original_table, :participants
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.to_h do |start, finish|
count = participants.between_ages(start, finish, at_time: participation_date).count
[
"#{start} - #{finish}",
{
range: range_description(start, finish),
count: count,
percentage: calculate_percentage(count, total_participants)
}
]
end
end
def participants_by_geozone
geozones.to_h do |geozone|
count = participants.where(geozone: geozone).count
[
geozone.name,
{
count: count,
percentage: calculate_percentage(count, total_participants)
}
]
end
end
def calculate_percentage(fraction, total)
return 0.0 if total.zero?
(fraction * 100.0 / total).round(3)
end
def advanced?
resource.advanced_stats_enabled?
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 create_participants_table
User.connection.create_table(
participants_table_name,
temporary: true,
as: participants_from_original_table.to_sql
)
User.connection.add_index participants_table_name, :date_of_birth
User.connection.add_index participants_table_name, :geozone_id
@participants_table_created = true
end
def drop_participants_table
User.connection.drop_table(participants_table_name, if_exists: true, temporary: true)
@participants_table_created = false
end
def participants_table_name
@participants_table_name ||= "participants_#{resource.class.table_name}_#{resource.id}"
end
def participants_class
@participants_class ||= Class.new(User).tap { |klass| klass.table_name = participants_table_name }
end
def participants_table_created?
@participants_table_created.present?
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 geozones
Geozone.order("name")
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
def stats_cache(key, &block)
if cache
Rails.cache.fetch(full_cache_key_for(key), expires_at: Date.current.end_of_day, &block)
else
block.call
end
end
end