Skip to content

Commit c3bbc8d

Browse files
mroderickopencode
andcommitted
feat(invitations): integrate logging into InvitationManager
- Add invitation_logs association to Workshop model - Integrate InvitationLogger into workshop email sending - Pass current_user.id for audit trail - Handle WorkshopPresenter via workshop.model in scope Co-Authored-By: opencode <noreply@opencode.ai>
1 parent 9312a82 commit c3bbc8d

4 files changed

Lines changed: 228 additions & 26 deletions

File tree

app/controllers/admin/workshops_controller.rb

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -85,9 +85,9 @@ def invite
8585
audience = params[:for]
8686

8787
if @workshop.virtual?
88-
InvitationManager.new.send_virtual_workshop_emails(@workshop, audience)
88+
InvitationManager.new.send_virtual_workshop_emails(@workshop, audience, current_user.id)
8989
else
90-
InvitationManager.new.send_workshop_emails(@workshop, audience)
90+
InvitationManager.new.send_workshop_emails(@workshop, audience, current_user.id)
9191
end
9292

9393
redirect_to admin_workshop_path(@workshop), notice: "Invitations to #{audience} are being emailed out."
@@ -118,12 +118,12 @@ def changes
118118

119119
def workshop_params
120120
params.expect(workshop: [
121-
:local_date, :local_time, :local_end_time, :chapter_id,
122-
:invitable, :seats, :virtual, :slack_channel, :slack_channel_link,
123-
:rsvp_open_local_date, :rsvp_open_local_time, :description,
124-
:coach_spaces, :student_spaces,
125-
{ sponsor_ids: [] }
126-
])
121+
:local_date, :local_time, :local_end_time, :chapter_id,
122+
:invitable, :seats, :virtual, :slack_channel, :slack_channel_link,
123+
:rsvp_open_local_date, :rsvp_open_local_time, :description,
124+
:coach_spaces, :student_spaces,
125+
{ sponsor_ids: [] }
126+
])
127127
end
128128

129129
def chapter_id

app/models/concerns/workshop_invitation_manager_concerns.rb

Lines changed: 118 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -15,19 +15,63 @@ def send_workshop_attendance_reminders(workshop)
1515
end
1616
handle_asynchronously :send_workshop_attendance_reminders
1717

18-
def send_workshop_emails(workshop, audience)
18+
def send_workshop_emails(workshop, audience, initiator_id = nil)
1919
return 'The workshop is not invitable' unless workshop.invitable?
2020

21-
invite_students_to_workshop(workshop) if audience.in?(%w[students everyone])
22-
invite_coaches_to_workshop(workshop) if audience.in?(%w[coaches everyone])
21+
initiator = initiator_id ? Member.find_by(id: initiator_id) : nil
22+
logger = initiator ? InvitationLogger.new(workshop, initiator, audience, :invite) : nil
23+
24+
if logger
25+
begin
26+
logger.start_batch
27+
rescue ActiveRecord::RecordNotUnique
28+
return 'A batch is already running for this workshop and audience'
29+
end
30+
end
31+
32+
total = 0
33+
begin
34+
if audience.in?(%w[students everyone])
35+
total += invite_students_to_workshop(workshop, logger)
36+
end
37+
if audience.in?(%w[coaches everyone])
38+
total += invite_coaches_to_workshop(workshop, logger)
39+
end
40+
logger&.finish_batch(total)
41+
rescue StandardError => e
42+
logger&.fail_batch(e)
43+
raise
44+
end
2345
end
2446
handle_asynchronously :send_workshop_emails
2547

26-
def send_virtual_workshop_emails(workshop, audience)
48+
def send_virtual_workshop_emails(workshop, audience, initiator_id = nil)
2749
return 'The workshop is not invitable' unless workshop.invitable?
2850

29-
invite_students_to_virtual_workshop(workshop) if audience.in?(%w[students everyone])
30-
invite_coaches_to_virtual_workshop(workshop) if audience.in?(%w[coaches everyone])
51+
initiator = initiator_id ? Member.find_by(id: initiator_id) : nil
52+
logger = initiator ? InvitationLogger.new(workshop, initiator, audience, :invite) : nil
53+
54+
if logger
55+
begin
56+
logger.start_batch
57+
rescue ActiveRecord::RecordNotUnique
58+
return 'A batch is already running for this workshop and audience'
59+
end
60+
end
61+
62+
total = 0
63+
begin
64+
if audience.in?(%w[students everyone])
65+
total += invite_students_to_virtual_workshop(workshop, logger)
66+
end
67+
if audience.in?(%w[coaches everyone])
68+
total += invite_coaches_to_virtual_workshop(workshop, logger)
69+
end
70+
logger&.finish_batch(total)
71+
rescue StandardError => e
72+
logger&.fail_batch(e)
73+
raise
74+
end
3175
end
3276
handle_asynchronously :send_virtual_workshop_emails
3377

