Skip to content

Commit 8c21362

Browse files
committed
Add ability to show PII related information on the timeline
A show_pii flag allows optional exclusion of personally-identifiable information from the event details and audited changes relating to a patient. This has been set to false by default in the controller, but can be toggled on or off with a check box in the UI.
1 parent e038fe5 commit 8c21362

File tree

6 files changed

+193
-35
lines changed

6 files changed

+193
-35
lines changed

app/components/app_timeline_filter_component.html.erb

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,15 @@
158158
"data-turbo-permanent": "true" %>
159159
<% end %>
160160
<% end %>
161+
162+
<div class="nhsuk-u-margin-bottom-4">
163+
<%= f.govuk_check_box :show_pii, true,
164+
label: { text: "Show PII" },
165+
checked: @show_pii,
166+
"data-autosubmit-target": "field",
167+
"data-action": "autosubmit#submit",
168+
"data-turbo-permanent": "true" %>
169+
</div>
161170

162171
<%= helpers.govuk_button_link_to "Reset filters",
163172
@reset_url,

app/components/app_timeline_filter_component.rb

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@ def initialize(
1111
class_imports:,
1212
cohort_imports:,
1313
sessions:,
14-
reset_url:
14+
reset_url:,
15+
show_pii:
1516
)
1617
@url = url
1718
@patient = patient
@@ -23,6 +24,7 @@ def initialize(
2324
@cohort_imports = cohort_imports
2425
@sessions = sessions
2526
@reset_url = reset_url
27+
@show_pii = show_pii
2628
end
2729

2830
attr_reader :url,
@@ -33,5 +35,6 @@ def initialize(
3335
:class_imports,
3436
:cohort_imports,
3537
:sessions,
36-
:reset_url
38+
:reset_url,
39+
:show_pii
3740
end

app/controllers/inspect/timeline/patients_controller.rb

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ class Inspect::Timeline::PatientsController < ApplicationController
77

88
layout "full"
99

10+
SHOW_PII = false
11+
1012
DEFAULT_EVENT_NAMES = %w[
1113
consents
1214
school_moves
@@ -20,6 +22,8 @@ class Inspect::Timeline::PatientsController < ApplicationController
2022
].freeze
2123

2224
def show
25+
@show_pii = params[:show_pii] || SHOW_PII
26+
2327
params.reverse_merge!(event_names: DEFAULT_EVENT_NAMES)
2428
params[:audit_config] ||= {}
2529

@@ -43,7 +47,8 @@ def show
4347
TimelineRecords.new(
4448
@patient,
4549
detail_config: build_details_config,
46-
audit_config: audit_config
50+
audit_config: audit_config,
51+
show_pii: @show_pii
4752
).load_timeline_events(event_names)
4853

4954
@no_events_message = true if @patient_timeline.empty?
@@ -95,8 +100,10 @@ def sample_patient(compare_option)
95100
:invalid_patient
96101
end
97102
else
98-
raise ArgumentError,
99-
"Invalid patient comparison option: #{compare_option}"
103+
raise ArgumentError, <<~MESSAGE
104+
Invalid patient comparison option: #{compare_option}.
105+
Supported options are: class_import, cohort_import, session, manual_entry
106+
MESSAGE
100107
end
101108
end
102109

app/views/inspect/timeline/patients/show.html.erb

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
<%= "Rails console: Patient.find(#{@patient.id})" %>
77
</code>
88

9-
<% if params[:event_names].include?("audits") && @patient_timeline %>
9+
<% if params[:event_names].include?("audits") && @patient_timeline && !@show_pii %>
1010
<%= govuk_inset_text do %>
1111
<p class="nhsuk-body">
1212
Only audited changes that do not involve PII are included
@@ -16,13 +16,13 @@
1616

1717
<div class="nhsuk-grid-row nhsuk-u-margin-top-4">
1818

19-
<div class="nhsuk-grid-column-one-third app-grid-column--sticky">
19+
<div class="nhsuk-grid-column-one-half app-grid-column--sticky">
2020
<%= render AppTimelineFilterComponent.new(
2121
url: inspect_timeline_patient_path,
2222
patient: @patient,
2323
team: @patient.sessions.includes(:team).first.team,
2424
event_options: TimelineRecords::DEFAULT_DETAILS_CONFIG,
25-
timeline_fields: TimelineRecords::AVAILABLE_DETAILS_CONFIG,
25+
timeline_fields: @show_pii ? TimelineRecords::AVAILABLE_DETAILS_CONFIG_WITH_PII : TimelineRecords::AVAILABLE_DETAILS_CONFIG,
2626
class_imports: @patient_events[:class_imports],
2727
cohort_imports: @patient_events[:cohort_imports],
2828
sessions: @patient_events[:sessions],
@@ -31,11 +31,13 @@
3131
event_names: Inspect::Timeline::PatientsController::DEFAULT_EVENT_NAMES,
3232
detail_config: TimelineRecords::DEFAULT_DETAILS_CONFIG,
3333
compare_option: nil,
34+
show_pii: false,
3435
),
36+
show_pii: @show_pii,
3537
) %>
3638
</div>
3739

38-
<div class="nhsuk-grid-column-two-thirds">
40+
<div class="nhsuk-grid-column-one-half">
3941
<% if @no_events_message %>
4042
<%= render AppWarningCalloutComponent.new(
4143
heading: "No events found",

lib/timeline_records.rb

Lines changed: 137 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -2,38 +2,102 @@
22

33
class TimelineRecords
44
DEFAULT_DETAILS_CONFIG = {
5-
cohort_imports: [],
6-
class_imports: [],
7-
sessions: %i[],
8-
school_moves: %i[school_id source],
9-
school_move_log_entries: %i[school_id user_id],
105
consents: %i[response route],
6+
sessions: %i[],
7+
session_attendances: %i[],
118
triages: %i[status performed_by_user_id],
12-
vaccination_records: %i[outcome session_id]
9+
vaccination_records: %i[outcome session_id],
10+
cohort_imports: %i[],
11+
class_imports: %i[],
12+
parents: %i[],
13+
gillick_assessments: %i[],
14+
parent_relationships: %i[],
15+
school_moves: %i[school_id source],
16+
school_move_log_entries: %i[school_id user_id]
1317
}.freeze
1418

1519
AVAILABLE_DETAILS_CONFIG = {
16-
cohort_imports: %i[rows_count status uploaded_by_user_id],
17-
class_imports: %i[rows_count year_groups uploaded_by_user_id location_id],
18-
sessions: %i[],
19-
school_moves: %i[source school_id home_educated],
20-
school_move_log_entries: %i[user_id school_id home_educated],
21-
consents: %i[
22-
programme_id
23-
response
24-
route
25-
parent_id
26-
withdrawn_at
27-
invalidated_at
20+
consents: %i[response route updated_at withdrawn_at invalidated_at],
21+
sessions: %i[slug academic_year],
22+
session_attendances: %i[attending updated_at],
23+
triages: %i[status updated_at invalidated_at performed_by_user_id],
24+
vaccination_records: %i[
25+
outcome
26+
performed_at
27+
updated_at
28+
discarded_at
29+
uuid
30+
session_id
31+
],
32+
cohort_imports: %i[
33+
csv_filename
34+
processed_at
35+
status
36+
rows_count
37+
new_record_count
38+
exact_duplicate_record_count
39+
changed_record_count
2840
],
29-
triages: %i[status performed_by_user_id programme_id invalidated_at],
41+
class_imports: %i[
42+
csv_filename
43+
processed_at
44+
status
45+
rows_count
46+
new_record_count
47+
exact_duplicate_record_count
48+
changed_record_count
49+
year_groups
50+
],
51+
parents: %i[],
52+
gillick_assessments: %i[],
53+
parent_relationships: %i[],
54+
school_moves: %i[school_id source],
55+
school_move_log_entries: %i[school_id user_id]
56+
}.freeze
57+
58+
AVAILABLE_DETAILS_CONFIG_WITH_PII = {
59+
consents: %i[response route updated_at withdrawn_at invalidated_at],
60+
sessions: %i[slug academic_year],
61+
session_attendances: %i[attending updated_at],
62+
triages: %i[status updated_at invalidated_at performed_by_user_id],
3063
vaccination_records: %i[
3164
outcome
32-
performed_by_user_id
33-
programme_id
65+
performed_at
66+
updated_at
67+
discarded_at
68+
uuid
3469
session_id
35-
vaccine_id
36-
]
70+
],
71+
cohort_imports: %i[
72+
csv_filename
73+
processed_at
74+
status
75+
rows_count
76+
new_record_count
77+
exact_duplicate_record_count
78+
changed_record_count
79+
],
80+
class_imports: %i[
81+
csv_filename
82+
processed_at
83+
status
84+
rows_count
85+
new_record_count
86+
exact_duplicate_record_count
87+
changed_record_count
88+
year_groups
89+
],
90+
parents: %i[full_name email phone],
91+
gillick_assessments: %i[
92+
knows_vaccination
93+
knows_disease
94+
knows_consequences
95+
knows_delivery
96+
knows_side_effects
97+
],
98+
parent_relationships: %i[type other_name],
99+
school_moves: %i[school_id source],
100+
school_move_log_entries: %i[school_id user_id]
37101
}.freeze
38102

39103
DEFAULT_AUDITS_CONFIG = {
@@ -69,14 +133,59 @@ class TimelineRecords
69133
source
70134
].freeze
71135

72-
def initialize(patient, detail_config: {}, audit_config: {})
136+
ALLOWED_AUDITED_CHANGES_WITH_PII = %i[
137+
full_name
138+
email
139+
phone
140+
nhs_number
141+
given_name
142+
family_name
143+
date_of_birth
144+
address_line_1
145+
address_line_2
146+
address_town
147+
address_postcode
148+
home_educated
149+
updated_from_pds_at
150+
restricted_at
151+
date_of_death
152+
pending_changes
153+
patient_id
154+
session_id
155+
location_id
156+
programme_id
157+
vaccine_id
158+
organisation_id
159+
team_id
160+
school_id
161+
gp_practice_id
162+
uploaded_by_user_id
163+
performed_by_user_id
164+
user_id
165+
parent_id
166+
status
167+
outcome
168+
response
169+
route
170+
date_of_death_recorded_at
171+
restricted_at
172+
invalidated_at
173+
withdrawn_at
174+
rows_count
175+
year_groups
176+
home_educated
177+
source
178+
].freeze
179+
180+
def initialize(patient, detail_config: {}, audit_config: {}, show_pii: false)
73181
@patient = patient
74182
@patient_id = @patient.id
75183
@patient_events = patient_events(@patient)
76184
@additional_events = additional_events(@patient)
77185
@detail_config = extract_detail_config(detail_config)
78186
@events = []
79187
@audit_config = audit_config
188+
@show_pii = show_pii
80189
end
81190

82191
def render_timeline(*event_names, truncate_columns: false)
@@ -240,12 +349,15 @@ def audits_events
240349
end
241350
).reject { it.audited_changes.keys == ["updated_from_pds_at"] }
242351

352+
allowed_changes =
353+
@show_pii ? ALLOWED_AUDITED_CHANGES_WITH_PII : ALLOWED_AUDITED_CHANGES
354+
243355
audit_events.map do |audit|
244356
filtered_changes =
245357
audit
246358
.audited_changes
247359
.each_with_object({}) do |(key, value), hash|
248-
if ALLOWED_AUDITED_CHANGES.include?(key.to_sym)
360+
if allowed_changes.include?(key.to_sym)
249361
hash[key] = value
250362
elsif audits[:include_filtered_audit_changes]
251363
hash[key] = "[FILTERED]"

spec/lib/timeline_records_spec.rb

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,16 @@
22

33
describe TimelineRecords do
44
subject(:timeline) do
5-
described_class.new(patient, detail_config: detail_config)
5+
described_class.new(
6+
patient,
7+
detail_config: detail_config,
8+
show_pii: show_pii
9+
)
610
end
711

812
let(:programme) { create(:programme, :hpv) }
913
let(:detail_config) { {} }
14+
let(:show_pii) { false }
1015
let(:team) { create(:team, programmes: [programme]) }
1116
let(:session) { create(:session, team:, programmes: [programme]) }
1217
let(:class_import) { create(:class_import, session:) }
@@ -398,6 +403,26 @@
398403
end
399404
end
400405

406+
context "with show_pii: true" do
407+
let(:show_pii) { true }
408+
409+
it "includes PII-based audited changes" do
410+
patient.audits.create!(
411+
audited_changes: {
412+
given_name: %w[Alessia Alice],
413+
organisation_id: [nil, 1]
414+
}
415+
)
416+
events = timeline.load_events(["audits"])
417+
expect(events.first[:details][:audited_changes]).to have_key(
418+
:given_name
419+
)
420+
expect(events.first[:details][:audited_changes]).to have_key(
421+
:organisation_id
422+
)
423+
end
424+
end
425+
401426
context "with include_associated_audits: false" do
402427
let(:timeline) do
403428
described_class.new(

0 commit comments

Comments
 (0)