Skip to content

Commit fcbd9c7

Browse files
authored
Cache register status in database (#3275)
This introduces a new model, `PatientSession::RegistrationStatus`, which stores the vaccination status of a patient-session date pair in the database so it can queried and rendered quickly. Locally, I'm seeing the register outcomes page on programmes with 50,000 patients load almost instantly even when filtering on statuses. The session overview page has a slight improvement, but that will only load quickly once the other statuses are cached. I've decided not to implement a state machine as had been previously discussed, but this could be built on top of this change to enhancement the functionality. For now, this effectively caches the existing logic that runs on the fly to the database. I've also decided not to implement this using Rails callbacks and instead be explicit where the status is refreshed. This allows us to optimise for bulk refreshes, as is necessary when adding new programmes to sessions, running the seeds, or generally updating the status of an entire session. This follows up from #3273 which applies the same to the session status.
2 parents 53cb60e + 60dd5ca commit fcbd9c7

23 files changed

+353
-192
lines changed

app/components/app_outcome_banner_component.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ def triage
6262
end
6363

6464
def session_attendance
65-
@session_attendance ||= patient_session.register_outcome.latest
65+
@session_attendance ||= patient_session.todays_attendance
6666
end
6767

6868
def show_location?

app/components/app_patient_session_search_result_card_component.rb

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ class AppPatientSessionSearchResultCardComponent < ViewComponent::Base
3131
end
3232
end %>
3333
34-
<% if context == :register && helpers.policy(patient_session.register_outcome.latest).new? %>
34+
<% if context == :register && can_register_attendance? %>
3535
<div class="app-button-group">
3636
<%= helpers.govuk_button_to "Attending", create_session_register_path(session, patient, "present", search_form: params[:search_form]&.permit!), class: "app-button--secondary app-button--small" %>
3737
<%= helpers.govuk_button_to "Absent", create_session_register_path(session, patient, "absent", search_form: params[:search_form]&.permit!), class: "app-button--secondary-warning app-button--small" %>
@@ -43,20 +43,30 @@ class AppPatientSessionSearchResultCardComponent < ViewComponent::Base
4343
def initialize(patient_session, context:)
4444
super
4545

46-
@patient_session = patient_session
47-
@patient = patient_session.patient
48-
@session = patient_session.session
49-
@context = context
50-
5146
unless context.in?(%i[consent triage register record outcome])
5247
raise "Unknown context: #{context}"
5348
end
49+
50+
@patient_session = patient_session
51+
@context = context
52+
53+
@patient = patient_session.patient
54+
@session = patient_session.session
5455
end
5556

5657
private
5758

5859
attr_reader :patient_session, :patient, :session, :context
5960

61+
def can_register_attendance?
62+
session_attendance =
63+
SessionAttendance.new(
64+
patient_session:,
65+
session_date: SessionDate.new(value: Date.current)
66+
)
67+
helpers.policy(session_attendance).new?
68+
end
69+
6070
def link_to
6171
programme = patient_session.programmes.first
6272
session_patient_programme_path(
@@ -86,7 +96,7 @@ def status_tag
8696
case context
8797
when :register
8898
render AppRegisterStatusTagComponent.new(
89-
patient_session.register_outcome.status
99+
patient_session.registration_status&.status || "unknown"
90100
)
91101
when :consent
92102
statuses =

app/components/app_session_actions_component.rb

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -83,19 +83,14 @@ def triage_required_row
8383
end
8484

8585
def register_attendance_row
86-
return nil unless session.today?
86+
status = "unknown"
8787

88-
count = patient_sessions.count { it.register_outcome.unknown? }
88+
count = patient_sessions.has_registration_status(status).count
8989

9090
return nil if count.zero?
9191

9292
href =
93-
session_register_path(
94-
session,
95-
search_form: {
96-
register_status: PatientSession::RegisterOutcome::UNKNOWN
97-
}
98-
)
93+
session_register_path(session, search_form: { register_status: status })
9994

10095
{
10196
key: {
@@ -113,7 +108,13 @@ def ready_for_vaccinator_row
113108

114109
counts_by_programme =
115110
session.programmes.index_with do |programme|
116-
patient_sessions.count { it.ready_for_vaccinator?(programme:) }
111+
patient_sessions
112+
.has_registration_status(%w[attending completed])
113+
.count do |patient_session|
114+
patient_session.patient.consent_given_and_safe_to_vaccinate?(
115+
programme:
116+
)
117+
end
117118
end
118119

119120
return nil if counts_by_programme.values.all?(&:zero?)

app/components/app_vaccinate_form_component.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@ def initialize(patient_session:, programme:, vaccinate_form:)
1212
def render?
1313
patient.consent_given_and_safe_to_vaccinate?(programme:) &&
1414
(
15-
patient_session.register_outcome.attending? ||
16-
patient_session.register_outcome.completed?
15+
patient_session.registration_status&.attending? ||
16+
patient_session.registration_status&.completed? || false
1717
)
1818
end
1919

app/controllers/sessions/record_controller.rb

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -16,20 +16,24 @@ class Sessions::RecordController < ApplicationController
1616

1717
def show
1818
scope =
19-
@session.patient_sessions.preload_for_status.in_programmes(
20-
@session.programmes
21-
)
19+
@session
20+
.patient_sessions
21+
.preload_for_status
22+
.in_programmes(@session.programmes)
23+
.has_registration_status(%w[attending completed])
24+
25+
scope = @form.apply(scope)
2226

2327
patient_sessions =
24-
@form.apply(scope) do |filtered_scope|
25-
filtered_scope.select(&:ready_for_vaccinator?)
28+
scope.select do |patient_session|
29+
patient_session.programmes.any? do |programme|
30+
patient_session.patient.consent_given_and_safe_to_vaccinate?(
31+
programme:
32+
)
33+
end
2634
end
2735

28-
if patient_sessions.is_a?(Array)
29-
@pagy, @patient_sessions = pagy_array(patient_sessions)
30-
else
31-
@pagy, @patient_sessions = pagy(patient_sessions)
32-
end
36+
@pagy, @patient_sessions = pagy_array(patient_sessions)
3337

3438
render layout: "full"
3539
end
@@ -65,10 +69,7 @@ def update_batch
6569
private
6670

6771
def set_session
68-
@session =
69-
policy_scope(Session).includes(:programmes).find_by!(
70-
slug: params[:session_slug]
71-
)
72+
@session = policy_scope(Session).find_by!(slug: params[:session_slug])
7273
end
7374

7475
def set_todays_batches

app/controllers/sessions/register_controller.rb

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ class Sessions::RegisterController < ApplicationController
1313
layout "full"
1414

1515
def show
16-
@statuses = PatientSession::RegisterOutcome::STATUSES
16+
@statuses = PatientSession::RegistrationStatus.statuses.keys
1717

1818
scope =
1919
@session.patient_sessions.preload_for_status.in_programmes(
@@ -22,15 +22,11 @@ def show
2222

2323
patient_sessions = @form.apply(scope)
2424

25-
if patient_sessions.is_a?(Array)
26-
@pagy, @patient_sessions = pagy_array(patient_sessions)
27-
else
28-
@pagy, @patient_sessions = pagy(patient_sessions)
29-
end
25+
@pagy, @patient_sessions = pagy(patient_sessions)
3026
end
3127

3228
def create
33-
session_attendance = authorize @patient_session.register_outcome.latest
29+
session_attendance = authorize @patient_session.todays_attendance
3430

3531
ActiveRecord::Base.transaction do
3632
session_attendance.update!(attending: params[:status] == "present")
@@ -54,10 +50,7 @@ def create
5450
private
5551

5652
def set_session
57-
@session =
58-
policy_scope(Session).includes(:programmes, :session_dates).find_by!(
59-
slug: params[:session_slug]
60-
)
53+
@session = policy_scope(Session).find_by!(slug: params[:session_slug])
6154
end
6255

6356
def set_patient_session

app/forms/search_form.rb

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -40,10 +40,6 @@ def apply(scope, programme: nil)
4040

4141
scope = scope.search_by_nhs_number(nil) if missing_nhs_number.present?
4242

43-
scope = scope.order_by_name
44-
45-
scope = yield(scope) if block_given?
46-
4743
if (status = consent_status).present?
4844
scope = scope.has_consent_status(status, programme:)
4945
end
@@ -57,13 +53,13 @@ def apply(scope, programme: nil)
5753
end
5854

5955
if (status = register_status&.to_sym).present?
60-
scope = scope.select { it.register_outcome.status == status }
56+
scope = scope.has_registration_status(status)
6157
end
6258

6359
if (status = triage_status&.to_sym).present?
6460
scope = scope.has_triage_status(status, programme:)
6561
end
6662

67-
scope
63+
scope.order_by_name
6864
end
6965
end

app/lib/status_updater.rb

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ def initialize(patient: nil, session: nil)
1212

1313
def call
1414
update_consent_statuses!
15+
update_registration_statuses!
1516
update_session_statuses!
1617
update_triage_statuses!
1718
update_vaccination_statuses!
@@ -48,6 +49,34 @@ def update_consent_statuses!
4849
end
4950
end
5051

52+
def update_registration_statuses!
53+
PatientSession::RegistrationStatus.import!(
54+
%i[patient_session_id],
55+
registration_statuses_to_import,
56+
on_duplicate_key_ignore: true
57+
)
58+
59+
PatientSession::RegistrationStatus
60+
.where(patient_session_id: patient_sessions.select(:id))
61+
.includes(
62+
:patient,
63+
:session_attendance,
64+
:vaccination_records,
65+
session: :programmes
66+
)
67+
.find_in_batches(batch_size: 10_000) do |batch|
68+
batch.each(&:assign_status)
69+
70+
PatientSession::RegistrationStatus.import!(
71+
batch.select(&:changed?),
72+
on_duplicate_key_update: {
73+
conflict_target: [:id],
74+
columns: %i[status]
75+
}
76+
)
77+
end
78+
end
79+
5180
def update_session_statuses!
5281
PatientSession::SessionStatus.import!(
5382
%i[patient_session_id programme_id],
@@ -142,6 +171,18 @@ def patient_session_statuses_to_import
142171
end
143172
end
144173

174+
def registration_statuses_to_import
175+
@registration_statuses_to_import ||=
176+
patient_sessions
177+
.joins(:patient)
178+
.pluck(:id, :"patients.birth_academic_year")
179+
.filter_map do |patient_session_id, birth_academic_year|
180+
if programme_ids_per_birth_academic_year.key?(birth_academic_year)
181+
[patient_session_id]
182+
end
183+
end
184+
end
185+
145186
def programme_ids_per_birth_academic_year
146187
@programme_ids_per_birth_academic_year ||=
147188
Programme

app/models/patient_session.rb

Lines changed: 25 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ class PatientSession < ApplicationRecord
3434
has_many :gillick_assessments, -> { order(:created_at) }
3535
has_many :pre_screenings, -> { order(:created_at) }
3636
has_many :session_statuses
37+
has_one :registration_status
3738

3839
has_one :location, through: :session
3940
has_one :team, through: :session
@@ -69,7 +70,7 @@ class PatientSession < ApplicationRecord
6970
scope :preload_for_status,
7071
-> do
7172
eager_load(:patient).preload(
72-
:session_attendances,
73+
:registration_status,
7374
:session_statuses,
7475
patient: %i[consent_statuses triage_statuses vaccination_statuses],
7576
session: :programmes
@@ -116,6 +117,17 @@ class PatientSession < ApplicationRecord
116117
)
117118
end
118119

120+
scope :has_registration_status,
121+
->(status) do
122+
where(
123+
PatientSession::RegistrationStatus
124+
.where("patient_session_id = patient_sessions.id")
125+
.where(status:)
126+
.arel
127+
.exists
128+
)
129+
end
130+
119131
scope :has_session_status,
120132
->(status, programme:) do
121133
where(
@@ -166,17 +178,11 @@ def session_status(programme:)
166178
session_statuses.build(programme:)
167179
end
168180

169-
def register_outcome
170-
@register_outcome ||= PatientSession::RegisterOutcome.new(self)
171-
end
172-
173-
def ready_for_vaccinator?(programme: nil)
174-
return false if register_outcome.unknown? || register_outcome.not_attending?
175-
176-
programmes_to_check = programme ? [programme] : programmes
177-
178-
programmes_to_check.any? do
179-
patient.consent_given_and_safe_to_vaccinate?(programme: it)
181+
def todays_attendance
182+
if (session_date = session.session_dates.today.first)
183+
session_attendances.includes(:session_date).find_or_initialize_by(
184+
session_date:
185+
)
180186
end
181187
end
182188

@@ -195,6 +201,11 @@ def next_activity(programme:)
195201
end
196202

197203
def outstanding_programmes
204+
if registration_status.nil? || registration_status.unknown? ||
205+
registration_status.not_attending?
206+
return []
207+
end
208+
198209
# If this patient hasn't been seen yet by a nurse for any of the programmes,
199210
# we don't want to show the banner.
200211
all_programmes_none_yet =
@@ -203,7 +214,8 @@ def outstanding_programmes
203214
return [] if all_programmes_none_yet
204215

205216
programmes.select do |programme|
206-
session_status(programme:).none_yet? && ready_for_vaccinator?(programme:)
217+
session_status(programme:).none_yet? &&
218+
patient.consent_given_and_safe_to_vaccinate?(programme:)
207219
end
208220
end
209221
end

0 commit comments

Comments
 (0)