Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions app/controllers/api/schools_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ def school_params
:reference,
:district_name,
:district_nces_id,
:school_roll_number,
:address_line_1,
:address_line_2,
:municipality,
Expand Down
3 changes: 3 additions & 0 deletions app/dashboards/school_dashboard.rb
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ class SchoolDashboard < Administrate::BaseDashboard
reference: Field::String,
district_name: Field::String,
district_nces_id: Field::String,
school_roll_number: Field::String,
verified_at: Field::DateTime,
rejected_at: Field::DateTime,
created_at: Field::DateTime,
Expand Down Expand Up @@ -62,6 +63,7 @@ class SchoolDashboard < Administrate::BaseDashboard
reference
district_name
district_nces_id
school_roll_number
website
address_line_1
address_line_2
Expand All @@ -86,6 +88,7 @@ class SchoolDashboard < Administrate::BaseDashboard
reference
district_name
district_nces_id
school_roll_number
website
address_line_1
address_line_2
Expand Down
15 changes: 15 additions & 0 deletions app/models/school.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ class School < ApplicationRecord
validates :country_code, presence: true, inclusion: { in: ISO3166::Country.codes }
validates :reference, uniqueness: { case_sensitive: false, allow_nil: true }, presence: false
validates :district_nces_id, uniqueness: { case_sensitive: false, allow_nil: true }, presence: false
validates :school_roll_number,
uniqueness: { conditions: -> { where(rejected_at: nil) }, case_sensitive: false, allow_nil: true },
presence: { if: :ireland? },
format: { with: /\A[0-9]+[A-Z]+\z/, allow_nil: true, message: I18n.t('validations.school.school_roll_number') }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we have a reference anywhere that shows us what the format of an Irish school roll number should be so we can verify this regex is correct?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The 3rd AC in the ticket mentions 'Given the "School Roll Number" field is visible, when I enter a valid alpha-numeric value (e.g., 01572D), then the data is accepted and saved with the application.'

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is that example meant to show what the precise format will be? I was thinking more of something official

Copy link
Contributor Author

@Mohamed-RPF Mohamed-RPF Dec 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These are some examples from the gov.ie website: 20375I, 20374G, 20378O, 20371A

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I checked public sources and there’s no official published spec for the roll-number format.
In the Department’s datasets, roll numbers consistently appear as 5 digits followed by an uppercase letter (e.g. 01572D).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

okay great, thanks for looking into this 👍

validates :creator_id, presence: true, uniqueness: true
validates :creator_agree_authority, presence: true, acceptance: true
validates :creator_agree_terms_and_conditions, presence: true, acceptance: true
Expand All @@ -34,6 +38,7 @@ class School < ApplicationRecord

before_validation :normalize_reference
before_validation :normalize_district_fields
before_validation :normalize_school_roll_number

before_save :format_uk_postal_code, if: :should_format_uk_postal_code?

Expand Down Expand Up @@ -102,6 +107,12 @@ def normalize_district_fields
self.district_nces_id = nil if district_nces_id.blank?
end

# Ensure the school_roll_number is nil, not an empty string
# Also normalize to uppercase for consistent validation
def normalize_school_roll_number
self.school_roll_number = school_roll_number.blank? ? nil : school_roll_number.upcase
end

def verified_at_cannot_be_changed
errors.add(:verified_at, 'cannot be changed after verification') if verified_at_was.present? && verified_at_changed?
end
Expand All @@ -118,6 +129,10 @@ def should_format_uk_postal_code?
country_code == 'GB' && postal_code.to_s.length >= 5
end

def ireland?
country_code == 'IE'
end

