Skip to content

Commit a496be4

Browse files
authored
Merge pull request #4373 from nhsuk/validate-child-names
Validate child names on import
2 parents 2bfd795 + 72c330a commit a496be4

File tree

5 files changed

+247
-8
lines changed

5 files changed

+247
-8
lines changed

app/lib/apostrophe_normaliser.rb

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# frozen_string_literal: true
2+
3+
module ApostropheNormaliser
4+
def self.call(value)
5+
pattern = <<-PATTERN
6+
(
7+
\u0060 # Grave accent, we have one instance of this already so far
8+
| \u2019 # Right single quotation mark, the preferred Unicode apostrophe
9+
# but not supported by PDS
10+
| \u02BC # Modifier letter apostrophe, also not supported by PDS
11+
)
12+
PATTERN
13+
14+
value&.gsub(Regexp.new(pattern, Regexp::EXTENDED), "'")
15+
end
16+
end

app/models/patient_import_row.rb

Lines changed: 90 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,35 @@ class PatientImportRow
44
include ActiveModel::Model
55

66
MAX_FIELD_LENGTH = 300
7+
VALID_NAME_REGEX = Regexp.new(<<~REGEXP, Regexp::EXTENDED).freeze
8+
^[
9+
\\w # ASCII alphanumeric characters
10+
\u00C0-\u00D6 # Latin 1 Supplement letters
11+
\u00D8-\u00F6 # Latin 1 Supplement letters
12+
\u00F8-\u017F # Latin 1 Supplement & Latin Extended A letters
13+
\u0020 # Space
14+
\u0027 # Apostrophe
15+
\u0060 # Grave accent, will be normalised to apostrophe
16+
\u2019 # Preferred Unicode apostrophe, will be normalised to apostrophe
17+
\u02BC # Modifier apostrophe, will be normalised to apostrophe
18+
\u002E # Full stop
19+
-]+$ # Hyphen has to come at the very end, even for an extended regexp
20+
REGEXP
721

822
validate :validate_date_of_birth,
923
:validate_existing_patients,
1024
:validate_first_name,
25+
:validate_preferred_first_name,
1126
:validate_gender_code,
1227
:validate_last_name,
28+
:validate_preferred_last_name,
1329
:validate_nhs_number,
1430
:validate_parent_1_email,
31+
:validate_parent_1_name,
1532
:validate_parent_1_phone,
1633
:validate_parent_1_relationship,
1734
:validate_parent_2_email,
35+
:validate_parent_2_name,
1836
:validate_parent_2_phone,
1937
:validate_parent_2_relationship,
2038
:validate_year_group
@@ -65,14 +83,14 @@ def to_parents
6583
if parent_1_exists?
6684
{
6785
email: parent_1_email_value,
68-
full_name: parent_1_name&.to_s,
86+
full_name: parent_1_name_value,
6987
phone: parent_1_phone_value
7088
}
7189
end,
7290
if parent_2_exists?
7391
{
7492
email: parent_2_email_value,
75-
full_name: parent_2_name&.to_s,
93+
full_name: parent_2_name_value,
7694
phone: parent_2_phone_value
7795
}
7896
end
@@ -182,20 +200,22 @@ def import_attributes
182200
address_town: address_town&.to_s,
183201
birth_academic_year: birth_academic_year_value,
184202
date_of_birth: date_of_birth.to_date,
185-
family_name: last_name.to_s,
203+
family_name: ApostropheNormaliser.call(last_name.to_s),
186204
gender_code: gender_code_value,
187-
given_name: first_name.to_s,
205+
given_name: ApostropheNormaliser.call(first_name.to_s),
188206
nhs_number: nhs_number_value,
189-
preferred_family_name: preferred_last_name&.to_s,
190-
preferred_given_name: preferred_first_name&.to_s,
207+
preferred_family_name:
208+
ApostropheNormaliser.call(preferred_last_name&.to_s),
209+
preferred_given_name:
210+
ApostropheNormaliser.call(preferred_first_name&.to_s),
191211
registration: registration&.to_s,
192212
registration_academic_year:
193213
}.compact
194214
end
195215

