Skip to content

Commit 4e416cb

Browse files
committed
Be careful to only delete students with a role in the current school.
1 parent e55a1bd commit 4e416cb

File tree

2 files changed

+148
-26
lines changed

2 files changed

+148
-26
lines changed

app/services/student_removal_service.rb

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,21 +11,25 @@ def initialize(students:, school:, remove_from_profile: false, token: nil)
1111
# Returns an array of hashes, one per student, with details of what was removed
1212
def remove_students
1313
results = []
14+
1415
@students.each do |user_id|
16+
# Ensure that the student has a role in this school and skip if not.
17+
student_roles = Role.student.where(user_id:, school_id: @school.id)
18+
next if student_roles.empty?
19+
1520
result = { user_id: }
1621
begin
1722
ActiveRecord::Base.transaction do
18-
# Delete all projects
23+
# Delete all projects for this user
1924
projects = Project.where(user_id: user_id)
2025
projects.destroy_all
2126

2227
# Remove from classes
23-
class_assignments = ClassStudent.where(student_id: user_id)
28+
class_assignments = ClassStudent.joins(:school_class).where(student_id: user_id, school_class: { school_id: @school.id })
2429
class_assignments.destroy_all
2530

2631
# Remove roles
27-
roles = Role.student.where(user_id: user_id)
28-
roles.destroy_all
32+
student_roles.destroy_all
2933
end
3034

3135
# Remove from profile if requested

spec/services/student_removal_service_spec.rb

Lines changed: 140 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
describe StudentRemovalService do
66
let(:school) { create(:school) }
7+
let(:other_school) { create(:school) }
78
let(:owner) { create(:owner, school: school) }
89
let(:teacher) { create(:teacher, school: school) }
910
let(:student) { create(:student, school: school) }
@@ -15,32 +16,149 @@
1516
create(:class_student, student_id: student.id, school_class: school_class)
1617
end
1718

18-
it 'removes student from classes, roles, and deletes all projects' do
19-
create(:project, user_id: student.id)
19+
describe '#remove_students' do
20+
context 'when student has a role in the school' do
21+
it 'removes student from classes, roles, and deletes all projects' do
22+
school_project = create(:project, user_id: student.id, school: school)
23+
personal_project = create(:project, user_id: student.id, school: nil)
2024

21-
expect(Role.where(user_id: student.id, role: :student)).to exist
22-
expect(ClassStudent.where(student_id: student.id)).to exist
23-
expect(Project.where(user_id: student.id)).to exist
25+
expect(Role.where(user_id: student.id, role: :student, school_id: school.id)).to exist
26+
expect(ClassStudent.where(student_id: student.id)).to exist
27+
expect(Project.where(user_id: student.id)).to exist
2428

25-
results = service.remove_students
29+
results = service.remove_students
2630

27-
expect(results.first[:user_id]).to eq(student.id)
28-
expect(results.first[:error]).to be_nil
29-
expect(ClassStudent.where(student_id: student.id)).not_to exist
30-
expect(Role.where(user_id: student.id, role: :student)).not_to exist
31-
expect(Project.where(user_id: student.id)).not_to exist
32-
end
31+
expect(results.first[:user_id]).to eq(student.id)
32+
expect(results.first[:error]).to be_nil
33+
expect(ClassStudent.where(student_id: student.id)).not_to exist
34+
expect(Role.where(user_id: student.id, role: :student, school_id: school.id)).not_to exist
35+
expect(Project.exists?(school_project.id)).to be false
36+
expect(Project.exists?(personal_project.id)).to be false
37+
end
3338

