From 7539761dbf475effc54f2f9feab0ab7b030e39a2 Mon Sep 17 00:00:00 2001 From: Thomas Leese Date: Thu, 21 Aug 2025 15:48:08 +0100 Subject: [PATCH] Replace session attendance patient session foreign key This replaces the foreign key association between session attendances and patient sessions to instead link directly to the patient. This is needed as we eventually want to replace the PatientSession model and to do that we need to make sure all foreign keys to it have been replaced. The functionality should be the same before and after. Jira-Issue: MAV-1820 --- ...nt_session_search_result_card_component.rb | 4 +- .../session_attendances_controller.rb | 10 +-- app/lib/generate/vaccination_records.rb | 17 ++--- app/lib/status_updater.rb | 1 + app/models/patient.rb | 4 +- app/models/patient_session.rb | 12 ++-- .../patient_session/registration_status.rb | 11 ++-- app/models/session.rb | 1 + app/models/session_attendance.rb | 26 ++++---- app/policies/session_attendance_policy.rb | 21 +++---- ...atient_session_from_session_attendances.rb | 42 +++++++++++++ db/schema.rb | 7 ++- spec/factories/patient_sessions.rb | 12 +++- spec/factories/patients.rb | 7 ++- spec/factories/session_attendances.rb | 25 ++++---- spec/features/manage_attendance_spec.rb | 7 +-- .../lib/status_generator/registration_spec.rb | 6 +- spec/lib/status_generator/session_spec.rb | 62 +++++++++---------- .../registration_status_spec.rb | 10 +-- spec/models/patient_session_spec.rb | 6 +- spec/models/session_attendance_spec.rb | 34 ++++++++++ spec/models/session_date_spec.rb | 8 +-- .../session_attendance_policy_spec.rb | 18 ++++-- 23 files changed, 224 insertions(+), 127 deletions(-) create mode 100644 db/migrate/20250821135954_remove_patient_session_from_session_attendances.rb create mode 100644 spec/models/session_attendance_spec.rb diff --git a/app/components/app_patient_session_search_result_card_component.rb b/app/components/app_patient_session_search_result_card_component.rb index f75929eb7f..a30901cbda 100644 --- a/app/components/app_patient_session_search_result_card_component.rb +++ b/app/components/app_patient_session_search_result_card_component.rb @@ -97,8 +97,8 @@ def initialize(patient_session, context:, programmes: []) def can_register_attendance? session_attendance = SessionAttendance.new( - patient_session:, - session_date: SessionDate.new(value: Date.current) + patient:, + session_date: SessionDate.new(session:, value: Date.current) ) policy(session_attendance).new? diff --git a/app/controllers/patient_sessions/session_attendances_controller.rb b/app/controllers/patient_sessions/session_attendances_controller.rb index 9f2c762d33..77fee1c494 100644 --- a/app/controllers/patient_sessions/session_attendances_controller.rb +++ b/app/controllers/patient_sessions/session_attendances_controller.rb @@ -43,10 +43,12 @@ def update def set_session_attendance @session_attendance = - authorize @patient_session - .session_attendances - .includes(:patient, :session_date) - .find_or_initialize_by(session_date: @session_date) + authorize( + @patient + .session_attendances + .includes(:patient, session_date: { session: :programmes }) + .find_or_initialize_by(session_date: @session_date) + ) end def session_attendance_params diff --git a/app/lib/generate/vaccination_records.rb b/app/lib/generate/vaccination_records.rb index da86227814..269fc05c12 100644 --- a/app/lib/generate/vaccination_records.rb +++ b/app/lib/generate/vaccination_records.rb @@ -23,17 +23,20 @@ def create_vaccinations vaccination_records = [] random_patient_sessions.each do |patient_session| - patient_session_id = patient_session.id - session_date_ids = patient_session.session.session_dates.pluck(:id) - - unless SessionAttendance.exists?( - patient_session_id:, - session_date_id: session_date_ids + patient = patient_session.patient + session = patient_session.session + + unless SessionAttendance.joins(:session_date).exists?( + patient:, + session_date: { + session: + } ) session_attendances << FactoryBot.build( :session_attendance, :present, - patient_session: + patient:, + session: ) end diff --git a/app/lib/status_updater.rb b/app/lib/status_updater.rb index d9ad1c577c..c4d8fa3e71 100644 --- a/app/lib/status_updater.rb +++ b/app/lib/status_updater.rb @@ -59,6 +59,7 @@ def update_registration_statuses! .where(patient_session_id: patient_sessions.select(:id)) .includes( :session_attendances, + :session_date, :vaccination_records, patient_session: { session: :programmes diff --git a/app/models/patient.rb b/app/models/patient.rb index 25d3dd631f..f05c5c1fc6 100644 --- a/app/models/patient.rb +++ b/app/models/patient.rb @@ -65,6 +65,7 @@ class Patient < ApplicationRecord has_many :consent_notifications has_many :consent_statuses has_many :consents + has_many :gillick_assessments has_many :notes has_many :notify_log_entries has_many :parent_relationships, -> { order(:created_at) } @@ -73,6 +74,7 @@ class Patient < ApplicationRecord has_many :pre_screenings has_many :school_move_log_entries has_many :school_moves + has_many :session_attendances has_many :session_notifications has_many :triage_statuses has_many :triages @@ -80,10 +82,8 @@ class Patient < ApplicationRecord has_many :vaccination_statuses has_many :patient_specific_directions - has_many :gillick_assessments has_many :parents, through: :parent_relationships has_many :patient_specific_directions - has_many :session_attendances, through: :patient_sessions has_many :sessions, through: :patient_sessions has_many :teams, -> { distinct }, through: :sessions diff --git a/app/models/patient_session.rb b/app/models/patient_session.rb index 66ba09bcd7..e9d93c4872 100644 --- a/app/models/patient_session.rb +++ b/app/models/patient_session.rb @@ -44,7 +44,6 @@ class PatientSession < ApplicationRecord belongs_to :patient belongs_to :session - has_many :session_attendances, dependent: :destroy has_one :registration_status has_one :location, through: :session @@ -67,6 +66,10 @@ class PatientSession < ApplicationRecord -> { where(patient_id: it.patient_id) }, through: :session + has_many :session_attendances, + -> { where(patient_id: it.patient_id) }, + through: :session + has_many :session_notifications, -> { where(session_id: it.session_id) }, through: :patient @@ -315,9 +318,10 @@ def programmes = session.programmes_for(patient:, academic_year:) def todays_attendance if (session_date = session.session_dates.today.first) - session_attendances.includes(:session_date).find_or_initialize_by( - session_date: - ) + patient + .session_attendances + .includes(:patient, session_date: { session: :programmes }) + .find_or_initialize_by(session_date:) end end diff --git a/app/models/patient_session/registration_status.rb b/app/models/patient_session/registration_status.rb index abbebd3180..79f1bc4c5a 100644 --- a/app/models/patient_session/registration_status.rb +++ b/app/models/patient_session/registration_status.rb @@ -21,21 +21,24 @@ class PatientSession::RegistrationStatus < ApplicationRecord belongs_to :patient_session has_one :patient, through: :patient_session + has_one :session, through: :patient_session has_many :vaccination_records, -> { kept.order(performed_at: :desc) }, through: :patient - has_many :session_attendances, - -> { includes(:session_date) }, - through: :patient_session + has_one :session_date, -> { today }, through: :session, source: :session_dates + + has_many :session_attendances, through: :patient enum :status, { unknown: 0, attending: 1, not_attending: 2, completed: 3 }, default: :unknown, validate: true - def session_attendance = session_attendances.find(&:today?) + def session_attendance + session_attendances.find { it.session_date_id == session_date.id } + end def assign_status self.status = generator.status diff --git a/app/models/session.rb b/app/models/session.rb index 47d536e000..f69d5d936c 100644 --- a/app/models/session.rb +++ b/app/models/session.rb @@ -55,6 +55,7 @@ class Session < ApplicationRecord has_many :programmes, through: :session_programmes has_many :gillick_assessments, through: :session_dates has_many :patients, through: :patient_sessions + has_many :session_attendances, through: :session_dates has_many :vaccines, through: :programmes has_many :location_programme_year_groups, diff --git a/app/models/session_attendance.rb b/app/models/session_attendance.rb index ebd4603849..c430bb9780 100644 --- a/app/models/session_attendance.rb +++ b/app/models/session_attendance.rb @@ -4,31 +4,31 @@ # # Table name: session_attendances # -# id :bigint not null, primary key -# attending :boolean not null -# created_at :datetime not null -# updated_at :datetime not null -# patient_session_id :bigint not null -# session_date_id :bigint not null +# id :bigint not null, primary key +# attending :boolean not null +# created_at :datetime not null +# updated_at :datetime not null +# patient_id :bigint not null +# session_date_id :bigint not null # # Indexes # -# idx_on_patient_session_id_session_date_id_be8bd21ddf (patient_session_id,session_date_id) UNIQUE -# index_session_attendances_on_session_date_id (session_date_id) +# index_session_attendances_on_patient_id (patient_id) +# index_session_attendances_on_patient_id_and_session_date_id (patient_id,session_date_id) UNIQUE +# index_session_attendances_on_session_date_id (session_date_id) # # Foreign Keys # -# fk_rails_... (patient_session_id => patient_sessions.id) +# fk_rails_... (patient_id => patients.id) # fk_rails_... (session_date_id => session_dates.id) # class SessionAttendance < ApplicationRecord - audited associated_with: :patient_session + audited associated_with: :patient - belongs_to :patient_session + belongs_to :patient belongs_to :session_date - has_one :session, through: :patient_session - has_one :patient, through: :patient_session + has_one :session, through: :session_date has_one :location, through: :session scope :today, -> { joins(:session_date).merge(SessionDate.today) } diff --git a/app/policies/session_attendance_policy.rb b/app/policies/session_attendance_policy.rb index 171058fe17..c5ce680a88 100644 --- a/app/policies/session_attendance_policy.rb +++ b/app/policies/session_attendance_policy.rb @@ -11,23 +11,22 @@ def update? private - delegate :patient_session, :session_date, to: :record - - def academic_year = patient_session.session.academic_year + delegate :patient, :session_date, to: :record + delegate :session, to: :session_date + delegate :academic_year, to: :session def already_vaccinated? - patient_session.programmes.all? do |programme| - patient_session - .patient - .vaccination_status(programme:, academic_year:) - .vaccinated? - end + session + .programmes_for(patient:, academic_year:) + .all? do |programme| + patient.vaccination_status(programme:, academic_year:).vaccinated? + end end def was_seen_by_nurse? VaccinationRecord.kept.exists?( - patient_id: patient_session.patient_id, - session_id: patient_session.session_id, + patient:, + session:, performed_at: session_date.value.all_day ) end diff --git a/db/migrate/20250821135954_remove_patient_session_from_session_attendances.rb b/db/migrate/20250821135954_remove_patient_session_from_session_attendances.rb new file mode 100644 index 0000000000..6b040cb6ac --- /dev/null +++ b/db/migrate/20250821135954_remove_patient_session_from_session_attendances.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +class RemovePatientSessionFromSessionAttendances < ActiveRecord::Migration[8.0] + def up + change_table :session_attendances, bulk: true do |t| + t.references :patient, foreign_key: true + t.index %i[patient_id session_date_id], unique: true + end + + SessionAttendance.find_each do |session_attendance| + patient_session = + PatientSession.find(session_attendance.patient_session_id) + patient_id = patient_session.patient_id + session_attendance.update_column(:patient_id, patient_id) + end + + change_table :session_attendances, bulk: true do |t| + t.change_null :patient_id, false + t.remove_references :patient_session + end + end + + def down + add_reference :session_attendances, :patient_session + + SessionAttendance.find_each do |session_attendance| + session_id = + SessionDate.find(session_attendance.session_date_id).session_id + patient_session = + PatientSession.find_by!( + patient_id: session_attendance.patient_id, + session_id: + ) + session_attendance.update_column(:patient_session_id, patient_session.id) + end + + change_table :session_attendances, bulk: true do |t| + t.change_null :patient_session_id, false + t.remove_references :patient + end + end +end diff --git a/db/schema.rb b/db/schema.rb index 778bb65ab3..927d47fb8c 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -837,12 +837,13 @@ end create_table "session_attendances", force: :cascade do |t| - t.bigint "patient_session_id", null: false t.bigint "session_date_id", null: false t.boolean "attending", null: false t.datetime "created_at", null: false t.datetime "updated_at", null: false - t.index ["patient_session_id", "session_date_id"], name: "idx_on_patient_session_id_session_date_id_be8bd21ddf", unique: true + t.bigint "patient_id", null: false + t.index ["patient_id", "session_date_id"], name: "index_session_attendances_on_patient_id_and_session_date_id", unique: true + t.index ["patient_id"], name: "index_session_attendances_on_patient_id" t.index ["session_date_id"], name: "index_session_attendances_on_session_date_id" end @@ -1148,7 +1149,7 @@ add_foreign_key "school_moves", "locations", column: "school_id" add_foreign_key "school_moves", "patients" add_foreign_key "school_moves", "teams" - add_foreign_key "session_attendances", "patient_sessions" + add_foreign_key "session_attendances", "patients" add_foreign_key "session_attendances", "session_dates" add_foreign_key "session_dates", "sessions" add_foreign_key "session_notifications", "patients" diff --git a/spec/factories/patient_sessions.rb b/spec/factories/patient_sessions.rb index a9d8cb79fc..119f40eb08 100644 --- a/spec/factories/patient_sessions.rb +++ b/spec/factories/patient_sessions.rb @@ -52,9 +52,6 @@ end trait :in_attendance do - session_attendances do - [association(:session_attendance, :present, patient_session: instance)] - end registration_status do association( :patient_session_registration_status, @@ -62,6 +59,15 @@ patient_session: instance ) end + + after(:create) do |patient_session| + create( + :session_attendance, + :present, + patient: patient_session.patient, + session: patient_session.session + ) + end end trait :added_to_session diff --git a/spec/factories/patients.rb b/spec/factories/patients.rb index d1f6c278e5..b8f64583e0 100644 --- a/spec/factories/patients.rb +++ b/spec/factories/patients.rb @@ -131,7 +131,12 @@ ) if evaluator.in_attendance - create(:session_attendance, :present, patient_session:) + create( + :session_attendance, + :present, + patient:, + session: evaluator.session + ) create( :patient_session_registration_status, :attending, diff --git a/spec/factories/session_attendances.rb b/spec/factories/session_attendances.rb index 42e1ffae0d..ba17f19af0 100644 --- a/spec/factories/session_attendances.rb +++ b/spec/factories/session_attendances.rb @@ -4,27 +4,30 @@ # # Table name: session_attendances # -# id :bigint not null, primary key -# attending :boolean not null -# created_at :datetime not null -# updated_at :datetime not null -# patient_session_id :bigint not null -# session_date_id :bigint not null +# id :bigint not null, primary key +# attending :boolean not null +# created_at :datetime not null +# updated_at :datetime not null +# patient_id :bigint not null +# session_date_id :bigint not null # # Indexes # -# idx_on_patient_session_id_session_date_id_be8bd21ddf (patient_session_id,session_date_id) UNIQUE -# index_session_attendances_on_session_date_id (session_date_id) +# index_session_attendances_on_patient_id (patient_id) +# index_session_attendances_on_patient_id_and_session_date_id (patient_id,session_date_id) UNIQUE +# index_session_attendances_on_session_date_id (session_date_id) # # Foreign Keys # -# fk_rails_... (patient_session_id => patient_sessions.id) +# fk_rails_... (patient_id => patients.id) # fk_rails_... (session_date_id => session_dates.id) # FactoryBot.define do factory :session_attendance do - patient_session - session_date { patient_session.session.session_dates.first } + transient { session { association(:session) } } + + patient + session_date { session.session_dates.first } trait :present do attending { true } diff --git a/spec/features/manage_attendance_spec.rb b/spec/features/manage_attendance_spec.rb index 2abbb5bfcc..c09e024ad8 100644 --- a/spec/features/manage_attendance_spec.rb +++ b/spec/features/manage_attendance_spec.rb @@ -154,12 +154,7 @@ def when_i_go_to_the_session_patients end def and_i_go_to_a_patient - click_link PatientSession - .where - .missing(:session_attendances) - .find_by(session: @session) - .patient - .full_name + click_link Patient.where.missing(:session_attendances).first.full_name end def then_the_patient_is_not_registered_yet diff --git a/spec/lib/status_generator/registration_spec.rb b/spec/lib/status_generator/registration_spec.rb index a6aa1b61e7..c4afd30adb 100644 --- a/spec/lib/status_generator/registration_spec.rb +++ b/spec/lib/status_generator/registration_spec.rb @@ -35,7 +35,7 @@ create( :session_attendance, :present, - patient_session:, + patient:, session_date: session.session_dates.first ) end @@ -48,7 +48,7 @@ create( :session_attendance, :present, - patient_session:, + patient:, session_date: session.session_dates.second ) end @@ -61,7 +61,7 @@ create( :session_attendance, :absent, - patient_session:, + patient:, session_date: session.session_dates.second ) end diff --git a/spec/lib/status_generator/session_spec.rb b/spec/lib/status_generator/session_spec.rb index 387736aae1..05c25b4e27 100644 --- a/spec/lib/status_generator/session_spec.rb +++ b/spec/lib/status_generator/session_spec.rb @@ -79,7 +79,7 @@ end context "when not attending the session" do - before { create(:session_attendance, :absent, patient_session:) } + before { create(:session_attendance, :absent, patient:, session:) } it { should be(:absent_from_session) } end @@ -263,10 +263,10 @@ create( :vaccination_record, :absent_from_session, - patient: patient, - session: session, - programme: programme, - performed_at: performed_at + patient:, + session:, + programme:, + performed_at: ) end @@ -275,9 +275,7 @@ context "with absent from session attendance" do before do - attendance = - create(:session_attendance, :absent, patient_session: patient_session) - attendance.update_column(:created_at, created_at) + create(:session_attendance, :absent, patient:, session:, created_at:) end it { should eq(created_at) } @@ -292,19 +290,19 @@ create( :vaccination_record, :absent_from_session, - patient: patient, - session: session, - programme: programme, + patient:, + session:, + programme:, performed_at: earlier_date ) - attendance = - create( - :session_attendance, - :absent, - patient_session: patient_session - ) - attendance.update_column(:created_at, later_date) + create( + :session_attendance, + :absent, + patient:, + session:, + created_at: later_date + ) end it { should eq(earlier_date) } @@ -315,19 +313,19 @@ create( :vaccination_record, :absent_from_session, - patient: patient, - session: session, - programme: programme, + patient:, + session:, + programme:, performed_at: later_date ) - attendance = - create( - :session_attendance, - :absent, - patient_session: patient_session - ) - attendance.update_column(:created_at, earlier_date) + create( + :session_attendance, + :absent, + patient:, + session:, + created_at: earlier_date + ) end it { should eq(earlier_date) } @@ -339,10 +337,10 @@ create( :vaccination_record, :not_administered, - patient: patient, - session: session, - programme: programme, - performed_at: performed_at + patient:, + session:, + programme:, + performed_at: ) end diff --git a/spec/models/patient_session/registration_status_spec.rb b/spec/models/patient_session/registration_status_spec.rb index e14021b812..d4657c65af 100644 --- a/spec/models/patient_session/registration_status_spec.rb +++ b/spec/models/patient_session/registration_status_spec.rb @@ -57,7 +57,7 @@ create( :session_attendance, :present, - patient_session:, + patient:, session_date: session.session_dates.find_by(value: Date.current) ) end @@ -66,7 +66,7 @@ create( :session_attendance, :absent, - patient_session:, + patient:, session_date: session.session_dates.find_by(value: Date.yesterday) ) end @@ -87,7 +87,7 @@ create( :session_attendance, :present, - patient_session:, + patient:, session_date: session.session_dates.first ) end @@ -100,7 +100,7 @@ create( :session_attendance, :present, - patient_session:, + patient:, session_date: session.session_dates.second ) end @@ -113,7 +113,7 @@ create( :session_attendance, :absent, - patient_session:, + patient:, session_date: session.session_dates.second ) end diff --git a/spec/models/patient_session_spec.rb b/spec/models/patient_session_spec.rb index 0c20c6fb71..46e5c1d36f 100644 --- a/spec/models/patient_session_spec.rb +++ b/spec/models/patient_session_spec.rb @@ -168,7 +168,7 @@ it { should be true } it "is safe with only absent attendances" do - create(:session_attendance, :absent, patient_session:) + create(:session_attendance, :absent, patient:, session:) expect(safe_to_destroy?).to be true end end @@ -185,12 +185,12 @@ end it "is unsafe with present attendances" do - create(:session_attendance, :present, patient_session:) + create(:session_attendance, :present, patient:, session:) expect(safe_to_destroy?).to be false end it "is unsafe with mixed conditions" do - create(:session_attendance, :absent, patient_session:) + create(:session_attendance, :absent, patient:, session:) create(:vaccination_record, programme:, patient:, session:) expect(safe_to_destroy?).to be false end diff --git a/spec/models/session_attendance_spec.rb b/spec/models/session_attendance_spec.rb new file mode 100644 index 0000000000..fc037f2ffe --- /dev/null +++ b/spec/models/session_attendance_spec.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +# == Schema Information +# +# Table name: session_attendances +# +# id :bigint not null, primary key +# attending :boolean not null +# created_at :datetime not null +# updated_at :datetime not null +# patient_id :bigint not null +# session_date_id :bigint not null +# +# Indexes +# +# index_session_attendances_on_patient_id (patient_id) +# index_session_attendances_on_patient_id_and_session_date_id (patient_id,session_date_id) UNIQUE +# index_session_attendances_on_session_date_id (session_date_id) +# +# Foreign Keys +# +# fk_rails_... (patient_id => patients.id) +# fk_rails_... (session_date_id => session_dates.id) +# +describe SessionAttendance do + subject(:session_attendance) { build(:session_attendance) } + + describe "associations" do + it { should belong_to(:patient) } + it { should belong_to(:session_date) } + + it { should have_one(:session).through(:session_date) } + end +end diff --git a/spec/models/session_date_spec.rb b/spec/models/session_date_spec.rb index d4ae33e676..48ef5f4e0f 100644 --- a/spec/models/session_date_spec.rb +++ b/spec/models/session_date_spec.rb @@ -73,13 +73,7 @@ end context "with a session attendance" do - before do - create( - :session_attendance, - :present, - patient_session: create(:patient_session, session:) - ) - end + before { create(:session_attendance, :present, session:) } it { should be(true) } end diff --git a/spec/policies/session_attendance_policy_spec.rb b/spec/policies/session_attendance_policy_spec.rb index 47fb584ee9..c2940806db 100644 --- a/spec/policies/session_attendance_policy_spec.rb +++ b/spec/policies/session_attendance_policy_spec.rb @@ -10,17 +10,19 @@ let(:session) { create(:session, team:, programmes:) } let(:patient) { create(:patient, session:, year_group: 8) } - let(:patient_session) { patient.patient_sessions.includes(:session).first } - shared_examples "allow if not yet vaccinated or seen by nurse" do context "with a new session attendance" do - let(:session_attendance) { build(:session_attendance, patient_session:) } + let(:session_attendance) do + build(:session_attendance, patient:, session:) + end it { should be(true) } end context "with session attendance and one vaccination record from a different session" do - let(:session_attendance) { build(:session_attendance, patient_session:) } + let(:session_attendance) do + build(:session_attendance, patient:, session:) + end before do create( @@ -37,7 +39,9 @@ end context "with session attendance and both vaccination records" do - let(:session_attendance) { build(:session_attendance, patient_session:) } + let(:session_attendance) do + build(:session_attendance, patient:, session:) + end before do programmes.each do |programme| @@ -57,7 +61,9 @@ end context "with session attendance and both vaccination records from a different date" do - let(:session_attendance) { build(:session_attendance, patient_session:) } + let(:session_attendance) do + build(:session_attendance, patient:, session:) + end around { |example| travel_to(Date.new(2025, 8, 31)) { example.run } }