convert pdf to md, add OVERVIEW and docs
This commit is contained in:
@@ -0,0 +1,103 @@
|
||||
---
|
||||
type: process
|
||||
universe: live
|
||||
status: verified
|
||||
consumes: ["../objects/org/classroom.md"]
|
||||
produces: ["../objects/identity/student.md", "../objects/money/portfolio.md", "../objects/org/classroom-enrollment.md"]
|
||||
---
|
||||
|
||||
# import-students
|
||||
|
||||
An admin uploads a CSV and gets students with generated passwords, portfolios, and
|
||||
enrollments.
|
||||
|
||||
Verified 2026-08-16 against commit `63732df`.
|
||||
|
||||
## Input → Movement → Output
|
||||
|
||||
An admin posts a CSV of `classroom_id,username` pairs. `BulkStudentImportService` walks
|
||||
the rows and hands each to `ImportStudentService`, which creates a
|
||||
[student](../objects/identity/student.md) with a generated password. Each successful
|
||||
create cascades into a [portfolio](../objects/money/portfolio.md) and a primary
|
||||
[classroom-enrollment](../objects/org/classroom-enrollment.md) via `Student`'s callbacks.
|
||||
The admin is redirected with per-line counts.
|
||||
|
||||
## Why this shape
|
||||
|
||||
**Skip is a success, not a failure.** `ImportStudentService::Result` has three actions —
|
||||
`created`, `skipped`, `failed` — and a skip returns `success?: true`
|
||||
(`app/services/import_student_service.rb:4,35-42`). Duplicate usernames, blank usernames,
|
||||
and blank classroom IDs are all skips (`:25-27`), so re-uploading the same file is safe
|
||||
and reports zero new students rather than erroring.
|
||||
|
||||
**Line numbers start at 2.** `with_index(2)` accounts for the header row
|
||||
(`app/services/bulk_student_import_service.rb:11`), so reported numbers match what the
|
||||
admin sees in a spreadsheet.
|
||||
|
||||
**Passwords are generated, never chosen.** `MemorablePasswordGenerator` concatenates two
|
||||
Faker superhero names and a number, stripping spaces, hyphens, and apostrophes
|
||||
(`app/services/memorable_password_generator.rb:8-19`) — memorable enough for a
|
||||
middle-schooler to type. `faker` is therefore a **production** dependency (`Gemfile:13`),
|
||||
not a test one. The file carries its own `TODO: more robust solution later` (`:3`).
|
||||
|
||||
**The whole import hangs on `classroom_id` being present**, because
|
||||
`Student#create_initial_enrollment` only fires when it is
|
||||
(`app/models/student.rb:10,82-86`). A row without it is skipped outright, which is what
|
||||
keeps enrollment-less students out of the system by this path.
|
||||
|
||||
## Steps
|
||||
|
||||
1. Admin posts to `POST /admin/students/import` (`config/routes.rb:63`);
|
||||
`Admin::StudentsController#import` rejects a blank file
|
||||
(`app/controllers/admin/students_controller.rb:117-118`).
|
||||
2. `BulkStudentImportService.import_from_csv` reads with `headers: true`
|
||||
(`app/services/bulk_student_import_service.rb:8,11`).
|
||||
3. Rows missing either field are dropped **before** the service is called and produce no
|
||||
result at all (`:15`) — they are invisible in the summary counts.
|
||||
4. `ImportStudentService.call` strips whitespace, then skips on blank username, existing
|
||||
username, or blank classroom ID (`app/services/import_student_service.rb:22-28`).
|
||||
5. `Student.new(username:, classroom_id:, password: MemorablePasswordGenerator.generate)`
|
||||
and save (`:38-46`). `ActiveRecord::InvalidForeignKey` — a classroom ID that does not
|
||||
exist — is rescued into a `failed` result (`:50-52`).
|
||||
6. Saving triggers `Student` callbacks: `ensure_portfolio` and
|
||||
`create_initial_enrollment` (`app/models/student.rb:9-10`).
|
||||
7. Results are wrapped with line numbers (`:22`) and partitioned for the flash message
|
||||
(`app/controllers/admin/students_controller.rb:182,199`).
|
||||
8. `GET /admin/students/template` downloads a sample CSV
|
||||
(`app/services/bulk_student_import_service.rb:28-36`).
|
||||
|
||||
Malformed CSV is caught at the controller and reported
|
||||
(`app/controllers/admin/students_controller.rb:123`).
|
||||
|
||||
## If you change this
|
||||
|
||||
- **Hits:** [student](../objects/identity/student.md) creation and both of its callbacks;
|
||||
[portfolio](../objects/money/portfolio.md) and
|
||||
[classroom-enrollment](../objects/org/classroom-enrollment.md), created as a side
|
||||
effect; `Admin::StudentsController`.
|
||||
- **Does not hit:** [grade-book](../objects/gradebook/grade-book.md) or
|
||||
[grade-entry](../objects/gradebook/grade-entry.md). Importing students does **not**
|
||||
create gradebook entries for them — gradebooks are created per classroom, and nothing
|
||||
backfills entries for students who arrive afterwards.
|
||||
|
||||
## Notes
|
||||
|
||||
- There is no transaction around the batch. A CSV that fails halfway leaves the earlier
|
||||
students created.
|
||||
- The import is row-at-a-time with a `Student.exists?` query per row; large files are
|
||||
slow but bounded.
|
||||
|
||||
## Surfaces
|
||||
|
||||
| Surface | Role |
|
||||
|---|---|
|
||||
| `Admin::StudentsController#import` / `#template` | admin writes |
|
||||
| `BulkStudentImportService`, `ImportStudentService` | create |
|
||||
| `MemorablePasswordGenerator` | generates credentials |
|
||||
|
||||
## See
|
||||
|
||||
- Objects: [student](../objects/identity/student.md),
|
||||
[classroom-enrollment](../objects/org/classroom-enrollment.md)
|
||||
- Source: `app/services/bulk_student_import_service.rb`,
|
||||
`app/services/import_student_service.rb`
|
||||
Reference in New Issue
Block a user