Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions app/controllers/admin/workshop_invitation_logs_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
module Admin
class WorkshopInvitationLogsController < Admin::ApplicationController
def index
@workshop = Workshop.find(params[:workshop_id])
authorize @workshop, :show?
logs = InvitationLog.where(loggable: @workshop)
.order(created_at: :desc)
.includes(:initiator, :entries)
@pagy, @logs = pagy(logs)
end

def show
@workshop = Workshop.find(params[:workshop_id])
authorize @workshop, :show?
@log = InvitationLog.find(params[:id])
@entries = @log.entries.order(processed_at: :desc).includes(:member)
end
end
end
16 changes: 8 additions & 8 deletions app/controllers/admin/workshops_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -85,9 +85,9 @@ def invite
audience = params[:for]

if @workshop.virtual?
InvitationManager.new.send_virtual_workshop_emails(@workshop, audience)
InvitationManager.new.send_virtual_workshop_emails(@workshop, audience, current_user.id)
else
InvitationManager.new.send_workshop_emails(@workshop, audience)
InvitationManager.new.send_workshop_emails(@workshop, audience, current_user.id)
end

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

def workshop_params
params.expect(workshop: [
:local_date, :local_time, :local_end_time, :chapter_id,
:invitable, :seats, :virtual, :slack_channel, :slack_channel_link,
:rsvp_open_local_date, :rsvp_open_local_time, :description,
:coach_spaces, :student_spaces,
{ sponsor_ids: [] }
])
:local_date, :local_time, :local_end_time, :chapter_id,
:invitable, :seats, :virtual, :slack_channel, :slack_channel_link,
:rsvp_open_local_date, :rsvp_open_local_time, :description,
:coach_spaces, :student_spaces,
{ sponsor_ids: [] }
])
end

def chapter_id
Expand Down
7 changes: 7 additions & 0 deletions app/jobs/cleanup_expired_invitation_logs_job.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
class CleanupExpiredInvitationLogsJob < ApplicationJob
queue_as :default

def perform
InvitationLog.where('expires_at < ?', Time.current).destroy_all
end
end
151 changes: 131 additions & 20 deletions app/models/concerns/workshop_invitation_manager_concerns.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,19 +15,63 @@ def send_workshop_attendance_reminders(workshop)
end
handle_asynchronously :send_workshop_attendance_reminders

def send_workshop_emails(workshop, audience)
def send_workshop_emails(workshop, audience, initiator_id = nil)
return 'The workshop is not invitable' unless workshop.invitable?

invite_students_to_workshop(workshop) if audience.in?(%w[students everyone])
invite_coaches_to_workshop(workshop) if audience.in?(%w[coaches everyone])
initiator = initiator_id ? Member.find_by(id: initiator_id) : nil
logger = initiator ? InvitationLogger.new(workshop, initiator, audience, :invite) : nil

if logger
begin
logger.start_batch
rescue ActiveRecord::RecordNotUnique
return 'A batch is already running for this workshop and audience'
end
end

total = 0
begin
if audience.in?(%w[students everyone])
total += invite_students_to_workshop(workshop, logger)
end
if audience.in?(%w[coaches everyone])
total += invite_coaches_to_workshop(workshop, logger)
end
logger&.finish_batch(total)
rescue StandardError => e
logger&.fail_batch(e)
raise
end
end
handle_asynchronously :send_workshop_emails

def send_virtual_workshop_emails(workshop, audience)
def send_virtual_workshop_emails(workshop, audience, initiator_id = nil)
return 'The workshop is not invitable' unless workshop.invitable?

invite_students_to_virtual_workshop(workshop) if audience.in?(%w[students everyone])
invite_coaches_to_virtual_workshop(workshop) if audience.in?(%w[coaches everyone])
initiator = initiator_id ? Member.find_by(id: initiator_id) : nil
logger = initiator ? InvitationLogger.new(workshop, initiator, audience, :invite) : nil

if logger
begin
logger.start_batch
rescue ActiveRecord::RecordNotUnique
return 'A batch is already running for this workshop and audience'
end
end

