3.3 KiB
type, cluster, universe, status, entity
| type | cluster | universe | status | entity |
|---|---|---|---|---|
| object | identity | live | verified | app/models/user.rb |
User
Every human in the app. STI base class for Student and Teacher — but admin is a
boolean column on this table, not a subclass.
Verified 2026-08-16 against commit 63732df.
Why this shape
The users are middle-school students, so email cannot be the login. Devise is
reconfigured to authenticate on username (config/initializers/devise.rb:49), email is
optional, and its uniqueness index is partial — it applies only where email is non-null
and non-empty (db/schema.rb:388), so any number of students can have no email at all.
Student actively forces blank email back to nil to stay inside that index
(app/models/student.rb:74-76).
Hard deletes are blocked because a user owns a financial ledger. destroy and destroy!
are overridden to discard, and outside production they raise rather than silently
soft-delete (app/models/user.rb:6-14,75-82). really_destroy! is the deliberate escape
hatch (:16-18).
Shape
- Table
users,db/schema.rb:372-391 type—"User" | "Student" | "Teacher", validated atapp/models/user.rb:39admin— boolean, default false (db/schema.rb:373); scope at:43username—null: false, unique index, the login key (db/schema.rb:385,390)email— nullable, partial unique index (db/schema.rb:388); required only for teachers and admins (app/models/user.rb:61-63)discarded_at— soft delete viaDiscard::Model(app/models/user.rb:4)classroom_id— direct membership. See the roster trap in../../CONTEXT.md
email_changed? is hard-coded to false (app/models/user.rb:65-67), which suppresses
Devise's reconfirmation path. The code wins over the method name — it is not a real
dirty-check.
Connected to
- owns: portfolio (
has_one, students only), order (has_many) - owned-by: classroom (
belongs_to, optional) - joins: classroom-enrollment as
Student,teacher_classroomsasTeacher - looks-like-but-is-not:
adminis not an STI type — there is noAdminclass. ATeacherwithadmin: trueis one row, not two.
If you change this
- Hits: student and teacher (same table);
portfolio —
Portfoliovalidates its user is a student (app/models/portfolio.rb:102-104); every Pundit policy, which branches onuser.admin?/user.student?(app/policies/application_policy.rb:39-53); Devise sign-in if you touchusernameoremailnullability. - Does not hit: portfolio-transaction. It hangs
off
Portfolio, notUser— discarding a user leaves the ledger fully intact and still summable. That is deliberate, not an oversight.
Surfaces
| Surface | Role |
|---|---|
| Devise controllers | reads (sign-in by username) |
Admin::UsersController, Admin::StudentsController, Admin::TeachersController |
read/write |
StudentsController (nested under classrooms, teacher-facing) |
read/write |
ApplicationController#authenticate_user! |
reads every request |
See
- Source:
app/models/user.rb,db/schema.rb:372-391 - Login config:
config/initializers/devise.rb:49