Skip to content
Open
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 Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -558,6 +558,7 @@ GEM
PLATFORMS
aarch64-linux
arm64-darwin-24
arm64-darwin-25
x86_64-linux

DEPENDENCIES
Expand Down
5 changes: 4 additions & 1 deletion app/controllers/api/schools_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,10 @@ def create
@school = result[:school]
render :show, formats: [:json], status: :created
else
render json: { error: result[:error] }, status: :unprocessable_entity
render json: {
error: result[:error],
error_types: result[:error_types]
}, status: :unprocessable_entity
end
end

Expand Down
5 changes: 4 additions & 1 deletion app/jobs/school_import_job.rb
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,10 @@ def build_school_params(school_data)
creator_agree_authority: true,
creator_agree_terms_and_conditions: true,
creator_agree_responsible_safeguarding: true,
user_origin: 'experience_cs'
user_origin: 'experience_cs',
district_name: school_data[:district_name],
district_nces_id: school_data[:district_nces_id],
school_roll_number: school_data[:school_roll_number]
}.compact
end

Expand Down
27 changes: 22 additions & 5 deletions app/models/school.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,21 @@ class School < ApplicationRecord
validates :address_line_1, presence: true
validates :municipality, presence: true
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 :reference,
uniqueness: { conditions: -> { where(rejected_at: nil) }, case_sensitive: false, allow_blank: true, message: I18n.t('validations.school.reference_urn_exists') },
format: { with: /\A\d{5,6}\z/, allow_nil: true, message: I18n.t('validations.school.reference') },
if: :united_kingdom?
validates :district_nces_id,
uniqueness: { conditions: -> { where(rejected_at: nil) }, case_sensitive: false, allow_blank: true, message: I18n.t('validations.school.district_nces_id_exists') },
Copy link

Copilot AI Jan 6, 2026

Choose a reason for hiding this comment

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

The format validation uses allow_nil: true, but the uniqueness validation uses allow_blank: true. This creates an inconsistency where an empty string would pass uniqueness validation but fail format validation.

Since there's a normalize_district_fields callback (lines 114-117) that converts blank values to nil, both validations should use allow_nil: true for consistency, or the format validation should also use allow_blank: true.

Suggested change
uniqueness: { conditions: -> { where(rejected_at: nil) }, case_sensitive: false, allow_blank: true, message: I18n.t('validations.school.district_nces_id_exists') },
uniqueness: { conditions: -> { where(rejected_at: nil) }, case_sensitive: false, allow_nil: true, message: I18n.t('validations.school.district_nces_id_exists') },

Copilot uses AI. Check for mistakes.
format: { with: /\A\d{12}\z/, allow_nil: true, message: I18n.t('validations.school.district_nces_id') },
presence: true,
if: :united_states?
validates :district_name, presence: true, if: :united_states?
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') }
uniqueness: { conditions: -> { where(rejected_at: nil) }, case_sensitive: false, allow_blank: true, message: I18n.t('validations.school.school_roll_number_exists') },
format: { with: /\A[0-9]+[A-Z]+\z/, allow_nil: true, message: I18n.t('validations.school.school_roll_number') },
presence: true,
if: :ireland?
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 Down Expand Up @@ -129,6 +138,14 @@ def should_format_uk_postal_code?
country_code == 'GB' && postal_code.to_s.length >= 5
end

def united_kingdom?
country_code == 'GB'
end

def united_states?
country_code == 'US'
end

def ireland?
country_code == 'IE'
end
Expand Down
5 changes: 5 additions & 0 deletions config/locales/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,12 @@ en:
validations:
school:
website: "must be a valid URL"
reference: "must be 5-6 digits (e.g., 100000)"
reference_urn_exists: "URN number already exists"
Copy link

Copilot AI Jan 6, 2026

Choose a reason for hiding this comment

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

The custom uniqueness error message "URN number already exists" is inconsistent with the standard Rails error messages used elsewhere. According to the PR description's API Error Responses table, the uniqueness error should be "has already been taken" to match Rails conventions.

Additionally, using "URN number" is redundant since URN already stands for "Unique Reference Number". Consider using either "URN has already been taken" or "reference has already been taken" for consistency.

Suggested change
reference_urn_exists: "URN number already exists"
reference_urn_exists: "URN has already been taken"

Copilot uses AI. Check for mistakes.
district_nces_id: "must be 12 digits (e.g., 010000000001)"
district_nces_id_exists: "NCES ID already exists"
Copy link

Copilot AI Jan 6, 2026

Choose a reason for hiding this comment

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

The custom uniqueness error message "NCES ID already exists" is inconsistent with the standard Rails error messages. According to the PR description's API Error Responses table, the uniqueness error should be "has already been taken" to match Rails conventions.

Consider changing this to "has already been taken" for consistency with Rails standards and the documented API response format.

Suggested change
district_nces_id_exists: "NCES ID already exists"
district_nces_id_exists: "has already been taken"