total = 0
begin
if audience.in?(%w[students everyone])
total += invite_students_to_virtual_workshop(workshop, logger)
end
if audience.in?(%w[coaches everyone])
total += invite_coaches_to_virtual_workshop(workshop, logger)
end
logger&.finish_batch(total)
rescue StandardError => e
logger&.fail_batch(e)
raise
end
end
handle_asynchronously :send_virtual_workshop_emails

Expand All @@ -51,36 +95,103 @@ def send_workshop_waiting_list_reminders(workshop)
private

def create_invitation(workshop, member, role)
invitation = WorkshopInvitation.create(workshop: workshop, member: member, role: role)
invitation.persisted? ? invitation : nil
WorkshopInvitation.find_or_create_by(workshop: workshop, member: member, role: role)
rescue StandardError => e
log_invitation_failure(workshop, member, role, e)
nil
end

def log_invitation_failure(workshop, member, role, error)
Rails.logger.error(
'[InvitationManager] Failed to create invitation: ' \
"workshop_id=#{workshop.id}, chapter_id=#{workshop.chapter_id}, " \
"member_id=#{member.id}, role=#{role}, " \
"error=#{error.class.name}: #{error.message}"
)
end

def invite_coaches_to_virtual_workshop(workshop)
def invite_coaches_to_virtual_workshop(workshop, logger = nil)
count = 0
chapter_coaches(workshop.chapter).shuffle.each do |coach|
invitation = create_invitation(workshop, coach, 'Coach') || next
VirtualWorkshopInvitationMailer.invite_coach(workshop, coach, invitation).deliver_now
invitation = create_invitation(workshop, coach, 'Coach')
next unless invitation

count += 1
if logger
begin
VirtualWorkshopInvitationMailer.invite_coach(workshop, coach, invitation).deliver_now
logger.log_success(coach, invitation)
rescue StandardError => e
logger.log_failure(coach, invitation, e)
end
else
VirtualWorkshopInvitationMailer.invite_coach(workshop, coach, invitation).deliver_now
end
end
count
end

def invite_coaches_to_workshop(workshop)
def invite_coaches_to_workshop(workshop, logger = nil)
count = 0
chapter_coaches(workshop.chapter).shuffle.each do |coach|
invitation = create_invitation(workshop, coach, 'Coach') || next
WorkshopInvitationMailer.invite_coach(workshop, coach, invitation).deliver_now
invitation = create_invitation(workshop, coach, 'Coach')
next unless invitation

count += 1
if logger
begin
WorkshopInvitationMailer.invite_coach(workshop, coach, invitation).deliver_now
logger.log_success(coach, invitation)
rescue StandardError => e
logger.log_failure(coach, invitation, e)
end
else
WorkshopInvitationMailer.invite_coach(workshop, coach, invitation).deliver_now
end
end
count
end

def invite_students_to_virtual_workshop(workshop)
def invite_students_to_virtual_workshop(workshop, logger = nil)
count = 0
chapter_students(workshop.chapter).shuffle.each do |student|
invitation = create_invitation(workshop, student, 'Student') || next
VirtualWorkshopInvitationMailer.invite_student(workshop, student, invitation).deliver_now
invitation = create_invitation(workshop, student, 'Student')
next unless invitation

count += 1
if logger
begin
VirtualWorkshopInvitationMailer.invite_student(workshop, student, invitation).deliver_now
logger.log_success(student, invitation)
rescue StandardError => e
logger.log_failure(student, invitation, e)
end
else
VirtualWorkshopInvitationMailer.invite_student(workshop, student, invitation).deliver_now
end
end
count
end

def invite_students_to_workshop(workshop)
def invite_students_to_workshop(workshop, logger = nil)
count = 0
chapter_students(workshop.chapter).shuffle.each do |student|
invitation = create_invitation(workshop, student, 'Student') || next
WorkshopInvitationMailer.invite_student(workshop, student, invitation).deliver_now
invitation = create_invitation(workshop, student, 'Student')
next unless invitation

