From fe83aaa2e77f7bb03a17dec29372ae52a69b8e1c Mon Sep 17 00:00:00 2001 From: Morgan Roderick Date: Fri, 10 Jul 2026 08:54:31 +0200 Subject: [PATCH 1/2] refactor: consolidate duplicate clear_member_cache callback into InvitationConcerns MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The same after_save callback + private method was duplicated across Invitation, WorkshopInvitation, and MeetingInvitation. All three already include InvitationConcerns, so move the callback there. Score 2/5 per the layered-architecture callback rubric (pure side effect — cache busting). Acceptable in a concern shared by all three models rather than extracting a separate service, keeping the fix minimal. --- app/models/concerns/invitation_concerns.rb | 5 +++++ app/models/invitation.rb | 8 -------- app/models/meeting_invitation.rb | 8 -------- app/models/workshop_invitation.rb | 8 -------- 4 files changed, 5 insertions(+), 24 deletions(-) diff --git a/app/models/concerns/invitation_concerns.rb b/app/models/concerns/invitation_concerns.rb index 367ec2edd..65b050e62 100644 --- a/app/models/concerns/invitation_concerns.rb +++ b/app/models/concerns/invitation_concerns.rb @@ -15,6 +15,7 @@ module InvitationConcerns scope :taken_place, -> { where('date_and_time < ?', Time.zone.now) } before_create :set_token + after_save :clear_member_cache, if: :saved_change_to_attending? end module InstanceMethods @@ -36,6 +37,10 @@ def for_participant? private + def clear_member_cache + member.clear_attending_event_ids_cache! + end + def set_token self.token = loop do random_token = SecureRandom.urlsafe_base64(nil, false) diff --git a/app/models/invitation.rb b/app/models/invitation.rb index aa4c63db4..46d554406 100644 --- a/app/models/invitation.rb +++ b/app/models/invitation.rb @@ -13,8 +13,6 @@ class Invitation < ApplicationRecord scope :coaches, -> { where(role: 'Coach') } scope :verified, -> { where(verified: true).order(:updated_at) } - after_save :clear_member_cache, if: :saved_change_to_attending? - def student_spaces? for_student? && event.student_spaces? end @@ -26,10 +24,4 @@ def coach_spaces? def to_param token end - - private - - def clear_member_cache - member.clear_attending_event_ids_cache! - end end diff --git a/app/models/meeting_invitation.rb b/app/models/meeting_invitation.rb index d866e516b..e5f5aef8e 100644 --- a/app/models/meeting_invitation.rb +++ b/app/models/meeting_invitation.rb @@ -10,13 +10,5 @@ class MeetingInvitation < ApplicationRecord scope :accepted, -> { where(attending: true) } scope :attended, -> { where(attended: true) } - after_save :clear_member_cache, if: :saved_change_to_attending? - alias event meeting - - private - - def clear_member_cache - member.clear_attending_event_ids_cache! - end end diff --git a/app/models/workshop_invitation.rb b/app/models/workshop_invitation.rb index d4dd6596d..1f206db00 100644 --- a/app/models/workshop_invitation.rb +++ b/app/models/workshop_invitation.rb @@ -25,8 +25,6 @@ class WorkshopInvitation < ApplicationRecord scope :on_waiting_list, -> { joins(:waiting_list) } scope :with_notes_and_their_authors, -> { includes(member: [{ member_notes: :author }, :attendance_warnings]).includes(:overrider) } - after_save :clear_member_cache, if: :saved_change_to_attending? - def waiting_list_position @waiting_list_position ||= WaitingList.by_workshop(workshop) .where_role(role) @@ -49,10 +47,4 @@ def student_attending? def not_attending? attending == false end - - private - - def clear_member_cache - member.clear_attending_event_ids_cache! - end end From 5d193c79070096c6486b1043cccebe8dd96d3fdd Mon Sep 17 00:00:00 2001 From: Morgan Roderick Date: Fri, 10 Jul 2026 08:54:37 +0200 Subject: [PATCH 2/2] test: move cache-invalidation test to shared example, remove redundant individual test The cache-invalidation test was only in invitation_spec.rb, leaving WorkshopInvitation and MeetingInvitation untested for the same callback. Add the test to the behaves_like_an_invitation shared example so all three models are covered. Remove the now-redundant test from invitation_spec.rb -- the shared example runs it for Invitation too. The member_spec tests for attending_event_ids caching and clear_attending_event_ids_cache! remain unchanged -- they verify the member-side contract rather than the callback trigger. --- spec/models/invitation_spec.rb | 11 ----------- .../shared_examples/behaves_like_an_invitation.rb | 7 +++++++ 2 files changed, 7 insertions(+), 11 deletions(-) diff --git a/spec/models/invitation_spec.rb b/spec/models/invitation_spec.rb index 7322d51ae..1150cd1a2 100644 --- a/spec/models/invitation_spec.rb +++ b/spec/models/invitation_spec.rb @@ -27,15 +27,4 @@ expect(coach_invitation.coach_spaces?).to eq(true) end end - - describe 'cache invalidation' do - let(:member) { Fabricate(:member) } - let(:event) { Fabricate(:event) } - - it 'clears member cache when attending changes' do - invitation = Fabricate(:invitation, member: member, event: event, attending: false) - expect(member).to receive(:clear_attending_event_ids_cache!) - invitation.update!(attending: true) - end - end end diff --git a/spec/support/shared_examples/behaves_like_an_invitation.rb b/spec/support/shared_examples/behaves_like_an_invitation.rb index 9524f2544..464d4dccb 100644 --- a/spec/support/shared_examples/behaves_like_an_invitation.rb +++ b/spec/support/shared_examples/behaves_like_an_invitation.rb @@ -6,6 +6,13 @@ expect(invitation.token).not_to be_nil end + describe 'cache invalidation' do + it 'clears member cache when attending changes' do + expect(invitation.member).to receive(:clear_attending_event_ids_cache!) + invitation.update!(attending: !invitation.attending) + end + end + describe '#scopes' do describe '#not_accepted' do it 'selects when attended nil' do