convert pdf to md, add OVERVIEW and docs
This commit is contained in:
@@ -0,0 +1,78 @@
|
||||
---
|
||||
type: object
|
||||
cluster: identity
|
||||
universe: live
|
||||
status: verified
|
||||
entity: 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 at `app/models/user.rb:39`
|
||||
- `admin` — boolean, default false (`db/schema.rb:373`); scope at `:43`
|
||||
- `username` — `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 via `Discard::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](../money/portfolio.md) (`has_one`, students only),
|
||||
[order](../trading/order.md) (`has_many`)
|
||||
- **owned-by:** [classroom](../org/classroom.md) (`belongs_to`, optional)
|
||||
- **joins:** [classroom-enrollment](../org/classroom-enrollment.md) as `Student`,
|
||||
`teacher_classrooms` as `Teacher`
|
||||
- **looks-like-but-is-not:** `admin` is not an STI type — there is no `Admin` class.
|
||||
A `Teacher` with `admin: true` is one row, not two.
|
||||
|
||||
## If you change this
|
||||
|
||||
- **Hits:** [student](student.md) and [teacher](teacher.md) (same table);
|
||||
[portfolio](../money/portfolio.md) — `Portfolio` validates its user is a student
|
||||
(`app/models/portfolio.rb:102-104`); every Pundit policy, which branches on
|
||||
`user.admin?` / `user.student?` (`app/policies/application_policy.rb:39-53`);
|
||||
Devise sign-in if you touch `username` or `email` nullability.
|
||||
- **Does not hit:** [portfolio-transaction](../money/portfolio-transaction.md). It hangs
|
||||
off `Portfolio`, not `User` — 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`
|
||||
Reference in New Issue
Block a user