34-
it 'calls ProfileApiClient if remove_from_profile is true and token is present' do
35-
token = 'abc123'
36-
service = described_class.new(students: [student.id], school: school, remove_from_profile: true, token: token)
37-
service.remove_students
38-
expect(ProfileApiClient).to have_received(:delete_school_student).with(token: token, school_id: school.id, student_id: student.id)
39-
end
39+
it 'only deletes class assignments for the current school' do
40+
# Create a different student for the other school
41+
other_student = create(:student, school: other_school)
42+
other_school_class = create(:school_class, school: other_school)
43+
other_class_assignment = create(:class_student, student_id: other_student.id, school_class: other_school_class)
44+
45+
# Original student should be removed from this school only
46+
results = service.remove_students
47+
48+
expect(results.first[:error]).to be_nil
49+
expect(ClassStudent.joins(:school_class).where(student_id: student.id, school_class: { school_id: school.id })).not_to exist
50+
# Other student's assignment should remain
51+
expect(ClassStudent.exists?(other_class_assignment.id)).to be true
52+
end
53+
54+
it 'only deletes roles for the current school' do
55+
# Create a different student for the other school
56+
other_student = create(:student, school: other_school)
57+
58+
results = service.remove_students
59+
60+
expect(results.first[:error]).to be_nil
61+
expect(Role.where(user_id: student.id, role: :student, school_id: school.id)).not_to exist
62+
# Other student's role should remain
63+
expect(Role.where(user_id: other_student.id, school_id: other_school.id)).to exist
64+
end
65+
66+
it 'deletes all projects for the user regardless of school' do
67+
school_project = create(:project, user_id: student.id, school: school)
68+
personal_project = create(:project, user_id: student.id, school: nil)
69+
70+
# Create a different student for the other school
71+
other_student = create(:student, school: other_school)
72+
other_school_project = create(:project, user_id: other_student.id, school: other_school)
73+
74+
results = service.remove_students
75+
76+
expect(results.first[:error]).to be_nil
77+
expect(Project.exists?(school_project.id)).to be false
78+
expect(Project.exists?(personal_project.id)).to be false
79+
# Other student's project should remain
80+
expect(Project.exists?(other_school_project.id)).to be true
81+
end
82+
end
83+
84+
context 'when student does not have a role in the school' do
85+
let(:student_without_role) { create(:user) }
86+
let(:service) { described_class.new(students: [student_without_role.id], school: school) }
87+
88+
it 'skips the student and does not remove anything' do
89+
results = service.remove_students
90+
91+
expect(results).to be_empty
92+
end
93+
end
94+
95+
context 'when processing multiple students' do
96+
let(:second_student) { create(:student, school: school) }
97+
let(:student_without_role) { create(:user) }
98+
let(:service) { described_class.new(students: [student.id, second_student.id, student_without_role.id], school: school) }
99+
100+
before do
101+
create(:class_student, student_id: second_student.id, school_class: school_class)
102+
end
103+
104+
it 'processes students with roles and skips those without' do
105+
create(:project, user_id: student.id, school: school)
106+
create(:project, user_id: second_student.id, school: school)
107+
108+
results = service.remove_students
109+
110+
expect(results.length).to eq(2)
111+
expect(results.pluck(:user_id)).to contain_exactly(student.id, second_student.id)
112+
expect(Role.where(user_id: student.id, school: school, role: :student)).not_to exist
113+
expect(Role.where(user_id: second_student.id, school: school, role: :student)).not_to exist
114+
end
115+
end
116+
117+
context 'with profile API integration' do
118+
it 'calls ProfileApiClient if remove_from_profile is true and token is present' do
119+
token = 'abc123'
120+
service = described_class.new(students: [student.id], school: school, remove_from_profile: true, token: token)
121+
service.remove_students
122+
expect(ProfileApiClient).to have_received(:delete_school_student).with(token: token, school_id: school.id, student_id: student.id)
123+
end
124+
125+
it 'does not call ProfileApiClient if remove_from_profile is false' do
126+
service = described_class.new(students: [student.id], school: school, remove_from_profile: false, token: 'token')
127+
service.remove_students
128+
expect(ProfileApiClient).not_to have_received(:delete_school_student)
129+
end
130+
131+
it 'does not call ProfileApiClient if token is not present' do
132+
service = described_class.new(students: [student.id], school: school, remove_from_profile: true, token: nil)
133+
service.remove_students
134+
expect(ProfileApiClient).not_to have_received(:delete_school_student)
135+
end
136+
end
137+
138+
context 'when handling errors' do
139+
it 'handles errors gracefully' do
140+
allow(ClassStudent).to receive(:joins).and_raise(StandardError, 'fail')
141+
results = service.remove_students
142+
expect(results.first[:error]).to match(/StandardError: fail/)
143+
end
144+
145+
it 'continues processing other students after an error' do
146+
second_student = create(:student, school: school)
147+
service = described_class.new(students: [student.id, second_student.id], school: school)
148+
149+
# Mock to raise error on first student's projects
150+
projects_relation = instance_double(ActiveRecord::Relation)
151+
allow(projects_relation).to receive(:destroy_all).and_raise(StandardError, 'fail')
152+
allow(Project).to receive(:where).with(user_id: student.id).and_return(projects_relation)
153+
allow(Project).to receive(:where).with(user_id: second_student.id).and_call_original
154+
155+
results = service.remove_students
40156

41-
it 'handles errors gracefully' do
42-
allow(ClassStudent).to receive(:where).and_raise(StandardError, 'fail')
43-
results = service.remove_students
44-
expect(results.first[:error]).to match(/StandardError: fail/)
157+
expect(results.length).to eq(2)
158+
expect(results.first[:error]).to match(/StandardError/)
159+
# Second student should succeed
160+
expect(Role.where(user_id: second_student.id, school: school, role: :student)).not_to exist
161+
end
162+
end
45163
end
46164
end

0 commit comments

Comments
 (0)