3.0 KiB
type, cluster, universe, status, entity
| type | cluster | universe | status | entity |
|---|---|---|---|---|
| object | identity | live | verified | app/models/student.rb |
Student
A User with type: "Student" — the only user kind that owns a portfolio and can trade.
Verified 2026-08-16 against commit 63732df.
Why this shape
Every money path downstream assumes a portfolio exists, so Student guarantees one on
create rather than letting callers remember (app/models/student.rb:9,78-80). Nothing in
the trading code null-checks for a missing portfolio because of this hook.
The class also carries the roster bridge. A student's classroom is reachable two ways
and Student is where they meet: primary_classroom prefers the enrollment record and
falls back to the legacy classroom_id column (:37-43). On create it writes both — but
create_initial_enrollment fires only if classroom_id is present (:10,82-86), so
a student created without it has no enrollment either. Read the roster trap in
../../CONTEXT.md before touching this.
Shape
- STI subclass of user; no table of its own
has_many :classroom_enrollments,has_many :classrooms, through:(:4-5)- Callbacks:
set_default_emailforces blank →nil(:8,74-76);ensure_portfolio(:9,78-80);create_initial_enrollment(:10,82-86) - Reads:
current_enrollments(:17-19),current_classrooms(:24-28),primary_enrollment(:33-35),primary_classroom(:41-43) - Writes:
enroll_in!(:51-59),unenroll_from!(:66-70)
enroll_in! always creates the row with primary: false and then promotes it via
make_primary! (:52-57) — the promotion is what enforces one-primary, not the insert.
Connected to
- owns: portfolio (guaranteed on create), order
- owned-by: classroom — twice over, see Why
- joins: classroom-enrollment
- looks-like-but-is-not:
student.classrooms(through enrollments) is notstudent.classroom(theclassroom_idcolumn). They can disagree.
If you change this
- Hits: classroom-enrollment and
classroom — both rosters; portfolio if
you touch
ensure_portfolio;Admin::StudentsControllerandStudentsController; the import-students movement, which creates students by this exact path. - Does not hit: teacher. Same table, but no shared callbacks —
Teacherrunssync_username_from_emailinstead and shares none of the hooks above.
Surfaces
| Surface | Role |
|---|---|
StudentsController (nested under classroom) |
teacher creates/edits, resets passwords |
Admin::StudentsController |
admin CRUD, CSV import, restore, manual transactions |
ImportStudentService |
writes |
| student's own portfolio + orders pages | reads |
See
- Source:
app/models/student.rb - Base class: user