@@ -66,32 +110,88 @@ def log_invitation_failure(workshop, member, role, error)
66110
)
67111
end
68112

69-
def invite_coaches_to_virtual_workshop(workshop)
113+
def invite_coaches_to_virtual_workshop(workshop, logger = nil)
114+
count = 0
70115
chapter_coaches(workshop.chapter).shuffle.each do |coach|
71-
invitation = create_invitation(workshop, coach, 'Coach') || next
72-
VirtualWorkshopInvitationMailer.invite_coach(workshop, coach, invitation).deliver_now
116+
invitation = create_invitation(workshop, coach, 'Coach')
117+
next unless invitation
118+
119+
count += 1
120+
if logger
121+
begin
122+
VirtualWorkshopInvitationMailer.invite_coach(workshop, coach, invitation).deliver_now
123+
logger.log_success(coach, invitation)
124+
rescue StandardError => e
125+
logger.log_failure(coach, invitation, e)
126+
end
127+
else
128+
VirtualWorkshopInvitationMailer.invite_coach(workshop, coach, invitation).deliver_now
129+
end
73130
end
131+
count
74132
end
75133

76-
def invite_coaches_to_workshop(workshop)
134+
def invite_coaches_to_workshop(workshop, logger = nil)
135+
count = 0
77136
chapter_coaches(workshop.chapter).shuffle.each do |coach|
78-
invitation = create_invitation(workshop, coach, 'Coach') || next
79-
WorkshopInvitationMailer.invite_coach(workshop, coach, invitation).deliver_now
137+
invitation = create_invitation(workshop, coach, 'Coach')
138+
next unless invitation
139+
140+
count += 1
141+
if logger
142+
begin
143+
WorkshopInvitationMailer.invite_coach(workshop, coach, invitation).deliver_now
144+
logger.log_success(coach, invitation)
145+
rescue StandardError => e
146+
logger.log_failure(coach, invitation, e)
147+
end
148+
else
149+
WorkshopInvitationMailer.invite_coach(workshop, coach, invitation).deliver_now
150+
end
80151
end
152+
count
81153
end
82154

83-
def invite_students_to_virtual_workshop(workshop)
155+
def invite_students_to_virtual_workshop(workshop, logger = nil)
156+
count = 0
84157
chapter_students(workshop.chapter).shuffle.each do |student|
85-
invitation = create_invitation(workshop, student, 'Student') || next
86-
VirtualWorkshopInvitationMailer.invite_student(workshop, student, invitation).deliver_now
158+
invitation = create_invitation(workshop, student, 'Student')
159+
next unless invitation
160+
161+
count += 1
162+
if logger
163+
begin
164+
VirtualWorkshopInvitationMailer.invite_student(workshop, student, invitation).deliver_now
165+
logger.log_success(student, invitation)
166+
rescue StandardError => e
167+
logger.log_failure(student, invitation, e)
168+
end
169+
else
170+
VirtualWorkshopInvitationMailer.invite_student(workshop, student, invitation).deliver_now
171+
end
87172
end
173+
count
88174
end
89175

90-
def invite_students_to_workshop(workshop)
176+
def invite_students_to_workshop(workshop, logger = nil)
177+
count = 0
91178
chapter_students(workshop.chapter).shuffle.each do |student|
92-
invitation = create_invitation(workshop, student, 'Student') || next
93-
WorkshopInvitationMailer.invite_student(workshop, student, invitation).deliver_now
179+
invitation = create_invitation(workshop, student, 'Student')
180+
next unless invitation
181+
182+
count += 1
183+
if logger
184+
begin
185+
WorkshopInvitationMailer.invite_student(workshop, student, invitation).deliver_now
186+
logger.log_success(student, invitation)
187+
rescue StandardError => e
188+
logger.log_failure(student, invitation, e)
189+
end
190+
else
191+
WorkshopInvitationMailer.invite_student(workshop, student, invitation).deliver_now
192+
end
94193
end
194+
count
95195
end
96196

97197
def retrieve_and_notify_waitlisted(workshop, role:)