def format_uk_postal_code
cleaned_postal_code = postal_code.delete(' ')
# insert a space as the third-from-last character in the postcode, eg. SW1A1AA -> SW1A 1AA
Expand Down
1 change: 1 addition & 0 deletions app/views/api/schools/_school.json.jbuilder
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ json.call(
:reference,
:district_name,
:district_nces_id,
:school_roll_number,
:address_line_1,
:address_line_2,
:municipality,
Expand Down
1 change: 1 addition & 0 deletions config/locales/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ en:
validations:
school:
website: "must be a valid URL"
school_roll_number: "must be numbers followed by letters (e.g., 01572D)"
invitation:
email_address: "'%<value>s' is invalid"
activerecord:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# frozen_string_literal: true

class AddSchoolRollNumberToSchools < ActiveRecord::Migration[7.2]
def change
add_column :schools, :school_roll_number, :string

add_index :schools, :school_roll_number, unique: true
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# frozen_string_literal: true

class ChangeSchoolRollNumberIndexToPartial < ActiveRecord::Migration[7.2]
def change
remove_index :schools, :school_roll_number
add_index :schools, :school_roll_number, unique: true, where: 'rejected_at IS NULL'
end
end
4 changes: 3 additions & 1 deletion db/schema.rb

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions spec/factories/school.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
address_line_1 { 'Address Line 1' }
municipality { 'Greater London' }
country_code { 'GB' }
school_roll_number { nil }
creator_id { SecureRandom.uuid }
creator_agree_authority { true }
creator_agree_terms_and_conditions { true }
Expand Down
2 changes: 1 addition & 1 deletion spec/features/my_school/showing_my_school_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@

it "includes the school details and user's roles in the JSON" do
school_json = school.to_json(only: %i[
id name website reference address_line_1 address_line_2 municipality administrative_area postal_code country_code code verified_at created_at updated_at district_name district_nces_id
id name website reference address_line_1 address_line_2 municipality administrative_area postal_code country_code code verified_at created_at updated_at district_name district_nces_id school_roll_number
])
expected_data = JSON.parse(school_json, symbolize_names: true).merge(roles: ['owner'], import_in_progress: school.import_in_progress?)

Expand Down
71 changes: 71 additions & 0 deletions spec/models/school_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,77 @@
expect(duplicate_school).not_to be_valid
end

it 'does not require a school_roll_number for non-Ireland schools' do
school.country_code = 'GB'
school.school_roll_number = nil
expect(school).to be_valid
end

it 'requires school_roll_number for Ireland schools' do
school.country_code = 'IE'
school.school_roll_number = nil
expect(school).not_to be_valid
expect(school.errors[:school_roll_number]).to include("can't be blank")
end

it 'requires school_roll_number to be unique if provided' do
school.school_roll_number = '01572D'
school.save!

duplicate_school = build(:school, school_roll_number: '01572d')
expect(duplicate_school).not_to be_valid
end

it 'accepts a valid alphanumeric school_roll_number' do
school.school_roll_number = '01572D'
expect(school).to be_valid
end

it 'accepts a school_roll_number with one or more letters' do
school.school_roll_number = '12345ABC'
expect(school).to be_valid
end

it 'rejects a school_roll_number with only numbers' do
school.school_roll_number = '01572'
expect(school).not_to be_valid
expect(school.errors[:school_roll_number]).to include('must be numbers followed by letters (e.g., 01572D)')
end

it 'rejects a school_roll_number with only letters' do
school.school_roll_number = 'ABCDE'
expect(school).not_to be_valid
expect(school.errors[:school_roll_number]).to include('must be numbers followed by letters (e.g., 01572D)')
end

it 'rejects a school_roll_number with special characters' do
school.school_roll_number = '01572-D'
expect(school).not_to be_valid
expect(school.errors[:school_roll_number]).to include('must be numbers followed by letters (e.g., 01572D)')
end

it 'normalizes blank school_roll_number to nil' do
school.school_roll_number = ' '
expect(school).to be_valid
expect(school.school_roll_number).to be_nil
end

it 'normalizes school_roll_number to uppercase' do
school.school_roll_number = '01572d'
expect(school).to be_valid
expect(school.school_roll_number).to eq('01572D')
end

it 'allows school_roll_number reuse when original school is rejected' do
school.school_roll_number = '01572D'
school.save!
school.reject

new_school = build(:school, school_roll_number: '01572D')
expect(new_school).to be_valid
expect { new_school.save! }.not_to raise_error
end

it 'requires an address_line_1' do
school.address_line_1 = ' '
expect(school).not_to be_valid
Expand Down