Copilot uses AI. Check for mistakes.
school_roll_number: "must be numbers followed by letters (e.g., 01572D)"
school_roll_number_exists: "School roll number already exists"
Copy link

Copilot AI Jan 6, 2026

Choose a reason for hiding this comment

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

The custom uniqueness error message "School roll number already exists" is inconsistent with the standard Rails error messages. According to the PR description's API Error Responses table and Rails conventions, the uniqueness error should be "has already been taken".

Consider changing this to "has already been taken" for consistency with Rails standards.

Suggested change
school_roll_number_exists: "School roll number already exists"
school_roll_number_exists: "has already been taken"

Copilot uses AI. Check for mistakes.
invitation:
email_address: "'%<value>s' is invalid"
activerecord:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# frozen_string_literal: true

class ChangeReferenceAndNcesIdIndexesToPartial < ActiveRecord::Migration[7.2]
def change
# Convert reference (UK URN) index to partial index
# This allows rejected schools to release their URN for reuse
remove_index :schools, :reference
add_index :schools, :reference, unique: true, where: 'rejected_at IS NULL'

# Convert district_nces_id (US NCES ID) index to partial index
# This allows rejected schools to release their NCES ID for reuse
remove_index :schools, :district_nces_id
add_index :schools, :district_nces_id, unique: true, where: 'rejected_at IS NULL'
end
end

6 changes: 3 additions & 3 deletions db/schema.rb

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

2 changes: 2 additions & 0 deletions lib/concepts/school/operations/create.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ def call(school_params:, creator_id:)
rescue StandardError => e
Sentry.capture_exception(e)
response[:error] = response[:school].errors
response[:error_types] = response[:school].errors.details

response
end

Expand Down
10 changes: 9 additions & 1 deletion lib/tasks/seeds_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,23 @@ module SeedsHelper
def create_school(creator_id, school_id = nil)
School.find_or_create_by!(creator_id:, id: school_id) do |school|
Rails.logger.info 'Seeding a school...'
country_code = Faker::Address.country_code
school.name = Faker::Educator.secondary_school
school.website = Faker::Internet.url(scheme: 'https')
school.address_line_1 = Faker::Address.street_address
school.municipality = Faker::Address.city
school.country_code = Faker::Address.country_code
school.country_code = country_code
school.creator_id = creator_id
school.creator_agree_authority = true
school.creator_agree_terms_and_conditions = true
school.creator_agree_to_ux_contact = true
# Country-specific required fields
school.reference = format('%06d', rand(100_000..999_999)) if country_code == 'GB'
if country_code == 'US'
school.district_nces_id = format('%012d', rand(10**12))
school.district_name = Faker::Address.community
end
school.school_roll_number = "#{rand(10_000..99_999)}#{('A'..'Z').to_a.sample}" if country_code == 'IE'
end
end

Expand Down
1 change: 1 addition & 0 deletions spec/concepts/school/create_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
address_line_1: 'Address Line 1',
municipality: 'Greater London',
country_code: 'GB',
reference: '100000',
creator_agree_authority: true,
creator_agree_terms_and_conditions: true,
creator_agree_to_ux_contact: true,
Expand Down
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' }
sequence(:reference) { |n| format('%06d', 100_000 + n) }
school_roll_number { nil }
creator_id { SecureRandom.uuid }
creator_agree_authority { true }
Expand Down
2 changes: 1 addition & 1 deletion spec/features/admin/schools_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@

describe 'when the school is in the United States and has a postal code' do
before do
school.update(country_code: 'US', postal_code: '90210')
school.update(country_code: 'US', postal_code: '90210', district_name: 'Some District', district_nces_id: '010000000001', reference: nil)
get admin_school_path(school)
end

Expand Down
1 change: 1 addition & 0 deletions spec/features/school/creating_a_school_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
address_line_1: 'Address Line 1',
municipality: 'Greater London',
country_code: 'GB',
reference: '100000',
creator_agree_authority: true,
creator_agree_terms_and_conditions: true,
creator_agree_to_ux_contact: true,
Expand Down
12 changes: 9 additions & 3 deletions spec/jobs/school_import_job_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,19 @@
address_line_1: '123 Main St',
municipality: 'Springfield',
country_code: 'US',
owner_email: '[email protected]'
owner_email: '[email protected]',
district_name: 'Some District',
district_nces_id: '010000000001'
},
{
name: 'Test School 2',
website: 'https://test2.example.com',
address_line_1: '456 Oak Ave',
municipality: 'Boston',
country_code: 'US',
owner_email: '[email protected]'
owner_email: '[email protected]',
district_name: 'Other District',
district_nces_id: '010000000002'
}
]
end
Expand Down Expand Up @@ -124,7 +128,9 @@
'address_line_1' => '123 Main St',
'municipality' => 'Springfield',
'country_code' => 'us',
'owner_email' => '[email protected]'
'owner_email' => '[email protected]',
'district_name' => 'Some District',
'district_nces_id' => '010000000001'
}
]
end
Expand Down
Loading