Display only existing stats

So if we don't have information regarding gender, age or geozone, stats
regarding those topics will not be shown.

Note we're using `spec/models/statisticable_spec.rb` because having the
same file in `spec/models/concerns` caused the tests to be executed
twice.

Also note the implementation behind the `gender?`, `age?` and `geozone?`
methods is a bit primitive. We might need to make it more robust in the
future.
This commit is contained in:
Javi Martín
2019-03-18 18:07:09 +01:00
parent 76c7827cf4
commit 793bfed372
9 changed files with 277 additions and 85 deletions

View File

@@ -1,5 +1,6 @@
module Statisticable
extend ActiveSupport::Concern
PARTICIPATIONS = %w[gender age geozone]
included do
attr_reader :resource
@@ -9,7 +10,27 @@ module Statisticable
end
def generate
self.class.stats_methods.each { |stat_name| send(stat_name) }
stats_methods.each { |stat_name| send(stat_name) }
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).any?
end
def geozone?
participants.where(geozone: geozones).any?
end
def total_male_participants
@@ -66,6 +87,14 @@ module Statisticable
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 total_participants_with_gender
participants.where.not(gender: nil).distinct.count
end
@@ -115,10 +144,28 @@ module Statisticable
class_methods do
def stats_methods
%i[total_participants
total_male_participants total_female_participants total_unknown_gender_or_age
male_percentage female_percentage
participants_by_age participants_by_geozone]
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 total_unknown_gender_or_age
male_percentage female_percentage]
end
def age_methods
[:participants_by_age]
end
def geozone_methods
[:participants_by_geozone]
end
def stats_cache(*method_names)

View File

@@ -40,7 +40,7 @@
<div class="row margin">
<div class="small-12 medium-3 column sidebar">
<%= render "shared/stats/links" %>
<%= render "shared/stats/links", stats: @stats %>
<p><strong><%= link_to t("stats.advanced"), "#advanced_statistics" %></strong></p>
<ul class="menu vertical">

View File

@@ -7,7 +7,7 @@
<div class="row margin">
<div class="small-12 medium-3 column sidebar">
<%= render "shared/stats/links" %>
<%= render "shared/stats/links", stats: @stats %>
<p><strong><%= link_to t("stats.advanced"), "#advanced_statistics" %></strong></p>
<ul class="menu vertical">

View File

@@ -0,0 +1,28 @@
<div id="participants_by_age" class="stats-group">
<h4><%= t("stats.by_age") %></h4>
<table>
<thead>
<tr>
<th class="small-4"><%= t("stats.age") %></th>
<th><%= t("stats.total") %></th>
</tr>
</thead>
<tbody>
<% stats.participants_by_age.values.each do |group| %>
<tr>
<td><%= group[:range] %></td>
<td>
<strong>
<%= "#{group[:count]} (#{number_to_stats_percentage(group[:percentage])})" %>
</strong>
<div class="progress" tabindex="0">
<span class="progress-meter" style="width: <%= group[:percentage] %>%"></span>
</div>
</td>
</tr>
<% end %>
</tbody>
</table>
</div>

View File

@@ -0,0 +1,15 @@
<div id="participants_by_gender" class="stats-group">
<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)),
html_class: "participants male"
) %>
<%= number_with_info_tags(
stats.total_female_participants,
t("stats.women_percentage", percentage: number_to_stats_percentage(stats.female_percentage)),
html_class: "participants female"
) %>
</div>

View File

@@ -0,0 +1,23 @@
<div id="participants_by_geozone" class="stats-group">
<h4><%= t("stats.by_geozone") %></h4>
<table>
<thead>
<tr>
<th><%= t("stats.geozone") %></th>
<th><%= t("stats.total") %></th>
<th><%= t("stats.geozone_participation") %></th>
</tr>
</thead>
<tbody>
<% stats.participants_by_geozone.each do |geozone, participants| %>
<tr>
<td><%= geozone %></td>
<td><%= "#{participants[:total][:count]} (#{number_to_stats_percentage(participants[:total][:percentage])})" %></td>
<td><%= number_to_stats_percentage(participants[:percentage]) %></td>
</tr>
<% end %>
</tbody>
</table>
</div>

View File

@@ -3,13 +3,10 @@
<li>
<%= link_to t("stats.total_participants"), "#total_participants" %>
</li>
<li>
<%= link_to t("stats.by_gender"), "#participants_by_gender" %>
</li>
<li>
<%= link_to t("stats.by_age"), "#participants_by_age" %>
</li>
<li>
<%= link_to t("stats.by_geozone"), "#participants_by_geozone" %>
</li>
<% stats.participations.each do |stat| %>
<li>
<%= link_to t("stats.by_#{stat}"), "#participants_by_#{stat}" %>
</li>
<% end %>
</ul>

View File

