Skip to content

Commit 02be4f3

Browse files
Add class code to the school class model (#521)
closes #518 --------- Co-authored-by: create-issue-branch[bot] <53036503+create-issue-branch[bot]@users.noreply.github.com> Co-authored-by: Lois Wells <[email protected]> Co-authored-by: Lois Wells <[email protected]>
1 parent 21602cc commit 02be4f3

13 files changed

+135
-9
lines changed

app/models/school.rb

+1-1
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ def rejected?
5454
def verify!
5555
attempts = 0
5656
begin
57-
update!(verified_at: Time.zone.now, code: SchoolCodeGenerator.generate)
57+
update!(verified_at: Time.zone.now, code: ForEducationCodeGenerator.generate)
5858
rescue ActiveRecord::RecordInvalid => e
5959
raise unless e.record.errors[:code].include?('has already been taken') && attempts <= 5
6060

app/models/school_class.rb

+23
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,11 @@ class SchoolClass < ApplicationRecord
99

1010
scope :with_teachers, ->(user_id) { joins(:teachers).where(teachers: { id: user_id }) }
1111

12+
before_validation :assign_class_code, on: :create
13+
1214
validates :name, presence: true
15+
validates :code, uniqueness: { scope: :school_id }, presence: true, format: { with: /\d\d-\d\d-\d\d/, allow_nil: false }
16+
validate :code_cannot_be_changed
1317
validate :school_class_has_at_least_one_teacher
1418

1519
has_paper_trail(
@@ -36,11 +40,30 @@ def with_teachers
3640
[self, User.from_userinfo(ids: teacher_ids)]
3741
end
3842

43+
def assign_class_code
44+
return if code.present?
45+
46+
5.times do
47+
self.code = ForEducationCodeGenerator.generate
48+
return if code_is_unique_within_school
49+
end
50+
51+
errors.add(:code, 'could not be generated')
52+
end
53+
3954
private
4055

4156
def school_class_has_at_least_one_teacher
4257
return if teachers.present?
4358

4459
errors.add(:teachers, 'must have at least one teacher')
4560
end
61+
62+
def code_cannot_be_changed
63+
errors.add(:code, 'cannot be changed after verification') if code_was.present? && code_changed?
64+
end
65+
66+
def code_is_unique_within_school
67+
code.present? && SchoolClass.where(code:, school:).none?
68+
end
4669
end

app/views/api/school_classes/show.json.jbuilder

+1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ json.call(
88
:description,
99
:school_id,
1010
:name,
11+
:code,
1112
:created_at,
1213
:updated_at
1314
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
class AddSchoolClassCode < ActiveRecord::Migration[7.1]
2+
def change
3+
add_column :school_classes, :code, :string
4+
add_index :school_classes, [:code, :school_id], unique: true
5+
end
6+
end
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
class AssignCodeToExistingClasses < ActiveRecord::Migration[7.1]
2+
def up
3+
SchoolClass.find_each do |school_class|
4+
school_class.assign_class_code
5+
school_class.save!
6+
end
7+
end
8+
9+
def down
10+
SchoolClass.update_all(code: nil)
11+
end
12+
end

db/schema.rb

+3-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

lib/school_code_generator.rb lib/for_education_code_generator.rb

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# frozen_string_literal: true
22

3-
class SchoolCodeGenerator
3+
class ForEducationCodeGenerator
44
MAX_CODE = 1_000_000
55

66
def self.generate

spec/factories/school.rb

+1-1
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,6 @@
1414

1515
factory :verified_school, parent: :school do
1616
verified_at { Time.current }
17-
code { SchoolCodeGenerator.generate }
17+
code { ForEducationCodeGenerator.generate }
1818
end
1919
end

spec/factories/school_class.rb

+1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
FactoryBot.define do
44
factory :school_class do
55
sequence(:name) { |n| "Class #{n}" }
6+
code { ForEducationCodeGenerator.generate }
67

78
transient do
89
teacher_ids { [SecureRandom.uuid] }

spec/features/school_class/showing_a_school_class_spec.rb

+7
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,13 @@
4242
expect(data[:name]).to eq('Test School Class')
4343
end
4444

45+
it 'includes the school class code in the response' do
46+
get("/api/schools/#{school.id}/classes/#{school_class.id}", headers:)
47+
data = JSON.parse(response.body, symbolize_names: true)
48+
49+
expect(data[:code]).to eq(school_class.code)
50+
end
51+
4552
it 'responds with the teacher JSON' do
4653
get("/api/schools/#{school.id}/classes/#{school_class.id}", headers:)
4754
data = JSON.parse(response.body, symbolize_names: true)

spec/lib/school_code_generator_spec.rb spec/lib/for_education_code_generator_spec.rb

+2-2
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,11 @@
22

33
require 'rails_helper'
44

5-
RSpec.describe SchoolCodeGenerator do
5+
RSpec.describe ForEducationCodeGenerator do
66
describe '.generate' do
77
it 'uses Random#rand to generate a random number up to the maximum' do
88
random = instance_double(Random)
9-
allow(random).to receive(:rand).with(SchoolCodeGenerator::MAX_CODE).and_return(123)
9+
allow(random).to receive(:rand).with(ForEducationCodeGenerator::MAX_CODE).and_return(123)
1010
allow(Random).to receive(:new).and_return(random)
1111

1212
expect(described_class.generate).to eq('00-01-23')

spec/models/school_class_spec.rb

+74
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,42 @@
6363
school_class.name = ' '
6464
expect(school_class).to be_invalid
6565
end
66+
67+
it 'assigns class code before validating' do
68+
school_class.code = nil
69+
school_class.valid?
70+
expect(school_class.code).to match(/\d\d-\d\d-\d\d/)
71+
end
72+
73+
it 'requires a unique class code within the same school' do
74+
school_class.save!
75+
school_class_with_duplicate_code = build(:school_class, school: school_class.school, code: school_class.code)
76+
school_class_with_duplicate_code.valid?
77+
expect(school_class_with_duplicate_code.errors[:code]).to include('has already been taken')
78+
end
79+
80+
it 'permits a duplicate class code in a different school' do
81+
school_class.save!
82+
school_class_with_duplicate_code = build(:school_class, school: build(:school), code: school_class.code)
83+
expect(school_class_with_duplicate_code).to be_valid
84+
end
85+
86+
it 'requires a valid class code format' do
87+
school_class.code = 'invalid'
88+
expect(school_class).to be_invalid
89+
end
90+
91+
it 'accepts a valid class code format' do
92+
school_class.code = '12-34-56'
93+
expect(school_class).to be_valid
94+
end
95+
96+
it 'does not allow the class code to be changed' do
97+
school_class.code = '12-34-56'
98+
school_class.save!
99+
school_class.code = '65-43-21'
100+
expect(school_class).to be_invalid
101+
end
66102
end
67103

68104
describe '.teachers' do
@@ -141,6 +177,44 @@
141177
end
142178
end
143179

180+
describe '#assign_class_code' do
181+
it 'assigns a class code if not already present' do
182+
school_class = build(:school_class, code: nil, school:)
183+
school_class.assign_class_code
184+
expect(school_class.code).to match(/\d\d-\d\d-\d\d/)
185+
end
186+
187+
it 'does not assign a class code if already present' do
188+
school_class = build(:school_class, code: '12-34-56', school:)
189+
school_class.assign_class_code
190+
expect(school_class.code).to eq('12-34-56')
191+
end
192+
193+
it 'retries 5 times if the school code is not unique within the school' do
194+
school_class = create(:school_class, code: '12-34-56', school:)
195+
allow(ForEducationCodeGenerator).to receive(:generate).and_return(*([school_class.code] * 4), '00-00-00')
196+
another_school_class = create(:school_class, school: school_class.school, code: nil)
197+
another_school_class.assign_class_code
198+
expect(another_school_class.code).to eq('00-00-00')
199+
end
200+
201+
it 'raises adds error if unique code cannot be generated in 5 retries' do
202+
school_class = create(:school_class, code: '12-34-56', school:)
203+
allow(ForEducationCodeGenerator).to receive(:generate).and_return(*([school_class.code] * 5))
204+
another_school_class = build(:school_class, school: school_class.school, code: nil)
205+
another_school_class.assign_class_code
206+
expect(another_school_class.errors[:code]).to include('could not be generated')
207+
end
208+
209+
it 'does not add error if class code shared by class from another school' do
210+
school_class = create(:school_class, code: '12-34-56', school:)
211+
allow(ForEducationCodeGenerator).to receive(:generate).and_return(school_class.code)
212+
another_school_class = build(:school_class, school: build(:school), code: nil)
213+
another_school_class.assign_class_code
214+
expect(another_school_class.errors[:code]).to be_empty
215+
end
216+
end
217+
144218
describe 'auditing' do
145219
subject(:school_class) { create(:school_class, teacher_ids: [teacher.id], school:) }
146220

spec/models/school_spec.rb

+3-3
Original file line numberDiff line numberDiff line change
@@ -309,22 +309,22 @@
309309
end
310310

311311
it 'uses the school code generator to generates and set the code' do
312-
allow(SchoolCodeGenerator).to receive(:generate).and_return('00-00-00')
312+
allow(ForEducationCodeGenerator).to receive(:generate).and_return('00-00-00')
313313
school.verify!
314314
expect(school.code).to eq('00-00-00')
315315
end
316316

317317
it 'retries 5 times if the school code is not unique' do
318318
school.verify!
319-
allow(SchoolCodeGenerator).to receive(:generate).and_return(school.code, school.code, school.code, school.code, '00-00-00')
319+
allow(ForEducationCodeGenerator).to receive(:generate).and_return(school.code, school.code, school.code, school.code, '00-00-00')
320320
another_school = create(:school)
321321
another_school.verify!
322322
expect(another_school.code).to eq('00-00-00')
323323
end
324324

325325
it 'raises exception if unique code cannot be generated in 5 retries' do
326326
school.verify!
327-
allow(SchoolCodeGenerator).to receive(:generate).and_return(school.code, school.code, school.code, school.code, school.code)
327+
allow(ForEducationCodeGenerator).to receive(:generate).and_return(school.code, school.code, school.code, school.code, school.code)
328328
another_school = create(:school)
329329
expect { another_school.verify! }.to raise_error(ActiveRecord::RecordInvalid)
330330
end

0 commit comments

Comments
 (0)