In commite51e03446, 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 commite51e03446. Co-authored-by: Javi Martín <javim@elretirao.net>
238 lines
5.5 KiB
Ruby
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
|