@@ -11,73 +11,7 @@
) %>
</div>
<div id="participants_by_gender" class="stats-group">
<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)),
html_class: "participants male"
) %>
<%= number_with_info_tags(
stats.total_female_participants,
t("stats.women_percentage", percentage: number_to_stats_percentage(stats.female_percentage)),
html_class: "participants female"
) %>
</div>
<div id="participants_by_age" class="stats-group">
<h4><%= t("stats.by_age") %></h4>
<table>
<thead>
<tr>
<th class="small-4"><%= t("stats.age") %></th>
<th><%= t("stats.total") %></th>
</tr>
</thead>
<tbody>
<% stats.participants_by_age.values.each do |group| %>
<tr>
<td><%= group[:range] %></td>
<td>
<strong>
<%= "#{group[:count]} (#{number_to_stats_percentage(group[:percentage])})" %>
</strong>
<div class="progress" tabindex="0">
<span class="progress-meter" style="width: <%= group[:percentage] %>%">
</span>
</div>
</td>
</tr>
<% end %>
</tbody>
</table>
</div>
<div id="participants_by_geozone" class="stats-group">
<h4><%= t("stats.by_geozone") %></h4>
<table>
<thead>
<tr>
<th><%= t("stats.geozone") %></th>
<th><%= t("stats.total") %></th>
<th><%= t("stats.geozone_participation") %></th>
</tr>
</thead>
<tbody>
<% stats.participants_by_geozone.each do |geozone, participants| %>
<tr>
<td><%= geozone %></td>
<td><%= "#{participants[:total][:count]} (#{number_to_stats_percentage(participants[:total][:percentage])})" %></td>
<td><%= number_to_stats_percentage(participants[:percentage]) %></td>
</tr>
<% end %>
</tbody>
</table>
</div>
<% stats.participations.each do |participation| %>
<%= render "shared/stats/#{participation}", stats: stats %>
<% end %>
</div>

View File

@@ -0,0 +1,148 @@
require "rails_helper"
describe Statisticable do
class DummyStats
include Statisticable
def participants
User.all
end
end
let(:stats) { DummyStats.new(nil) }
describe "#gender?" do
context "No participants" do
it "is false" do
expect(stats.gender?).to be false
end
end
context "All participants have no defined gender" do
before { create(:user, gender: nil) }
it "is false" do
expect(stats.gender?).to be false
end
end
context "There's a male participant" do
before { create(:user, gender: "male") }
it "is true" do
expect(stats.gender?).to be true
end
end
context "There's a female participant" do
before { create(:user, gender: "female") }
it "is true" do
expect(stats.gender?).to be true
end
end
end
describe "#age?" do
context "No participants" do
it "is false" do
expect(stats.age?).to be false
end
end
context "All participants have no defined age" do
before { create(:user, date_of_birth: nil) }
it "is false" do
expect(stats.age?).to be false
end
end
context "All participants have impossible ages" do
before do
create(:user, date_of_birth: 3.seconds.ago)
create(:user, date_of_birth: 3000.years.ago)
end
it "is false" do
expect(stats.age?).to be false
end
end
context "There's a participant with a defined age" do
before { create(:user, date_of_birth: 30.years.ago) }
it "is true" do
expect(stats.age?).to be true
end
end
end
describe "#geozone?" do
context "No participants" do
it "is false" do
expect(stats.geozone?).to be false
end
end
context "All participants have no defined geozone" do
before { create(:user, geozone: nil) }
it "is false" do
expect(stats.geozone?).to be false
end
end
context "There's a participant with a defined geozone" do
before { create(:user, geozone: create(:geozone)) }
it "is true" do
expect(stats.geozone?).to be true
end
end
end
describe "#stats_methods" do
it "includes total participants" do
expect(stats.stats_methods).to include(:total_participants)
end
context "no gender stats" do
before { allow(stats).to receive(:gender?).and_return(false) }
it "doesn't include gender methods" do
expect(stats.stats_methods).not_to include(:total_male_participants)
end
end
context "no age stats" do
before { allow(stats).to receive(:age?).and_return(false) }
it "doesn't include age methods" do
expect(stats.stats_methods).not_to include(:participants_by_age)
end
end
context "no geozone stats" do
before { allow(stats).to receive(:geozone?).and_return(false) }
it "doesn't include age methods" do
expect(stats.stats_methods).not_to include(:participants_by_geozone)
end
end
context "all gender, age and geozone stats" do
before do
allow(stats).to receive(:gender?).and_return(true)
allow(stats).to receive(:age?).and_return(true)
allow(stats).to receive(:geozone?).and_return(true)
end
it "includes all stats methods" do
expect(stats.stats_methods).to include(:total_male_participants)
expect(stats.stats_methods).to include(:participants_by_age)
expect(stats.stats_methods).to include(:participants_by_geozone)
end
end
end
end