count += 1
if logger
begin
WorkshopInvitationMailer.invite_student(workshop, student, invitation).deliver_now
logger.log_success(student, invitation)
rescue StandardError => e
logger.log_failure(student, invitation, e)
end
else
WorkshopInvitationMailer.invite_student(workshop, student, invitation).deliver_now
end
end
count
end

def retrieve_and_notify_waitlisted(workshop, role:)
Expand Down
18 changes: 18 additions & 0 deletions app/models/invitation_log.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
class InvitationLog < ApplicationRecord
enum :action, { invite: 'invite', reminder: 'reminder', waiting_list_notification: 'waiting_list_notification' },
prefix: false
enum :status, { running: 'running', completed: 'completed', failed: 'failed' }, prefix: false

belongs_to :loggable, polymorphic: true
belongs_to :initiator, class_name: 'Member', optional: true
belongs_to :chapter, optional: true
has_many :entries, class_name: 'InvitationLogEntry', dependent: :destroy

before_create :set_expires_at

private

def set_expires_at
self.expires_at ||= 180.days.from_now
end
end
9 changes: 9 additions & 0 deletions app/models/invitation_log_entry.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
class InvitationLogEntry < ApplicationRecord
enum :status, { success: 'success', failed: 'failed', skipped: 'skipped' }, prefix: false

belongs_to :invitation_log
belongs_to :member
belongs_to :invitation, polymorphic: true, optional: true

validates :member_id, uniqueness: { scope: %i[invitation_type invitation_id] }, allow_nil: true
end
26 changes: 23 additions & 3 deletions app/models/invitation_manager.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,11 @@ def send_monthly_attendance_reminder_emails(monthly)
def send_meeting_emails(meeting)
meeting.invitees.not_banned.each do |invitee|
invitation = MeetingInvitation.new(meeting: meeting, member: invitee, role: 'Participant')
MeetingInvitationMailer.invite(meeting, invitee, invitation).deliver_now if invitation.save
next unless invitation.save

MeetingInvitationMailer.invite(meeting, invitee, invitation).deliver_now
rescue StandardError => e
log_event_meeting_invitation_failure("meeting_id=#{meeting.id}", invitee, e)
end
end
handle_asynchronously :send_meeting_emails
Expand All @@ -30,17 +34,33 @@ def send_meeting_emails(meeting)
def invite_students_to_event(event, chapter)
chapter_students(chapter).each do |student|
invitation = Invitation.new(event: event, member: student, role: 'Student')
EventInvitationMailer.invite_student(event, student, invitation).deliver_now if invitation.save
next unless invitation.save

EventInvitationMailer.invite_student(event, student, invitation).deliver_now
rescue StandardError => e
log_event_meeting_invitation_failure("event_id=#{event.id}", student, e)
end
end

def invite_coaches_to_event(event, chapter)
chapter_coaches(chapter).each do |coach|
invitation = Invitation.new(event: event, member: coach, role: 'Coach')
EventInvitationMailer.invite_coach(event, coach, invitation).deliver_now if invitation.save
next unless invitation.save

EventInvitationMailer.invite_coach(event, coach, invitation).deliver_now
rescue StandardError => e
log_event_meeting_invitation_failure("event_id=#{event.id}", coach, e)
end
end

def log_event_meeting_invitation_failure(context, member, error)
Rails.logger.error(
'[InvitationManager] Failed to create invitation: ' \
"#{context}, member_id=#{member.id}, " \
"error=#{error.class.name}: #{error.message}"
)
end

def chapter_students(chapter)
Member.in_group(chapter.groups.students)
end
Expand Down
1 change: 1 addition & 0 deletions app/models/workshop.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ class Workshop < ApplicationRecord
has_one :host, through: :workshop_host, source: :sponsor
has_many :organisers, -> { where('permissions.name' => 'organiser') }, through: :permissions, source: :members
has_many :feedbacks
has_many :invitation_logs, as: :loggable

belongs_to :chapter

Expand Down
9 changes: 9 additions & 0 deletions app/policies/invitation_log_policy.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
class InvitationLogPolicy < ApplicationPolicy
def index?
is_admin_or_chapter_organiser?
end

def show?
is_admin_or_chapter_organiser?
end
end
Loading