<% end %>
diff --git a/app/views/sessions/edit/programmes.html.erb b/app/views/sessions/edit/programmes.html.erb
index 2b9ce43a7c..91603c3f02 100644
--- a/app/views/sessions/edit/programmes.html.erb
+++ b/app/views/sessions/edit/programmes.html.erb
@@ -1,11 +1,11 @@
<% content_for :before_main do %>
- <%= govuk_back_link(href: edit_session_path(@session)) %>
+ <%= govuk_back_link(href: session_edit_path(@session)) %>
<% end %>
<% legend = "Which programmes is this session part of?" %>
<% content_for :page_title, legend %>
-<%= form_with model: @form, url: edit_programmes_session_path(@session), method: :put do |f| %>
+<%= form_with model: @form, url: programmes_session_edit_path(@session), method: :put do |f| %>
<% content_for(:before_content) { f.govuk_error_summary } %>
<%= f.govuk_collection_check_boxes :programme_ids, policy_scope(Programme), :id, :name,
diff --git a/app/views/sessions/edit/send_consent_requests_at.html.erb b/app/views/sessions/edit/send_consent_requests_at.html.erb
index e5eb88dd10..dc591990ba 100644
--- a/app/views/sessions/edit/send_consent_requests_at.html.erb
+++ b/app/views/sessions/edit/send_consent_requests_at.html.erb
@@ -1,11 +1,11 @@
<% content_for :before_main do %>
- <%= govuk_back_link(href: edit_session_path(@session)) %>
+ <%= govuk_back_link(href: session_edit_path(@session)) %>
<% end %>
<% legend = "When should parents get a request to give consent?" %>
<% content_for :page_title, legend %>
-<%= form_with model: @session, url: edit_send_consent_requests_at_session_path(@session), method: :put do |f| %>
+<%= form_with model: @session, url: send_consent_requests_at_session_edit_path(@session), method: :put do |f| %>
<% content_for(:before_content) { f.govuk_error_summary } %>
<%= f.govuk_date_field :send_consent_requests_at,
diff --git a/app/views/sessions/edit/send_invitations_at.html.erb b/app/views/sessions/edit/send_invitations_at.html.erb
index 7933b53f7e..bc0148a645 100644
--- a/app/views/sessions/edit/send_invitations_at.html.erb
+++ b/app/views/sessions/edit/send_invitations_at.html.erb
@@ -1,11 +1,11 @@
<% content_for :before_main do %>
- <%= govuk_back_link(href: edit_session_path(@session)) %>
+ <%= govuk_back_link(href: session_edit_path(@session)) %>
<% end %>
<% legend = "When should parents get an invitation?" %>
<% content_for :page_title, legend %>
-<%= form_with model: @session, url: edit_send_invitations_at_session_path(@session), method: :put do |f| %>
+<%= form_with model: @session, url: send_invitations_at_session_edit_path(@session), method: :put do |f| %>
<% content_for(:before_content) { f.govuk_error_summary } %>
<%= f.govuk_date_field :send_invitations_at,
diff --git a/app/views/sessions/edit.html.erb b/app/views/sessions/edit/show.html.erb
similarity index 83%
rename from app/views/sessions/edit.html.erb
rename to app/views/sessions/edit/show.html.erb
index 65781c7425..200387edcb 100644
--- a/app/views/sessions/edit.html.erb
+++ b/app/views/sessions/edit/show.html.erb
@@ -14,7 +14,7 @@
summary_list.with_row do |row|
row.with_key { "Programmes" }
row.with_value { render AppProgrammeTagsComponent.new(@session.programmes) }
- row.with_action(text: "Change", href: edit_programmes_session_path(@session), visually_hidden_text: "programmes")
+ row.with_action(text: "Change", href: programmes_session_edit_path(@session), visually_hidden_text: "programmes")
end
summary_list.with_row do |row|
@@ -37,7 +37,7 @@
row.with_key { "Consent requests" }
row.with_value { "Send on #{send_consent_requests_at.to_fs(:long_day_of_week)}" }
if @session.can_change_notification_dates?
- row.with_action(text: "Change", href: edit_send_consent_requests_at_session_path(@session), visually_hidden_text: "consent requests")
+ row.with_action(text: "Change", href: send_consent_requests_at_session_edit_path(@session), visually_hidden_text: "consent requests")
end
end
end
@@ -52,7 +52,7 @@
], tag.br)
end
if @session.can_change_notification_dates?
- row.with_action(text: "Change", href: edit_weeks_before_consent_reminders_session_path(@session), visually_hidden_text: "consent reminders")
+ row.with_action(text: "Change", href: weeks_before_consent_reminders_session_edit_path(@session), visually_hidden_text: "consent reminders")
end
end
end
@@ -62,7 +62,7 @@
row.with_key { "Invitations" }
row.with_value { "Send on #{send_invitations_at.to_fs(:long_day_of_week)}" }
if @session.can_change_notification_dates?
- row.with_action(text: "Change", href: edit_send_invitations_at_session_path(@session), visually_hidden_text: "invitations")
+ row.with_action(text: "Change", href: send_invitations_at_session_edit_path(@session), visually_hidden_text: "invitations")
end
end
end
diff --git a/app/views/sessions/edit/weeks_before_consent_reminders.html.erb b/app/views/sessions/edit/weeks_before_consent_reminders.html.erb
index f31f8d2caa..95d6ac52a8 100644
--- a/app/views/sessions/edit/weeks_before_consent_reminders.html.erb
+++ b/app/views/sessions/edit/weeks_before_consent_reminders.html.erb
@@ -1,11 +1,11 @@
<% content_for :before_main do %>
- <%= govuk_back_link(href: edit_session_path(@session)) %>
+ <%= govuk_back_link(href: session_edit_path(@session)) %>
<% end %>
<% legend = "When should parents get a reminder to give consent?" %>
<% content_for :page_title, legend %>
-<%= form_with model: @session, url: edit_weeks_before_consent_reminders_session_path(@session), method: :put do |f| %>
+<%= form_with model: @session, url: weeks_before_consent_reminders_session_edit_path(@session), method: :put do |f| %>
<% content_for(:before_content) { f.govuk_error_summary } %>
<%= f.govuk_text_field :weeks_before_consent_reminders,
diff --git a/app/views/sessions/show.html.erb b/app/views/sessions/show.html.erb
index 4f885c386f..a2e1792aaf 100644
--- a/app/views/sessions/show.html.erb
+++ b/app/views/sessions/show.html.erb
@@ -31,14 +31,14 @@
<% if @session.unscheduled? %>
<% if policy(@session).edit? %>
- <%= govuk_button_link_to "Schedule sessions", edit_session_path(@session), secondary: true %>
+ <%= govuk_button_link_to "Schedule sessions", session_edit_path(@session), secondary: true %>
<% end %>
<% else %>
<%= render AppSessionActionsComponent.new(@session) %>
<% if policy(@session).edit? %>
- <%= govuk_button_link_to "Edit session", edit_session_path(@session), secondary: true %>
+ <%= govuk_button_link_to "Edit session", session_edit_path(@session), secondary: true %>
<% end %>
<%= govuk_link_to "Record offline", session_path(@session, format: :xlsx) %>
diff --git a/config/routes.rb b/config/routes.rb
index 90b303a50b..b6fe8a71df 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -211,7 +211,7 @@
get "download", on: :member
end
- resources :sessions, only: %i[edit index show], param: :slug do
+ resources :sessions, only: %i[index show], param: :slug do
resource :patients, only: :show, controller: "sessions/patients"
resource :consent, only: :show, controller: "sessions/consent"
resource :triage, only: :show, controller: "sessions/triage"
@@ -225,6 +225,21 @@
post "batch/:programme_type/:vaccine_method", action: :update_batch
end
+ resource :edit, only: :show, controller: "sessions/edit" do
+ get "programmes"
+ put "programmes", action: :update_programmes
+
+ get "send-consent-requests-at"
+ put "send-consent-requests-at", action: :update_send_consent_requests_at
+
+ get "send-invitations-at"
+ put "send-invitations-at", action: :update_send_invitations_at
+
+ get "weeks-before-consent-reminders"
+ put "weeks-before-consent-reminders",
+ action: :update_weeks_before_consent_reminders
+ end
+
resource :invite_to_clinic,
path: "invite-to-clinic",
only: %i[edit update],
@@ -233,34 +248,6 @@
member do
get "import"
- get "edit/programmes",
- controller: "sessions/edit",
- action: "edit_programmes"
- put "edit/programmes",
- controller: "sessions/edit",
- action: "update_programmes"
-
- get "edit/send-consent-requests-at",
- controller: "sessions/edit",
- action: "edit_send_consent_requests_at"
- put "edit/send-consent-requests-at",
- controller: "sessions/edit",
- action: "update_send_consent_requests_at"
-
- get "edit/send-invitations-at",
- controller: "sessions/edit",
- action: "edit_send_invitations_at"
- put "edit/send-invitations-at",
- controller: "sessions/edit",
- action: "update_send_invitations_at"
-
- get "edit/weeks-before-consent-reminders",
- controller: "sessions/edit",
- action: "edit_weeks_before_consent_reminders"
- put "edit/weeks-before-consent-reminders",
- controller: "sessions/edit",
- action: "update_weeks_before_consent_reminders"
-
constraints -> { Flipper.enabled?(:dev_tools) } do
put "make-in-progress", to: "sessions#make_in_progress"
end
From 03f08c03bb404ee94669838137eab76ba42234ac Mon Sep 17 00:00:00 2001
From: Thomas Leese
Date: Wed, 27 Aug 2025 08:19:10 +0100
Subject: [PATCH 03/27] Restrict who can edit session details
This restricts who can edit the details of a session to nurses and
admins, ensuring that healthcare assistants can't enable protocols that
would allow them to administer vaccines.
Jira-Issue: MAV-1357
---
app/controllers/sessions/edit_controller.rb | 23 +++++++++++++
app/policies/session_policy.rb | 4 +++
spec/factories/users.rb | 2 +-
spec/policies/session_policy_spec.rb | 36 +++++++++++++++------
4 files changed, 55 insertions(+), 10 deletions(-)
diff --git a/app/controllers/sessions/edit_controller.rb b/app/controllers/sessions/edit_controller.rb
index 3c7a60ba62..9b456e7446 100644
--- a/app/controllers/sessions/edit_controller.rb
+++ b/app/controllers/sessions/edit_controller.rb
@@ -3,6 +3,21 @@
class Sessions::EditController < ApplicationController
before_action :set_session
+ before_action :authorize_session_edit,
+ except: %i[
+ update_programmes
+ update_send_consent_requests_at
+ update_send_invitations_at
+ update_weeks_before_consent_reminders
+ ]
+ before_action :authorize_session_update,
+ only: %i[
+ update_programmes
+ update_send_consent_requests_at
+ update_send_invitations_at
+ update_weeks_before_consent_reminders
+ ]
+
def show
end
@@ -71,6 +86,14 @@ def set_session
@session = policy_scope(Session).find_by!(slug: params[:session_slug])
end
+ def authorize_session_edit
+ authorize @session, :edit?
+ end
+
+ def authorize_session_update
+ authorize @session, :update?
+ end
+
def programmes_params
params.expect(session_programmes_form: { programme_ids: [] })
end
diff --git a/app/policies/session_policy.rb b/app/policies/session_policy.rb
index 46ef0b9caa..3c2d9e2866 100644
--- a/app/policies/session_policy.rb
+++ b/app/policies/session_policy.rb
@@ -1,6 +1,10 @@
# frozen_string_literal: true
class SessionPolicy < ApplicationPolicy
+ def update?
+ user.is_nurse? || user.is_admin?
+ end
+
def import? = show?
def make_in_progress? = edit?
diff --git a/spec/factories/users.rb b/spec/factories/users.rb
index 6874af3a8a..07f7b46942 100644
--- a/spec/factories/users.rb
+++ b/spec/factories/users.rb
@@ -93,7 +93,7 @@
trait :healthcare_assistant do
sequence(:email) { |n| "healthcare-assistant-#{n}@example.com" }
- role_code { CIS2Info::ADMIN_ROLE }
+ role_code { nil }
activity_codes do
[CIS2Info::PERSONAL_MEDICATION_ADMINISTRATION_ACTIVITY_CODE]
end
diff --git a/spec/policies/session_policy_spec.rb b/spec/policies/session_policy_spec.rb
index a8454738e6..83c9054f9b 100644
--- a/spec/policies/session_policy_spec.rb
+++ b/spec/policies/session_policy_spec.rb
@@ -35,6 +35,22 @@
it { should be(true) }
end
end
+
+ context "with a healthcare assistant" do
+ let(:user) { create(:healthcare_assistant) }
+
+ context "with a scheduled session" do
+ let(:session) { create(:session, :scheduled) }
+
+ it { should be(false) }
+ end
+
+ context "with an unscheduled session" do
+ let(:session) { create(:session, :unscheduled) }
+
+ it { should be(false) }
+ end
+ end
end
describe "#edit?" do
@@ -49,17 +65,19 @@
include_examples "edit/update session"
end
- describe "Scope#resolve" do
- subject { SessionPolicy::Scope.new(user, Session).resolve }
+ describe SessionPolicy::Scope do
+ describe "#resolve" do
+ subject { described_class.new(user, Session).resolve }
- let(:programmes) { [create(:programme)] }
- let(:team) { create(:team, programmes:) }
- let(:user) { create(:user, team:) }
+ let(:programmes) { [create(:programme)] }
+ let(:team) { create(:team, programmes:) }
+ let(:user) { create(:user, team:) }
- let(:users_teams_session) { create(:session, team:, programmes:) }
- let(:another_teams_session) { create(:session, programmes:) }
+ let(:users_teams_session) { create(:session, team:, programmes:) }
+ let(:another_teams_session) { create(:session, programmes:) }
- it { should include(users_teams_session) }
- it { should_not include(another_teams_session) }
+ it { should include(users_teams_session) }
+ it { should_not include(another_teams_session) }
+ end
end
end
From 1ba204e79286df58accae9e2063a8d70190403d5 Mon Sep 17 00:00:00 2001
From: Thomas Leese
Date: Thu, 21 Aug 2025 17:00:52 +0100
Subject: [PATCH 04/27] Add ability to edit session registration
This adds a new page allowing users to edit whether or not a session
should register attendance, this matches the latest designs in the
prototype.
Jira-Issue: MAV-1357
---
app/controllers/sessions/edit_controller.rb | 17 ++++++++++
.../edit/register_attendance.html.erb | 20 ++++++++++++
app/views/sessions/edit/show.html.erb | 6 ++++
config/routes.rb | 3 ++
spec/features/manage_attendance_spec.rb | 31 +++++++++++++++++--
5 files changed, 75 insertions(+), 2 deletions(-)
create mode 100644 app/views/sessions/edit/register_attendance.html.erb
diff --git a/app/controllers/sessions/edit_controller.rb b/app/controllers/sessions/edit_controller.rb
index 9b456e7446..35c9af7145 100644
--- a/app/controllers/sessions/edit_controller.rb
+++ b/app/controllers/sessions/edit_controller.rb
@@ -9,6 +9,7 @@ class Sessions::EditController < ApplicationController
update_send_consent_requests_at
update_send_invitations_at
update_weeks_before_consent_reminders
+ update_register_attendance
]
before_action :authorize_session_update,
only: %i[
@@ -16,6 +17,7 @@ class Sessions::EditController < ApplicationController
update_send_consent_requests_at
update_send_invitations_at
update_weeks_before_consent_reminders
+ update_register_attendance
]
def show
@@ -80,6 +82,17 @@ def update_weeks_before_consent_reminders
end
end
+ def register_attendance
+ end
+
+ def update_register_attendance
+ if @session.update(register_attendance_params)
+ redirect_to session_edit_path(@session)
+ else
+ render :register_attendance, status: :unprocessable_content
+ end
+ end
+
private
def set_session
@@ -127,4 +140,8 @@ def send_invitations_at_params
def weeks_before_consent_reminders_params
params.expect(session: :weeks_before_consent_reminders)
end
+
+ def register_attendance_params
+ params.expect(session: :requires_registration)
+ end
end
diff --git a/app/views/sessions/edit/register_attendance.html.erb b/app/views/sessions/edit/register_attendance.html.erb
new file mode 100644
index 0000000000..6dafde2a8c
--- /dev/null
+++ b/app/views/sessions/edit/register_attendance.html.erb
@@ -0,0 +1,20 @@
+<% content_for :before_main do %>
+ <%= govuk_back_link(href: session_edit_path(@session)) %>
+<% end %>
+
+<% legend = "Do you want to register children’s attendance before recording vaccinations?" %>
+<% content_for :page_title, legend %>
+
+<%= form_with model: @session, url: register_attendance_session_edit_path(@session), method: :put do |f| %>
+ <% content_for(:before_content) { f.govuk_error_summary } %>
+
+ <%= f.govuk_radio_buttons_fieldset :requires_registration,
+ legend: { text: legend, size: "l", tag: "h1" },
+ caption: { text: @session.location.name, size: "l" },
+ link_errors: true do %>
+ <%= f.govuk_radio_button :requires_registration, "true", label: { text: "Yes" } %>
+ <%= f.govuk_radio_button :requires_registration, "false", label: { text: "No" } %>
+ <% end %>
+
+ <%= f.govuk_submit "Continue" %>
+<% end %>
diff --git a/app/views/sessions/edit/show.html.erb b/app/views/sessions/edit/show.html.erb
index 200387edcb..6d60634e38 100644
--- a/app/views/sessions/edit/show.html.erb
+++ b/app/views/sessions/edit/show.html.erb
@@ -66,6 +66,12 @@
end
end
end
+
+ summary_list.with_row do |row|
+ row.with_key { "Register attendance" }
+ row.with_value { @session.requires_registration ? "Yes" : "No" }
+ row.with_action(text: "Change", href: register_attendance_session_edit_path(@session), visually_hidden_text: "register attendance")
+ end
end %>
<% end %>
diff --git a/config/routes.rb b/config/routes.rb
index b6fe8a71df..a173ebc6cd 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -238,6 +238,9 @@
get "weeks-before-consent-reminders"
put "weeks-before-consent-reminders",
action: :update_weeks_before_consent_reminders
+
+ get "register-attendance"
+ put "register-attendance", action: :update_register_attendance
end
resource :invite_to_clinic,
diff --git a/spec/features/manage_attendance_spec.rb b/spec/features/manage_attendance_spec.rb
index 419f2272e5..2abbb5bfcc 100644
--- a/spec/features/manage_attendance_spec.rb
+++ b/spec/features/manage_attendance_spec.rb
@@ -47,13 +47,30 @@
and_the_session_has_patients
when_i_go_to_the_session
- then_i_should_not_see_the_register_tab
+ then_i_do_not_see_the_register_tab
when_i_go_to_the_session_patients
and_i_go_to_a_patient
then_i_should_not_see_link_to_update_attendance
end
+ scenario "Turning off attendance" do
+ given_my_team_is_running_an_hpv_vaccination_programme
+ and_there_is_a_vaccination_session_today
+ and_the_session_has_patients
+
+ when_i_go_to_the_session
+ and_i_click_on_the_register_tab
+ then_i_see_the_register_tab
+
+ when_i_go_to_the_session
+ and_i_edit_the_session
+ and_i_turn_off_register_attendance
+
+ when_i_go_to_the_session
+ then_i_do_not_see_the_register_tab
+ end
+
def given_my_team_is_running_an_hpv_vaccination_programme
@programmes = [create(:programme, :hpv_all_vaccines)]
@team = create(:team, :with_one_nurse, programmes: @programmes)
@@ -103,7 +120,7 @@ def when_i_click_on_the_record_vaccinations_tab
click_link "Record vaccinations"
end
- def then_i_should_not_see_the_register_tab
+ def then_i_do_not_see_the_register_tab
expect(page).not_to have_content("Register")
end
@@ -204,4 +221,14 @@ def when_i_go_to_the_activity_log
def then_i_see_the_attendance_event
expect(page).to have_content("Attended session")
end
+
+ def and_i_edit_the_session
+ click_on "Edit session"
+ end
+
+ def and_i_turn_off_register_attendance
+ click_on "Change register attendance"
+ choose "No"
+ click_on "Continue"
+ end
end
From 2a8ede59742b8d1c66b58ee774fa345232f240b3 Mon Sep 17 00:00:00 2001
From: Thomas Leese
Date: Thu, 21 Aug 2025 17:23:57 +0100
Subject: [PATCH 05/27] Add ability for users to enable/disable delgation
This adds a new page which users can get to from the "Edit session" page
allowing them to enable or disable using PSDs or the national protocol
at a per-session level.
Jira-Issue: MAV-1357
---
app/controllers/sessions/edit_controller.rb | 17 ++++
app/views/sessions/edit/delegation.html.erb | 38 ++++++++
app/views/sessions/edit/show.html.erb | 16 +++-
config/routes.rb | 3 +
spec/features/delegation_spec.rb | 99 +++++++++++++++++++++
5 files changed, 172 insertions(+), 1 deletion(-)
create mode 100644 app/views/sessions/edit/delegation.html.erb
create mode 100644 spec/features/delegation_spec.rb
diff --git a/app/controllers/sessions/edit_controller.rb b/app/controllers/sessions/edit_controller.rb
index 35c9af7145..4f92bd9fdf 100644
--- a/app/controllers/sessions/edit_controller.rb
+++ b/app/controllers/sessions/edit_controller.rb
@@ -10,6 +10,7 @@ class Sessions::EditController < ApplicationController
update_send_invitations_at
update_weeks_before_consent_reminders
update_register_attendance
+ update_delegation
]
before_action :authorize_session_update,
only: %i[
@@ -18,6 +19,7 @@ class Sessions::EditController < ApplicationController
update_send_invitations_at
update_weeks_before_consent_reminders
update_register_attendance
+ update_delegation
]
def show
@@ -93,6 +95,17 @@ def update_register_attendance
end
end
+ def delegation
+ end
+
+ def update_delegation
+ if @session.update(delegation_params)
+ redirect_to session_edit_path(@session)
+ else
+ render :delegation, status: :unprocessable_content
+ end
+ end
+
private
def set_session
@@ -144,4 +157,8 @@ def weeks_before_consent_reminders_params
def register_attendance_params
params.expect(session: :requires_registration)
end
+
+ def delegation_params
+ params.expect(session: %i[psd_enabled national_protocol_enabled])
+ end
end
diff --git a/app/views/sessions/edit/delegation.html.erb b/app/views/sessions/edit/delegation.html.erb
new file mode 100644
index 0000000000..996bf8d1ba
--- /dev/null
+++ b/app/views/sessions/edit/delegation.html.erb
@@ -0,0 +1,38 @@
+<% content_for :before_main do %>
+ <%= govuk_back_link(href: session_edit_path(@session)) %>
+<% end %>
+
+<%= form_with model: @session, url: delegation_session_edit_path(@session), method: :put do |f| %>
+ <% content_for(:before_content) { f.govuk_error_summary } %>
+
+ <%= @session.location.name %>
+ <%= h1 "Delegation" %>
+
+ <%= f.govuk_radio_buttons_fieldset :psd_enabled,
+ legend: { text: "Can healthcare assistants administer the flu nasal spray vaccine using a patient specific direction (PSD)?" },
+ link_errors: true do %>
+ <%= f.govuk_radio_button :psd_enabled,
+ "false",
+ label: { text: "No" },
+ hint: { text: "Healthcare assistants can only administer a nasal spray when supplied by a nurse" } %>
+ <%= f.govuk_radio_button :psd_enabled,
+ "true",
+ label: { text: "Yes" },
+ hint: { text: "Healthcare assistants can administer the nasal spray vaccine to children who are covered by a PSD" } %>
+ <% end %>
+
+ <%= f.govuk_radio_buttons_fieldset :national_protocol_enabled,
+ legend: { text: "Can healthcare assistants administer the injected flu vaccine using the national protocol?" },
+ link_errors: true do %>
+ <%= f.govuk_radio_button :national_protocol_enabled,
+ "false",
+ label: { text: "No" },
+ hint: { text: "Only nurses can administer the injected flu vaccine" } %>
+ <%= f.govuk_radio_button :national_protocol_enabled,
+ "true",
+ label: { text: "Yes" },
+ hint: { text: "Healthcare assistants can administer an injected flu vaccine when supplied by a nurse" } %>
+ <% end %>
+
+ <%= f.govuk_submit "Continue" %>
+<% end %>
diff --git a/app/views/sessions/edit/show.html.erb b/app/views/sessions/edit/show.html.erb
index 6d60634e38..b0d33bf9c6 100644
--- a/app/views/sessions/edit/show.html.erb
+++ b/app/views/sessions/edit/show.html.erb
@@ -69,9 +69,23 @@
summary_list.with_row do |row|
row.with_key { "Register attendance" }
- row.with_value { @session.requires_registration ? "Yes" : "No" }
+ row.with_value { @session.requires_registration? ? "Yes" : "No" }
row.with_action(text: "Change", href: register_attendance_session_edit_path(@session), visually_hidden_text: "register attendance")
end
+
+ if Flipper.enabled?(:delegation) && @session.supports_delegation?
+ summary_list.with_row do |row|
+ row.with_key { "Use patient specific direction (PSD)" }
+ row.with_value { @session.psd_enabled? ? "Yes" : "No" }
+ row.with_action(text: "Change", href: delegation_session_edit_path(@session), visually_hidden_text: "use patient specific direction (PSD)")
+ end
+
+ summary_list.with_row do |row|
+ row.with_key { "Use national protocol" }
+ row.with_value { @session.national_protocol_enabled? ? "Yes" : "No" }
+ row.with_action(text: "Change", href: delegation_session_edit_path(@session), visually_hidden_text: "use national protocol")
+ end
+ end
end %>
<% end %>
diff --git a/config/routes.rb b/config/routes.rb
index a173ebc6cd..f6b1d37cde 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -241,6 +241,9 @@
get "register-attendance"
put "register-attendance", action: :update_register_attendance
+
+ get "delegation"
+ put "delegation", action: :update_delegation
end
resource :invite_to_clinic,
diff --git a/spec/features/delegation_spec.rb b/spec/features/delegation_spec.rb
new file mode 100644
index 0000000000..ca57c80089
--- /dev/null
+++ b/spec/features/delegation_spec.rb
@@ -0,0 +1,99 @@
+# frozen_string_literal: true
+
+describe "Delegation" do
+ after { given_the_feature_flag_is_disabled }
+
+ scenario "feature flag off and in a flu session" do
+ given_the_feature_flag_is_disabled
+ and_a_flu_session_exists
+
+ when_i_visit_the_session_as_a_nurse
+ and_i_go_to_the_edit_page
+ then_i_see_nothing_about_delegation
+ end
+
+ scenario "feature flag on and in a HPV session" do
+ given_the_feature_flag_is_enabled
+ and_an_hpv_session_exists
+
+ when_i_visit_the_session_as_a_nurse
+ and_i_go_to_the_edit_page
+ then_i_see_nothing_about_delegation
+ end
+
+ scenario "feature flag on and in a flu session" do
+ given_the_feature_flag_is_enabled
+ and_a_flu_session_exists
+
+ when_i_visit_the_session_as_a_nurse
+ and_i_go_to_the_edit_page
+ then_i_see_the_delegation_options
+
+ when_i_enable_delegation
+ then_i_see_the_options_are_enabled
+ end
+
+ def given_the_feature_flag_is_enabled
+ Flipper.enable(:delegation)
+ end
+
+ def given_the_feature_flag_is_disabled
+ Flipper.disable(:delegation)
+ end
+
+ def and_a_flu_session_exists
+ @programme = create(:programme, :flu)
+ @team = create(:team, programmes: [@programme])
+ @nurse = create(:nurse, teams: [@team])
+
+ @session = create(:session, programmes: [@programme], team: @team)
+ end
+
+ def and_an_hpv_session_exists
+ @programme = create(:programme, :hpv)
+ @team = create(:team, programmes: [@programme])
+ @nurse = create(:nurse, teams: [@team])
+
+ @session = create(:session, programmes: [@programme], team: @team)
+ end
+
+ def when_i_visit_the_session_as_a_nurse
+ sign_in @nurse
+ visit session_path(@session)
+ end
+
+ def and_i_go_to_the_edit_page
+ click_on "Edit session"
+ end
+
+ def then_i_see_nothing_about_delegation
+ expect(page).not_to have_content("patient specific direction")
+ expect(page).not_to have_content("national protocol")
+ end
+
+ def then_i_see_the_delegation_options
+ expect(page).to have_content("Use patient specific direction (PSD)")
+ expect(page).to have_content("Use national protocol")
+ end
+
+ def when_i_enable_delegation
+ click_on "Change use patient specific direction (PSD)"
+
+ # PSD
+ within all(".nhsuk-fieldset")[0] do
+ choose "Yes"
+ end
+
+ # National protocol
+ within all(".nhsuk-fieldset")[1] do
+ choose "Yes"
+ end
+
+ click_on "Continue"
+ end
+
+ def then_i_see_the_options_are_enabled
+ expect(page).to have_content("Use patient specific direction (PSD)Yes")
+ expect(page).to have_content("Use national protocolYes")
+ end
+end
From ee39713476fb1ef1e506d5308bbc50f19be23bd5 Mon Sep 17 00:00:00 2001
From: Thomas Leese
Date: Thu, 21 Aug 2025 11:09:50 +0100
Subject: [PATCH 06/27] Add `is_prescriber?`
This adds a method to `User` and `CIS2Info` which can be used to
determine with a particular user has permission to prescribe a
vaccination using either a PSD or the national protocol.
Jira-Issue: MAV-1400
---
.../concerns/authentication_concern.rb | 3 +-
app/models/cis2_info.rb | 5 +++
app/models/user.rb | 6 +++
spec/factories/users.rb | 8 ++++
spec/models/user_spec.rb | 44 +++++++++++++++++++
5 files changed, 65 insertions(+), 1 deletion(-)
diff --git a/app/controllers/concerns/authentication_concern.rb b/app/controllers/concerns/authentication_concern.rb
index ffff28bc18..9e67bb530c 100644
--- a/app/controllers/concerns/authentication_concern.rb
+++ b/app/controllers/concerns/authentication_concern.rb
@@ -50,7 +50,8 @@ def selected_cis2_workgroup_is_valid?
def selected_cis2_role_is_valid?
cis2_info.is_admin? || cis2_info.is_nurse? ||
- cis2_info.is_healthcare_assistant? || cis2_info.is_superuser?
+ cis2_info.is_healthcare_assistant? || cis2_info.is_superuser? ||
+ cis2_info.is_prescriber?
end
def storable_location?
diff --git a/app/models/cis2_info.rb b/app/models/cis2_info.rb
index 4d6e620c0c..c3052ba68e 100644
--- a/app/models/cis2_info.rb
+++ b/app/models/cis2_info.rb
@@ -8,6 +8,7 @@ class CIS2Info
SUPERUSER_WORKGROUP = "mavissuperusers"
+ INDEPENDENT_PRESCRIBING_ACTIVITY_CODE = "B0420"
PERSONAL_MEDICATION_ADMINISTRATION_ACTIVITY_CODE = "B0428"
attribute :organisation_name
@@ -50,6 +51,10 @@ def is_healthcare_assistant?
activity_codes.include?(PERSONAL_MEDICATION_ADMINISTRATION_ACTIVITY_CODE)
end
+ def is_prescriber?
+ activity_codes.include?(INDEPENDENT_PRESCRIBING_ACTIVITY_CODE)
+ end
+
def is_superuser?
workgroups.include?(SUPERUSER_WORKGROUP)
end
diff --git a/app/models/user.rb b/app/models/user.rb
index 75dad8c76e..ce86d95667 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -126,6 +126,8 @@ def role_description
role =
if is_healthcare_assistant?
"Healthcare Assistant"
+ elsif is_prescriber?
+ "Prescriber"
elsif is_nurse?
"Nurse"
else
@@ -151,6 +153,10 @@ def is_healthcare_assistant?
end
end
+ def is_prescriber?
+ cis2_enabled? ? cis2_info.is_prescriber? : fallback_role_prescriber?
+ end
+
def is_superuser?
cis2_enabled? ? cis2_info.is_superuser? : fallback_role_superuser?
end
diff --git a/spec/factories/users.rb b/spec/factories/users.rb
index 07f7b46942..dc57218e5c 100644
--- a/spec/factories/users.rb
+++ b/spec/factories/users.rb
@@ -100,6 +100,13 @@
fallback_role { :healthcare_assistant }
end
+ trait :prescriber do
+ sequence(:email) { |n| "prescriber-#{n}@example.com" }
+ role_code { nil }
+ activity_codes { [CIS2Info::INDEPENDENT_PRESCRIBING_ACTIVITY_CODE] }
+ fallback_role { :prescriber }
+ end
+
trait :signed_in do
current_sign_in_at { Time.current }
current_sign_in_ip { "127.0.0.1" }
@@ -108,5 +115,6 @@
factory :admin, parent: :user, traits: %i[admin]
factory :healthcare_assistant, parent: :user, traits: %i[healthcare_assistant]
+ factory :prescriber, parent: :user, traits: %i[prescriber]
factory :superuser, parent: :user, traits: %i[superuser]
end
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index ecf02f3694..9ecfe871a3 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -196,6 +196,50 @@
end
end
+ describe "#is_prescriber?" do
+ subject { user.is_prescriber? }
+
+ context "cis2 is enabled", cis2: :enabled do
+ context "when the user is a nurse" do
+ let(:user) { build(:nurse) }
+
+ it { should be(false) }
+ end
+
+ context "when the user is admin staff" do
+ let(:user) { build(:admin) }
+
+ it { should be(false) }
+ end
+
+ context "when the user is a prescriber" do
+ let(:user) { build(:prescriber) }
+
+ it { should be(true) }
+ end
+ end
+
+ context "cis2 is disabled", cis2: :disabled do
+ context "when the user is a nurse" do
+ let(:user) { build(:nurse) }
+
+ it { should be(false) }
+ end
+
+ context "when the user is admin staff" do
+ let(:user) { build(:admin) }
+
+ it { should be(false) }
+ end
+
+ context "when the user is a prescriber" do
+ let(:user) { build(:prescriber) }
+
+ it { should be(true) }
+ end
+ end
+ end
+
describe "#is_superuser?" do
subject { user.is_superuser? }
From 5fefcb838dec4183e84ee3562505bf95c056132a Mon Sep 17 00:00:00 2001
From: Thomas Leese
Date: Fri, 22 Aug 2025 20:32:38 +0100
Subject: [PATCH 07/27] Update policies for healthcare assistances
This updates the programme and session policy to ensure that if a user
is a healthcare assistant they can only see sessions and programmes
which support delegation, which at the moment is limited to flu.
Jira-Issue: MAV-1353
---
app/policies/programme_policy.rb | 7 +++-
app/policies/session_policy.rb | 6 +++-
spec/policies/programme_policy_spec.rb | 32 +++++++++++++++++++
spec/policies/session_policy_spec.rb | 44 +++++++++++++++++++++++---
4 files changed, 83 insertions(+), 6 deletions(-)
create mode 100644 spec/policies/programme_policy_spec.rb
diff --git a/app/policies/programme_policy.rb b/app/policies/programme_policy.rb
index 90ca12d5a5..e6b7cb86a9 100644
--- a/app/policies/programme_policy.rb
+++ b/app/policies/programme_policy.rb
@@ -9,7 +9,12 @@ def consent_form? = show?
class Scope < ApplicationPolicy::Scope
def resolve
- scope.where(id: user.selected_team.programmes.ids)
+ scope
+ .joins(:team_programmes)
+ .where(team_programmes: { team: user.selected_team })
+ .then do |scope|
+ user.is_healthcare_assistant? ? scope.supports_delegation : scope
+ end
end
end
end
diff --git a/app/policies/session_policy.rb b/app/policies/session_policy.rb
index 3c2d9e2866..acfc8b073f 100644
--- a/app/policies/session_policy.rb
+++ b/app/policies/session_policy.rb
@@ -11,7 +11,11 @@ def make_in_progress? = edit?
class Scope < ApplicationPolicy::Scope
def resolve
- scope.where(team: user.selected_team)
+ scope
+ .where(team: user.selected_team)
+ .then do |scope|
+ user.is_healthcare_assistant? ? scope.supports_delegation : scope
+ end
end
end
end
diff --git a/spec/policies/programme_policy_spec.rb b/spec/policies/programme_policy_spec.rb
new file mode 100644
index 0000000000..6f36f2e663
--- /dev/null
+++ b/spec/policies/programme_policy_spec.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+describe ProgrammePolicy do
+ describe ProgrammePolicy::Scope do
+ describe "#resolve" do
+ subject { described_class.new(user, Programme).resolve }
+
+ let(:flu_programme) { create(:programme, :flu) }
+ let(:hpv_programme) { create(:programme, :hpv) }
+
+ let(:team) { create(:team, programmes: [flu_programme, hpv_programme]) }
+
+ context "with an admin user" do
+ let(:user) { create(:admin, team:) }
+
+ it { should contain_exactly(flu_programme, hpv_programme) }
+ end
+
+ context "with a nurse user" do
+ let(:user) { create(:nurse, team:) }
+
+ it { should contain_exactly(flu_programme, hpv_programme) }
+ end
+
+ context "with a healthcare assistant user" do
+ let(:user) { create(:healthcare_assistant, team:) }
+
+ it { should contain_exactly(flu_programme) }
+ end
+ end
+ end
+end
diff --git a/spec/policies/session_policy_spec.rb b/spec/policies/session_policy_spec.rb
index 83c9054f9b..d93fffd38c 100644
--- a/spec/policies/session_policy_spec.rb
+++ b/spec/policies/session_policy_spec.rb
@@ -69,15 +69,51 @@
describe "#resolve" do
subject { described_class.new(user, Session).resolve }
- let(:programmes) { [create(:programme)] }
- let(:team) { create(:team, programmes:) }
let(:user) { create(:user, team:) }
+ let!(:flu_programme) { create(:programme, :flu) }
+ let!(:hpv_programme) { create(:programme, :hpv) }
+
let(:users_teams_session) { create(:session, team:, programmes:) }
let(:another_teams_session) { create(:session, programmes:) }
- it { should include(users_teams_session) }
- it { should_not include(another_teams_session) }
+ let(:programmes) { [hpv_programme] }
+ let(:team) { create(:team, programmes:) }
+
+ context "with a session part of the user's teams" do
+ let(:session) { create(:session, team:, programmes:) }
+
+ context "and an admin user" do
+ let(:user) { create(:admin, team:) }
+
+ it { should include(session) }
+ end
+
+ context "and a nurse user" do
+ let(:user) { create(:nurse, team:) }
+
+ it { should include(session) }
+ end
+
+ context "and a healthcare assistant user" do
+ let(:user) { create(:healthcare_assistant, team:) }
+
+ it { should_not include(session) }
+
+ context "and a flu session" do
+ let(:programmes) { [flu_programme] }
+
+ it { should include(session) }
+ end
+ end
+ end
+
+ context "with a session not part of the user's teams" do
+ let(:session) { create(:session, programmes:) }
+ let(:user) { create(:user, team:) }
+
+ it { should_not include(session) }
+ end
end
end
end
From 26b9bbbaa5b9c5e5e63330af2107a9066151d990 Mon Sep 17 00:00:00 2001
From: Thomas Leese
Date: Mon, 25 Aug 2025 17:45:25 +0100
Subject: [PATCH 08/27] Add `Session#vaccine_methods_for`
This adds a method which can be used to determine which vaccine methods
are approriate for a user in a particular session. This will be used to
limit which patients are shown to which users.
Jira-Issue: MAV-1353
---
app/models/session.rb | 12 +++++++++++-
spec/models/session_spec.rb | 26 ++++++++++++++++++++++++++
2 files changed, 37 insertions(+), 1 deletion(-)
diff --git a/app/models/session.rb b/app/models/session.rb
index 765452bed1..9f907c491f 100644
--- a/app/models/session.rb
+++ b/app/models/session.rb
@@ -205,7 +205,7 @@ def year_groups
end
def vaccine_methods
- programmes.flat_map(&:vaccine_methods).uniq.sort
+ @vaccine_methods ||= programmes.flat_map(&:vaccine_methods).uniq.sort
end
def programmes_for(year_group: nil, patient: nil, academic_year: nil)
@@ -218,6 +218,16 @@ def programmes_for(year_group: nil, patient: nil, academic_year: nil)
end
end
+ def vaccine_methods_for(user:)
+ if user.is_nurse?
+ vaccine_methods
+ elsif user.is_healthcare_assistant? && pgd_supply_enabled?
+ %w[nasal]
+ else
+ []
+ end
+ end
+
def dates
session_dates.map(&:value).compact
end
diff --git a/spec/models/session_spec.rb b/spec/models/session_spec.rb
index e4cf9925b0..18ea1316ce 100644
--- a/spec/models/session_spec.rb
+++ b/spec/models/session_spec.rb
@@ -296,6 +296,32 @@
it { should contain_exactly("injection", "nasal") }
end
+ describe "#vaccine_methods_for" do
+ subject { session.vaccine_methods_for(user:) }
+
+ let(:programmes) { [create(:programme, :flu)] }
+
+ let(:session) { create(:session, programmes:) }
+
+ context "with a nurse" do
+ let(:user) { create(:nurse) }
+
+ it { should match_array(%w[nasal injection]) }
+ end
+
+ context "with a healthcare assistant" do
+ let(:user) { create(:healthcare_assistant) }
+
+ it { should eq(%w[nasal]) }
+ end
+
+ context "with an admin staff" do
+ let(:user) { create(:admin) }
+
+ it { should be_empty }
+ end
+ end
+
describe "#today_or_future_dates" do
subject(:today_or_future_dates) do
travel_to(today) { session.today_or_future_dates }
From 75a35ae6d7438607ca8482095964f105daa7c91e Mon Sep 17 00:00:00 2001
From: Thomas Leese
Date: Mon, 25 Aug 2025 19:56:41 +0100
Subject: [PATCH 09/27] Don't render vaccination section heading
If the user is not allowed to perform a vaccination we should hide the
heading as well as the form since it doesn't make sense to show the
heading.
Jira-Issue: MAV-1353
---
app/components/app_patient_session_record_component.rb | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/app/components/app_patient_session_record_component.rb b/app/components/app_patient_session_record_component.rb
index 6322d761d1..e3afd26804 100644
--- a/app/components/app_patient_session_record_component.rb
+++ b/app/components/app_patient_session_record_component.rb
@@ -2,9 +2,8 @@
class AppPatientSessionRecordComponent < ViewComponent::Base
erb_template <<-ERB
-
<%= heading %>
-
<% if helpers.policy(VaccinationRecord).new? %>
+
<%= heading %>
<%= render AppVaccinateFormComponent.new(vaccinate_form) %>
<% end %>
ERB
From 63df0c890030dd3b04db35b65f25efcdc511ae0f Mon Sep 17 00:00:00 2001
From: Thomas Leese
Date: Mon, 25 Aug 2025 19:56:57 +0100
Subject: [PATCH 10/27] Refactor recording already vaccinated
This moves the method related to determining whether the user is able to
take this action in to the policy method, closer to where it would be
used.
Jira-Issue: MAV-1353
---
.../patient_sessions/programmes_controller.rb | 10 +++++-----
app/models/patient_session.rb | 5 -----
app/policies/vaccination_record_policy.rb | 8 ++++++++
app/views/patient_sessions/_header.html.erb | 2 +-
4 files changed, 14 insertions(+), 11 deletions(-)
diff --git a/app/controllers/patient_sessions/programmes_controller.rb b/app/controllers/patient_sessions/programmes_controller.rb
index 75f617552c..669b2e9278 100644
--- a/app/controllers/patient_sessions/programmes_controller.rb
+++ b/app/controllers/patient_sessions/programmes_controller.rb
@@ -8,11 +8,11 @@ def show
end
def record_already_vaccinated
- unless @patient_session.can_record_as_already_vaccinated?(
- programme: @programme
- )
- redirect_to session_patient_path and return
- end
+ authorize VaccinationRecord.new(
+ patient: @patient,
+ session: @session,
+ programme: @programme
+ )
draft_vaccination_record =
DraftVaccinationRecord.new(request_session: session, current_user:)
diff --git a/app/models/patient_session.rb b/app/models/patient_session.rb
index 15f6339b2f..336f7aa464 100644
--- a/app/models/patient_session.rb
+++ b/app/models/patient_session.rb
@@ -277,11 +277,6 @@ def destroy_if_safe!
destroy! if safe_to_destroy?
end
- def can_record_as_already_vaccinated?(programme:)
- !session.today? &&
- patient.vaccination_status(programme:, academic_year:).none_yet?
- end
-
def programmes = session.programmes_for(patient:, academic_year:)
def todays_attendance
diff --git a/app/policies/vaccination_record_policy.rb b/app/policies/vaccination_record_policy.rb
index 8148875788..d032c924ed 100644
--- a/app/policies/vaccination_record_policy.rb
+++ b/app/policies/vaccination_record_policy.rb
@@ -9,6 +9,11 @@ def new?
create?
end
+ def record_already_vaccinated?
+ user.is_nurse? && !session.today? &&
+ patient.vaccination_status(programme:, academic_year:).none_yet?
+ end
+
def edit?
user.is_nurse? && record.session_id.present? &&
record.performed_ods_code == user.selected_organisation.ods_code
@@ -22,6 +27,9 @@ def destroy?
user.is_superuser?
end
+ delegate :patient, :session, :programme, to: :record
+ delegate :academic_year, to: :session
+
class Scope < ApplicationPolicy::Scope
def resolve
organisation = user.selected_organisation
diff --git a/app/views/patient_sessions/_header.html.erb b/app/views/patient_sessions/_header.html.erb
index 0cc87b922a..96a191679e 100644
--- a/app/views/patient_sessions/_header.html.erb
+++ b/app/views/patient_sessions/_header.html.erb
@@ -39,7 +39,7 @@
<% end %>
<% end %>
- <% if policy(VaccinationRecord).create? && @programme && @patient_session.can_record_as_already_vaccinated?(programme: @programme) %>
+ <% if @programme && policy(VaccinationRecord.new(patient: @patient, session: @session, programme: @programme)).record_already_vaccinated? %>
<%= link_to "Record as already vaccinated",
session_patient_programme_record_already_vaccinated_path(@session, @patient, @programme) %>
From 908754374ab7afd055f0bfd31fb5e867a2d2973c Mon Sep 17 00:00:00 2001
From: Thomas Leese
Date: Mon, 25 Aug 2025 17:11:29 +0100
Subject: [PATCH 11/27] Pass VaccinationRecord instance to policy
This allows us to check various properties on the vaccination record to
support delegation where only certain users will be able to perform
certain actions.
Jira-Issue: MAV-1353
---
app/components/app_patient_session_record_component.rb | 6 +++++-
app/controllers/draft_vaccination_records_controller.rb | 7 ++++++-
.../patient_sessions/vaccinations_controller.rb | 6 +++++-
app/models/draft_vaccination_record.rb | 6 +++++-
app/views/patient_sessions/_header.html.erb | 8 ++++++--
5 files changed, 27 insertions(+), 6 deletions(-)
diff --git a/app/components/app_patient_session_record_component.rb b/app/components/app_patient_session_record_component.rb
index e3afd26804..9967d9b9c3 100644
--- a/app/components/app_patient_session_record_component.rb
+++ b/app/components/app_patient_session_record_component.rb
@@ -2,7 +2,7 @@
class AppPatientSessionRecordComponent < ViewComponent::Base
erb_template <<-ERB
- <% if helpers.policy(VaccinationRecord).new? %>
+ <% if helpers.policy(vaccination_record).new? %>
<%= heading %>
<%= render AppVaccinateFormComponent.new(vaccinate_form) %>
<% end %>
@@ -32,6 +32,10 @@ def render?
delegate :patient, :session, to: :patient_session
delegate :academic_year, to: :session
+ def vaccination_record
+ VaccinationRecord.new(patient:, session:, programme:)
+ end
+
def default_vaccinate_form
pre_screening_confirmed = patient.pre_screenings.today.exists?(programme:)
session_date = session.session_dates.today.first
diff --git a/app/controllers/draft_vaccination_records_controller.rb b/app/controllers/draft_vaccination_records_controller.rb
index f6f14ff349..ffdee3518b 100644
--- a/app/controllers/draft_vaccination_records_controller.rb
+++ b/app/controllers/draft_vaccination_records_controller.rb
@@ -193,7 +193,12 @@ def set_programme
def set_vaccination_record
@vaccination_record =
- @draft_vaccination_record.vaccination_record || VaccinationRecord.new
+ @draft_vaccination_record.vaccination_record ||
+ VaccinationRecord.new(
+ patient: @patient,
+ session: @session,
+ programme: @programme
+ )
end
def set_steps
diff --git a/app/controllers/patient_sessions/vaccinations_controller.rb b/app/controllers/patient_sessions/vaccinations_controller.rb
index 72a9b2268c..03b54e9750 100644
--- a/app/controllers/patient_sessions/vaccinations_controller.rb
+++ b/app/controllers/patient_sessions/vaccinations_controller.rb
@@ -10,7 +10,11 @@ class PatientSessions::VaccinationsController < PatientSessions::BaseController
after_action :verify_authorized
def create
- authorize VaccinationRecord
+ authorize VaccinationRecord.new(
+ patient: @patient,
+ session: @session,
+ programme: @programme
+ )
draft_vaccination_record =
DraftVaccinationRecord.new(request_session: session, current_user:)
diff --git a/app/models/draft_vaccination_record.rb b/app/models/draft_vaccination_record.rb
index 5be3d2e840..fefba2efc4 100644
--- a/app/models/draft_vaccination_record.rb
+++ b/app/models/draft_vaccination_record.rb
@@ -187,7 +187,11 @@ def programme=(value)
def session
return nil if session_id.nil?
- SessionPolicy::Scope.new(@current_user, Session).resolve.find(session_id)
+ SessionPolicy::Scope
+ .new(@current_user, Session)
+ .resolve
+ .includes(:programmes)
+ .find(session_id)
end
def session=(value)
diff --git a/app/views/patient_sessions/_header.html.erb b/app/views/patient_sessions/_header.html.erb
index 96a191679e..471c6ba2c5 100644
--- a/app/views/patient_sessions/_header.html.erb
+++ b/app/views/patient_sessions/_header.html.erb
@@ -9,9 +9,13 @@
].compact) %>
<% end %>
-<% if policy(VaccinationRecord).new? && (outstanding_programmes = @patient_session.outstanding_programmes).any? %>
+<% if (outstanding_programmes = @patient_session.outstanding_programmes).any? %>
+ <% programmes_can_record_vaccination = outstanding_programmes.filter do |programme|
+ policy(VaccinationRecord.new(patient: @patient, session: @session, programme:)).new?
+ end %>
+
<%= govuk_notification_banner(title_text: "Important") do |notification_banner| %>
- <% notification_banner.with_heading(text: "You still need to record an outcome for #{outstanding_programmes.map(&:name).to_sentence}.") %>
+ <% notification_banner.with_heading(text: "You still need to record an outcome for #{programmes_can_record_vaccination.map(&:name).to_sentence}.") %>
<% end %>
<% end %>
From 6e4bc0f45b43356b8d38fb903bdcfb2c658823ce Mon Sep 17 00:00:00 2001
From: Thomas Leese
Date: Mon, 25 Aug 2025 19:01:55 +0100
Subject: [PATCH 12/27] Restrict patients by user
This makes it so that healthcare assistants don't have permission to
record vaccinations against injection patients by hiding them from the
"Record" tab.
Jira-Issue: MAV-1353
---
app/controllers/sessions/record_controller.rb | 16 ++++
app/policies/vaccination_record_policy.rb | 20 ++--
app/views/sessions/record/show.html.erb | 2 +-
config/initializers/inflections.rb | 1 +
.../flu_vaccination_hca_pgd_supply_spec.rb | 93 +++++++++++++++++++
5 files changed, 120 insertions(+), 12 deletions(-)
create mode 100644 spec/features/flu_vaccination_hca_pgd_supply_spec.rb
diff --git a/app/controllers/sessions/record_controller.rb b/app/controllers/sessions/record_controller.rb
index ffbd77d3c7..de56fff23b 100644
--- a/app/controllers/sessions/record_controller.rb
+++ b/app/controllers/sessions/record_controller.rb
@@ -25,6 +25,22 @@ def show
scope = scope.has_registration_status(%w[attending completed])
end
+ @vaccine_methods = @session.vaccine_methods_for(user: current_user)
+
+ if @vaccine_methods != @session.vaccine_methods
+ scope =
+ if @vaccine_methods.empty?
+ scope.none
+ else
+ @vaccine_methods.reduce(scope) do |accumulator, vaccine_method|
+ accumulator.has_vaccine_method(
+ vaccine_method,
+ programme: @session.programmes
+ )
+ end
+ end
+ end
+
patient_sessions =
@form.apply(scope).consent_given_and_ready_to_vaccinate(
programmes: @form.programmes,
diff --git a/app/policies/vaccination_record_policy.rb b/app/policies/vaccination_record_policy.rb
index d032c924ed..ebe1c66d53 100644
--- a/app/policies/vaccination_record_policy.rb
+++ b/app/policies/vaccination_record_policy.rb
@@ -2,12 +2,14 @@
class VaccinationRecordPolicy < ApplicationPolicy
def create?
- user.is_nurse?
+ user.is_nurse? ||
+ (
+ patient.approved_vaccine_methods(programme:, academic_year:) &
+ session.vaccine_methods_for(user:)
+ ).present?
end
- def new?
- create?
- end
+ def new? = create?
def record_already_vaccinated?
user.is_nurse? && !session.today? &&
@@ -15,17 +17,13 @@ def record_already_vaccinated?
end
def edit?
- user.is_nurse? && record.session_id.present? &&
+ user.is_nurse? && record.recorded_in_service? &&
record.performed_ods_code == user.selected_organisation.ods_code
end
- def update?
- edit?
- end
+ def update? = edit?
- def destroy?
- user.is_superuser?
- end
+ def destroy? = user.is_superuser?
delegate :patient, :session, :programme, to: :record
delegate :academic_year, to: :session
diff --git a/app/views/sessions/record/show.html.erb b/app/views/sessions/record/show.html.erb
index e72878d8be..22696722e1 100644
--- a/app/views/sessions/record/show.html.erb
+++ b/app/views/sessions/record/show.html.erb
@@ -45,7 +45,7 @@
url: session_record_path(@session),
programmes: @session.programmes,
year_groups: @session.year_groups,
- vaccine_methods: @session.vaccine_methods.then { it.length > 1 ? it : [] },
+ vaccine_methods: @vaccine_methods.length > 1 ? @vaccine_methods : [],
) %>
diff --git a/config/initializers/inflections.rb b/config/initializers/inflections.rb
index fe502d3e48..f0899e9a52 100644
--- a/config/initializers/inflections.rb
+++ b/config/initializers/inflections.rb
@@ -23,6 +23,7 @@
inflect.acronym "FHIR"
inflect.acronym "GIAS"
inflect.acronym "GP"
+ inflect.acronym "HCA"
inflect.acronym "JWKS"
inflect.acronym "NHS"
inflect.acronym "OAuth2"
diff --git a/spec/features/flu_vaccination_hca_pgd_supply_spec.rb b/spec/features/flu_vaccination_hca_pgd_supply_spec.rb
new file mode 100644
index 0000000000..38755a9891
--- /dev/null
+++ b/spec/features/flu_vaccination_hca_pgd_supply_spec.rb
@@ -0,0 +1,93 @@
+# frozen_string_literal: true
+
+describe "Flu vaccination" do
+ around { |example| travel_to(Time.zone.local(2024, 10, 1)) { example.run } }
+
+ scenario "Administered by HCA" do
+ given_a_session_exists
+ and_patients_exist
+
+ when_i_visit_the_session_record_tab
+ then_i_only_see_nasal_spray_patients
+
+ when_i_click_on_the_nasal_only_patient
+ then_i_am_able_to_vaccinate_them
+
+ when_i_click_on_the_nasal_and_injection_patient
+ then_i_am_able_to_vaccinate_them
+
+ when_i_click_on_the_injection_patient
+ then_i_am_not_able_to_vaccinate_them
+ end
+
+ def given_a_session_exists
+ programmes = [create(:programme, :flu)]
+
+ @team = create(:team, programmes:)
+ @user = create(:healthcare_assistant, team: @team)
+
+ @session =
+ create(
+ :session,
+ :today,
+ :requires_no_registration,
+ team: @team,
+ programmes:
+ )
+ end
+
+ def and_patients_exist
+ @patient_nasal_only =
+ create(
+ :patient,
+ :consent_given_nasal_only_triage_not_needed,
+ session: @session
+ )
+ @patient_nasal_and_injection =
+ create(
+ :patient,
+ :consent_given_nasal_or_injection_triage_not_needed,
+ session: @session
+ )
+ @patient_injection_only =
+ create(
+ :patient,
+ :consent_given_injection_only_triage_not_needed,
+ session: @session
+ )
+ end
+
+ def when_i_visit_the_session_record_tab
+ sign_in @user, role: :healthcare_assistant
+ visit session_record_path(@session)
+ end
+
+ def then_i_only_see_nasal_spray_patients
+ expect(page).to have_content(@patient_nasal_only.full_name)
+ expect(page).to have_content(@patient_nasal_and_injection.full_name)
+ expect(page).not_to have_content(@patient_injection_only.full_name)
+ end
+
+ def when_i_click_on_the_nasal_only_patient
+ click_on @patient_nasal_only.full_name
+ end
+
+ def when_i_click_on_the_nasal_and_injection_patient
+ click_on @patient_nasal_and_injection.full_name
+ end
+
+ def when_i_click_on_the_injection_patient
+ # This patient won't be in the "Record" tab.
+ within(".app-secondary-navigation") { click_on "Children" }
+ click_on @patient_injection_only.full_name
+ end
+
+ def then_i_am_able_to_vaccinate_them
+ click_on "Record vaccinations"
+ end
+
+ def then_i_am_not_able_to_vaccinate_them
+ expect(page).not_to have_content("Pre-screening checks")
+ expect(page).not_to have_content("ready for their")
+ end
+end
From cbc21e74d8e28940b8cabf9baaae179426f063ed Mon Sep 17 00:00:00 2001
From: Thomas Leese
Date: Mon, 25 Aug 2025 20:38:24 +0100
Subject: [PATCH 13/27] Add supplier user to vaccination records
This allows us to track who supplied the vaccine to the person who
performed the vaccination.
Jira-Issue: MAV-1353
---
app/models/vaccination_record.rb | 10 +++++++++-
...232_add_supplied_by_user_to_vaccination_records.rb | 11 +++++++++++
db/schema.rb | 3 +++
spec/factories/vaccination_records.rb | 3 +++
spec/models/vaccination_record_spec.rb | 4 ++++
5 files changed, 30 insertions(+), 1 deletion(-)
create mode 100644 db/migrate/20250825183232_add_supplied_by_user_to_vaccination_records.rb
diff --git a/app/models/vaccination_record.rb b/app/models/vaccination_record.rb
index beae07c967..bb38288ba4 100644
--- a/app/models/vaccination_record.rb
+++ b/app/models/vaccination_record.rb
@@ -34,6 +34,7 @@
# performed_by_user_id :bigint
# programme_id :bigint not null
# session_id :bigint
+# supplied_by_user_id :bigint
# vaccine_id :bigint
#
# Indexes
@@ -46,6 +47,7 @@
# index_vaccination_records_on_performed_by_user_id (performed_by_user_id)
# index_vaccination_records_on_programme_id (programme_id)
# index_vaccination_records_on_session_id (session_id)
+# index_vaccination_records_on_supplied_by_user_id (supplied_by_user_id)
# index_vaccination_records_on_uuid (uuid) UNIQUE
# index_vaccination_records_on_vaccine_id (vaccine_id)
#
@@ -56,6 +58,7 @@
# fk_rails_... (performed_by_user_id => users.id)
# fk_rails_... (programme_id => programmes.id)
# fk_rails_... (session_id => sessions.id)
+# fk_rails_... (supplied_by_user_id => users.id)
# fk_rails_... (vaccine_id => vaccines.id)
#
class VaccinationRecord < ApplicationRecord
@@ -87,9 +90,14 @@ class VaccinationRecord < ApplicationRecord
belongs_to :batch, optional: true
belongs_to :vaccine, optional: true
- belongs_to :performed_by_user, class_name: "User", optional: true
belongs_to :programme
+ belongs_to :performed_by_user, class_name: "User", optional: true
+ belongs_to :supplied_by,
+ class_name: "User",
+ foreign_key: :supplied_by_user_id,
+ optional: true
+
has_and_belongs_to_many :immunisation_imports
belongs_to :location, optional: true
diff --git a/db/migrate/20250825183232_add_supplied_by_user_to_vaccination_records.rb b/db/migrate/20250825183232_add_supplied_by_user_to_vaccination_records.rb
new file mode 100644
index 0000000000..cbcc185e83
--- /dev/null
+++ b/db/migrate/20250825183232_add_supplied_by_user_to_vaccination_records.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+class AddSuppliedByUserToVaccinationRecords < ActiveRecord::Migration[8.0]
+ def change
+ add_reference :vaccination_records,
+ :supplied_by_user,
+ foreign_key: {
+ to_table: :users
+ }
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 6bdbf8bda5..26d6f8ea3d 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -951,6 +951,7 @@
t.datetime "nhs_immunisations_api_sync_pending_at"
t.boolean "notify_parents"
t.bigint "location_id"
+ t.bigint "supplied_by_user_id"
t.index ["batch_id"], name: "index_vaccination_records_on_batch_id"
t.index ["discarded_at"], name: "index_vaccination_records_on_discarded_at"
t.index ["location_id"], name: "index_vaccination_records_on_location_id"
@@ -959,6 +960,7 @@
t.index ["performed_by_user_id"], name: "index_vaccination_records_on_performed_by_user_id"
t.index ["programme_id"], name: "index_vaccination_records_on_programme_id"
t.index ["session_id"], name: "index_vaccination_records_on_session_id"
+ t.index ["supplied_by_user_id"], name: "index_vaccination_records_on_supplied_by_user_id"
t.index ["uuid"], name: "index_vaccination_records_on_uuid", unique: true
t.index ["vaccine_id"], name: "index_vaccination_records_on_vaccine_id"
end
@@ -1106,6 +1108,7 @@
add_foreign_key "vaccination_records", "programmes"
add_foreign_key "vaccination_records", "sessions"
add_foreign_key "vaccination_records", "users", column: "performed_by_user_id"
+ add_foreign_key "vaccination_records", "users", column: "supplied_by_user_id"
add_foreign_key "vaccination_records", "vaccines"
add_foreign_key "vaccines", "programmes"
end
diff --git a/spec/factories/vaccination_records.rb b/spec/factories/vaccination_records.rb
index 69e37ad9dd..b5ef753a34 100644
--- a/spec/factories/vaccination_records.rb
+++ b/spec/factories/vaccination_records.rb
@@ -34,6 +34,7 @@
# performed_by_user_id :bigint
# programme_id :bigint not null
# session_id :bigint
+# supplied_by_user_id :bigint
# vaccine_id :bigint
#
# Indexes
@@ -46,6 +47,7 @@
# index_vaccination_records_on_performed_by_user_id (performed_by_user_id)
# index_vaccination_records_on_programme_id (programme_id)
# index_vaccination_records_on_session_id (session_id)
+# index_vaccination_records_on_supplied_by_user_id (supplied_by_user_id)
# index_vaccination_records_on_uuid (uuid) UNIQUE
# index_vaccination_records_on_vaccine_id (vaccine_id)
#
@@ -56,6 +58,7 @@
# fk_rails_... (performed_by_user_id => users.id)
# fk_rails_... (programme_id => programmes.id)
# fk_rails_... (session_id => sessions.id)
+# fk_rails_... (supplied_by_user_id => users.id)
# fk_rails_... (vaccine_id => vaccines.id)
#
FactoryBot.define do
diff --git a/spec/models/vaccination_record_spec.rb b/spec/models/vaccination_record_spec.rb
index 2774f7a723..16baeb88ab 100644
--- a/spec/models/vaccination_record_spec.rb
+++ b/spec/models/vaccination_record_spec.rb
@@ -34,6 +34,7 @@
# performed_by_user_id :bigint
# programme_id :bigint not null
# session_id :bigint
+# supplied_by_user_id :bigint
# vaccine_id :bigint
#
# Indexes
@@ -46,6 +47,7 @@
# index_vaccination_records_on_performed_by_user_id (performed_by_user_id)
# index_vaccination_records_on_programme_id (programme_id)
# index_vaccination_records_on_session_id (session_id)
+# index_vaccination_records_on_supplied_by_user_id (supplied_by_user_id)
# index_vaccination_records_on_uuid (uuid) UNIQUE
# index_vaccination_records_on_vaccine_id (vaccine_id)
#
@@ -56,6 +58,7 @@
# fk_rails_... (performed_by_user_id => users.id)
# fk_rails_... (programme_id => programmes.id)
# fk_rails_... (session_id => sessions.id)
+# fk_rails_... (supplied_by_user_id => users.id)
# fk_rails_... (vaccine_id => vaccines.id)
#
describe VaccinationRecord do
@@ -63,6 +66,7 @@
describe "associations" do
it { should have_one(:identity_check).autosave(true).dependent(:destroy) }
+ it { should belong_to(:supplied_by).optional }
end
describe "validations" do
From 8ea39821f372abd83daa6cd92e1ac9664ac2acaf Mon Sep 17 00:00:00 2001
From: John Henderson
Date: Wed, 27 Aug 2025 10:16:14 +0100
Subject: [PATCH 14/27] Remove column PatientSpecificDirection#full_dose
We don't need this column because it's already record on the
vaccination record. It also simplifies the process of adding a
PSD since PSDs will be used only for nasal flu which don't have
the concept of dose.
---
app/models/patient_specific_direction.rb | 3 ---
...12_remove_full_dose_from_patient_specific_directions.rb | 7 +++++++
db/schema.rb | 3 +--
spec/factories/patient_specific_directions.rb | 6 ------
spec/models/patient_specific_direction_spec.rb | 3 ---
5 files changed, 8 insertions(+), 14 deletions(-)
create mode 100644 db/migrate/20250827091512_remove_full_dose_from_patient_specific_directions.rb
diff --git a/app/models/patient_specific_direction.rb b/app/models/patient_specific_direction.rb
index 571be4e7ef..e121f0e805 100644
--- a/app/models/patient_specific_direction.rb
+++ b/app/models/patient_specific_direction.rb
@@ -7,7 +7,6 @@
# id :bigint not null, primary key
# academic_year :integer not null
# delivery_site :integer not null
-# full_dose :boolean not null
# vaccine_method :integer not null
# created_at :datetime not null
# updated_at :datetime not null
@@ -39,8 +38,6 @@ class PatientSpecificDirection < ApplicationRecord
belongs_to :programme
belongs_to :vaccine
- validates :full_dose, inclusion: { in: [true, false] }
-
enum :delivery_site,
{
left_arm_upper_position: 2,
diff --git a/db/migrate/20250827091512_remove_full_dose_from_patient_specific_directions.rb b/db/migrate/20250827091512_remove_full_dose_from_patient_specific_directions.rb
new file mode 100644
index 0000000000..f3b88e2e54
--- /dev/null
+++ b/db/migrate/20250827091512_remove_full_dose_from_patient_specific_directions.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+class RemoveFullDoseFromPatientSpecificDirections < ActiveRecord::Migration[8.0]
+ def change
+ remove_column :patient_specific_directions, :full_dose, :boolean
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 26d6f8ea3d..218947573c 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema[8.0].define(version: 2025_08_26_135132) do
+ActiveRecord::Schema[8.0].define(version: 2025_08_27_091512) do
# These are extensions that must be enabled in order to support this database
enable_extension "pg_catalog.plpgsql"
enable_extension "pg_trgm"
@@ -641,7 +641,6 @@
t.bigint "vaccine_id", null: false
t.integer "vaccine_method", null: false
t.integer "delivery_site", null: false
- t.boolean "full_dose", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.integer "academic_year", null: false
diff --git a/spec/factories/patient_specific_directions.rb b/spec/factories/patient_specific_directions.rb
index e0b73a6c82..b1db4560e9 100644
--- a/spec/factories/patient_specific_directions.rb
+++ b/spec/factories/patient_specific_directions.rb
@@ -7,7 +7,6 @@
# id :bigint not null, primary key
# academic_year :integer not null
# delivery_site :integer not null
-# full_dose :boolean not null
# vaccine_method :integer not null
# created_at :datetime not null
# updated_at :datetime not null
@@ -40,11 +39,6 @@
delivery_site { "left_arm_upper_position" }
vaccine_method { "injection" }
- full_dose { true }
academic_year { Time.current.to_date.academic_year }
-
- trait :half_dose do
- full_dose { false }
- end
end
end
diff --git a/spec/models/patient_specific_direction_spec.rb b/spec/models/patient_specific_direction_spec.rb
index 861f94957c..1ee1fc1dff 100644
--- a/spec/models/patient_specific_direction_spec.rb
+++ b/spec/models/patient_specific_direction_spec.rb
@@ -7,7 +7,6 @@
# id :bigint not null, primary key
# academic_year :integer not null
# delivery_site :integer not null
-# full_dose :boolean not null
# vaccine_method :integer not null
# created_at :datetime not null
# updated_at :datetime not null
@@ -70,7 +69,5 @@
:vaccine_method
).in_array(%w[injection nasal])
end
-
- it { should allow_values(true, false).for(:full_dose) }
end
end
From 031c32b37fe3b4387a7c351e021b31b522545022 Mon Sep 17 00:00:00 2001
From: John Henderson
Date: Wed, 20 Aug 2025 12:02:07 +0100
Subject: [PATCH 15/27] Add new policy PatientSpecificDirectionPolicy
This will be used by the UI to determine if the user is allowed
to prescribe PSDs.
---
.../patient_specific_direction_policy.rb | 7 ++++++
.../patient_specific_direction_policy_spec.rb | 25 +++++++++++++++++++
2 files changed, 32 insertions(+)
create mode 100644 app/policies/patient_specific_direction_policy.rb
create mode 100644 spec/policies/patient_specific_direction_policy_spec.rb
diff --git a/app/policies/patient_specific_direction_policy.rb b/app/policies/patient_specific_direction_policy.rb
new file mode 100644
index 0000000000..a6071735c8
--- /dev/null
+++ b/app/policies/patient_specific_direction_policy.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+class PatientSpecificDirectionPolicy < ApplicationPolicy
+ def create?
+ user.is_nurse?
+ end
+end
diff --git a/spec/policies/patient_specific_direction_policy_spec.rb b/spec/policies/patient_specific_direction_policy_spec.rb
new file mode 100644
index 0000000000..ef3680203e
--- /dev/null
+++ b/spec/policies/patient_specific_direction_policy_spec.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+RSpec.describe PatientSpecificDirectionPolicy do
+ subject(:policy) { described_class.new(user, PatientSpecificDirection) }
+
+ context "cis2 is disabled", cis2: :disabled do
+ describe "#create?" do
+ context "when user is a nurse" do
+ let(:user) { build(:user, :nurse) }
+
+ it "permits creation" do
+ expect(policy.create?).to be(true)
+ end
+ end
+
+ context "when user is not a nurse" do
+ let(:user) { build(:user, :healthcare_assistant) }
+
+ it "denies creation" do
+ expect(policy.create?).to be(false)
+ end
+ end
+ end
+ end
+end
From 0274a91a208ea5694120c11268f1f0db75a0f561 Mon Sep 17 00:00:00 2001
From: John Henderson
Date: Wed, 20 Aug 2025 16:37:48 +0100
Subject: [PATCH 16/27] Change patient_specific_direction factory to use nasal
PSDs are only going to be used for nasal vaccines for the foreseeable
future. Therefore, it makes sense to have the factory generate PSDs for
nasal.
---
spec/factories/patient_specific_directions.rb | 8 ++++++--
1 file changed, 6 insertions(+), 2 deletions(-)
diff --git a/spec/factories/patient_specific_directions.rb b/spec/factories/patient_specific_directions.rb
index b1db4560e9..a95d663555 100644
--- a/spec/factories/patient_specific_directions.rb
+++ b/spec/factories/patient_specific_directions.rb
@@ -37,8 +37,12 @@
programme
vaccine { programme.vaccines.sample || association(:vaccine) }
- delivery_site { "left_arm_upper_position" }
- vaccine_method { "injection" }
+ delivery_site { "nose" }
+ vaccine_method { "nasal" }
academic_year { Time.current.to_date.academic_year }
+
+ trait :half_dose do
+ full_dose { false }
+ end
end
end
From 51e9dfb76757eeea95bbc5e2c42b8af7d196f3c8 Mon Sep 17 00:00:00 2001
From: Thomas Leese
Date: Tue, 26 Aug 2025 15:32:12 +0100
Subject: [PATCH 17/27] Allow HCAs to administer nasal flu
This adds support for healthcase assistants to administer the nasal flu
vaccine, requiring them to specify which nurse supplied to them the
vaccine.
Jira-Issue: MAV-1353
---
.../app_patient_session_record_component.rb | 6 ++--
.../app_vaccinate_form_component.html.erb | 16 +++++++++-
.../app_vaccinate_form_component.rb | 8 ++---
...pp_vaccination_record_summary_component.rb | 21 +++++++++++++
.../draft_vaccination_records_controller.rb | 8 ++++-
.../vaccinations_controller.rb | 6 ++--
app/forms/vaccinate_form.rb | 21 +++++++++++--
app/models/draft_vaccination_record.rb | 23 ++++++++++++--
app/models/user.rb | 2 ++
app/policies/vaccination_record_policy.rb | 3 +-
.../confirm.html.erb | 1 +
.../supplier.html.erb | 22 ++++++++++++++
.../patient_sessions/programmes/show.html.erb | 2 +-
config/locales/en.yml | 3 ++
...p_patient_session_record_component_spec.rb | 2 ++
.../app_vaccinate_form_component_spec.rb | 9 ++++--
spec/factories/users.rb | 8 +++--
.../flu_vaccination_hca_pgd_supply_spec.rb | 30 ++++++++++++++++++-
spec/forms/vaccinate_form_spec.rb | 6 ++--
19 files changed, 173 insertions(+), 24 deletions(-)
create mode 100644 app/views/draft_vaccination_records/supplier.html.erb
diff --git a/app/components/app_patient_session_record_component.rb b/app/components/app_patient_session_record_component.rb
index 9967d9b9c3..2a39ecc89f 100644
--- a/app/components/app_patient_session_record_component.rb
+++ b/app/components/app_patient_session_record_component.rb
@@ -8,11 +8,12 @@ class AppPatientSessionRecordComponent < ViewComponent::Base
<% end %>
ERB
- def initialize(patient_session, programme:, vaccinate_form: nil)
+ def initialize(patient_session, programme:, current_user:, vaccinate_form:)
super
@patient_session = patient_session
@programme = programme
+ @current_user = current_user
@vaccinate_form = vaccinate_form || default_vaccinate_form
end
@@ -27,7 +28,7 @@ def render?
private
- attr_reader :patient_session, :programme, :vaccinate_form
+ attr_reader :patient_session, :current_user, :programme, :vaccinate_form
delegate :patient, :session, to: :patient_session
delegate :academic_year, to: :session
@@ -41,6 +42,7 @@ def default_vaccinate_form
session_date = session.session_dates.today.first
VaccinateForm.new(
+ current_user:,
patient:,
session_date:,
programme:,
diff --git a/app/components/app_vaccinate_form_component.html.erb b/app/components/app_vaccinate_form_component.html.erb
index ebbdd14b73..afd935956a 100644
--- a/app/components/app_vaccinate_form_component.html.erb
+++ b/app/components/app_vaccinate_form_component.html.erb
@@ -1,5 +1,5 @@
<%= form_with(
- model: vaccinate_form,
+ model: form,
url:,
method: :post,
class: "nhsuk-card",
@@ -54,6 +54,20 @@
<% end %>
<%= f.govuk_text_area :pre_screening_notes, label: { text: "Pre-screening notes (optional)" }, rows: 3 %>
+
+ <% if form.requires_supplied_by_user_id? %>
+ <%= f.govuk_select :supplied_by_user_id,
+ label: { text: "Which nurse identified and pre-screened the child and supplied the vaccine?" },
+ data: { module: "autocomplete" } do %>
+ <%= tag.option "", value: "" %>
+ <% form.supplied_by_users.each do |user| %>
+ <%= tag.option user.full_name,
+ value: user.id,
+ selected: user.id == form.supplied_by_user_id,
+ data: { hint: user.email } %>
+ <% end %>
+ <% end %>
+ <% end %>
diff --git a/app/components/app_vaccinate_form_component.rb b/app/components/app_vaccinate_form_component.rb
index 06dc19a26c..f5e0a46287 100644
--- a/app/components/app_vaccinate_form_component.rb
+++ b/app/components/app_vaccinate_form_component.rb
@@ -1,17 +1,17 @@
# frozen_string_literal: true
class AppVaccinateFormComponent < ViewComponent::Base
- def initialize(vaccinate_form)
+ def initialize(form)
super
- @vaccinate_form = vaccinate_form
+ @form = form
end
private
- attr_reader :vaccinate_form
+ attr_reader :form
- delegate :patient, :session, :programme, to: :vaccinate_form
+ delegate :patient, :session, :programme, to: :form
delegate :academic_year, to: :session
def url
diff --git a/app/components/app_vaccination_record_summary_component.rb b/app/components/app_vaccination_record_summary_component.rb
index 46bc5f9fcf..43268356f9 100644
--- a/app/components/app_vaccination_record_summary_component.rb
+++ b/app/components/app_vaccination_record_summary_component.rb
@@ -201,6 +201,20 @@ def call
end
end
+ if @vaccination_record.supplied_by.present?
+ summary_list.with_row do |row|
+ row.with_key { "Supplier" }
+ row.with_value { supplier_value }
+ if (href = @change_links[:supplier])
+ row.with_action(
+ text: "Change",
+ visually_hidden_text: "supplier",
+ href:
+ )
+ end
+ end
+ end
+
if @vaccination_record.performed_by.present?
summary_list.with_row do |row|
row.with_key { "Vaccinator" }
@@ -331,6 +345,13 @@ def time_value
)
end
+ def supplier_value
+ highlight_if(
+ @vaccination_record.supplied_by&.full_name,
+ @vaccination_record.supplied_by_user_id_changed?
+ )
+ end
+
def vaccinator_value
value =
if @vaccination_record.performed_by == @current_user
diff --git a/app/controllers/draft_vaccination_records_controller.rb b/app/controllers/draft_vaccination_records_controller.rb
index ffdee3518b..215532a948 100644
--- a/app/controllers/draft_vaccination_records_controller.rb
+++ b/app/controllers/draft_vaccination_records_controller.rb
@@ -17,6 +17,7 @@ class DraftVaccinationRecordsController < ApplicationController
before_action :validate_params, only: :update
before_action :set_batches, if: -> { current_step == :batch }
before_action :set_locations, if: -> { current_step == :location }
+ before_action :set_supplied_by_users, if: -> { current_step == :supplier }
before_action :set_back_link_path
after_action :verify_authorized
@@ -165,7 +166,8 @@ def update_params
],
location: %i[location_id],
notes: %i[notes],
- outcome: %i[outcome]
+ outcome: %i[outcome],
+ supplier: %i[supplied_by_user_id]
}.fetch(current_step)
params
@@ -231,6 +233,10 @@ def set_locations
@locations = policy_scope(Location).community_clinic
end
+ def set_supplied_by_users
+ @supplied_by_users = current_team.users.show_in_suppliers
+ end
+
def set_back_link_path
@back_link_path =
if @draft_vaccination_record.editing?
diff --git a/app/controllers/patient_sessions/vaccinations_controller.rb b/app/controllers/patient_sessions/vaccinations_controller.rb
index 03b54e9750..762f859e0f 100644
--- a/app/controllers/patient_sessions/vaccinations_controller.rb
+++ b/app/controllers/patient_sessions/vaccinations_controller.rb
@@ -32,9 +32,10 @@ def create
if @vaccinate_form.save(draft_vaccination_record:)
steps = draft_vaccination_record.wizard_steps
- steps.delete(:notes) # this is on the confirmation page
- steps.delete(:identity) # this can only be changed from confirmation page
steps.delete(:dose) # this can only be changed from confirmation page
+ steps.delete(:identity) # this can only be changed from confirmation page
+ steps.delete(:notes) # this is on the confirmation page
+ steps.delete(:supplier) # this can only be changed from confirmation page
steps.delete(:date_and_time)
steps.delete(:outcome) if draft_vaccination_record.administered?
@@ -69,6 +70,7 @@ def vaccinate_form_params
identity_check_confirmed_by_patient
pre_screening_confirmed
pre_screening_notes
+ supplied_by_user_id
vaccine_id
vaccine_method
]
diff --git a/app/forms/vaccinate_form.rb b/app/forms/vaccinate_form.rb
index c354de203b..4e97ba8ee0 100644
--- a/app/forms/vaccinate_form.rb
+++ b/app/forms/vaccinate_form.rb
@@ -19,6 +19,8 @@ class VaccinateForm
attribute :pre_screening_confirmed, :boolean
attribute :pre_screening_notes, :string
+ attribute :supplied_by_user_id, :integer
+
attribute :vaccine_method, :string
attribute :delivery_site, :string
attribute :dose_sequence, :integer
@@ -37,10 +39,16 @@ class VaccinateForm
maximum: 300
}
- validates :vaccine_method, inclusion: { in: :vaccine_method_options }
validates :pre_screening_notes, length: { maximum: 1000 }
-
validates :pre_screening_confirmed, presence: true, if: :administered?
+
+ validates :supplied_by_user_id,
+ inclusion: {
+ in: :supplied_by_user_id_values
+ },
+ if: :requires_supplied_by_user_id?
+
+ validates :vaccine_method, inclusion: { in: :vaccine_method_options }
validates :delivery_site,
inclusion: {
in: :delivery_site_options
@@ -65,6 +73,12 @@ def delivery_site
super
end
+ def supplied_by_users
+ current_user.selected_team.users.show_in_suppliers
+ end
+
+ def requires_supplied_by_user_id? = !current_user.show_in_suppliers
+
def save(draft_vaccination_record:)
return nil if invalid?
@@ -99,6 +113,7 @@ def save(draft_vaccination_record:)
draft_vaccination_record.patient_id = patient.id
draft_vaccination_record.performed_at = Time.current
draft_vaccination_record.performed_by_user = current_user
+ draft_vaccination_record.supplied_by_user_id = supplied_by_user_id
draft_vaccination_record.performed_ods_code = organisation.ods_code
draft_vaccination_record.programme = programme
draft_vaccination_record.session_id = session.id
@@ -112,6 +127,8 @@ def save(draft_vaccination_record:)
def administered? = vaccine_method != "none"
+ def supplied_by_user_id_values = supplied_by_users.pluck(:id)
+
def vaccine_method_options
programme.vaccine_methods + ["none"]
end
diff --git a/app/models/draft_vaccination_record.rb b/app/models/draft_vaccination_record.rb
index fefba2efc4..fb80ae67dc 100644
--- a/app/models/draft_vaccination_record.rb
+++ b/app/models/draft_vaccination_record.rb
@@ -11,8 +11,8 @@ class DraftVaccinationRecord
attribute :delivery_method, :string
attribute :delivery_site, :string
attribute :dose_sequence, :integer
+ attribute :first_active_wizard_step, :string
attribute :full_dose, :boolean
- attribute :protocol, :string
attribute :identity_check_confirmed_by_other_name, :string
attribute :identity_check_confirmed_by_other_relationship, :string
attribute :identity_check_confirmed_by_patient, :boolean
@@ -27,8 +27,9 @@ class DraftVaccinationRecord
attribute :performed_by_user_id, :integer
attribute :performed_ods_code, :string
attribute :programme_id, :integer
+ attribute :protocol, :string
attribute :session_id, :integer
- attribute :first_active_wizard_step, :string
+ attribute :supplied_by_user_id, :integer
def initialize(current_user:, **attributes)
@current_user = current_user
@@ -47,6 +48,7 @@ def wizard_steps
:notes,
:date_and_time,
(:outcome if can_change_outcome?),
+ (:supplier if requires_supplied_by?),
(:delivery if administered?),
(:dose if administered? && can_be_half_dose?),
(:batch if administered?),
@@ -198,6 +200,16 @@ def session=(value)
self.session_id = value.id
end
+ def supplied_by
+ return nil if supplied_by_user_id.nil?
+
+ User.find(supplied_by_user_id)
+ end
+
+ def supplied_by=(value)
+ self.supplied_by_user_id = value.id
+ end
+
def vaccination_record
return nil if editing_id.nil?
@@ -276,7 +288,6 @@ def writable_attribute_names
delivery_site
dose_sequence
full_dose
- protocol
identity_check
location_id
location_name
@@ -289,7 +300,9 @@ def writable_attribute_names
performed_by_user_id
performed_ods_code
programme_id
+ protocol
session_id
+ supplied_by_user_id
vaccine_id
]
end
@@ -348,6 +361,10 @@ def can_change_outcome?
outcome != "already_had" || editing? || session.nil? || session.today?
end
+ def requires_supplied_by?
+ performed_by_user && !performed_by_user&.show_in_suppliers
+ end
+
def delivery_site_matches_delivery_method
return if delivery_method.blank?
diff --git a/app/models/user.rb b/app/models/user.rb
index ce86d95667..5f6b4bcaef 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -72,6 +72,8 @@ class User < ApplicationRecord
scope :recently_active,
-> { where(last_sign_in_at: 1.week.ago..Time.current) }
+ scope :show_in_suppliers, -> { where(show_in_suppliers: true) }
+
enum :fallback_role,
{
nurse: 0,
diff --git a/app/policies/vaccination_record_policy.rb b/app/policies/vaccination_record_policy.rb
index ebe1c66d53..2f8cfd9ddb 100644
--- a/app/policies/vaccination_record_policy.rb
+++ b/app/policies/vaccination_record_policy.rb
@@ -17,7 +17,8 @@ def record_already_vaccinated?
end
def edit?
- user.is_nurse? && record.recorded_in_service? &&
+ (record.performed_by_user_id == user.id || user.is_nurse?) &&
+ record.recorded_in_service? &&
record.performed_ods_code == user.selected_organisation.ods_code
end
diff --git a/app/views/draft_vaccination_records/confirm.html.erb b/app/views/draft_vaccination_records/confirm.html.erb
index b47e62c642..55ef1f9b40 100644
--- a/app/views/draft_vaccination_records/confirm.html.erb
+++ b/app/views/draft_vaccination_records/confirm.html.erb
@@ -20,6 +20,7 @@
delivery_site: wizard_path("delivery"),
dose_volume: @draft_vaccination_record.wizard_steps.include?(:dose) ? wizard_path("dose") : nil,
identity: wizard_path("identity"),
+ supplier: wizard_path("supplier"),
location: @draft_vaccination_record.wizard_steps.include?(:location) ? wizard_path("location") : nil,
notes: wizard_path("notes"),
outcome: @draft_vaccination_record.wizard_steps.include?(:outcome) ? wizard_path("outcome") : nil,
diff --git a/app/views/draft_vaccination_records/supplier.html.erb b/app/views/draft_vaccination_records/supplier.html.erb
new file mode 100644
index 0000000000..95727b5879
--- /dev/null
+++ b/app/views/draft_vaccination_records/supplier.html.erb
@@ -0,0 +1,22 @@
+<% content_for :before_main do %>
+ <%= govuk_back_link(href: @back_link_path) %>
+<% end %>
+
+<% title = "Which nurse identified and pre-screened the child and supplied the vaccine?" %>
+<% content_for :page_title, title %>
+
+<%= form_with model: @draft_vaccination_record, url: wizard_path, method: :put do |f| %>
+ <%= f.govuk_error_summary %>
+
+ <%= f.govuk_radio_buttons_fieldset :supplied_by_user_id,
+ caption: { text: @patient.full_name, size: "l" },
+ legend: { size: "l", tag: "h1",
+ text: title } do %>
+
+ <% @supplied_by_users.each do |user| %>
+ <%= f.govuk_radio_button :supplied_by_user_id, user.id, label: { text: user.full_name } %>
+ <% end %>
+ <% end %>
+
+ <%= f.govuk_submit "Continue" %>
+<% end %>
diff --git a/app/views/patient_sessions/programmes/show.html.erb b/app/views/patient_sessions/programmes/show.html.erb
index 3a0b79ba8b..b1f5de97d0 100644
--- a/app/views/patient_sessions/programmes/show.html.erb
+++ b/app/views/patient_sessions/programmes/show.html.erb
@@ -10,7 +10,7 @@
<%= render AppPatientSessionTriageComponent.new(@patient_session, programme: @programme, triage_form: @triage_form) %>
- <%= render AppPatientSessionRecordComponent.new(@patient_session, programme: @programme, vaccinate_form: @vaccinate_form) %>
+ <%= render AppPatientSessionRecordComponent.new(@patient_session, programme: @programme, current_user:, vaccinate_form: @vaccinate_form) %>
<%= render AppPatientSessionOutcomeComponent.new(@patient_session, programme: @programme) %>
diff --git a/config/locales/en.yml b/config/locales/en.yml
index 2d0a5dbc77..da53c92769 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -162,6 +162,8 @@ en:
blank: Confirm you’ve checked the pre-screening statements are true
pre_screening_notes:
too_long: Enter notes that are less than %{count} characters long
+ supplied_by_user_id:
+ inclusion: Choose which nurse identified and pre-screened the child and supplied the vaccine
vaccine_method:
inclusion: Choose if they are ready to vaccinate
vaccination_report:
@@ -739,6 +741,7 @@ en:
route: route
school: school
session: session
+ supplier: supplier
timeline: timeline
triage: triage
type: type
diff --git a/spec/components/app_patient_session_record_component_spec.rb b/spec/components/app_patient_session_record_component_spec.rb
index 1eb5b27e25..b63caa08fd 100644
--- a/spec/components/app_patient_session_record_component_spec.rb
+++ b/spec/components/app_patient_session_record_component_spec.rb
@@ -7,10 +7,12 @@
described_class.new(
patient_session,
programme: programmes.first,
+ current_user:,
vaccinate_form: VaccinateForm.new
)
end
+ let(:current_user) { create(:user) }
let(:programmes) { [create(:programme, :hpv)] }
let(:session) { create(:session, :today, programmes:) }
let(:patient) do
diff --git a/spec/components/app_vaccinate_form_component_spec.rb b/spec/components/app_vaccinate_form_component_spec.rb
index 40ef233d3a..c7eede75cb 100644
--- a/spec/components/app_vaccinate_form_component_spec.rb
+++ b/spec/components/app_vaccinate_form_component_spec.rb
@@ -5,8 +5,13 @@
let(:programme) { create(:programme) }
let(:programmes) { [programme] }
- let(:session) { create(:session, :today, programmes:) }
+
+ let(:team) { create(:team, programmes:) }
+ let(:current_user) { create(:user, team:) }
+
+ let(:session) { create(:session, :today, team:, programmes:) }
let(:session_date) { session.session_dates.first }
+
let(:patient) do
create(
:patient,
@@ -20,7 +25,7 @@
end
let(:vaccinate_form) do
- VaccinateForm.new(patient:, session_date:, programme:)
+ VaccinateForm.new(current_user:, patient:, session_date:, programme:)
end
let(:component) { described_class.new(vaccinate_form) }
diff --git a/spec/factories/users.rb b/spec/factories/users.rb
index dc57218e5c..b5d2703cc4 100644
--- a/spec/factories/users.rb
+++ b/spec/factories/users.rb
@@ -34,10 +34,10 @@
factory :user,
aliases: %i[
nurse
- assessor
created_by
- recorded_by
performed_by
+ recorded_by
+ supplied_by
uploaded_by
] do
transient do
@@ -61,6 +61,7 @@
sequence(:email) { |n| "nurse-#{n}@example.com" }
fallback_role { :nurse }
+ show_in_suppliers { true }
given_name { "Test" }
family_name { "User" }
@@ -83,12 +84,14 @@
sequence(:email) { |n| "admin-#{n}@example.com" }
role_code { CIS2Info::ADMIN_ROLE }
fallback_role { :admin }
+ show_in_suppliers { false }
end
trait :superuser do
sequence(:email) { |n| "superuser-#{n}@example.com" }
role_workgroups { [CIS2Info::SUPERUSER_WORKGROUP] }
fallback_role { :superuser }
+ show_in_suppliers { false }
end
trait :healthcare_assistant do
@@ -98,6 +101,7 @@
[CIS2Info::PERSONAL_MEDICATION_ADMINISTRATION_ACTIVITY_CODE]
end
fallback_role { :healthcare_assistant }
+ show_in_suppliers { false }
end
trait :prescriber do
diff --git a/spec/features/flu_vaccination_hca_pgd_supply_spec.rb b/spec/features/flu_vaccination_hca_pgd_supply_spec.rb
index 38755a9891..91613f5407 100644
--- a/spec/features/flu_vaccination_hca_pgd_supply_spec.rb
+++ b/spec/features/flu_vaccination_hca_pgd_supply_spec.rb
@@ -21,9 +21,20 @@
end
def given_a_session_exists
- programmes = [create(:programme, :flu)]
+ @programme = create(:programme, :flu)
+ programmes = [@programme]
@team = create(:team, programmes:)
+
+ @batch =
+ create(
+ :batch,
+ :not_expired,
+ team: @team,
+ vaccine: @programme.vaccines.nasal.first
+ )
+
+ @nurse = create(:nurse, team: @team)
@user = create(:healthcare_assistant, team: @team)
@session =
@@ -78,11 +89,28 @@ def when_i_click_on_the_nasal_and_injection_patient
def when_i_click_on_the_injection_patient
# This patient won't be in the "Record" tab.
+ expect(page).not_to have_content(@patient_injection_only.full_name)
+
within(".app-secondary-navigation") { click_on "Children" }
click_on @patient_injection_only.full_name
end
def then_i_am_able_to_vaccinate_them
+ check "I have checked that the above statements are true"
+ select @nurse.full_name
+ within all("section")[1] do
+ choose "Yes"
+ end
+ click_on "Continue"
+
+ choose @batch.name
+ click_on "Continue"
+
+ click_on "Change supplier"
+ choose @nurse.full_name
+ 4.times { click_on "Continue" }
+
+ click_on "Confirm"
click_on "Record vaccinations"
end
diff --git a/spec/forms/vaccinate_form_spec.rb b/spec/forms/vaccinate_form_spec.rb
index e638da39fc..012b89465e 100644
--- a/spec/forms/vaccinate_form_spec.rb
+++ b/spec/forms/vaccinate_form_spec.rb
@@ -1,9 +1,10 @@
# frozen_string_literal: true
describe VaccinateForm do
- subject(:form) { described_class.new(programme:) }
+ subject(:form) { described_class.new(programme:, current_user:) }
let(:programme) { create(:programme) }
+ let(:current_user) { create(:user) }
describe "validations" do
it do
@@ -28,7 +29,8 @@
subject(:form) do
described_class.new(
identity_check_confirmed_by_patient: false,
- programme:
+ programme:,
+ current_user:
)
end
From 8634a32e06684f67f1b4982ceebf6170daf3c678 Mon Sep 17 00:00:00 2001
From: John Henderson
Date: Wed, 20 Aug 2025 12:20:10 +0100
Subject: [PATCH 18/27] Basic functionality to add PSD to a triaged patient
Features include:
- Additional question presented in triage section if prescriber
wishes to add a PSD so that a healthcare worker can do it. This
option is only displayed if the user is a prescriber and the
session has PSD enabled.
- Backend updated to handle PSD creation
- The tag "PSD added" shows up in the action list header on the
patient page if it has a PSD
---
.../app_triage_form_component.html.erb | 10 ++-
app/components/app_triage_form_component.rb | 7 ++
.../patient_sessions/triages_controller.rb | 21 +++++-
app/forms/triage_form.rb | 8 +++
app/models/patient.rb | 1 +
app/models/patient_session.rb | 7 ++
app/views/patient_sessions/_header.html.erb | 6 ++
config/locales/en.yml | 3 +
spec/features/triage_spec.rb | 68 ++++++++++++++++++-
spec/forms/triage_form_spec.rb | 6 ++
10 files changed, 134 insertions(+), 3 deletions(-)
diff --git a/app/components/app_triage_form_component.html.erb b/app/components/app_triage_form_component.html.erb
index d69912ce52..56799dacde 100644
--- a/app/components/app_triage_form_component.html.erb
+++ b/app/components/app_triage_form_component.html.erb
@@ -3,7 +3,15 @@
<%= f.govuk_radio_buttons_fieldset :status_and_vaccine_method, **fieldset_options do %>
<% triage_form.safe_to_vaccinate_options.each do |option| %>
- <%= f.govuk_radio_button :status_and_vaccine_method, option %>
+ <%= f.govuk_radio_button :status_and_vaccine_method, option do %>
+ <% if show_psd_options?(option) %>
+ <%= f.govuk_radio_buttons_fieldset :psd_action,
+ legend: { text: "Do you want to add a PSD?", size: "s" } do %>
+ <%= f.govuk_radio_button :add_psd, "true", label: { text: "Yes" } %>
+ <%= f.govuk_radio_button :add_psd, "false", label: { text: "No" } %>
+ <% end %>
+ <% end %>
+ <% end %>
<% end %>
<%= f.govuk_radio_divider %>
<% triage_form.other_options.each do |option| %>
diff --git a/app/components/app_triage_form_component.rb b/app/components/app_triage_form_component.rb
index fa48c813cc..8fe093edb4 100644
--- a/app/components/app_triage_form_component.rb
+++ b/app/components/app_triage_form_component.rb
@@ -26,6 +26,13 @@ def initialize(
def builder = GOVUKDesignSystemFormBuilder::FormBuilder
+ def show_psd_options?(option)
+ Flipper.enabled?(:delegation) &&
+ helpers.policy(PatientSpecificDirection).create? &&
+ patient_session.session.psd_enabled? &&
+ option == "safe_to_vaccinate_nasal"
+ end
+
def fieldset_options
text = "Is it safe to vaccinate #{patient.given_name}?"
hint =
diff --git a/app/controllers/patient_sessions/triages_controller.rb b/app/controllers/patient_sessions/triages_controller.rb
index 1bb20d24cc..cde089ef07 100644
--- a/app/controllers/patient_sessions/triages_controller.rb
+++ b/app/controllers/patient_sessions/triages_controller.rb
@@ -46,6 +46,8 @@ def create
)
.each { send_triage_confirmation(@patient_session, @programme, it) }
+ ensure_psd_exists if @triage_form.add_psd?
+
redirect_to redirect_path, flash: { success: "Triage outcome updated" }
else
render "patient_sessions/programmes/show",
@@ -57,7 +59,7 @@ def create
private
def triage_form_params
- params.expect(triage_form: %i[status_and_vaccine_method notes])
+ params.expect(triage_form: %i[status_and_vaccine_method notes add_psd])
end
def redirect_path
@@ -68,4 +70,21 @@ def redirect_path
return_to: "triage"
)
end
+
+ def ensure_psd_exists
+ psd_attributes = {
+ academic_year: @academic_year,
+ patient: @patient,
+ programme: @programme,
+ vaccine: @programme.vaccines.first,
+ vaccine_method: :nasal,
+ delivery_site: :nose
+ }
+
+ return if PatientSpecificDirection.exists?(**psd_attributes)
+
+ PatientSpecificDirection.create!(
+ psd_attributes.merge(created_by: current_user)
+ )
+ end
end
diff --git a/app/forms/triage_form.rb b/app/forms/triage_form.rb
index c891186a77..374aebed89 100644
--- a/app/forms/triage_form.rb
+++ b/app/forms/triage_form.rb
@@ -7,6 +7,7 @@ class TriageForm
attr_accessor :patient_session, :programme, :current_user
attribute :status_and_vaccine_method, :string
+ attribute :add_psd, :boolean
attribute :notes, :string
attribute :vaccine_methods, array: true, default: []
@@ -15,6 +16,13 @@ class TriageForm
in: :status_and_vaccine_method_options
}
validates :notes, length: { maximum: 1000 }
+ validates :add_psd,
+ inclusion: {
+ in: [true, false]
+ },
+ if: -> { add_psd.present? }
+
+ def add_psd? = add_psd
def triage=(triage)
self.status_and_vaccine_method =
diff --git a/app/models/patient.rb b/app/models/patient.rb
index eac95b4e5b..d1eaf0d017 100644
--- a/app/models/patient.rb
+++ b/app/models/patient.rb
@@ -78,6 +78,7 @@ class Patient < ApplicationRecord
has_many :triages
has_many :vaccination_records, -> { kept }
has_many :vaccination_statuses
+ has_many :patient_specific_directions
has_many :gillick_assessments
has_many :parents, through: :parent_relationships
diff --git a/app/models/patient_session.rb b/app/models/patient_session.rb
index 336f7aa464..f48ec87b56 100644
--- a/app/models/patient_session.rb
+++ b/app/models/patient_session.rb
@@ -268,6 +268,13 @@ class PatientSession < ApplicationRecord
delegate :academic_year, to: :session
+ def psd_added?(programme:)
+ patient
+ .patient_specific_directions
+ .where(programme:, academic_year:)
+ .exists?
+ end
+
def safe_to_destroy?
vaccination_records.empty? && gillick_assessments.empty? &&
session_attendances.none?(&:attending?)
diff --git a/app/views/patient_sessions/_header.html.erb b/app/views/patient_sessions/_header.html.erb
index 471c6ba2c5..132d2fa14b 100644
--- a/app/views/patient_sessions/_header.html.erb
+++ b/app/views/patient_sessions/_header.html.erb
@@ -29,6 +29,12 @@
<% if (session_attendance = @patient_session.todays_attendance) %>
+ <% if @patient_session.psd_added?(programme: @programme) %>
+
\ No newline at end of file
diff --git a/app/views/sessions/patient_specific_directions/show.html.erb b/app/views/sessions/patient_specific_directions/show.html.erb
new file mode 100644
index 0000000000..6dd58dff39
--- /dev/null
+++ b/app/views/sessions/patient_specific_directions/show.html.erb
@@ -0,0 +1,41 @@
+<% content_for :before_main do %>
+ <%= render AppBreadcrumbComponent.new(items: [
+ { text: t("dashboard.index.title"), href: dashboard_path },
+ { text: t("sessions.index.title"), href: sessions_path },
+ { text: @session.location.name, href: session_path(@session) },
+ ]) %>
+<% end %>
+
+<%= render "sessions/header" %>
+
+<%= govuk_inset_text do %>
+ Information:
+
+ There are <%= @eligible_for_bulk_psd_count %> children with consent for the nasal flu vaccine
+ who do not require triage and do not yet have a PSD in place.
+
+ <% if current_user.is_nurse? %>
+
+ <%= link_to bulk_add_session_patient_specific_directions_path(@session), class: "nhsuk-action-link__link" do %>
+
+ Add new PSDs
+ <% end %>
+
+ <% end %>
+<% end %>
+
+
+
+
+
+ <%= render AppSessionNeedsReviewWarningComponent.new(session: @session) %>
+ <%= render AppSearchResultsComponent.new(@pagy, label: "children", heading: "Review PSDs") do %>
+ <% @patient_sessions.each do |patient_session| %>
+ <%= render AppPatientSessionSearchResultCardComponent.new(patient_session, context: :patient_specific_direction) %>
+ <% end %>
+ <% end %>
+
+
diff --git a/config/locales/en.yml b/config/locales/en.yml
index da53c92769..5a6c72cb1f 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -687,6 +687,7 @@ en:
record: Record vaccinations
register: Register
triage: Triage
+ patient_specific_directions: PSDs
table:
no_filtered_results: We couldn’t find any children that matched your filters.
no_results: No results
diff --git a/config/locales/status.en.yml b/config/locales/status.en.yml
index c9c7f3f059..24a83e772c 100644
--- a/config/locales/status.en.yml
+++ b/config/locales/status.en.yml
@@ -5,6 +5,7 @@ en:
register: Registration status
triage: Triage status
vaccination: Outcome
+ patient_specific_direction: PSD status
consent:
label:
conflicts: Conflicting consent
@@ -74,3 +75,10 @@ en:
could_not_vaccinate: red
none_yet: white
vaccinated: green
+ patient_specific_direction:
+ label:
+ added: PSD added
+ not_added: PSD not added
+ colour:
+ added: green
+ not_added: grey
\ No newline at end of file
diff --git a/config/routes.rb b/config/routes.rb
index f6b1d37cde..69917c5e40 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -215,6 +215,11 @@
resource :patients, only: :show, controller: "sessions/patients"
resource :consent, only: :show, controller: "sessions/consent"
resource :triage, only: :show, controller: "sessions/triage"
+ resource :patient_specific_directions,
+ only: %i[show create],
+ controller: "sessions/patient_specific_directions" do
+ get "bulk-add", action: :bulk_add
+ end
resource :register, only: :show, controller: "sessions/register" do
post ":patient_id/:status", as: :create, action: :create
end
diff --git a/spec/factories/teams.rb b/spec/factories/teams.rb
index 92c8c12be5..ae06bcc347 100644
--- a/spec/factories/teams.rb
+++ b/spec/factories/teams.rb
@@ -56,6 +56,10 @@
users { [create(:user, :admin, team: instance)] }
end
+ trait :with_one_healthcare_assistant do
+ users { [create(:user, :healthcare_assistant, team: instance)] }
+ end
+
trait :with_generic_clinic do
after(:create) { |team| GenericClinicFactory.call(team:) }
end
diff --git a/spec/features/patient_specific_directions_spec.rb b/spec/features/patient_specific_directions_spec.rb
new file mode 100644
index 0000000000..232cc6fd06
--- /dev/null
+++ b/spec/features/patient_specific_directions_spec.rb
@@ -0,0 +1,106 @@
+# frozen_string_literal: true
+
+describe "Patient Specific Directions" do
+ before { given_delegation_feature_flag_is_enabled }
+
+ scenario "prescriber can bulk add PSDs to patients that don't require triage" do
+ given_a_flu_programme_with_a_running_session(user_type: :with_one_nurse)
+ and_a_patient_who_doesnt_need_triage_exists
+ and_i_am_signed_in
+
+ when_i_go_to_the_session_psds_tab
+ then_the_patient_should_have_psd_status_not_added
+ and_i_should_see_one_child_eligible_for_psd
+
+ when_i_click_add_new_psds
+ and_should_see_again_one_child_eligible_for_psd
+
+ when_i_click_on_button_to_bulk_add_psds
+ then_the_patient_should_have_psd_status_added
+ and_zero_children_should_be_eligible_for_psd
+ end
+
+ scenario "admin cannot bulk add PSDs to patients" do
+ given_a_flu_programme_with_a_running_session(user_type: :with_one_admin)
+ and_a_patient_who_doesnt_need_triage_exists
+ and_i_am_signed_in(role: :admin)
+
+ when_i_go_to_the_session_psds_tab
+ then_i_should_not_see_link_to_bulk_add_psds
+ end
+
+ scenario "healthcare assistant cannot bulk add PSDs to patients" do
+ given_a_flu_programme_with_a_running_session(
+ user_type: :with_one_healthcare_assistant
+ )
+ and_a_patient_who_doesnt_need_triage_exists
+ and_i_am_signed_in(role: :healthcare_assistant)
+
+ when_i_go_to_the_session_psds_tab
+ then_i_should_not_see_link_to_bulk_add_psds
+ end
+
+ def given_delegation_feature_flag_is_enabled
+ Flipper.enable(:delegation)
+ end
+
+ def given_a_flu_programme_with_a_running_session(user_type:)
+ programmes = [create(:programme, :flu)]
+ @team = create(:team, user_type, programmes:)
+
+ @batch =
+ create(:batch, team: @team, vaccine: programmes.first.vaccines.first)
+
+ @session = create(:session, team: @team, programmes:)
+ end
+
+ def and_a_patient_who_doesnt_need_triage_exists
+ @patient_triage_not_needed =
+ create(
+ :patient_session,
+ :consent_given_triage_not_needed,
+ session: @session
+ ).patient
+ end
+
+ def and_i_am_signed_in(role: :nurse)
+ @user = @team.users.first
+ sign_in @user, role:
+ end
+
+ def when_i_go_to_the_session_psds_tab
+ visit session_patient_specific_directions_path(@session)
+ end
+
+ def then_the_patient_should_have_psd_status_not_added
+ expect(page).to have_text("PSD not added")
+ end
+
+ def then_the_patient_should_have_psd_status_added
+ expect(page).to have_text("PSD added")
+ end
+
+ def and_i_should_see_one_child_eligible_for_psd
+ expect(page).to have_text("There are 1 children")
+ end
+
+ def and_should_see_again_one_child_eligible_for_psd
+ expect(page).to have_text("1 new PSDs?")
+ end
+
+ def and_zero_children_should_be_eligible_for_psd
+ expect(page).to have_text("There are 0 children")
+ end
+
+ def when_i_click_add_new_psds
+ click_link "Add new PSDs"
+ end
+
+ def when_i_click_on_button_to_bulk_add_psds
+ click_button "Yes, add PSDs"
+ end
+
+ def then_i_should_not_see_link_to_bulk_add_psds
+ expect(page).not_to have_text("Add new PSDs")
+ end
+end
From 90cb4d48a582cd440bca7304b4c923b5a48b7946 Mon Sep 17 00:00:00 2001
From: Thomas Leese
Date: Wed, 27 Aug 2025 15:39:36 +0100
Subject: [PATCH 21/27] Small code improvements to PSD functionality
This makes a number of small tweaks to the code related to PSD
functionality to tidy things up, there should be no user-facing
changes.
---
app/components/app_triage_form_component.rb | 7 +++----
.../sessions/patient_specific_directions_controller.rb | 10 ++++------
config/routes.rb | 1 +
3 files changed, 8 insertions(+), 10 deletions(-)
diff --git a/app/components/app_triage_form_component.rb b/app/components/app_triage_form_component.rb
index 8fe093edb4..42bfd69cbd 100644
--- a/app/components/app_triage_form_component.rb
+++ b/app/components/app_triage_form_component.rb
@@ -27,10 +27,9 @@ def initialize(
def builder = GOVUKDesignSystemFormBuilder::FormBuilder
def show_psd_options?(option)
- Flipper.enabled?(:delegation) &&
- helpers.policy(PatientSpecificDirection).create? &&
- patient_session.session.psd_enabled? &&
- option == "safe_to_vaccinate_nasal"
+ patient_session.session.psd_enabled? &&
+ option == "safe_to_vaccinate_nasal" &&
+ helpers.policy(PatientSpecificDirection).create?
end
def fieldset_options
diff --git a/app/controllers/sessions/patient_specific_directions_controller.rb b/app/controllers/sessions/patient_specific_directions_controller.rb
index df66c2c91f..2adc92df7d 100644
--- a/app/controllers/sessions/patient_specific_directions_controller.rb
+++ b/app/controllers/sessions/patient_specific_directions_controller.rb
@@ -21,12 +21,10 @@ def show
end
def create
- ActiveRecord::Base.transaction do
- PatientSpecificDirection.import!(
- psds_to_create,
- on_duplicate_key_ignore: true
- )
- end
+ PatientSpecificDirection.import!(
+ psds_to_create,
+ on_duplicate_key_ignore: true
+ )
redirect_to session_patient_specific_directions_path(@session),
flash: {
diff --git a/config/routes.rb b/config/routes.rb
index 69917c5e40..9f6d959ab1 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -216,6 +216,7 @@
resource :consent, only: :show, controller: "sessions/consent"
resource :triage, only: :show, controller: "sessions/triage"
resource :patient_specific_directions,
+ path: "patient-specific-directions",
only: %i[show create],
controller: "sessions/patient_specific_directions" do
get "bulk-add", action: :bulk_add
From 0776664a5d14d7195a735f2c6a21a7accd3edb61 Mon Sep 17 00:00:00 2001
From: Thomas Leese
Date: Thu, 28 Aug 2025 08:37:09 +0100
Subject: [PATCH 22/27] Rename `bulk_add` action
This renames it to `new` to match the Rails convention of a `new` and
`create` action.
---
.../sessions/patient_specific_directions_controller.rb | 8 ++++----
.../{bulk_add.html.erb => new.html.erb} | 4 ++--
.../sessions/patient_specific_directions/show.html.erb | 2 +-
config/routes.rb | 6 ++----
4 files changed, 9 insertions(+), 11 deletions(-)
rename app/views/sessions/patient_specific_directions/{bulk_add.html.erb => new.html.erb} (91%)
diff --git a/app/controllers/sessions/patient_specific_directions_controller.rb b/app/controllers/sessions/patient_specific_directions_controller.rb
index 2adc92df7d..c0fd1bfcf0 100644
--- a/app/controllers/sessions/patient_specific_directions_controller.rb
+++ b/app/controllers/sessions/patient_specific_directions_controller.rb
@@ -20,6 +20,10 @@ def show
@pagy, @patient_sessions = pagy(patient_sessions)
end
+ def new
+ @eligible_for_bulk_psd_count = patient_sessions_allowed_psd.count
+ end
+
def create
PatientSpecificDirection.import!(
psds_to_create,
@@ -32,10 +36,6 @@ def create
}
end
- def bulk_add
- @eligible_for_bulk_psd_count = patient_sessions_allowed_psd.count
- end
-
private
def set_session
diff --git a/app/views/sessions/patient_specific_directions/bulk_add.html.erb b/app/views/sessions/patient_specific_directions/new.html.erb
similarity index 91%
rename from app/views/sessions/patient_specific_directions/bulk_add.html.erb
rename to app/views/sessions/patient_specific_directions/new.html.erb
index 3d75be1f81..35ee48c52b 100644
--- a/app/views/sessions/patient_specific_directions/bulk_add.html.erb
+++ b/app/views/sessions/patient_specific_directions/new.html.erb
@@ -14,9 +14,9 @@
-
\ No newline at end of file
+
diff --git a/app/views/sessions/patient_specific_directions/show.html.erb b/app/views/sessions/patient_specific_directions/show.html.erb
index 6dd58dff39..c73abcf084 100644
--- a/app/views/sessions/patient_specific_directions/show.html.erb
+++ b/app/views/sessions/patient_specific_directions/show.html.erb
@@ -16,7 +16,7 @@
<% if current_user.is_nurse? %>
- <%= link_to bulk_add_session_patient_specific_directions_path(@session), class: "nhsuk-action-link__link" do %>
+ <%= link_to new_session_patient_specific_directions_path(@session), class: "nhsuk-action-link__link" do %>