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