Skip to content

Commit cd866c3

Browse files
authored
Merge pull request #2466 from mikej/show-chapter-how-you-found-us-stats
Add "how you found us" summary to the chapter admin page
2 parents dbf5165 + e3f4039 commit cd866c3

6 files changed

Lines changed: 179 additions & 15 deletions

File tree

app/assets/stylesheets/_bootstrap-custom.scss

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ $carousel-control-width: 3rem;
3737
@import "bootstrap/pagination";
3838
@import "bootstrap/badge";
3939
@import "bootstrap/alert";
40-
// @import "bootstrap/progress";
40+
@import "bootstrap/progress";
4141
@import "bootstrap/list-group";
4242
@import "bootstrap/close";
4343
// @import "bootstrap/toasts";

app/controllers/admin/chapters_controller.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ def show
2727
@sponsors = @chapter.sponsors.uniq
2828
@groups = @chapter.groups
2929
@subscribers = @chapter.subscriptions.last(20).reverse
30+
@how_you_found_us = HowYouFoundUsPresenter.new(@chapter)
3031
end
3132

3233
def edit
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
class HowYouFoundUsPresenter
2+
3+
def initialize(chapter)
4+
@chapter = chapter
5+
end
6+
7+
def by_percentage
8+
return how_values.to_h { |how| [how, 0] } unless data_present?
9+
10+
# use the largest remainder algorithm so that percentages are whole
11+
# numbers but always add up to 100
12+
# https://stackoverflow.com/a/13483710/
13+
entries = how_values.map do |how|
14+
count = raw_stats.fetch(how, 0)
15+
exact = (count / total_responses.to_f) * 100
16+
percentage_value = exact.floor
17+
remainder = exact - percentage_value
18+
{ how: how, percentage_value: percentage_value, remainder: remainder }
19+
end
20+
21+
allocated_so_far = entries.sum { |entry| entry[:percentage_value] }
22+
left_to_allocate = 100 - allocated_so_far
23+
24+
entries
25+
.sort_by { |entry| [-entry[:remainder], entry[:how].to_s] }
26+
.first(left_to_allocate)
27+
.each { |entry| entry[:percentage_value] += 1 }
28+
29+
entries.to_h { |entry| [entry[:how], entry[:percentage_value]] }
30+
end
31+
32+
def total_responses
33+
raw_stats.values.sum(0)
34+
end
35+
36+
def data_present?
37+
total_responses.positive?
38+
end
39+
40+
private
41+
42+
def raw_stats
43+
@stats ||= @chapter.members.where.not(how_you_found_us: nil).group(:how_you_found_us).count
44+
end
45+
46+
def how_values
47+
Member.how_you_found_us.keys
48+
end
49+
end

app/views/admin/chapters/show.html.haml

Lines changed: 31 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -23,21 +23,38 @@
2323
%span.badge.bg-warning.text-dark Phone no. not set
2424
- if current_user.is_admin?
2525
= link_to 'Edit organisers', admin_chapter_organisers_path(@chapter), class: 'btn btn-primary btn-sm'
26-
%ul.nav.flex-column.ms-0.mb-4
27-
- @groups.each do |group|
28-
%li.nav-item
29-
= link_to [ :admin, group ], class: 'nav-link' do
30-
#{group.name} (#{group.members.count})
31-
%li.nav-item
32-
= link_to admin_chapter_members_path(@chapter, type: group.name.downcase), class: 'nav-link' do
33-
View #{group.name} emails
34-
%li.nav-item
35-
= link_to 'View all sponsors', admin_sponsors_path, class: 'nav-link'
36-
%li.nav-item
37-
= link_to 'View all workshops', admin_chapter_workshops_path(@chapter), class: 'nav-link'
3826

