Skip to content

Commit 8811c27

Browse files
committed
Add endpoint for mass user deletion.
This code: 1. Modifies StudentRemovalService to mass-delete users and their projects instead of skipping users who have projects. 2. Adds an endpoint for school owners to mass-delete students.
1 parent 09270f8 commit 8811c27

File tree

7 files changed

+121
-25
lines changed

7 files changed

+121
-25
lines changed

app/controllers/api/school_students_controller.rb

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,40 @@ def destroy
119119
end
120120
end
121121

122+
def destroy_batch
123+
student_ids = student_ids_params
124+
125+
if student_ids.blank? || student_ids.empty?
126+
render json: {
127+
error: 'No student IDs provided',
128+
error_type: :unprocessable_entity
129+
},
130+
status: :unprocessable_entity
131+
return
132+
end
133+
134+
# Invoke StudentRemovalService
135+
service = StudentRemovalService.new(
136+
students: student_ids,
137+
school: @school,
138+
remove_from_profile: false,
139+
token: current_user.token
140+
)
141+
142+
results = service.remove_students
143+
144+
# Check if any errors occurred
145+
errors = results.select { |r| r[:error] }
146+
if errors.any?
147+
render json: {
148+
results: results,
149+
error: "#{errors.size} student(s) failed to be removed"
150+
}, status: :partial_content
151+
else
152+
render json: { results: results }, status: :ok
153+
end
154+
end
155+
122156
private
123157

124158
def school_student_params
@@ -135,6 +169,10 @@ def school_students_params
135169
end
136170
end
137171

172+
def student_ids_params
173+
params.fetch(:student_ids, [])
174+
end
175+
138176
def create_safeguarding_flags
139177
create_teacher_safeguarding_flag
140178
create_owner_safeguarding_flag

app/models/ability.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ def define_school_owner_abilities(school:)
6666
can(%i[read create create_batch destroy], ClassStudent, school_class: { school: { id: school.id } })
6767
can(%i[read create destroy], :school_owner)
6868
can(%i[read create destroy], :school_teacher)
69-
can(%i[read create create_batch update destroy], :school_student)
69+
can(%i[read create create_batch update destroy destroy_batch], :school_student)
7070
can(%i[create create_copy], Lesson, school_id: school.id)
7171
can(%i[read update destroy], Lesson, school_id: school.id, visibility: %w[teachers students public])
7272
can(%i[exchange_code], :google_auth)

app/services/student_removal_service.rb

Lines changed: 13 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -14,24 +14,22 @@ def remove_students
1414
@students.each do |user_id|
1515
result = { user_id: }
1616
begin
17-
# Skip if student has projects
18-
projects = Project.where(user_id: user_id)
19-
result[:skipped] = true if projects.length.positive?
17+
ActiveRecord::Base.transaction do
18+
# Delete all projects
19+
projects = Project.where(user_id: user_id)
20+
projects.destroy_all
2021

21-
unless result[:skipped]
22-
ActiveRecord::Base.transaction do
23-
# Remove from classes
24-
class_assignments = ClassStudent.where(student_id: user_id)
25-
class_assignments.destroy_all
22+
# Remove from classes
23+
class_assignments = ClassStudent.where(student_id: user_id)
24+
class_assignments.destroy_all
2625

27-
# Remove roles
28-
roles = Role.student.where(user_id: user_id)
29-
roles.destroy_all
30-
end
31-
32-
# Remove from profile if requested
33-
ProfileApiClient.delete_school_student(token: @token, school_id: @school.id, student_id: user_id) if @remove_from_profile && @token.present?
26+
# Remove roles
27+
roles = Role.student.where(user_id: user_id)
28+
roles.destroy_all
3429
end
30+
31+
# Remove from profile if requested
32+
ProfileApiClient.delete_school_student(token: @token, school_id: @school.id, student_id: user_id) if @remove_from_profile && @token.present?
3533
rescue StandardError => e
3634
result[:error] = "#{e.class}: #{e.message}"
3735
end

config/routes.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@
6969
resources :teachers, only: %i[index create], controller: 'school_teachers'
7070
resources :students, only: %i[index create update destroy], controller: 'school_students' do
7171
post :batch, on: :collection, to: 'school_students#create_batch'
72+
delete :batch, on: :collection, to: 'school_students#destroy_batch'
7273
end
7374
end
7475