196216
def parent_1_import_attributes
197217
{
198-
full_name: parent_1_name&.to_s,
218+
full_name: parent_1_name_value,
199219
email: parent_1_email_value,
200220
phone: parent_1_phone_value,
201221
relationship: parent_1_relationship&.to_s
@@ -204,7 +224,7 @@ def parent_1_import_attributes
204224

205225
def parent_2_import_attributes
206226
{
207-
full_name: parent_2_name&.to_s,
227+
full_name: parent_2_name_value,
208228
email: parent_2_email_value,
209229
phone: parent_2_phone_value,
210230
relationship: parent_2_relationship&.to_s
@@ -356,10 +376,18 @@ def gender_code_value
356376
gender_code&.to_s&.downcase&.gsub(" ", "_")
357377
end
358378

379+
def parent_1_name_value
380+
ApostropheNormaliser.call(parent_1_name&.to_s)
381+
end
382+
359383
def parent_1_email_value
360384
parent_1_email&.to_s&.downcase
361385
end
362386

387+
def parent_2_name_value
388+
ApostropheNormaliser.call(parent_2_name&.to_s)
389+
end
390+
363391
def parent_2_email_value
364392
parent_2_email&.to_s&.downcase
365393
end
@@ -405,6 +433,8 @@ def validate_first_name
405433
first_name.header,
406434
"is greater than #{MAX_FIELD_LENGTH} characters long"
407435
)
436+
elsif !first_name.to_s.match?(VALID_NAME_REGEX)
437+
errors.add(first_name.header, "includes invalid character(s)")
408438
end
409439
end
410440

@@ -415,6 +445,18 @@ def validate_gender_code
415445
end
416446
end
417447

448+
def validate_preferred_first_name
449+
return if preferred_first_name.blank?
450+
if preferred_first_name.to_s.length > MAX_FIELD_LENGTH
451+
errors.add(
452+
preferred_first_name.header,
453+
"is greater than #{MAX_FIELD_LENGTH} characters long"
454+
)
455+
elsif !preferred_first_name.to_s.match?(VALID_NAME_REGEX)
456+
errors.add(preferred_first_name.header, "includes invalid character(s)")
457+
end
458+
end
459+
418460
def validate_last_name
419461
if last_name.nil?
420462
errors.add(:base, "<code>CHILD_LAST_NAME</code> is missing")
@@ -425,6 +467,20 @@ def validate_last_name
425467
last_name.header,
426468
"is greater than #{MAX_FIELD_LENGTH} characters long"
427469
)
470+
elsif !last_name.to_s.match?(VALID_NAME_REGEX)
471+
errors.add(last_name.header, "includes invalid character(s)")
472+
end
473+
end
474+
475+
def validate_preferred_last_name
476+
return if preferred_last_name.blank?
477+
if preferred_last_name.to_s.length > MAX_FIELD_LENGTH
478+
errors.add(
479+
preferred_last_name.header,
480+
"is greater than #{MAX_FIELD_LENGTH} characters long"
481+
)
482+
elsif !preferred_last_name.to_s.match?(VALID_NAME_REGEX)
483+
errors.add(preferred_last_name.header, "includes invalid character(s)")
428484
end
429485
end
430486

@@ -438,6 +494,19 @@ def validate_nhs_number
438494
).validate_each(self, nhs_number.header, nhs_number_value)
439495
end
440496

497+
def validate_parent_1_name
498+
return if parent_1_name.blank?
499+
500+
if parent_1_name.to_s.length > MAX_FIELD_LENGTH
501+
errors.add(
502+
parent_1_name.header,
503+
"is greater than #{MAX_FIELD_LENGTH} characters long"
504+
)
505+
elsif !parent_1_name.to_s.match?(VALID_NAME_REGEX)
506+
errors.add(parent_1_name.header, "includes invalid character(s)")
507+
end
508+
end
509+
441510
def validate_parent_1_email
442511
return if parent_1_email.blank?
443512