39-
.col-12.col-lg-7.offset-lg-1
40-
.mb-4
27+
.card.border-info.my-4.my-md-0.my-lg-4.ms-md-4.ms-lg-0
28+
.card-body
29+
%ul.nav.flex-column.ms-0.mb-0
30+
- @groups.each do |group|
31+
%li.nav-item
32+
= link_to [ :admin, group ], class: 'nav-link' do
33+
#{group.name} (#{group.members.count})
34+
%li.nav-item
35+
= link_to admin_chapter_members_path(@chapter, type: group.name.downcase), class: 'nav-link' do
36+
View #{group.name} emails
37+
%li.nav-item
38+
= link_to 'View all sponsors', admin_sponsors_path, class: 'nav-link'
39+
%li.nav-item
40+
= link_to 'View all workshops', admin_chapter_workshops_path(@chapter), class: 'nav-link'
41+
42+
- if @how_you_found_us.data_present?
43+
.card.border-info.my-4.my-md-0.my-lg-4.ms-md-4.ms-lg-0
44+
.card-body
45+
%h3 How members found this chapter
46+
- @how_you_found_us.by_percentage.each do |(how, percent)|
47+
- label = t("member.details.edit.how_you_found_us_options.#{how}")
48+
.mb-3
49+
.d-flex.justify-content-between
50+
%div= label
51+
%div #{percent}%
52+
.progress
53+
.progress-bar.bg-primary{ role: 'progressbar', style: "width: #{percent}%", "aria-valuenow": percent, "aria-valuemin": 0, "aria-valuemax": 100 }
54+
%div.text-muted.small= "Based on #{pluralize(@how_you_found_us.total_responses, 'response')}"
55+
56+
.col-12.col-lg-8
57+
.mb-4.mt-md-4.mt-lg-0
4158
.d-md-flex.justify-content-between.align-items-center
4259
%h3 Upcoming Workshops
4360
= link_to 'New workshop', new_admin_workshop_path, class: 'btn btn-primary btn-sm'

spec/features/admin/chapters_spec.rb

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,4 +119,29 @@
119119
expect(page).not_to have_content(coach_email)
120120
end
121121
end
122+
123+
context 'how you found us card' do
124+
let(:chapter) { Fabricate(:chapter) }
125+
let(:group) { Fabricate(:group, chapter: chapter) }
126+
127+
before do
128+
login_as_admin(member)
129+
end
130+
131+
scenario 'shows the card when there are responses' do
132+
member_with_response = Fabricate(:member, how_you_found_us: :from_a_friend)
133+
Fabricate(:subscription, member: member_with_response, group: group)
134+
135+
visit admin_chapter_path(chapter)
136+
137+
expect(page).to have_content('How members found this chapter')
138+
expect(page).to have_content('Based on 1 response')
139+
end
140+
141+
scenario 'does not show the card when there are no responses' do
142+
visit admin_chapter_path(chapter)
143+
144+
expect(page).not_to have_content('How members found this chapter')
145+
end
146+
end
122147
end
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
RSpec.describe HowYouFoundUsPresenter do
2+
def add_member(group, how)
3+
member = Fabricate(:member, how_you_found_us: how)
4+
Fabricate(:subscription, member: member, group: group)
5+
member
6+
end
7+
8+
def add_member_without_how(group)
9+
member = Fabricate(:member, how_you_found_us: nil)
10+
Fabricate(:subscription, member: member, group: group)
11+
member
12+
end
13+
14+
let(:chapter) { Fabricate(:chapter_without_organisers) }
15+
let(:group) { Fabricate(:group, chapter: chapter) }
16+
let(:presenter) { HowYouFoundUsPresenter.new(chapter) }
17+
18+
describe '#by_percentage' do
19+
it 'returns integer percentages for all enum values in enum order using largest remainder rounding' do
20+
add_member(group, :from_a_friend)
21+
add_member(group, :search_engine)
22+
add_member(group, :search_engine)
23+
add_member(group, :social_media)
24+
add_member(group, :social_media)
25+
add_member(group, :social_media)
26+
27+
expect(presenter.by_percentage).to eq(
28+
{
29+
'from_a_friend' => 17,
30+
'search_engine' => 33,
31+
'social_media' => 50,
32+
'codebar_host_or_partner' => 0,
33+
'other' => 0
34+
}
35+
)
36+
expect(presenter.by_percentage.values.sum).to eq(100)
37+
end
38+
39+
it 'returns all enum values with zeros when there is no data' do
40+
expect(presenter.by_percentage).to eq(
41+
{
42+
'from_a_friend' => 0,
43+
'search_engine' => 0,
44+
'social_media' => 0,
45+
'codebar_host_or_partner' => 0,
46+
'other' => 0
47+
}
48+
)
49+
end
50+
end
51+
52+
describe '#total_responses' do
53+
it 'sums the counts' do
54+
add_member(group, :from_a_friend)
55+
add_member(group, :search_engine)
56+
57+
expect(presenter.total_responses).to eq(2)
58+
end
59+
end
60+
61+
describe '#data_present?' do
62+
it 'returns true when there are responses' do
63+
add_member(group, :from_a_friend)
64+
65+
expect(presenter.data_present?).to eq(true)
66+
end
67+
68+
it 'returns false when there are no responses' do
69+
expect(presenter.data_present?).to eq(false)
70+
end
71+
end
72+
end

0 commit comments

Comments
 (0)