lib/tasks/remove_students.rake

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ namespace :remove_students do
3939
puts "REMOVE_FROM_PROFILE: #{remove_from_profile}"
4040
puts "Students to remove: #{students.size}"
4141
puts "====================\n\n"
42+
puts "WARNING: All student projects will be deleted permanently and this operation is not reversible.\n"
4243
puts "Please confirm deletion of #{students.size} user(s), and that recent Postgres backups have been captured for all services affected (https://devcenter.heroku.com/articles/heroku-postgres-backups#manual-backups)"
4344
print 'Are you sure you want to continue? (yes/no): '
4445
confirmation = $stdin.gets.strip.downcase
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
# frozen_string_literal: true
2+
3+
require 'rails_helper'
4+
5+
RSpec.describe 'Batch deleting school students', type: :request do
6+
let(:school) { create(:school) }
7+
let(:owner) { create(:owner, school: school) }
8+
let(:teacher) { create(:teacher, school: school) }
9+
let(:school_class) { create(:school_class, school: school, teacher_ids: [teacher.id]) }
10+
let(:student_1) { create(:student, school: school) }
11+
let(:student_2) { create(:student, school: school) }
12+
let(:headers) { { Authorization: UserProfileMock::TOKEN } }
13+
14+
before do
15+
authenticated_in_hydra_as(owner)
16+
stub_profile_api_create_safeguarding_flag
17+
create(:class_student, student_id: student_1.id, school_class: school_class)
18+
create(:class_student, student_id: student_2.id, school_class: school_class)
19+
end
20+
21+
describe 'DELETE /api/schools/:school_id/students/batch' do
22+
it 'deletes all students and their projects' do
23+
project_1 = create(:project, user_id: student_1.id)
24+
project_2 = create(:project, user_id: student_2.id)
25+
26+
expect(ClassStudent.where(student_id: student_1.id)).to exist
27+
expect(ClassStudent.where(student_id: student_2.id)).to exist
28+
expect(Project.where(id: project_1.id)).to exist
29+
expect(Project.where(id: project_2.id)).to exist
30+
31+
delete "/api/schools/#{school.id}/students/batch",
32+
params: { student_ids: [student_1.id, student_2.id] },
33+
headers: headers
34+
35+
expect(response).to have_http_status(:ok)
36+
37+
json = JSON.parse(response.body)
38+
expect(json['results'].size).to eq(2)
39+
expect(json['results'].all? { |r| r['error'].nil? }).to be true
40+
41+
# Verify students removed from classes
42+
expect(ClassStudent.where(student_id: student_1.id)).not_to exist
43+
expect(ClassStudent.where(student_id: student_2.id)).not_to exist
44+
45+
# Verify projects deleted
46+
expect(Project.where(id: project_1.id)).not_to exist
47+
expect(Project.where(id: project_2.id)).not_to exist
48+
end
49+
50+
it 'returns error when no student IDs provided' do
51+
delete "/api/schools/#{school.id}/students/batch",
52+
headers: headers
53+
54+
expect(response).to have_http_status(:unprocessable_entity)
55+
json = JSON.parse(response.body)
56+
expect(json['error']).to eq('No student IDs provided')
57+
end
58+
end
59+
end

spec/services/student_removal_service_spec.rb

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,25 +11,24 @@
1111
let(:service) { described_class.new(students: [student.id], school: school) }
1212

1313
before do
14-
allow(Project).to receive(:where).and_return([])
1514
allow(ProfileApiClient).to receive(:delete_school_student)
1615
create(:class_student, student_id: student.id, school_class: school_class)
1716
end
1817

19-
it 'removes student from classes and roles' do
18+
it 'removes student from classes, roles, and deletes all projects' do
19+
create(:project, user_id: student.id)
20+
2021
expect(Role.where(user_id: student.id, role: :student)).to exist
2122
expect(ClassStudent.where(student_id: student.id)).to exist
23+
expect(Project.where(user_id: student.id)).to exist
24+
2225
results = service.remove_students
26+
2327
expect(results.first[:user_id]).to eq(student.id)
24-
expect(results.first[:skipped]).to be_nil
28+
expect(results.first[:error]).to be_nil
2529
expect(ClassStudent.where(student_id: student.id)).not_to exist
2630
expect(Role.where(user_id: student.id, role: :student)).not_to exist
27-
end
28-
29-
it 'skips removal if student has projects' do
30-
allow(Project).to receive(:where).and_return([instance_double(Project)])
31-
results = service.remove_students
32-
expect(results.first[:skipped]).to be true
31+
expect(Project.where(user_id: student.id)).not_to exist
3332
end
3433

3534
it 'calls ProfileApiClient if remove_from_profile is true and token is present' do

0 commit comments

Comments
 (0)