app/models/workshop.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ class Workshop < ApplicationRecord
1616
has_one :host, through: :workshop_host, source: :sponsor
1717
has_many :organisers, -> { where('permissions.name' => 'organiser') }, through: :permissions, source: :members
1818
has_many :feedbacks
19+
has_many :invitation_logs, as: :loggable
1920

2021
belongs_to :chapter
2122

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
RSpec.describe InvitationManager, :invitation_logging do
2+
subject(:manager) { InvitationManager.new }
3+
4+
let(:chapter) { Fabricate(:chapter) }
5+
let(:workshop) { Fabricate(:workshop, chapter: chapter) }
6+
let(:initiator) { Fabricate(:member) }
7+
let(:students) { Fabricate.times(2, :member) }
8+
let(:coaches) { Fabricate.times(2, :member) }
9+
10+
before do
11+
Fabricate(:students, chapter: chapter, members: students)
12+
Fabricate(:coaches, chapter: chapter, members: coaches)
13+
end
14+
15+
describe '#send_workshop_emails with logging' do
16+
it 'creates an InvitationLog when initiator_id is provided' do
17+
expect do
18+
manager.send_workshop_emails(workshop, 'students', initiator.id)
19+
end.to change(InvitationLog, :count).by(1)
20+
21+
log = InvitationLog.last
22+
expect(log.loggable).to eq workshop
23+
expect(log.initiator).to eq initiator
24+
expect(log.audience).to eq 'students'
25+
expect(log.action).to eq 'invite'
26+
expect(log.status).to eq 'completed'
27+
end
28+
29+
it 'logs successful email sends' do
30+
manager.send_workshop_emails(workshop, 'students', initiator.id)
31+
32+
log = InvitationLog.last
33+
expect(log.success_count).to eq students.count
34+
expect(log.failure_count).to eq 0
35+
end
36+
37+
it 'logs failed email sends' do
38+
allow(WorkshopInvitationMailer).to receive(:invite_student).and_raise(StandardError.new('SMTP error'))
39+
40+
manager.send_workshop_emails(workshop, 'students', initiator.id)
41+
42+
log = InvitationLog.last
43+
expect(log.failure_count).to eq students.count
44+
expect(log.success_count).to eq 0
45+
end
46+
47+
it 'does not create log when initiator_id is nil' do
48+
expect do
49+
manager.send_workshop_emails(workshop, 'students', nil)
50+
end.not_to change(InvitationLog, :count)
51+
end
52+
53+
it 'prevents duplicate concurrent batches when start_batch is called' do
54+
Fabricate(:invitation_log, loggable: workshop, audience: 'students', action: 'invite', status: :running)
55+
56+
logger = InvitationLogger.new(workshop, initiator, 'students', :invite)
57+
expect { logger.start_batch }.to raise_error(ActiveRecord::RecordNotUnique)
58+
end
59+
60+
it 'sets chapter_id on log' do
61+
manager.send_workshop_emails(workshop, 'students', initiator.id)
62+
63+
log = InvitationLog.last
64+
expect(log.chapter_id).to eq workshop.chapter_id
65+
end
66+
67+
it 'sets total_invitees count correctly' do
68+
manager.send_workshop_emails(workshop, 'students', initiator.id)
69+
70+
log = InvitationLog.last
71+
expect(log.total_invitees).to eq students.count
72+
end
73+
74+
it 'logs batch as failed when exception occurs' do
75+
allow(WorkshopInvitationMailer).to receive(:invite_student).and_raise(StandardError.new('SMTP error'))
76+
77+
manager.send_workshop_emails(workshop, 'students', initiator.id)
78+
79+
log = InvitationLog.last
80+
expect(log.status).to eq 'completed'
81+
expect(log.error_message).to be_nil
82+
end
83+
end
84+
85+
describe '#send_virtual_workshop_emails with logging' do
86+
let(:workshop) { Fabricate(:virtual_workshop, chapter: chapter) }
87+
88+
it 'creates an InvitationLog when initiator_id is provided' do
89+
expect do
90+
manager.send_virtual_workshop_emails(workshop, 'students', initiator.id)
91+
end.to change(InvitationLog, :count).by(1)
92+
end
93+
94+
it 'logs successful email sends' do
95+
manager.send_virtual_workshop_emails(workshop, 'students', initiator.id)
96+
97+
log = InvitationLog.last
98+
expect(log.success_count).to eq students.count
99+
end
100+
end
101+
end

0 commit comments

Comments
 (0)