-
Notifications
You must be signed in to change notification settings - Fork 5
Improve student removal service #588
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
… raise on errors, allowing it to be more adaptable. Updated the task to handle iterating over students and passing to service, and handling errors, allowing better output. More comprehensive tests also added.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull Request Overview
This PR refactors the student removal service to process students individually rather than in batches, with improved error handling and more granular control. The main purpose is to make the service more adaptable and provide better error reporting.
- Refactored
StudentRemovalService
to process one student at a time with proper exception handling - Updated the Rake task to handle iteration and error management externally
- Added comprehensive validation and custom error classes for different failure scenarios
Reviewed Changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 3 comments.
File | Description |
---|---|
app/services/student_removal_service.rb | Completely refactored to process single students with custom exceptions and improved validation |
lib/tasks/remove_students.rake | Updated to iterate over students and handle service exceptions with better output formatting |
spec/services/student_removal_service_spec.rb | Comprehensive test rewrite covering new error scenarios and single-student processing |
spec/lib/tasks/remove_students_spec.rb | New test file covering Rake task functionality including validation and error handling |
Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.
lib/tasks/remove_students.rake
Outdated
school = School.find_by(id: school_id) | ||
if school.nil? |
Copilot
AI
Sep 5, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using find_by
here could be inconsistent with expectations. Since the code already handles the nil case explicitly on line 17-20, consider using School.find(school_id)
and rescuing ActiveRecord::RecordNotFound
instead, which would provide clearer intent and more specific error handling.
school = School.find_by(id: school_id) | |
if school.nil? | |
begin | |
school = School.find(school_id) | |
rescue ActiveRecord::RecordNotFound |
Copilot uses AI. Check for mistakes.
rescue StandardError => e | ||
results << "Student: #{student_id} | Error: #{e.message}" |
Copilot
AI
Sep 5, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Catching StandardError
is too broad and could mask unexpected errors. Consider catching specific exceptions like StudentRemovalService::StudentHasProjectsError
, StudentRemovalService::NoopError
, etc., and re-raising unexpected errors to avoid silently handling system errors.
rescue StandardError => e | |
results << "Student: #{student_id} | Error: #{e.message}" | |
rescue StudentRemovalService::StudentHasProjectsError, StudentRemovalService::NoopError => e | |
results << "Student: #{student_id} | Error: #{e.message}" | |
rescue => e | |
raise |
Copilot uses AI. Check for mistakes.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This all makes sense and some nice tweak improvements.
Worth looking at the Copilot suggestions some of them make sense.
classes_destroyed = remove_from_classes(student_id) | ||
remove_from_profile(student_id) if should_remove_from_profile? | ||
|
||
raise NoopError, 'Student has no roles or class assignments to remove' if roles_destroyed.zero? && classes_destroyed.zero? && should_raise_on_noop? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does anyone care if it is a noop? Seems a bit extra, and would simplify code if it didn't need to check this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I added this because I was struggling without it, when running the task I had no idea if anything was actually being done when should_remove_from_profile
was false, so I think it helps to get a summary of what has happened even if it's nothing
results << result | ||
# rubocop:disable Metrics/CyclomaticComplexity | ||
def remove_student(student_id) | ||
raise NoSchoolError, 'School not found' if @school.nil? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This might be better to go in the initializer. Also the error message isn't correct "School not found" isn't what's going on here. Maybe the caller (where the school lookup is happening) should be sorting this.
# rubocop:disable Metrics/CyclomaticComplexity | ||
def remove_student(student_id) | ||
raise NoSchoolError, 'School not found' if @school.nil? | ||
raise NoClassesError, 'School has no classes' if @school.classes.empty? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this something that is important to check? I don't think the caller cares if there are no classes. They might want the student removing from the school roles, or from profile.
end | ||
|
||
def should_remove_from_profile? | ||
@remove_from_profile && @token.present? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd probably just move this into the remove_from_profile
method, and return early if no token is given, or @remove_from_profile
is set.
end | ||
|
||
it 'exits when school is not found' do | ||
ENV['SCHOOL_ID'] = 'non-existent-id' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use ClimateControl to manage ENV
in testing.
class NoSchoolError < StandardError; end | ||
class NoClassesError < StandardError; end | ||
class StudentHasProjectsError < StandardError; end | ||
class NoopError < StandardError; end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you set up a BaseError
class, then in the rake task you can just rescue anything
# Set up a BaseError class so we can rescue all errors
BaseError = Class.new(StandardError)
## Specific errors
NoSchoolError = Class.new(BaseError)
NoClassesError = Class.new(BaseError)
StudentHasProjectsError = Class.new(BaseError)
NoopError = Class.new(BaseError)
then in the rake task
begin
...
rescue StudentRemovalService::BaseError => e
# whatever it is here
end
class StudentHasProjectsError < StandardError; end | ||
class NoopError < StandardError; end | ||
|
||
def initialize(school:, remove_from_profile: false, token: nil, raise_on_noop: false) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess a fundamental question is why is this asking for a school when the service is a "student removal service"? I'd suggest maybe renaming it to SchoolStudentsRemovalService
or let it handle student IDs directly, and pass them in one at a time.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it's because you can only remove them from profile from a school context (i.e. by passing a school), and that is also how support requests will come in - a student is unique to the school, not globally
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
but yes, that's good call about altering the name - that'll help make it clearer
Status
Closes https://github.com/RaspberryPiFoundation/digital-editor-issues/issues/864
What's changed?
Steps to perform after deploying to production
N/A