@@ -465,6 +534,19 @@ def validate_parent_1_relationship
465534
end
466535
end
467536

537+
def validate_parent_2_name
538+
return if parent_2_name.blank?
539+
540+
if parent_2_name.to_s.length > MAX_FIELD_LENGTH
541+
errors.add(
542+
parent_2_name.header,
543+
"is greater than #{MAX_FIELD_LENGTH} characters long"
544+
)
545+
elsif !parent_2_name.to_s.match?(VALID_NAME_REGEX)
546+
errors.add(parent_2_name.header, "includes invalid character(s)")
547+
end
548+
end
549+
468550
def validate_parent_2_email
469551
return if parent_2_email.blank?
470552

spec/fixtures/cohort_import/invalid_fields.csv

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,5 @@ invalid,John Smith,Father,john@example.com,07412345678,,,,,Jimmy,Smith,Jim,2010-
1818
123456,John Smith,Father,john@example.com,07412345678,,,,,Jimmy,Smith,Jim,invalid,10 Downing Street,,London,SW1A 1AA,9990000018
1919
123456,John Smith,Father,john@example.com,07412345678,,,,,Jimmy,Smith,Jim,2010-01-01,10 Downing Street,,London,invalid,9990000018
2020
123456,John Smith,Father,john@example.com,07412345678,,,,,Jimmy,Smith,Jim,2010-01-01,10 Downing Street,,London,SW1A 1AA,invalid
21+
123456,John Smith,Father,john@example.com,07412345678,,,,,J£mmy,Smith,Jim,2010-01-01,10 Downing Street,,London,SW1A 1AA,9990000018
22+
123456,John Smith,Father,john@example.com,07412345678,,,,,Jimmy,Sm©th,Jim,2010-01-01,10 Downing Street,,London,SW1A 1AA,9990000018

spec/models/cohort_import_row_spec.rb

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,82 @@
123123
)
124124
end
125125
end
126+
127+
context "when the given name has invalid characters" do
128+
let(:data) { valid_data.merge("CHILD_FIRST_NAME" => "J£mmy") }
129+
130+
it "is invalid" do
131+
expect(cohort_import_row).to be_invalid
132+
expect(cohort_import_row.errors.size).to eq(1)
133+
expect(cohort_import_row.errors["CHILD_FIRST_NAME"]).to contain_exactly(
134+
"includes invalid character(s)"
135+
)
136+
end
137+
end
138+
139+
context "when the family name has invalid characters" do
140+
let(:data) { valid_data.merge("CHILD_LAST_NAME" => "ᶚmith") }
141+
142+
it "is invalid" do
143+
expect(cohort_import_row).to be_invalid
144+
expect(cohort_import_row.errors.size).to eq(1)
145+
expect(cohort_import_row.errors["CHILD_LAST_NAME"]).to contain_exactly(
146+
"includes invalid character(s)"
147+
)
148+
end
149+
end
150+
151+
context "when the preferred given name has invalid characters" do
152+
let(:data) { valid_data.merge("CHILD_PREFERRED_FIRST_NAME" => "J£m") }
153+
154+
it "is invalid" do
155+
expect(cohort_import_row).to be_invalid
156+
expect(cohort_import_row.errors.size).to eq(1)
157+
expect(
158+
cohort_import_row.errors["CHILD_PREFERRED_FIRST_NAME"]
159+
).to contain_exactly("includes invalid character(s)")
160+
end
161+
end
162+
163+
context "when the preferred family name has invalid characters" do
164+
let(:data) { valid_data.merge("CHILD_PREFERRED_LAST_NAME" => "ᶚmithy") }
165+
166+
it "is invalid" do
167+
expect(cohort_import_row).to be_invalid
168+
expect(cohort_import_row.errors.size).to eq(1)
169+
expect(
170+
cohort_import_row.errors["CHILD_PREFERRED_LAST_NAME"]
171+
).to contain_exactly("includes invalid character(s)")
172+
end
173+
end
174+
175+
context "when the first parent's full name has invalid characters" do
176+
let(:data) do
177+
valid_data.merge(parent_1_data).merge("PARENT_1_NAME" => "John© Smith")
178+
end
179+
180+
it "is invalid" do
181+
expect(cohort_import_row).to be_invalid
182+
expect(cohort_import_row.errors.size).to eq(1)
183+
expect(cohort_import_row.errors["PARENT_1_NAME"]).to contain_exactly(
184+
"includes invalid character(s)"
185+
)
186+
end
187+
end
188+
189+
context "when the second parent's full name has invalid characters" do
190+
let(:data) do
191+
valid_data.merge(parent_2_data).merge("PARENT_2_NAME" => "John© Smith")
192+
end
193+
194+
it "is invalid" do
195+
expect(cohort_import_row).to be_invalid
196+
expect(cohort_import_row.errors.size).to eq(1)
197+
expect(cohort_import_row.errors["PARENT_2_NAME"]).to contain_exactly(
198+
"includes invalid character(s)"
199+
)
200+
end
201+
end
126202
end
127203

