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 } }