convert pdf to md, add OVERVIEW and docs

This commit is contained in:
2026-08-17 22:30:18 +00:00
parent 95d5787868
commit 4df62d2609
44 changed files with 3307 additions and 1 deletions

View File

@@ -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`