convert pdf to md, add OVERVIEW and docs
This commit is contained in:
@@ -0,0 +1,81 @@
|
||||
---
|
||||
type: object
|
||||
cluster: org
|
||||
universe: live
|
||||
status: verified
|
||||
entity: app/models/classroom_enrollment.rb
|
||||
---
|
||||
|
||||
# ClassroomEnrollment
|
||||
|
||||
A dated membership of one [student](../identity/student.md) in one
|
||||
[classroom](classroom.md), with history. **The newer of the app's two roster paths** —
|
||||
read `../../CONTEXT.md` before changing either.
|
||||
|
||||
Verified 2026-08-16 against commit `63732df`.
|
||||
|
||||
## Why this shape
|
||||
|
||||
It exists because `users.classroom_id` can only say where a student is *now*. A student
|
||||
who moves classrooms mid-year, or returns to one next year, needs rows — so membership
|
||||
became a record with `enrolled_at` / `unenrolled_at`, and "current" is simply
|
||||
`unenrolled_at IS NULL` (`app/models/classroom_enrollment.rb:26-27`). Nothing is deleted
|
||||
on unenrollment; the row is closed (`:50-53`).
|
||||
|
||||
**The `primary` flag is enforced in Ruby only.** `only_one_primary_per_student` does an
|
||||
`exists?` check before save (`:24,78-86`) and `make_primary!` demotes siblings inside a
|
||||
transaction (`:36-44`) — but the supporting index is *not* unique. It is a partial index
|
||||
on `[student_id, primary] WHERE primary = true` (`db/schema.rb:74`), which speeds the
|
||||
lookup without constraining it. Two concurrent writes can therefore produce two primary
|
||||
enrollments, and `primary_enrollment` will just take `.first`
|
||||
(`app/models/student.rb:34`).
|
||||
|
||||
`unenroll!` clears `primary` as well as setting the date (`:51`), so unenrolling a
|
||||
student's primary classroom leaves them with **no** primary at all — `primary_classroom`
|
||||
then falls back to the legacy `classroom_id` column
|
||||
(`app/models/student.rb:41-43`), which `unenroll!` never touched.
|
||||
|
||||
## Shape
|
||||
|
||||
- Table `classroom_enrollments`, `db/schema.rb:64-76`
|
||||
- `enrolled_at` `null: false`; `unenrolled_at` nullable = still enrolled
|
||||
- `primary` boolean, default false, `null: false` (`db/schema.rb:68`)
|
||||
- Scopes: `current`, `historical`, `primary_enrollment`, `for_student`, `for_classroom`
|
||||
(`:26-30`)
|
||||
- Writes: `make_primary!` (`:36-44`), `unenroll!` (`:50-53`)
|
||||
- Validation: `unenrolled_at` must be ≥ `enrolled_at` (`:23,71-76`)
|
||||
- No uniqueness constraint on `[student_id, classroom_id]` — repeat enrollments in the
|
||||
same classroom are intentional (`:3-8`)
|
||||
|
||||
## Connected to
|
||||
|
||||
- **owns:** —
|
||||
- **owned-by:** [student](../identity/student.md), [classroom](classroom.md)
|
||||
- **joins:** student ↔ classroom, over time
|
||||
- **looks-like-but-is-not:** this is **not** `users.classroom_id`. Both are live. A
|
||||
student can be enrolled here and absent from `Classroom#students`, or the reverse.
|
||||
|
||||
## If you change this
|
||||
|
||||
- **Hits:** [student](../identity/student.md) — `current_classrooms`,
|
||||
`primary_enrollment`, `primary_classroom`, `enroll_in!`, `unenroll_from!`;
|
||||
[classroom](classroom.md)`#current_students` / `#historical_students`;
|
||||
`ClassroomEnrollmentsController`; `ClassroomFacade`, which builds the teacher's roster
|
||||
view.
|
||||
- **Does not hit:** [grade-entry](../gradebook/grade-entry.md). Entries are keyed to
|
||||
`grade_book_id` + `user_id` (`db/schema.rb:118`) and carry no enrollment reference —
|
||||
unenrolling a student does **not** remove or hide their gradebook rows, and
|
||||
`DistributeEarnings` will still pay them.
|
||||
|
||||
## Surfaces
|
||||
|
||||
| Surface | Role |
|
||||
|---|---|
|
||||
| `ClassroomEnrollmentsController` | create, destroy, unenroll |
|
||||
| `ClassroomFacade` | reads the roster |
|
||||
| `Student#enroll_in!` / `#unenroll_from!` | writes |
|
||||
|
||||
## See
|
||||
|
||||
- Source: `app/models/classroom_enrollment.rb`, `db/schema.rb:64-76`
|
||||
- The trap: `../../CONTEXT.md`
|
||||
@@ -0,0 +1,83 @@
|
||||
---
|
||||
type: object
|
||||
cluster: org
|
||||
universe: live
|
||||
status: verified
|
||||
entity: app/models/classroom.rb
|
||||
---
|
||||
|
||||
# Classroom
|
||||
|
||||
One teacher's class within a [school-year](school-year.md). The unit teachers actually
|
||||
work in, and **the switch that turns trading on**.
|
||||
|
||||
Verified 2026-08-16 against commit `63732df`.
|
||||
|
||||
## Why this shape
|
||||
|
||||
Two things make this more than a grouping.
|
||||
|
||||
**1. `trading_enabled` defaults to `false`** (`db/schema.rb:93`). Order creation validates
|
||||
it (`app/models/order.rb:26,203-207`), reaching the classroom by delegation through the
|
||||
user (`app/models/user.rb:26`). A brand-new classroom therefore **cannot trade** until a
|
||||
teacher flips it via `PATCH /classrooms/:id/toggle_trading` (`config/routes.rb:22`). If
|
||||
trading "silently doesn't work," check this column first.
|
||||
|
||||
**2. Creating a classroom creates its gradebooks** — one per quarter of its school-year,
|
||||
via `after_create` (`app/models/classroom.rb:29,112-116`). It uses `find_or_create_by!`,
|
||||
so it is idempotent, but it only runs on create: adding a quarter later does **not**
|
||||
backfill gradebooks for existing classrooms.
|
||||
|
||||
The class also holds **both rosters** (see the trap in `../../CONTEXT.md`):
|
||||
`students` reads the legacy `users.classroom_id` column (`:20`) while `current_students`
|
||||
reads [classroom-enrollment](classroom-enrollment.md) (`:74-78`). They can disagree.
|
||||
|
||||
## Shape
|
||||
|
||||
- Table `classrooms`, `db/schema.rb:88-96` — `name`, `archived`, `trading_enabled`,
|
||||
`school_year_id`
|
||||
- `GRADE_RANGE` — a frozen **Array** of levels 5–8, built from `MIN_GRADE`/`MAX_GRADE`
|
||||
(`:4-6`); middle school only
|
||||
- Rosters: `has_many :students, -> { kept }` on `classroom_id` (`:20`);
|
||||
`has_many :enrolled_students, through: :classroom_enrollments` (`:19`)
|
||||
- `has_many :users, dependent: :nullify` (`:15`) — deleting a classroom orphans users
|
||||
rather than deleting them
|
||||
- `has_many :grade_books, dependent: :destroy` (`:23`)
|
||||
- `has_many :grades, through: :classroom_grades` (`:22`); must have at least one (`:27,118-120`)
|
||||
- Sorting: `apply_sorting` + three scopes, including `order_by_total_earnings`, which
|
||||
joins all the way to `portfolio_transactions` (`:41-49`)
|
||||
- `grades_display` collapses `[5,6,7]` to `"5th-7th"` (`:89-104`)
|
||||
|
||||
## Connected to
|
||||
|
||||
- **owns:** [grade-book](../gradebook/grade-book.md),
|
||||
[classroom-enrollment](classroom-enrollment.md)
|
||||
- **owned-by:** [school-year](school-year.md)
|
||||
- **joins:** [teacher](../identity/teacher.md) via `teacher_classrooms`,
|
||||
[grade-level](grade-level.md) via `classroom_grades`
|
||||
- **looks-like-but-is-not:** `classroom.students` ≠ `classroom.current_students`.
|
||||
The first is the `classroom_id` column, the second is active enrollments.
|
||||
|
||||
## If you change this
|
||||
|
||||
- **Hits:** [order](../trading/order.md) — creation is gated on `trading_enabled`;
|
||||
[grade-book](../gradebook/grade-book.md) via the create cascade and `dependent: :destroy`;
|
||||
[student](../identity/student.md) rosters, both of them; `Order.for_teacher`
|
||||
(`app/models/order.rb:40-42`) and `GradeBooksController`, which redirects
|
||||
non-admins away from archived classrooms (`app/controllers/grade_books_controller.rb:47-50`).
|
||||
- **Does not hit:** [portfolio](../money/portfolio.md). Portfolios belong to users and
|
||||
survive `dependent: :nullify` intact — archiving or deleting a classroom never touches
|
||||
a balance or a holding.
|
||||
|
||||
## Surfaces
|
||||
|
||||
| Surface | Role |
|
||||
|---|---|
|
||||
| `ClassroomsController` | teacher CRUD, `toggle_trading` |
|
||||
| `Admin::ClassroomsController` | admin CRUD, `toggle_archive` |
|
||||
| `ClassroomFacade`, `ClassroomPresenter` | read |
|
||||
| `GradeBooksController` | reads (authorization + archive gate) |
|
||||
|
||||
## See
|
||||
|
||||
- Source: `app/models/classroom.rb`, `db/schema.rb:88-96`
|
||||
@@ -0,0 +1,73 @@
|
||||
---
|
||||
type: object
|
||||
cluster: org
|
||||
universe: live
|
||||
status: verified
|
||||
entity: app/models/grade.rb
|
||||
---
|
||||
|
||||
# Grade level — class `Grade`
|
||||
|
||||
A school grade level: 5th through 8th. **Not a letter grade.** The class is called
|
||||
`Grade`; this card is named `grade-level` to keep the two apart.
|
||||
|
||||
Verified 2026-08-16 against commit `63732df`.
|
||||
|
||||
## Why this shape
|
||||
|
||||
A classroom can span several grade levels, so the link is many-to-many through
|
||||
`classroom_grades` rather than a column on `classrooms`. A classroom must carry at least
|
||||
one (`app/models/classroom.rb:27,118-120`), and `Classroom#grades_display` collapses a
|
||||
contiguous set into `"5th-7th"` for display (`app/models/classroom.rb:89-104`).
|
||||
|
||||
`Grade` rows are reference data seeded once, not created by users — hence
|
||||
`dependent: :restrict_with_error` (`app/models/grade.rb:4`): a level in use cannot be
|
||||
deleted.
|
||||
|
||||
**The name collision is the point of this card.** `Grade#level` is `5..8`;
|
||||
[grade-entry](../gradebook/grade-entry.md)`#math_grade` is `"A+"`…`"F"`. They share the
|
||||
word "grade" and nothing else — no association, no foreign key, no shared table.
|
||||
|
||||
## Shape
|
||||
|
||||
- Table `grades`, `db/schema.rb:123-130` — `level` (integer) and `name` (string), both
|
||||
`null: false` and both uniquely indexed
|
||||
- `validates :name` uniqueness is `case_sensitive: false`; `:level` uniqueness is plain
|
||||
(`app/models/grade.rb:7-8`)
|
||||
- Join table `classroom_grades`, `db/schema.rb:78-86`, unique on
|
||||
`[classroom_id, grade_id]` (`db/schema.rb:83`). It has a model (`app/models/classroom_grade.rb`)
|
||||
but no behaviour, so no card.
|
||||
- `Classroom::GRADE_RANGE` (`app/models/classroom.rb:4-6`) is a **separate** frozen array
|
||||
of 5–8. The classroom form filters these rows through it —
|
||||
`Grade.where(level: Classroom::GRADE_RANGE)`
|
||||
(`app/views/classrooms/_form.html.erb:58`) — so a `Grade` row outside 5–8 exists but is
|
||||
unselectable. The constant is not derived from the rows and can drift from them.
|
||||
|
||||
## Connected to
|
||||
|
||||
- **owns:** —
|
||||
- **owned-by:** —
|
||||
- **joins:** [classroom](classroom.md), through `classroom_grades`
|
||||
- **looks-like-but-is-not:** not a letter grade
|
||||
([grade-entry](../gradebook/grade-entry.md)), and not
|
||||
[grade-book](../gradebook/grade-book.md).
|
||||
|
||||
## If you change this
|
||||
|
||||
- **Hits:** [classroom](classroom.md) — validation, `grades_display`, and the classroom
|
||||
forms; seeds (`db/seeds`), which create these rows.
|
||||
- **Does not hit:** any earnings. Nothing in `DistributeEarnings` or
|
||||
[grade-entry](../gradebook/grade-entry.md) reads `Grade` — payouts are computed from
|
||||
letter grades and attendance only, so adding or renaming a level never changes a
|
||||
payout.
|
||||
|
||||
## Surfaces
|
||||
|
||||
| Surface | Role |
|
||||
|---|---|
|
||||
| `ClassroomsController`, `Admin::ClassroomsController` | read (form checkboxes) |
|
||||
| seeds | writes |
|
||||
|
||||
## See
|
||||
|
||||
- Source: `app/models/grade.rb`, `app/models/classroom_grade.rb`, `db/schema.rb:123-130`
|
||||
@@ -0,0 +1,68 @@
|
||||
---
|
||||
type: object
|
||||
cluster: org
|
||||
universe: live
|
||||
status: verified
|
||||
entity: app/models/quarter.rb
|
||||
---
|
||||
|
||||
# Quarter
|
||||
|
||||
One of four grading periods inside a [school-year](school-year.md). Auto-created in sets
|
||||
of four; never made by hand.
|
||||
|
||||
Verified 2026-08-16 against commit `63732df`.
|
||||
|
||||
## Why this shape
|
||||
|
||||
`Quarter#previous` is **load-bearing for money**, not just navigation. Improvement
|
||||
bonuses compare a student's letter grade against the same student's grade in the previous
|
||||
quarter's gradebook, and `DistributeEarnings` finds it by calling `quarter.previous`
|
||||
(`app/services/distribute_earnings.rb:35-38`). If `previous` returns `nil`, the
|
||||
improvement bonus silently pays zero — the run still succeeds.
|
||||
|
||||
That is why `previous` and `next` cross **year** boundaries rather than stopping at 1 and
|
||||
4: quarter 1 reaches back to quarter 4 of the same school's previous year
|
||||
(`app/models/quarter.rb:20-24,44-58`), matching on `school` and `year`, not on ID order.
|
||||
So the bonus keeps working across a September rollover — but only if the previous year's
|
||||
`Year` record exists and its name parses (see [year](year.md)).
|
||||
|
||||
## Shape
|
||||
|
||||
- Table `quarters`, `db/schema.rb:188-196`; unique on `[school_year_id, number]` (`db/schema.rb:194`)
|
||||
- `belongs_to :school_year`; FK is `on_delete: :cascade` (`db/schema.rb:420`)
|
||||
- `has_many :grade_books, dependent: :restrict_with_error` (`:5`)
|
||||
- `number` — `1..4`, validated for inclusion and uniqueness per school-year (`:7-10`)
|
||||
- `scope :ordered` by number (`:12`)
|
||||
- `next` (`:14-18`), `previous` (`:20-24`) — both memoized, both may return `nil`
|
||||
|
||||
## Connected to
|
||||
|
||||
- **owns:** [grade-book](../gradebook/grade-book.md) (blocks its own deletion)
|
||||
- **owned-by:** [school-year](school-year.md)
|
||||
- **joins:** —
|
||||
- **looks-like-but-is-not:** `quarter.previous` is not "number − 1". At number 1 it is a
|
||||
**different school-year's** quarter 4, found by school + previous year.
|
||||
|
||||
## If you change this
|
||||
|
||||
- **Hits:** [grade-book](../gradebook/grade-book.md) — one per quarter per classroom;
|
||||
the [finalize-gradebook-earnings](../../processes/finalize-gradebook-earnings.md)
|
||||
movement, specifically the improvement bonus;
|
||||
[portfolio-transaction](../money/portfolio-transaction.md) amounts, one step further
|
||||
on, because that bonus becomes a deposit.
|
||||
- **Does not hit:** [classroom-enrollment](classroom-enrollment.md). Enrollment windows
|
||||
are plain timestamps (`enrolled_at` / `unenrolled_at`) and are **not** scoped to
|
||||
quarters — a quarter change does not move anyone on or off a roster.
|
||||
|
||||
## Surfaces
|
||||
|
||||
| Surface | Role |
|
||||
|---|---|
|
||||
| `GradeBooksController` | reads (to label the gradebook) |
|
||||
| `DistributeEarnings` | reads `previous` |
|
||||
| `SchoolYear#create_quarters` | writes |
|
||||
|
||||
## See
|
||||
|
||||
- Source: `app/models/quarter.rb`, `db/schema.rb:188-196`
|
||||
@@ -0,0 +1,68 @@
|
||||
---
|
||||
type: object
|
||||
cluster: org
|
||||
universe: live
|
||||
status: verified
|
||||
entity: app/models/school_year.rb
|
||||
---
|
||||
|
||||
# SchoolYear
|
||||
|
||||
One school's instance of one [year](year.md) — the join that everything academic hangs
|
||||
from.
|
||||
|
||||
Verified 2026-08-16 against commit `63732df`.
|
||||
|
||||
## Why this shape
|
||||
|
||||
It is a join table that grew behaviour. Creating a `SchoolYear` **auto-creates exactly
|
||||
four [quarters](quarter.md)** (`app/models/school_year.rb:12,20-24`), which is the first
|
||||
link in a two-step cascade that ends in gradebooks:
|
||||
|
||||
```
|
||||
SchoolYear created → 4 Quarters → (later) Classroom created → 1 GradeBook per quarter
|
||||
```
|
||||
|
||||
Neither half is optional and neither is done by a controller. If quarters or gradebooks
|
||||
are ever missing, the cause is almost always that this callback did not run — the object
|
||||
was built by `insert_all`, a fixture, or a migration that skipped callbacks.
|
||||
|
||||
`name` is computed, not stored: `"#{school_name} (#{year_name})"` (`:14-16`).
|
||||
|
||||
## Shape
|
||||
|
||||
- Table `school_years`, `db/schema.rb:198-206`; unique on `[school_id, year_id]` (`db/schema.rb:203`)
|
||||
- `belongs_to :school`, `belongs_to :year` (`:4-5`)
|
||||
- `has_many :classrooms, dependent: :restrict_with_error` (`:6`) — blocks deletion
|
||||
- `has_many :quarters, dependent: :destroy` (`:7`) — cascades
|
||||
- `after_create :create_quarters` (`:12`)
|
||||
|
||||
## Connected to
|
||||
|
||||
- **owns:** [quarter](quarter.md) (creates and destroys them),
|
||||
[classroom](classroom.md) (blocks its own deletion)
|
||||
- **owned-by:** [school](school.md), [year](year.md)
|
||||
- **joins:** school ↔ year
|
||||
- **looks-like-but-is-not:** not [year](year.md). Deleting a `Year` cascades to
|
||||
`SchoolYear`; deleting a `School` does not.
|
||||
|
||||
## If you change this
|
||||
|
||||
- **Hits:** [quarter](quarter.md) directly — the count, numbering, and names of quarters
|
||||
are decided here; [classroom](classroom.md), which validates its `school_year_id`
|
||||
(`app/models/classroom.rb:122-124`); [grade-book](../gradebook/grade-book.md) at one
|
||||
remove, since classrooms create one per quarter.
|
||||
- **Does not hit:** [grade-entry](../gradebook/grade-entry.md). Entries are created per
|
||||
student against an existing gradebook, never by this cascade — adding a quarter gives
|
||||
you empty gradebooks, not populated ones.
|
||||
|
||||
## Surfaces
|
||||
|
||||
| Surface | Role |
|
||||
|---|---|
|
||||
| `Admin::SchoolYearsController` | admin CRUD |
|
||||
| `SchoolYearPresenter` | reads |
|
||||
|
||||
## See
|
||||
|
||||
- Source: `app/models/school_year.rb`, `db/schema.rb:198-206`
|
||||
@@ -0,0 +1,59 @@
|
||||
---
|
||||
type: object
|
||||
cluster: org
|
||||
universe: live
|
||||
status: verified
|
||||
entity: app/models/school.rb
|
||||
---
|
||||
|
||||
# School
|
||||
|
||||
A participating school. Little more than a name — it exists to be the thing a
|
||||
[school-year](school-year.md) attaches to.
|
||||
|
||||
Verified 2026-08-16 against commit `63732df`.
|
||||
|
||||
## Why this shape
|
||||
|
||||
Deliberately thin: the table is `id`, `name`, timestamps (`db/schema.rb:208-212`). All
|
||||
real structure lives one level down in [school-year](school-year.md), because the same
|
||||
school recurs every year and nothing about the school itself changes when it does.
|
||||
|
||||
Deletion is blocked, not cascaded — `dependent: :restrict_with_error`
|
||||
(`app/models/school.rb:4`). A school with any history cannot be removed.
|
||||
|
||||
## Shape
|
||||
|
||||
- Table `schools`, `db/schema.rb:208-212` — `name` only
|
||||
- `has_many :school_years, dependent: :restrict_with_error` (`:4`)
|
||||
- `has_many :years, through: :school_years` (`:5`)
|
||||
- `validates :name, presence: true` (`:7`) — note the column itself is nullable
|
||||
|
||||
## Connected to
|
||||
|
||||
- **owns:** [school-year](school-year.md)
|
||||
- **owned-by:** —
|
||||
- **joins:** [year](year.md), through `school_years`
|
||||
- **looks-like-but-is-not:** `User#school` is a **delegation through classroom**
|
||||
(`app/models/user.rb:22-24`), not an association. A user with no classroom has no
|
||||
school, and that is why the delegate is `allow_nil`.
|
||||
|
||||
## If you change this
|
||||
|
||||
- **Hits:** [school-year](school-year.md) and everything under it;
|
||||
`Admin::SchoolsController`; `Portfolio#school_name`, which reaches back up through
|
||||
user → classroom → school (`app/models/portfolio.rb:9`).
|
||||
- **Does not hit:** the top-level `SchoolsController` and `app/views/schools/*`. Those
|
||||
are a **ghost** — no route reaches them (see `../../CONTEXT.md`). Editing them changes
|
||||
nothing that runs.
|
||||
|
||||
## Surfaces
|
||||
|
||||
| Surface | Role |
|
||||
|---|---|
|
||||
| `Admin::SchoolsController` | admin CRUD (the live one) |
|
||||
| `SchoolsController` | **none — unrouted ghost** |
|
||||
|
||||
## See
|
||||
|
||||
- Source: `app/models/school.rb`, `db/schema.rb:208-212`
|
||||
@@ -0,0 +1,68 @@
|
||||
---
|
||||
type: object
|
||||
cluster: org
|
||||
universe: live
|
||||
status: verified
|
||||
entity: app/models/year.rb
|
||||
---
|
||||
|
||||
# Year
|
||||
|
||||
An academic year, identified by the **string** `"2024 - 2025"`. Shared across all schools.
|
||||
|
||||
Verified 2026-08-16 against commit `63732df`.
|
||||
|
||||
## Why this shape
|
||||
|
||||
The whole model hangs on a parsed string. `years` has exactly one meaningful column,
|
||||
`name` (`db/schema.rb:393-398`) — there is no `start_year` or `end_year` integer. So:
|
||||
|
||||
- ordering casts a substring to int in SQL:
|
||||
`CAST(SUBSTRING(name FROM 1 FOR 4) AS INTEGER)` (`app/models/year.rb:10`)
|
||||
- `previous_year` / `next_year` do **string arithmetic** on the split halves
|
||||
(`:21-27`, `:39-41`)
|
||||
- `current_school_year` builds the expected name from today's date, rolling over in
|
||||
**July** — months 1–6 belong to the year that started last calendar year (`:12-19`)
|
||||
|
||||
The format `"YYYY - YYYY"` — spaces around the hyphen included — is therefore
|
||||
load-bearing. A record named `"2024-2025"` sorts and navigates wrong without raising.
|
||||
|
||||
## Shape
|
||||
|
||||
- Table `years`, `db/schema.rb:393-398`; `name` `null: false`, unique index
|
||||
- `validates :name, presence: true, uniqueness: true` (`:8`)
|
||||
- `has_many :school_years, dependent: :destroy` (`:5`) — **cascades**, unlike
|
||||
[school](school.md)
|
||||
- `has_many :classrooms, through: :school_years` (`:7`)
|
||||
- `scope :ordered_by_start_year` (`:10`)
|
||||
- `self.current_school_year(date = Date.current)` returns a **relation**, not a record
|
||||
(`:12-19`)
|
||||
|
||||
## Connected to
|
||||
|
||||
- **owns:** [school-year](school-year.md) (destroys them)
|
||||
- **owned-by:** —
|
||||
- **joins:** [school](school.md), through `school_years`
|
||||
- **looks-like-but-is-not:** `Year` is not [school-year](school-year.md). `Year` is the
|
||||
calendar span shared by every school; `SchoolYear` is one school's instance of it.
|
||||
|
||||
## If you change this
|
||||
|
||||
- **Hits:** [school-year](school-year.md) — `dependent: :destroy` means deleting a year
|
||||
deletes school-years, and their [quarters](quarter.md) cascade too
|
||||
(`db/schema.rb:420`); any admin year dropdown ordering (`:10`);
|
||||
[quarter](quarter.md)`#next`/`#previous`, which cross year boundaries by calling
|
||||
`Year#next_year` (`app/models/quarter.rb:30,46`).
|
||||
- **Does not hit:** [classroom](classroom.md) rows directly. Classrooms belong to a
|
||||
`school_year`, not a `year` — the `through:` association is read-only convenience.
|
||||
|
||||
## Surfaces
|
||||
|
||||
| Surface | Role |
|
||||
|---|---|
|
||||
| `Admin::SchoolYearsController` | reads for selection |
|
||||
| `SchoolYearPresenter` | reads |
|
||||
|
||||
## See
|
||||
|
||||
- Source: `app/models/year.rb`, `db/schema.rb:393-398`
|
||||
Reference in New Issue
Block a user