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:) %> +