• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

codebar / planner / 28187970692

25 Jun 2026 05:14PM UTC coverage: 95.237%. First build
28187970692

Pull #2671

github

mroderick
feat: add chapter status admin page

Admin page at /admin/chapters/status showing which chapters are
active, dormant, or inactive over a configurable 6 or 12 month
window. Each chapter shows workshop count and eligible member
counts (not banned, accepted terms, subscribed) per role.

Active chapters running no workshops in the last n-1 months are
flagged At Risk. The window includes future workshops (3 months
ahead) so chapters with upcoming events stay classified as active.

Uses ERB + ViewComponent.

Also seeds demo chapters for the three status categories:
Bournemouth (inactive), South London (dormant),
Edinburgh (at-risk).
Pull Request #2671: feat: add chapter status admin page

25 of 30 new or added lines in 3 files covered. (83.33%)

3579 of 3758 relevant lines covered (95.24%)

37.29 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

91.18
/app/controllers/admin/chapters_controller.rb
1
class Admin::ChaptersController < Admin::ApplicationController
6✔
2
  before_action :set_chapter, only: %i[show edit update]
6✔
3
  after_action :verify_authorized
6✔
4

5
  def new
6✔
6
    @chapter = Chapter.new
1✔
7
    authorize @chapter
1✔
8
  end
9

10
  def create
6✔
11
    @chapter = Chapter.new(chapter_params)
1✔
12
    authorize(@chapter)
1✔
13

14
    if @chapter.save
1✔
15
      flash[:notice] = "Chapter #{@chapter.name} has been successfully created"
1✔
16
      redirect_to [:admin, @chapter]
1✔
17
    else
18
      flash[:notice] = @chapter.errors.full_messages
×
19
      render 'new'
×
20
    end
21
  end
22

23
  def show
6✔
24
    authorize(@chapter)
6✔
25

26
    @workshops = @chapter.workshops.today_and_upcoming
6✔
27
    @sponsors = @chapter.sponsors.uniq
6✔
28
    @groups = @chapter.groups
6✔
29
    @subscribers = @chapter.subscriptions.last(20).reverse
6✔
30
    @how_you_found_us = HowYouFoundUsPresenter.new(@chapter)
6✔
31
  end
32

33
  def edit
6✔
34
    authorize @chapter
3✔
35
  end
36

37
  def update
6✔
38
    authorize(@chapter)
2✔
39

40
    if @chapter.update(chapter_params)
2✔
41
      flash[:notice] = "Chapter #{@chapter.name} has been successfully updated"
2✔
42
      redirect_to [:admin, @chapter]
2✔
43
    else
44
      flash[:notice] = @chapter.errors.full_messages
×
45
      render 'edit'
×
46
    end
47
  end
48

49
  def status
6✔
50
    skip_authorization
9✔
51
    @months = [6, 12].include?(params[:months].to_i) ? params[:months].to_i : 6
9✔
52
    period_start = @months.months.ago.beginning_of_day
9✔
53
    recent_cutoff = (@months - 1).months.ago.beginning_of_day
9✔
54

55
    chapters = Chapter.all.index_by(&:id)
9✔
56

57
    ws_data = Chapter.joins(:workshops)
9✔
58
      .where(workshops: { date_and_time: period_start..3.months.from_now })
59
      .pluck(:chapter_id, 'workshops.date_and_time')
60

61
    eligible = Member.not_banned.accepted_toc
9✔
62
      .joins(groups: :chapter)
63
      .where(groups: { name: %w[Students Coaches] })
64
      .group(:chapter_id, 'groups.name')
65
      .count
66

67
    ws_by_ch = ws_data.group_by(&:first)
9✔
68

69
    rows = chapters.map do |ch_id, ch|
9✔
70
      ws_dates = (ws_by_ch[ch_id] || []).map(&:second)
5✔
71
      ws_count = ws_dates.size
5✔
72
      ws_recent = ws_dates.count { |d| d >= recent_cutoff }
9✔
73
      {
74
        chapter: ch,
5✔
75
        workshops: ws_count,
76
        recent_workshops: ws_recent,
77
        eligible_students: eligible.fetch([ch_id, 'Students'], 0),
78
        eligible_coaches: eligible.fetch([ch_id, 'Coaches'], 0)
79
      }
80
    end
81

82
    @active = rows.select { |r| r[:workshops] > 0 }
14✔
83
      .sort_by { |r| [-r[:workshops], -(r[:eligible_students] + r[:eligible_coaches])] }
4✔
84

85
    @dormant = rows.select { |r| r[:workshops] == 0 && r[:chapter].active? }
14✔
NEW
86
      .sort_by { |r| -(r[:eligible_students] + r[:eligible_coaches]) }
×
87

88
    @inactive = rows.select { |r| !r[:chapter].active? }
14✔
89
      .sort_by { |r| -(r[:eligible_students] + r[:eligible_coaches]) }
1✔
90

91
    @at_risk_ids = @active.select { |r| r[:recent_workshops] == 0 }.map { |r| r[:chapter].id }.to_set
14✔
92
  end
93

94
  def members
6✔
95
    chapter = Chapter.find(params[:chapter_id])
2✔
96
    authorize chapter
2✔
97
    type = params[:type]
2✔
98

99
    @emails = member_emails(chapter, type)
2✔
100

101
    render plain: @emails
2✔
102
  end
103

104
  private
6✔
105

106
  def chapter_params
6✔
107
    params.expect(chapter: [:name, :email, :city, :time_zone, :description, :image])
3✔
108
  end
109

110
  def set_chapter
6✔
111
    @chapter = Chapter.find(params[:id])
11✔
112
  end
113

114
  def member_emails(chapter, type)
6✔
115
    members =
116
      case type
2✔
117
      when "students"
118
        chapter.students
1✔
119
      when "coaches"
120
        chapter.coaches
×
121
      else
122
        chapter.members
1✔
123
      end
124
    members.distinct.pluck(:email).join("\n")
2✔
125
  end
126
end
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc