-
Notifications
You must be signed in to change notification settings - Fork 5
Implement API Endpoints for bulk-import of Schools #622
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
3de1b35 to
68a1e78
Compare
2f964a2 to
b613fb8
Compare
b613fb8 to
c9e400a
Compare
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 is the final PR Bugbot will review for you during this billing cycle
Your free Bugbot reviews will reset on December 11
Details
Your team is on the Bugbot Free tier. On this plan, Bugbot will review limited PRs each billing cycle for each member of your team.
To receive Bugbot reviews on all of your PRs, visit the Cursor dashboard to activate Pro and start your 14-day free trial.
c9e400a to
1cbaf58
Compare
d0a84c3 to
90e1a35
Compare
app/jobs/school_import_job.rb
Outdated
| school.verify! | ||
|
|
||
| # Create owner role | ||
| Role.owner.create!(school_id: school.id, user_id: owner[: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.
Bug: Owner role creation fails for cross-school users
Creating an owner role will fail if the user already has any role in a different school due to the users_can_only_have_roles_in_one_school validation in the Role model. The import job doesn't check for existing roles in other schools before attempting to create the owner role, causing legitimate imports to fail when users are teachers or students elsewhere.
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 is supposed to be a failure condition, but all of this is wrapped in a transaction, so it's atomic.
90e1a35 to
48f5d00
Compare
4096e1d to
3681a33
Compare
62bb9ea to
166509b
Compare
36f73e2 to
3604789
Compare
86d1fda to
6eacf9a
Compare
danhalson
left a comment
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.
Looks great, but there are a few things I've spotted that are worth addressing and/or discussing
| <%= f.file_field :csv_file, accept: ".csv", required: true, class: "field-unit__field" %> | ||
| <p class="field-unit__hint"> | ||
| Select a CSV file containing school data to import. | ||
| Download the <%= link_to "CSV template", "/docs/import/school_import_template.csv" %> to see the required format. |
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 the correct path? I tried it out and it wasn't working
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.
Fixed in c05a9d1
| return | ||
| end | ||
|
|
||
| render json: build_job_response(job), status: :ok |
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.
we typically use jbuilder for json responses, was there a reason not to use it here?
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.
Skill issue on my part - thanks for pointing out. Have fixed.
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.
Fixed in ec078bf
| def define_editor_admin_abilities(user) | ||
| return unless user&.admin? | ||
|
|
||
| can :import, School |
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 be interested to know your opinion, but I'm thinking it could be beneficial to add a method define_school_import_abilities or maybe can_peform_a_school_import, with the school_import abilities grouped and then reference it from both methods here, just to help keep related abilities together
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.
Yeah, I agree with you there. define_editor_admin_abilities is new in this PR but I've kept it as a possible future extension point and makes it clearer that the two kinds of admin are defined separately.
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.
Addressed in 58b7bad
app/jobs/school_import_job.rb
Outdated
| end | ||
|
|
||
| # Check if this owner already has a school as creator | ||
| existing_school = School.find_by(creator_id: owner[: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.
I think this may also need to take into account if they're a teacher at a different school too, so I'd be inclined to just check for any roles here...the only permissible situation would be granting them an owner role at the same school they're already a teacher
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.
And if they're already a teacher in that school, then the school must already exist....
app/jobs/school_import_job.rb
Outdated
| school.verify! | ||
|
|
||
| # Create owner role | ||
| Role.owner.create!(school_id: school.id, user_id: owner[: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.
Just in case you're not aware, we assign both owner and teacher roles during verification as the owner role alone is quite restrictive and doesn't provide access to many basic things, it's not a super user with access to all classes etc. If you've already looked at this and are happy with those restrictions, that's fine, I just want to make sure you're aware if not.
Role definitions have been discussed quite a bit, and we will be working on them further down the line.
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.
So I actually realised that I was duplicating SchoolVerificationService in SchoolImportJob, so I refactored SchoolImportJob to use the verification service directly instead of calling school.verify! and setting roles on the owner.
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 addressed this in 26b48f4
app/jobs/school_import_job.rb
Outdated
| Sentry.capture_exception(e) | ||
| end | ||
|
|
||
| def find_owner(email) |
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.
could we rename this to find_user_account (or something like that) since it's really just the user_account we're looking for, but this confused me a bit with the check we're doing on the role
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.
Addressed in 6ea9cfc
| ) | ||
|
|
||
| if result.success? | ||
| render json: { |
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 would be good to use jbuilder here
| response | ||
| rescue StandardError => e | ||
| Sentry.capture_exception(e) | ||
| response ||= OperationResponse.new |
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 instantiating OperationResponse again here just to be defensive? maybe I'm missing something, but it seems unlikely to be necessary
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.
Honestly, I did this because either bugbot or Copilot (I forget which, but suspect bugbot) kept insisting that it might constitute a use-before-initialise. I'll pull that out.
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.
Addressed in fd38d22
| end | ||
|
|
||
| def valid_headers?(headers) | ||
| required = %i[name website address_line_1 municipality country_code owner_email] |
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.
We could define this as a constant since it's referenced multiple times, and could also use it in the csv_invalid_format error above
lib/user_info_api_client.rb
Outdated
| end | ||
| end | ||
|
|
||
| def stubbed_user_by_email(email) |
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 realise this is following prior art, but I don't feel like it was right in the first place to be stubbing here...would be better to put these into spec/support or consider using FactoryBot, though I realise the latter has wider implications in terms of env scoping (we don't currently have a staging env_type)
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.
Addressed in 2b1e126 (some tests changed or were deleted as a result of this change).
f3f67bc to
848ab06
Compare
We allow `editor-admin` and `experience-cs-admin` users to import schools and monitor the state of the resulting jobs.
This change adds a search_by_email method to allow lookup of users by email address to acquire the ID of proposed school owners.
This commit introduces a SchoolImportJob type of GoodJob job to allow for enqueueing imports and tracking their asynchronous completion. The controller will return to the caller a Job ID which can be used to track the progress of these jobs and retrieve their results when done. The SchoolImportResult class tracks the job and provides access to the results. There are structured error codes for possible issues in SchoolImportError.
This operation parses and validates the CSV and reports on errors or enqueues the job.
This commit modifies SchoolsController to add the endpoint for importing schools. It introduces SchoolImportJobsController as the controller for the endpoint though which clients can track job status.
These tests cover the core functionality and some additional complex edge cases.
This commit adds new UI in /admin to allow users to upload CSVs of schools and track the results of the upload jobs in the UI. They can also download CSV files of the resulting school structures. Add index, show and new views for Administrate Add SchoolImportResultDashboard Add routes for admin interface for school_import_results Add SchoolImportResultsController to show results in the UI. This commit also adds code to new.html.erb to show validation errors if the uploaded CSV can't be enqueued as a batch.
Only one Job is created per job, but an upload which is failed or retried could result in more than one execution.
…stead of duplicating.
| response | ||
| rescue StandardError => e | ||
| Sentry.capture_exception(e) | ||
| response[:error] = SchoolImportError.format_error(:unknown_error, e.message) |
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.
@danhalson Here's where I was being pestered about the reinitialisation of response - I agree that it seems unlikely to be a problem.
848ab06 to
c05a9d1
Compare
|
@danhalson Thanks for the comprehensive and careful review. I have left your comments unresolved but added links to the commits where I addressed your points. Hopefully that makes it easy to track in re-review? |
Status
Points for consideration:
editor-adminorexperience-cs-adminroles. All other roles and none should return401.userinfoto look up school owners by email - there is no bulk option for this.What's changed?
/api/schools/import- for POSTing CSV/api/school_import_jobs- for tracking job statuseditor-adminorexperience-cs-adminroles.ImportSchoolsJobGoodJob job type that will execute the import asynchronouslySchoolImportResultrecord type that tracks the results ofImportSchoolsJobs.Steps to perform after deploying to production
There is a database migration involved in this to add the table that tracks
SchoolImportResult.Tasks Outstanding
import_schoolsin some places andschool_importin others).editor-apiOAuth token/adminpathFollow-On Work
This is not the final form of this feature. In a follow-up task in the next sprint we intend to add a UI under the/adminroute to make it easier for non-developers to access this endpoint. The goal in the first instance is to develop and test the endpoints separately from the UI that accesses them.This PR now contains all the work necessary to make a complete feature, including web-based UI.
Note
Adds CSV-based bulk school import with admin UI, async GoodJob processing, job status API, and persisted results (with CSV export), restricted to editor/experience CS admins.
POST /api/schools/importto start imports;GET /api/school_import_jobs/:idto track status.School::ImportBatchwith structuredSchoolImportErrorcodes.SchoolImportJob(queue:import_schools_job) creates/verifies schools, assigns owner role, and saves results toSchoolImportResult.config/initializers/good_job.rb.admin/school_import_resultspages (index/show/new) for upload, status, results, and CSV download.StatusField,ResultsSummaryField,UserInfoFieldwith batched user lookups.SchoolImportResultmodel/table (+ migration) and dashboard.School#importand:school_import_jobread to editor/experience CS admins.UserInfoApiClientaddsfind_user_by_emailand improved transforms.docs/import/.Written by Cursor Bugbot for commit 6eacf9a. This will update automatically on new commits. Configure here.