128204
describe "#to_parents" do
@@ -195,6 +271,23 @@
195271

196272
it { should eq([existing_parent]) }
197273
end
274+
275+
context "with fancy apostrophes" do
276+
let(:data) do
277+
valid_data
278+
.merge(parent_1_data)
279+
.merge(parent_2_data)
280+
.merge(
281+
"PARENT_1_NAME" => "Jane OʼReilly",
282+
"PARENT_2_NAME" => "Jacob O`Reilly"
283+
)
284+
end
285+
286+
it "converts fancy apostrophes to plain apostrophes" do
287+
expect(parents.first.full_name).to eq("Jane O'Reilly")
288+
expect(parents.second.full_name).to eq("Jacob O'Reilly")
289+
end
290+
end
198291
end
199292

200293
describe "#to_patient" do
@@ -747,4 +840,28 @@
747840
end
748841
end
749842
end
843+
844+
describe "#import_attributes" do
845+
context "with fancy apostrophes" do
846+
let(:data) do
847+
valid_data.merge(
848+
{
849+
"CHILD_FIRST_NAME" => "Mickʼee",
850+
"CHILD_LAST_NAME" => "OʼReilly",
851+
"CHILD_PREFERRED_FIRST_NAME" => "Mickʼee",
852+
"CHILD_PREFERRED_LAST_NAME" => "OʼReilly"
853+
}
854+
)
855+
end
856+
857+
it "converts fancy apostrophes to plain apostrophes" do
858+
expect(cohort_import_row.import_attributes).to include(
859+
given_name: "Mick'ee",
860+
family_name: "O'Reilly",
861+
preferred_given_name: "Mick'ee",
862+
preferred_family_name: "O'Reilly"
863+
)
864+
end
865+
end
866+
end
750867
end

spec/models/cohort_import_spec.rb

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,10 @@
7474
expect(cohort_import).to be_invalid
7575
expect(cohort_import.rows).not_to be_empty
7676
end
77+
78+
it "is invalid" do
79+
expect(cohort_import).not_to be_valid
80+
end
7781
end
7882

7983
describe "with unrecognised fields" do
@@ -400,5 +404,23 @@
400404
expect { process! }.to change(session.patients, :count).from(0).to(2)
401405
end
402406
end
407+
408+
context "with invalid fields" do
409+
before { process! }
410+
411+
let(:file) { "invalid_fields.csv" }
412+
413+
describe "error for row with first name having invalid characters" do
414+
subject(:child_first_name) { cohort_import.errors[:row_21][0][0] }
415+
416+
it { should include "includes invalid character(s)" }
417+
end
418+
419+
describe "error for row with last name having invalid characters" do
420+
subject(:child_last_name) { cohort_import.errors[:row_22][0][0] }
421+
422+
it { should include "includes invalid character(s)" }
423+
end
424+
end
403425
end
404426
end

0 commit comments

Comments
 (0)