diff --git a/app/components/app_activity_log_component.rb b/app/components/app_activity_log_component.rb index 5ff76eb414..4e6fceca31 100644 --- a/app/components/app_activity_log_component.rb +++ b/app/components/app_activity_log_component.rb @@ -25,6 +25,9 @@ def initialize(team:, patient: nil, patient_session: nil) @patient_sessions = patient_session ? [patient_session] : patient.patient_sessions + @attendance_records = + (patient || patient_session).attendance_records.includes(:location) + @consents = @patient.consents.includes( :consent_form, @@ -52,9 +55,6 @@ def initialize(team:, patient: nil, patient_session: nil) @pre_screenings = (patient || patient_session).pre_screenings.includes(:performed_by) - @session_attendances = - (patient || patient_session).session_attendances.includes(:location) - @triages = @patient.triages.includes(:performed_by) @vaccination_records = @@ -78,7 +78,7 @@ def initialize(team:, patient: nil, patient_session: nil) :patient_sessions, :patient_specific_directions, :pre_screenings, - :session_attendances, + :attendance_records, :triages, :vaccination_records @@ -382,19 +382,19 @@ def vaccination_events end def attendance_events - session_attendances.map do |session_attendance| + attendance_records.map do |attendance_record| title = ( - if session_attendance.attending? + if attendance_record.attending? "Attended session" else "Absent from session" end ) - title += " at #{session_attendance.location.name}" + title += " at #{attendance_record.location.name}" - { title:, at: session_attendance.created_at } + { title:, at: attendance_record.created_at } end end 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 bda6a3e972..008be010cb 100644 --- a/app/components/app_patient_session_search_result_card_component.rb +++ b/app/components/app_patient_session_search_result_card_component.rb @@ -95,13 +95,16 @@ def initialize(patient_session, context:, programmes: []) delegate :academic_year, to: :session def can_register_attendance? - session_attendance = - SessionAttendance.new( + attendance_record = + AttendanceRecord.new( patient:, - session_date: SessionDate.new(session:, value: Date.current) + location: session.location, + date: Date.current ) - policy(session_attendance).new? + attendance_record.session = session + + policy(attendance_record).new? end def patient_path diff --git a/app/controllers/api/testing/teams_controller.rb b/app/controllers/api/testing/teams_controller.rb index c231b44b8d..d3907caf46 100644 --- a/app/controllers/api/testing/teams_controller.rb +++ b/app/controllers/api/testing/teams_controller.rb @@ -31,6 +31,7 @@ def destroy log_destroy(AccessLogEntry.where(patient_id: patient_ids)) log_destroy(ArchiveReason.where(patient_id: patient_ids)) + log_destroy(AttendanceRecord.where(patient_id: patient_ids)) log_destroy(ConsentNotification.where(patient_id: patient_ids)) log_destroy(GillickAssessment.where(patient_id: patient_ids)) log_destroy(Note.where(patient_id: patient_ids)) @@ -45,7 +46,6 @@ def destroy log_destroy(SchoolMove.where(patient_id: patient_ids)) log_destroy(SchoolMove.where(team:)) log_destroy(SchoolMoveLogEntry.where(patient_id: patient_ids)) - log_destroy(SessionAttendance.where(patient_id: patient_ids)) log_destroy(VaccinationRecord.where(patient_id: patient_ids)) log_destroy(SessionDate.where(session: sessions)) diff --git a/app/controllers/patient_sessions/attendances_controller.rb b/app/controllers/patient_sessions/attendances_controller.rb new file mode 100644 index 0000000000..6d2b142e4f --- /dev/null +++ b/app/controllers/patient_sessions/attendances_controller.rb @@ -0,0 +1,61 @@ +# frozen_string_literal: true + +class PatientSessions::AttendancesController < PatientSessions::BaseController + before_action :set_session_date + before_action :set_attendance_record + + def edit + end + + def update + @attendance_record.assign_attributes(attendance_record_params) + + if @attendance_record.attending.nil? + @attendance_record.destroy! + else + @attendance_record.save! + end => success + + StatusUpdater.call(patient: @patient) + + if success + name = @patient.full_name + + flash[:info] = if @attendance_record.attending? + t("attendance_flash.present", name:) + elsif @attendance_record.attending.nil? + t("attendance_flash.not_registered", name:) + else + t("attendance_flash.absent", name:) + end + + redirect_to session_patient_programme_path( + @session, + @patient, + @patient_session.programmes.first + ) + else + render :edit, status: :unprocessable_content + end + end + + private + + def set_attendance_record + attendance_record = + @patient.attendance_records.find_or_initialize_by( + location: @session.location, + date: @session_date.value + ) + + attendance_record.session = @session + + @attendance_record = authorize attendance_record + end + + def attendance_record_params + params + .expect(attendance_record: :attending) + .tap { it[:attending] = nil if it[:attending] == "not_registered" } + end +end diff --git a/app/controllers/patient_sessions/session_attendances_controller.rb b/app/controllers/patient_sessions/session_attendances_controller.rb deleted file mode 100644 index 77fee1c494..0000000000 --- a/app/controllers/patient_sessions/session_attendances_controller.rb +++ /dev/null @@ -1,59 +0,0 @@ -# frozen_string_literal: true - -class PatientSessions::SessionAttendancesController < PatientSessions::BaseController - before_action :set_session_date - before_action :set_session_attendance - - def edit - end - - def update - @session_attendance.assign_attributes(session_attendance_params) - - if @session_attendance.attending.nil? - @session_attendance.destroy! - else - @session_attendance.save! - end => success - - StatusUpdater.call(patient: @patient) - - if success - name = @patient.full_name - - flash[:info] = if @session_attendance.attending? - t("attendance_flash.present", name:) - elsif @session_attendance.attending.nil? - t("attendance_flash.not_registered", name:) - else - t("attendance_flash.absent", name:) - end - - redirect_to session_patient_programme_path( - @session, - @patient, - @patient_session.programmes.first - ) - else - render :edit, status: :unprocessable_content - end - end - - private - - def set_session_attendance - @session_attendance = - authorize( - @patient - .session_attendances - .includes(:patient, session_date: { session: :programmes }) - .find_or_initialize_by(session_date: @session_date) - ) - end - - def session_attendance_params - params - .expect(session_attendance: :attending) - .tap { |p| p[:attending] = nil if p[:attending] == "not_registered" } - end -end diff --git a/app/controllers/patients_controller.rb b/app/controllers/patients_controller.rb index 65c6856456..c660919e30 100644 --- a/app/controllers/patients_controller.rb +++ b/app/controllers/patients_controller.rb @@ -47,7 +47,7 @@ def set_patient :school, consents: %i[parent patient], parent_relationships: :parent, - patient_sessions: %i[location session_attendances], + patient_sessions: %i[location attendance_records], vaccination_records: :programme ).find(params[:id]) end diff --git a/app/controllers/sessions/register_controller.rb b/app/controllers/sessions/register_controller.rb index 6990e65137..aee1ed5f2e 100644 --- a/app/controllers/sessions/register_controller.rb +++ b/app/controllers/sessions/register_controller.rb @@ -5,8 +5,8 @@ class Sessions::RegisterController < ApplicationController before_action :set_session before_action :set_patient_search_form, only: :show + before_action :set_session_date, only: :create before_action :set_patient, only: :create - before_action :set_patient_session, only: :create layout "full" @@ -25,17 +25,24 @@ def show end def create - session_attendance = - ActiveRecord::Base.transaction do - record = authorize @patient_session.todays_attendance - record.update!(attending: params[:status] == "present") - StatusUpdater.call(patient: @patient_session.patient) - record - end + attendance_record = + @patient.attendance_records.find_or_initialize_by( + location: @session.location, + date: @session_date.value + ) + + attendance_record.session = @session + + authorize attendance_record + + ActiveRecord::Base.transaction do + attendance_record.update!(attending: params[:status] == "present") + StatusUpdater.call(patient: @patient) + end - name = @patient_session.patient.full_name + name = @patient.full_name - flash[:info] = if session_attendance.attending? + flash[:info] = if attendance_record.attending? t("attendance_flash.present", name:) else t("attendance_flash.absent", name:) @@ -50,12 +57,11 @@ def set_session @session = policy_scope(Session).find_by!(slug: params[:session_slug]) end - def set_patient - @patient = policy_scope(Patient).find(params[:patient_id]) + def set_session_date + @session_date = @session.session_dates.find_by!(value: Date.current) end - def set_patient_session - @patient_session = - PatientSession.find_by!(patient: @patient, session: @session) + def set_patient + @patient = policy_scope(Patient).find(params[:patient_id]) end end diff --git a/app/lib/generate/vaccination_records.rb b/app/lib/generate/vaccination_records.rb index 269fc05c12..65f5f6550b 100644 --- a/app/lib/generate/vaccination_records.rb +++ b/app/lib/generate/vaccination_records.rb @@ -19,21 +19,16 @@ def self.call(...) = new(...).call attr_reader :config, :team, :programme, :session, :administered def create_vaccinations - session_attendances = [] + attendance_records = [] vaccination_records = [] random_patient_sessions.each do |patient_session| patient = patient_session.patient session = patient_session.session - unless SessionAttendance.joins(:session_date).exists?( - patient:, - session_date: { - session: - } - ) - session_attendances << FactoryBot.build( - :session_attendance, + unless AttendanceRecord.exists?(patient:, location: session.location) + attendance_records << FactoryBot.build( + :attendance_record, :present, patient:, session: @@ -57,7 +52,7 @@ def create_vaccinations ) end - SessionAttendance.import!(session_attendances) + AttendanceRecord.import!(attendance_records) VaccinationRecord.import!(vaccination_records) StatusUpdater.call(patient: vaccination_records.map(&:patient)) diff --git a/app/lib/patient_merger.rb b/app/lib/patient_merger.rb index 4d826d51d6..dd7ea8a675 100644 --- a/app/lib/patient_merger.rb +++ b/app/lib/patient_merger.rb @@ -24,6 +24,10 @@ def call patient_to_destroy.archive_reasons.destroy_all + patient_to_destroy.attendance_records.update_all( + patient_id: patient_to_keep.id + ) + patient_to_destroy.consent_notifications.update_all( patient_id: patient_to_keep.id ) @@ -63,10 +67,6 @@ def call patient_id: patient_to_keep.id ) - patient_to_destroy.session_attendances.update_all( - patient_id: patient_to_keep.id - ) - patient_to_destroy.session_notifications.update_all( patient_id: patient_to_keep.id ) diff --git a/app/lib/status_generator/registration.rb b/app/lib/status_generator/registration.rb index 84c54ae4a6..ff3824044d 100644 --- a/app/lib/status_generator/registration.rb +++ b/app/lib/status_generator/registration.rb @@ -1,10 +1,10 @@ # frozen_string_literal: true class StatusGenerator::Registration - def initialize(patient:, session:, session_attendance:, vaccination_records:) + def initialize(patient:, session:, attendance_record:, vaccination_records:) @patient = patient @session = session - @session_attendance = session_attendance + @attendance_record = attendance_record @vaccination_records = vaccination_records end @@ -22,7 +22,7 @@ def status private - attr_reader :patient, :session, :session_attendance, :vaccination_records + attr_reader :patient, :session, :attendance_record, :vaccination_records delegate :academic_year, to: :session @@ -37,10 +37,10 @@ def status_should_be_completed? end def status_should_be_attending? - session_attendance&.attending + attendance_record&.attending end def status_should_be_not_attending? - session_attendance&.attending == false + attendance_record&.attending == false end end diff --git a/app/lib/status_generator/session.rb b/app/lib/status_generator/session.rb index 7f9c04b263..aecbb2357d 100644 --- a/app/lib/status_generator/session.rb +++ b/app/lib/status_generator/session.rb @@ -4,7 +4,7 @@ class StatusGenerator::Session def initialize( session_id:, academic_year:, - session_attendance:, + attendance_record:, programme:, patient:, consents:, @@ -13,7 +13,7 @@ def initialize( ) @session_id = session_id @academic_year = academic_year - @session_attendance = session_attendance + @attendance_record = attendance_record @programme = programme @patient = patient @consents = consents @@ -63,7 +63,7 @@ def status_changed_at attr_reader :session_id, :academic_year, - :session_attendance, + :attendance_record, :programme, :patient, :consents, @@ -113,7 +113,7 @@ def refusal_date def status_should_be_absent_from_session? vaccination_record&.absent_from_session? || - session_attendance&.attending == false + attendance_record&.attending == false end def absence_date @@ -121,7 +121,7 @@ def absence_date if vaccination_record&.absent_from_session? vaccination_record.performed_at end, - (session_attendance.created_at if session_attendance&.attending == false) + (attendance_record.created_at if attendance_record&.attending == false) ].compact.min end diff --git a/app/lib/status_updater.rb b/app/lib/status_updater.rb index 761ec45f56..c1af25ff92 100644 --- a/app/lib/status_updater.rb +++ b/app/lib/status_updater.rb @@ -62,8 +62,7 @@ def update_registration_statuses! ) .includes( :patient, - :session_attendances, - :session_date, + :attendance_records, :vaccination_records, session: :programmes ) @@ -118,7 +117,7 @@ def update_vaccination_statuses! :consents, :triages, :vaccination_records, - :session_attendance + :attendance_record ) .find_in_batches(batch_size: 10_000) do |batch| batch.each(&:assign_status) diff --git a/app/models/attendance_record.rb b/app/models/attendance_record.rb new file mode 100644 index 0000000000..200d837fe7 --- /dev/null +++ b/app/models/attendance_record.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +# == Schema Information +# +# Table name: attendance_records +# +# id :bigint not null, primary key +# attending :boolean not null +# date :date not null +# created_at :datetime not null +# updated_at :datetime not null +# location_id :bigint not null +# patient_id :bigint not null +# +# Indexes +# +# idx_on_patient_id_location_id_date_e5912f40c4 (patient_id,location_id,date) UNIQUE +# index_attendance_records_on_location_id (location_id) +# index_attendance_records_on_patient_id (patient_id) +# +# Foreign Keys +# +# fk_rails_... (location_id => locations.id) +# fk_rails_... (patient_id => patients.id) +# +class AttendanceRecord < ApplicationRecord + audited associated_with: :patient + + belongs_to :patient + belongs_to :location + + scope :today, -> { where(date: Date.current) } + + delegate :today?, to: :date + + # This is needed to be able to pass a session to the policy. + attr_accessor :session +end diff --git a/app/models/location.rb b/app/models/location.rb index 7e851e5e66..7b5f08207e 100644 --- a/app/models/location.rb +++ b/app/models/location.rb @@ -53,6 +53,7 @@ class Location < ApplicationRecord primary_key: :gias_code, optional: true + has_many :attendance_records has_many :consent_forms has_many :location_programme_year_groups has_many :patients, foreign_key: :school_id diff --git a/app/models/patient.rb b/app/models/patient.rb index d5cb77365c..655a4e5976 100644 --- a/app/models/patient.rb +++ b/app/models/patient.rb @@ -61,6 +61,7 @@ class Patient < ApplicationRecord has_many :access_log_entries has_many :archive_reasons + has_many :attendance_records has_many :changesets, class_name: "PatientChangeset" has_many :consent_notifications has_many :consent_statuses @@ -75,7 +76,6 @@ class Patient < ApplicationRecord has_many :registration_statuses 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 diff --git a/app/models/patient/registration_status.rb b/app/models/patient/registration_status.rb index 1e578ed21e..5c91ad9f6b 100644 --- a/app/models/patient/registration_status.rb +++ b/app/models/patient/registration_status.rb @@ -29,17 +29,15 @@ class Patient::RegistrationStatus < ApplicationRecord -> { kept.order(performed_at: :desc) }, through: :patient - has_one :session_date, -> { today }, through: :session, source: :session_dates - - has_many :session_attendances, through: :patient + has_many :attendance_records, -> { today }, through: :patient enum :status, { unknown: 0, attending: 1, not_attending: 2, completed: 3 }, default: :unknown, validate: true - def session_attendance - session_attendances.find { it.session_date_id == session_date&.id } + def attendance_record + attendance_records.find { it.location_id == session.location_id } end def assign_status @@ -53,7 +51,7 @@ def generator StatusGenerator::Registration.new( patient:, session:, - session_attendance:, + attendance_record:, vaccination_records: ) end diff --git a/app/models/patient/vaccination_status.rb b/app/models/patient/vaccination_status.rb index ce639c3f7f..3454fc8cbe 100644 --- a/app/models/patient/vaccination_status.rb +++ b/app/models/patient/vaccination_status.rb @@ -40,10 +40,10 @@ class Patient::VaccinationStatus < ApplicationRecord has_one :patient_session - has_one :session_attendance, + has_one :attendance_record, -> { today }, through: :patient, - source: :session_attendances + source: :attendance_records enum :status, { none_yet: 0, vaccinated: 1, could_not_vaccinate: 2 }, @@ -92,7 +92,7 @@ def session_generator StatusGenerator::Session.new( session_id:, academic_year:, - session_attendance:, + attendance_record:, programme:, patient:, consents:, diff --git a/app/models/patient_session.rb b/app/models/patient_session.rb index 3903dd789a..70b8685b35 100644 --- a/app/models/patient_session.rb +++ b/app/models/patient_session.rb @@ -70,9 +70,9 @@ class PatientSession < ApplicationRecord source: :registration_statuses, class_name: "Patient::RegistrationStatus" - has_many :session_attendances, + has_many :attendance_records, -> { where(patient_id: it.patient_id) }, - through: :session + through: :location has_many :session_notifications, -> { where(session_id: it.session_id) }, @@ -299,7 +299,7 @@ class PatientSession < ApplicationRecord -> do includes( :gillick_assessments, - :session_attendances, + :attendance_records, :vaccination_records ).find_each(&:destroy_if_safe!) end @@ -312,7 +312,7 @@ def has_patient_specific_direction?(**query) def safe_to_destroy? vaccination_records.empty? && gillick_assessments.empty? && - session_attendances.none?(&:attending?) + attendance_records.none?(&:attending?) end def destroy_if_safe! @@ -321,15 +321,6 @@ def destroy_if_safe! def programmes = session.programmes_for(patient:, academic_year:) - def todays_attendance - if (session_date = session.session_dates.today.first) - patient - .session_attendances - .includes(:patient, session_date: { session: :programmes }) - .find_or_initialize_by(session_date:) - end - end - def next_activity(programme:) if patient.vaccination_status(programme:, academic_year:).vaccinated? return nil diff --git a/app/models/session.rb b/app/models/session.rb index b30aef39a2..b483a90508 100644 --- a/app/models/session.rb +++ b/app/models/session.rb @@ -55,7 +55,6 @@ 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 deleted file mode 100644 index c430bb9780..0000000000 --- a/app/models/session_attendance.rb +++ /dev/null @@ -1,37 +0,0 @@ -# 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) -# -class SessionAttendance < ApplicationRecord - audited associated_with: :patient - - belongs_to :patient - belongs_to :session_date - - has_one :session, through: :session_date - has_one :location, through: :session - - scope :today, -> { joins(:session_date).merge(SessionDate.today) } - - delegate :today?, to: :session_date -end diff --git a/app/models/session_date.rb b/app/models/session_date.rb index 1e366eaaae..b9797da44a 100644 --- a/app/models/session_date.rb +++ b/app/models/session_date.rb @@ -21,9 +21,12 @@ class SessionDate < ApplicationRecord belongs_to :session + has_one :location, through: :session + has_many :gillick_assessments, dependent: :restrict_with_error has_many :pre_screenings, dependent: :restrict_with_error - has_many :session_attendances, dependent: :restrict_with_error + + has_many :attendance_records, -> { where(date: it.value) }, through: :location scope :for_session, -> { where("session_id = sessions.id") } @@ -45,7 +48,7 @@ def today_or_past? = today? || past? def today_or_future? = today? || future? def has_been_attended? - gillick_assessments.any? || pre_screenings.any? || session_attendances.any? + gillick_assessments.any? || pre_screenings.any? || attendance_records.any? end private diff --git a/app/policies/session_attendance_policy.rb b/app/policies/attendance_record_policy.rb similarity index 72% rename from app/policies/session_attendance_policy.rb rename to app/policies/attendance_record_policy.rb index c5ce680a88..f670bb7f03 100644 --- a/app/policies/session_attendance_policy.rb +++ b/app/policies/attendance_record_policy.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -class SessionAttendancePolicy < ApplicationPolicy +class AttendanceRecordPolicy < ApplicationPolicy def create? !already_vaccinated? && !was_seen_by_nurse? end @@ -11,8 +11,8 @@ def update? private - delegate :patient, :session_date, to: :record - delegate :session, to: :session_date + delegate :patient, :location_id, :date, :session, to: :record + delegate :academic_year, to: :session def already_vaccinated? @@ -26,8 +26,8 @@ def already_vaccinated? def was_seen_by_nurse? VaccinationRecord.kept.exists?( patient:, - session:, - performed_at: session_date.value.all_day + location_id:, + performed_at: date.all_day ) end end diff --git a/app/views/patient_sessions/_header.html.erb b/app/views/patient_sessions/_header.html.erb index ea53d249b7..96da41cbaf 100644 --- a/app/views/patient_sessions/_header.html.erb +++ b/app/views/patient_sessions/_header.html.erb @@ -38,16 +38,19 @@ <% end %> - <% if @session.requires_registration? && (session_attendance = @patient_session.todays_attendance) %> + <% if @session.requires_registration? && (date = @session.dates.find(&:today?)) %> + <% attendance_record = @patient.attendance_records.find_or_initialize_by(location: @session.location, date:) %> +
  • <%= render AppStatusTagComponent.new(@patient_session.registration_status&.status || "unknown", context: :register) %>
  • - <% if policy(session_attendance).edit? %> + <% attendance_record.session = @session %> + <% if policy(attendance_record).edit? %>
  • <%= link_to( "Update attendance", - edit_session_patient_session_attendance_path(@session, @patient) + edit_session_patient_attendance_path(@session, @patient) ) %>
  • <% end %> diff --git a/app/views/patient_sessions/session_attendances/edit.html.erb b/app/views/patient_sessions/attendances/edit.html.erb similarity index 87% rename from app/views/patient_sessions/session_attendances/edit.html.erb rename to app/views/patient_sessions/attendances/edit.html.erb index 807648855b..6ab254d845 100644 --- a/app/views/patient_sessions/session_attendances/edit.html.erb +++ b/app/views/patient_sessions/attendances/edit.html.erb @@ -5,8 +5,8 @@ <% title = "Is #{@patient.full_name} attending today’s session?" %> <% content_for :page_title, title %> -<%= form_with model: @session_attendance, - url: session_patient_session_attendance_path(@session, @patient), +<%= form_with model: @attendance_record, + url: session_patient_attendance_path(@session, @patient), method: :put do |f| %> <%= f.govuk_error_summary %> <%= f.govuk_radio_buttons_fieldset(:attending, @@ -28,7 +28,7 @@ <%= f.govuk_radio_button( :attending, "not_registered", label: { text: "They have not been registered yet" }, - checked: @session_attendance.attending.nil?, + checked: @attendance_record.attending.nil?, ) %> <% end %> diff --git a/config/routes.rb b/config/routes.rb index fa915e4a33..9ec0bdcdf7 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -285,7 +285,7 @@ only: [], module: :patient_sessions do resource :activity, only: %i[show create] - resource :session_attendance, path: "attendance", only: %i[edit update] + resource :attendance, only: %i[edit update] resources :programmes, path: "", param: :type, only: :show do get "record-already-vaccinated" diff --git a/db/migrate/20250908112554_rename_session_attendance.rb b/db/migrate/20250908112554_rename_session_attendance.rb new file mode 100644 index 0000000000..9564d9c589 --- /dev/null +++ b/db/migrate/20250908112554_rename_session_attendance.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class RenameSessionAttendance < ActiveRecord::Migration[8.0] + def change + rename_table :session_attendances, :attendance_records + end +end diff --git a/db/migrate/20250908125713_remove_session_from_attendance_record.rb b/db/migrate/20250908125713_remove_session_from_attendance_record.rb new file mode 100644 index 0000000000..fed23a070e --- /dev/null +++ b/db/migrate/20250908125713_remove_session_from_attendance_record.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +class RemoveSessionFromAttendanceRecord < ActiveRecord::Migration[8.0] + def up + change_table :attendance_records, bulk: true do |t| + t.date :date + t.references :location, foreign_key: true + end + + execute <<-SQL + UPDATE attendance_records + SET location_id = sessions.location_id, date = session_dates.value + FROM session_dates + JOIN sessions ON sessions.id = session_dates.session_id + WHERE session_dates.id = attendance_records.session_date_id + SQL + + change_table :attendance_records, bulk: true do |t| + t.change_null :date, false + t.change_null :location_id, false + end + + remove_column :attendance_records, :session_date_id + + execute <<-SQL + DELETE FROM attendance_records a + USING attendance_records b + WHERE a.id < b.id + AND a.patient_id = b.patient_id + AND a.location_id = b.location_id + AND a.date = b.date + SQL + + add_index :attendance_records, %i[patient_id location_id date], unique: true + end +end diff --git a/db/schema.rb b/db/schema.rb index 86ccc8931d..5104685a3f 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -49,6 +49,18 @@ t.index ["team_id"], name: "index_archive_reasons_on_team_id" end + create_table "attendance_records", force: :cascade do |t| + t.boolean "attending", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.bigint "patient_id", null: false + t.date "date", null: false + t.bigint "location_id", null: false + t.index ["location_id"], name: "index_attendance_records_on_location_id" + t.index ["patient_id", "location_id", "date"], name: "idx_on_patient_id_location_id_date_e5912f40c4", unique: true + t.index ["patient_id"], name: "index_attendance_records_on_patient_id" + end + create_table "audits", force: :cascade do |t| t.integer "auditable_id" t.string "auditable_type" @@ -839,17 +851,6 @@ t.index ["team_id"], name: "index_school_moves_on_team_id" end - create_table "session_attendances", force: :cascade do |t| - 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.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 - create_table "session_dates", force: :cascade do |t| t.bigint "session_id", null: false t.date "value", null: false @@ -1055,6 +1056,8 @@ add_foreign_key "archive_reasons", "patients" add_foreign_key "archive_reasons", "teams" add_foreign_key "archive_reasons", "users", column: "created_by_user_id" + add_foreign_key "attendance_records", "locations" + add_foreign_key "attendance_records", "patients" add_foreign_key "batches", "teams" add_foreign_key "batches", "vaccines" add_foreign_key "batches_immunisation_imports", "batches" @@ -1153,8 +1156,6 @@ 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", "patients" - add_foreign_key "session_attendances", "session_dates" add_foreign_key "session_dates", "sessions" add_foreign_key "session_notifications", "patients" add_foreign_key "session_notifications", "sessions" diff --git a/spec/factories/attendance_records.rb b/spec/factories/attendance_records.rb new file mode 100644 index 0000000000..d9f3237e85 --- /dev/null +++ b/spec/factories/attendance_records.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: true + +# == Schema Information +# +# Table name: attendance_records +# +# id :bigint not null, primary key +# attending :boolean not null +# date :date not null +# created_at :datetime not null +# updated_at :datetime not null +# location_id :bigint not null +# patient_id :bigint not null +# +# Indexes +# +# idx_on_patient_id_location_id_date_e5912f40c4 (patient_id,location_id,date) UNIQUE +# index_attendance_records_on_location_id (location_id) +# index_attendance_records_on_patient_id (patient_id) +# +# Foreign Keys +# +# fk_rails_... (location_id => locations.id) +# fk_rails_... (patient_id => patients.id) +# +FactoryBot.define do + factory :attendance_record do + patient + session + + location { session.location } + date { session.dates.first } + + trait :today do + date { Date.current } + end + + trait :yesterday do + date { Date.yesterday } + end + + trait :present do + attending { true } + end + + trait :absent do + attending { false } + end + end +end diff --git a/spec/factories/patient_sessions.rb b/spec/factories/patient_sessions.rb index 56f3283050..597abf6630 100644 --- a/spec/factories/patient_sessions.rb +++ b/spec/factories/patient_sessions.rb @@ -55,7 +55,7 @@ trait :in_attendance do after(:create) do |patient_session| create( - :session_attendance, + :attendance_record, :present, patient: patient_session.patient, session: patient_session.session diff --git a/spec/factories/patients.rb b/spec/factories/patients.rb index a626f468b0..5b8ec65924 100644 --- a/spec/factories/patients.rb +++ b/spec/factories/patients.rb @@ -129,7 +129,7 @@ if evaluator.in_attendance create( - :session_attendance, + :attendance_record, :present, patient:, session: evaluator.session diff --git a/spec/factories/session_attendances.rb b/spec/factories/session_attendances.rb deleted file mode 100644 index ba17f19af0..0000000000 --- a/spec/factories/session_attendances.rb +++ /dev/null @@ -1,40 +0,0 @@ -# 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) -# -FactoryBot.define do - factory :session_attendance do - transient { session { association(:session) } } - - patient - session_date { session.session_dates.first } - - trait :present do - attending { true } - end - - trait :absent do - attending { false } - end - end -end diff --git a/spec/features/manage_attendance_spec.rb b/spec/features/manage_attendance_spec.rb index 40818d746d..7f94525e47 100644 --- a/spec/features/manage_attendance_spec.rb +++ b/spec/features/manage_attendance_spec.rb @@ -201,7 +201,7 @@ def when_i_go_to_the_session_patients end def and_i_go_to_a_patient - click_link Patient.where.missing(:session_attendances).first.full_name + click_link Patient.where.missing(:attendance_records).first.full_name end def then_the_patient_is_not_registered_yet diff --git a/spec/lib/patient_merger_spec.rb b/spec/lib/patient_merger_spec.rb index 6a4b712998..93d2e60620 100644 --- a/spec/lib/patient_merger_spec.rb +++ b/spec/lib/patient_merger_spec.rb @@ -33,6 +33,9 @@ let(:access_log_entry) do create(:access_log_entry, patient: patient_to_destroy) end + let(:attendance_record) do + create(:attendance_record, :present, patient: patient_to_destroy) + end let(:consent) { create(:consent, patient: patient_to_destroy, programme:) } let(:consent_notification) do create( @@ -68,9 +71,6 @@ let(:duplicate_school_move) do create(:school_move, patient: patient_to_keep, school: school_move.school) end - let(:session_attendance) do - create(:session_attendance, :present, patient: patient_to_destroy) - end let(:session_notification) do create( :session_notification, @@ -110,6 +110,12 @@ ) end + it "moves attendance records" do + expect { call }.to change { attendance_record.reload.patient }.to( + patient_to_keep + ) + end + it "moves consents" do expect { call }.to change { consent.reload.patient }.to(patient_to_keep) end @@ -177,12 +183,6 @@ expect { school_move.reload }.to raise_error(ActiveRecord::RecordNotFound) end - it "moves session attendances" do - expect { call }.to change { session_attendance.reload.patient }.to( - patient_to_keep - ) - end - it "moves session notifications" do expect { call }.to change { session_notification.reload.patient }.to( patient_to_keep diff --git a/spec/lib/status_generator/registration_spec.rb b/spec/lib/status_generator/registration_spec.rb index ffc8274d56..98a6f348f4 100644 --- a/spec/lib/status_generator/registration_spec.rb +++ b/spec/lib/status_generator/registration_spec.rb @@ -5,10 +5,8 @@ described_class.new( patient:, session:, - session_attendance: - patient_session.session_attendances.find_by( - session_date: session.session_dates.last - ), + attendance_record: + patient_session.attendance_records.find_by(date: session.dates.last), vaccination_records: patient.vaccination_records ) end @@ -34,10 +32,11 @@ context "with a session attendance for a different day to today" do before do create( - :session_attendance, + :attendance_record, :present, patient:, - session_date: session.session_dates.first + session:, + date: session.dates.first ) end @@ -47,10 +46,11 @@ context "with a present session attendance for today" do before do create( - :session_attendance, + :attendance_record, :present, patient:, - session_date: session.session_dates.second + session:, + date: session.dates.second ) end @@ -60,10 +60,11 @@ context "with an absent session attendance for today" do before do create( - :session_attendance, + :attendance_record, :absent, patient:, - session_date: session.session_dates.second + session:, + date: session.dates.second ) end diff --git a/spec/lib/status_generator/session_spec.rb b/spec/lib/status_generator/session_spec.rb index 05c25b4e27..123dd50e9d 100644 --- a/spec/lib/status_generator/session_spec.rb +++ b/spec/lib/status_generator/session_spec.rb @@ -5,7 +5,7 @@ described_class.new( session_id: patient_session.session_id, academic_year: patient_session.academic_year, - session_attendance: patient_session.session_attendances.last, + attendance_record: patient_session.attendance_records.last, programme:, patient:, consents: patient.consents, @@ -79,7 +79,7 @@ end context "when not attending the session" do - before { create(:session_attendance, :absent, patient:, session:) } + before { create(:attendance_record, :absent, patient:, session:) } it { should be(:absent_from_session) } end @@ -275,7 +275,7 @@ context "with absent from session attendance" do before do - create(:session_attendance, :absent, patient:, session:, created_at:) + create(:attendance_record, :absent, patient:, session:, created_at:) end it { should eq(created_at) } @@ -297,7 +297,7 @@ ) create( - :session_attendance, + :attendance_record, :absent, patient:, session:, @@ -320,7 +320,7 @@ ) create( - :session_attendance, + :attendance_record, :absent, patient:, session:, diff --git a/spec/models/attendance_record_spec.rb b/spec/models/attendance_record_spec.rb new file mode 100644 index 0000000000..a8ba46f7a5 --- /dev/null +++ b/spec/models/attendance_record_spec.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +# == Schema Information +# +# Table name: attendance_records +# +# id :bigint not null, primary key +# attending :boolean not null +# date :date not null +# created_at :datetime not null +# updated_at :datetime not null +# location_id :bigint not null +# patient_id :bigint not null +# +# Indexes +# +# idx_on_patient_id_location_id_date_e5912f40c4 (patient_id,location_id,date) UNIQUE +# index_attendance_records_on_location_id (location_id) +# index_attendance_records_on_patient_id (patient_id) +# +# Foreign Keys +# +# fk_rails_... (location_id => locations.id) +# fk_rails_... (patient_id => patients.id) +# +describe AttendanceRecord do + subject(:attendance_record) { build(:attendance_record) } + + describe "associations" do + it { should belong_to(:patient) } + it { should belong_to(:location) } + end +end diff --git a/spec/models/patient/registration_status_spec.rb b/spec/models/patient/registration_status_spec.rb index dde9e6865e..69271d2751 100644 --- a/spec/models/patient/registration_status_spec.rb +++ b/spec/models/patient/registration_status_spec.rb @@ -47,12 +47,12 @@ it { should belong_to(:session) } end - describe "#session_attendance" do + describe "#attendance_record" do subject do described_class - .includes(:session_attendances) + .includes(:attendance_records) .find(patient_registration_status.id) - .session_attendance + .attendance_record end let(:patient_registration_status) do @@ -70,25 +70,15 @@ end context "with an attendance today and yesterday" do - let(:today_session_attendance) do - create( - :session_attendance, - :present, - patient:, - session_date: session.session_dates.find_by(value: Date.current) - ) + let(:today_attendance_record) do + create(:attendance_record, :present, :today, patient:, session:) end before do - create( - :session_attendance, - :absent, - patient:, - session_date: session.session_dates.find_by(value: Date.yesterday) - ) + create(:attendance_record, :absent, :yesterday, patient:, session:) end - it { should eq(today_session_attendance) } + it { should eq(today_attendance_record) } end end @@ -102,10 +92,11 @@ context "with a session attendance for a different day to today" do before do create( - :session_attendance, + :attendance_record, :present, patient:, - session_date: session.session_dates.first + session:, + date: session.dates.first ) end @@ -115,10 +106,11 @@ context "with a present session attendance for today" do before do create( - :session_attendance, + :attendance_record, :present, patient:, - session_date: session.session_dates.second + session:, + date: session.dates.second ) end @@ -128,10 +120,11 @@ context "with an absent session attendance for today" do before do create( - :session_attendance, + :attendance_record, :absent, patient:, - session_date: session.session_dates.second + session:, + date: session.dates.second ) end diff --git a/spec/models/patient_session_spec.rb b/spec/models/patient_session_spec.rb index 46e5c1d36f..1602acb0e7 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(:attendance_record, :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(:attendance_record, :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(:attendance_record, :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 deleted file mode 100644 index fc037f2ffe..0000000000 --- a/spec/models/session_attendance_spec.rb +++ /dev/null @@ -1,34 +0,0 @@ -# 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 48ef5f4e0f..96abb08118 100644 --- a/spec/models/session_date_spec.rb +++ b/spec/models/session_date_spec.rb @@ -73,7 +73,7 @@ end context "with a session attendance" do - before { create(:session_attendance, :present, session:) } + before { create(:attendance_record, :present, session:) } it { should be(true) } end diff --git a/spec/policies/session_attendance_policy_spec.rb b/spec/policies/attendance_record_policy_spec.rb similarity index 81% rename from spec/policies/session_attendance_policy_spec.rb rename to spec/policies/attendance_record_policy_spec.rb index c2940806db..bf9eb24d48 100644 --- a/spec/policies/session_attendance_policy_spec.rb +++ b/spec/policies/attendance_record_policy_spec.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true -describe SessionAttendancePolicy do - subject(:policy) { described_class.new(user, session_attendance) } +describe AttendanceRecordPolicy do + subject(:policy) { described_class.new(user, attendance_record) } let(:user) { create(:nurse) } @@ -12,17 +12,13 @@ shared_examples "allow if not yet vaccinated or seen by nurse" do context "with a new session attendance" do - let(:session_attendance) do - build(:session_attendance, patient:, session:) - end + let(:attendance_record) { build(:attendance_record, patient:, session:) } it { should be(true) } end context "with session attendance and one vaccination record from a different session" do - let(:session_attendance) do - build(:session_attendance, patient:, session:) - end + let(:attendance_record) { build(:attendance_record, patient:, session:) } before do create( @@ -39,9 +35,7 @@ end context "with session attendance and both vaccination records" do - let(:session_attendance) do - build(:session_attendance, patient:, session:) - end + let(:attendance_record) { build(:attendance_record, patient:, session:) } before do programmes.each do |programme| @@ -61,9 +55,7 @@ end context "with session attendance and both vaccination records from a different date" do - let(:session_attendance) do - build(:session_attendance, patient:, session:) - end + let(:attendance_record) { build(:attendance_record, patient:, session:) } around { |example| travel_to(Date.new(2025, 8, 31)) { example.run } }