Central Station / YPM-ARCHIVE-RELEASED-GRADES-ORGANIZATION

Released grades organization

archive/released-grades-organization.md · Updated 2026-05-09
GET /api/tickets/YPM-ARCHIVE-RELEASED-GRADES-ORGANIZATION

Summary

PR #109 merged and deployed behind the released-grades feature flag

2Questions 0Links 0Comments 1PRs
Open questions 2 items
  1. 1 Whether `Submission.releasedAt` (or equivalent) already exists. If yes, no schema change.
  2. 2 Where the released-grades view lives in the current route tree (assignments-unification may have moved it).
Spec body Markdown
# Released grades organization

The released-grades view groups submissions into collapsible piles by AssignmentType, with a by-student pivot, sane filters, and newest-first defaults — replacing today's flat oldest-first list.

## Current status

Shipped in `yawp-2.0` PR #109 on 2026-05-09 (`b6a6b4ee440f927c2940a801a783b51433c46bbf`) and deployed. The implementation remains flag-gated by `feature_released_grades_organization`, so rollout is still an operator decision even though the code is live.

## Problem

Today's released-grades view is a flat per-student list, sorted oldest-first, with most submissions defaulting to "untitled essay." It has no grouping, no filters, no sort controls, and gets monotonically worse with every batch of essays and every day of daily pages.

Reported teacher pains:

- "I want to see all *Macbeth Essay* submissions together" — they're scattered through the list, interleaved with daily pages and other assignments.
- Oldest-first is backwards from how teachers actually work.
- "Untitled essay" defaults make submissions visually indistinguishable.
- No way to filter by date, assignment, student, or grade.
- At UA scale (150+ students × multiple assignments + ongoing daily pages), the view is already approaching unusable.

## Goals

- Submissions group by AssignmentType in a collapsible accordion view; each pile shows submission count and most-recent-release age.
- Default sort: piles by most-recent release (top = freshest); within a pile, submissions newest-first.
- Filters: date range, student (multi-select), grade range. URL-stateful.
- A "By student" toggle pivots the same data into a per-student grouping.
- Defaults that work at UA scale (everything collapsed, lazy load on expand) and degrade gracefully at small scale (one assignment auto-expands).

## Non-goals

- **Replacing the per-student view.** Both pivots coexist.
- **Cross-class aggregation.** View is scoped to a single class. The teacher navigates classes separately.
- **Special-casing daily pages.** Released-grades view groups by AssignmentType, full stop. Daily-pages pollution (if multiple AssignmentTypes serve daily pages) is the daily-pages cleanup specs' problem, not this one.
- **AssignmentType-level metrics, cross-paper analysis surfaces, or grade redistribution.** Those live in [cross-paper-analysis](cross-paper-analysis.md) and adjacent specs.
- **Filter on document type.** With AssignmentType grouping in place, type is implicit per pile in the by-assignment view; in the by-student view, it adds noise. Not in scope.

## Domain notes

This spec lands cleanly on top of the [assignments-unification](assignments-unification.md) work. Specifically:

- **AssignmentType** — the grouping key. Each pile is one AssignmentType.
- **Assignment** — instance of an AssignmentType in a Class. Filters out submissions from other classes.
- **Submission** — the rows inside each pile. Default sort is by released-on timestamp (see Data model implications).
- **Class** — the scope for the entire view; the loader takes a class id.
- **StudentProfile** — secondary grouping key in the by-student pivot.

Daily pages: this spec is intentionally agnostic to how daily pages are modeled. The aggregation problem (one parent vs. many siblings) is solved upstream in [daily-pages-assignments](daily-pages-assignments.md) and [daily-pages-dashboard-decluttering](daily-pages-dashboard-decluttering.md). Whatever shape they take, the released-grades view inherits.

Naming clutter (untitled essays) is partially addressed by [editable-submission-titles](editable-submission-titles.md) and the title-disappears-on-submit bug. Those fixes make this view more useful but don't gate it.

## UX sketch

### Default (by-assignment) view, all collapsed

```
+--------------------------------------------------------+
| Released grades — Period 4 English                     |
|                                                        |
| [ By assignment | By student ]   [ Expand all ▾ ]      |
| [ Filters ▾ ]                                          |
+--------------------------------------------------------+
| ▸ Macbeth Essay              23 submissions · 3d ago   |
| ▸ Persuasive Essay           18 submissions · 1w ago   |
| ▸ Daily Pages                75 submissions · today    |
| ▸ Lab Report                 12 submissions · 2w ago   |
+--------------------------------------------------------+
```

- Each row: AssignmentType title, count, most-recent-release age.
- Click a row to expand; lazy-loads its submissions.
- "Expand all ▾" expands every pile (and persists the choice).

### Expanded pile

```
| ▾ Macbeth Essay              23 submissions · 3d ago   |
|     Lopez, Jamie       89   Released Apr 28            |
|     Patel, Anita       82   Released Apr 28            |
|     Brock, Ren         76   Released Apr 25            |
|     ... 20 more         [ Show more ▾ ]                |
```

- Click a row to open the submission (existing behavior).
- "Show more ▾" if more than ~50 submissions (paginate within pile).

### Filters open

```
| ▴ Filters                                              |
|   Released between [ Apr 1 ] – [ Apr 30 ]              |
|   Student          [ Jamie Lopez × ] [ Anita Patel × ] |
|   Grade range      [ ≥ 0 ] – [ ≤ 70 ]                  |
|                              [ Clear all ]             |
+--------------------------------------------------------+
```

- Active filters render as chips above the list.
- Filter state lives in the URL (`?from=...&to=...&student=...&minGrade=...&maxGrade=...&view=byStudent`).
- "Clear all" empties chips and clears URL params.

### By-student pivot

```
| ▸ Lopez, Jamie               4 submissions · 3d ago    |
| ▸ Patel, Anita               4 submissions · 3d ago    |
| ▸ Brock, Ren                 3 submissions · 5d ago    |
```

Same accordion shape, students as parents, their submissions inside (chronological by release).

### Empty state

```
| No released grades yet.                                |
| When you release a grade for a graded submission, it   |
| will show up here.                                     |
```

### Pivot persistence

The view pivot (by-assignment vs. by-student) and the expand-all preference persist via `localStorage` per-class. URL params (filters) override on first paint.

## Data model implications

**No new tables. One possible column addition.**

- **`Submission.releasedAt: DateTime?`** — the default sort and the "most-recent-release" subtitles both need a release timestamp. If today's schema models release as a boolean `released` (or similar) without a timestamp, add `releasedAt: DateTime?`. Backfill from any existing audit/event data; for unrecoverable rows fall back to `Submission.updatedAt`. Engineering should confirm this is needed before adding the column — it may already exist under another name.

- **No `kind` column on AssignmentType.** Daily-pages aggregation is upstream.

- **No `isDailyPage` flag on Submission.** Same reasoning.

### Backward compatibility

- Adding `releasedAt` (if needed) is purely additive. Old code paths that read `released` continue to work. New code paths read `releasedAt` and compare against current time. Backfill happens in the same migration.
- No risk of stale data: backfill covers every existing released submission.

## File paths in `yawp-2.0` likely to change

Best-guesses; engineering will refine.

- `services/web-app/app/routes/_teacher.classes.$classId.released*` (or wherever the released-grades route currently lives — search for "released" in routes).
- `services/web-app/app/components/released-grades/` — likely a new directory for the accordion + filter components.
- `packages/prisma/schema.prisma` — `Submission` model, possible `releasedAt` addition.
- `packages/prisma/migrations/<timestamp>_submission_released_at/` — migration only if the timestamp doesn't already exist.
- E2E tests under `services/web-app/tests/e2e/` covering the released-grades flow.

## Open questions

None blocking. Engineering should verify during implementation:

- Whether `Submission.releasedAt` (or equivalent) already exists. If yes, no schema change.
- Where the released-grades view lives in the current route tree (assignments-unification may have moved it).

## Edge cases

- **No releases yet.** Empty state copy above.
- **Single AssignmentType.** Sole pile auto-expands (degenerate accordion). Avoids a pointless click.
- **Filter intersection.** Piles with zero matching submissions hide while filters are active. Counts on visible piles reflect filtered counts.
- **By-student view, student with no released submissions.** Student doesn't render. No empty student rows.
- **UA-scale teacher.** ~800 submission rows possible. Strategy: collapsed-by-default piles render cheaply (just the pile metadata). Expanding lazy-loads in batches of ~50. By-student view paginates the student list, not submissions per student.
- **Cross-class navigation.** View is per-class; teacher uses existing class navigation to switch.
- **Expand-all + filters.** Expand-all expands every pile, including those hidden by filters? No — only visible piles expand. (Hidden piles stay hidden.)
- **Newly-released submission while view is open.** Out of scope to live-update; a refresh shows it. (Server-state revalidation on focus/return is fine if cheap.)

## Test plan

### Unit / integration

- Loader returns piles per class, sorted by most-recent-release timestamp, descending.
- Pile counts honor active filters.
- Submissions inside a pile sort newest-first, paginated at 50.
- By-student loader returns students with at least one released submission, sorted by most-recent-release.
- Filter param parsing handles missing/invalid URL params gracefully (defaults to no filter).
- `releasedAt` migration (if needed): postcheck asserts every previously-released submission has a non-null `releasedAt`.

### E2E (Playwright)

- Teacher with multiple AssignmentTypes: opens released-grades, sees collapsed piles, expands one, opens a submission. Confirms newest-first ordering.
- Teacher applies a date filter; piles outside the range disappear; URL updates.
- Teacher toggles to by-student view; data renders correctly; toggle persists across reload.
- Teacher with no released grades sees the empty state.
- Teacher at scale (seeded with 150 students × 5 assignments): pile expansion is responsive, pagination triggers correctly.

### Manual QA on preview

- Walk a UA-shaped data set through the view; sanity-check load times, pagination, filter responsiveness, pivot persistence.

## Rollout

Feature flag: `feature_released_grades_organization`. Default OFF.

1. Land schema migration (if needed) first — additive, behavior-neutral.
2. Land the new view behind the flag. Old view stays as a fallback (don't delete it).
3. Pilot order: UA professor first (highest-volume single user, fastest signal), then Washington schools, then Birmingham City, then default ON for all orgs.
4. Two weeks at default-ON without escalations → remove the flag and delete the old view in a follow-up cleanup PR.

## Engineering handoff checklist

- [x] Domain context covered
- [x] File paths in `yawp-2.0` listed
- [x] Data model implications spelled out, including backward-compat plan
- [x] UX sketch in prose
- [x] Edge cases enumerated
- [x] Test plan written
- [x] Rollout plan decided
Repo sync Metadata
{
  "url": "https://github.com/The-Connell-School/yawp-2.0/pull/109",
  "repo": "The-Connell-School/yawp-2.0",
  "draft": false,
  "state": "MERGED",
  "title": "Released grades organization: collapsed-pile accordion view (flag-gated)",
  "branch": "released-grades-organization",
  "checks": {
    "total": 4,
    "failing": 2,
    "pending": 0,
    "successful": 2
  },
  "number": 109,
  "syncedAt": "2026-05-26T21:38:23.522Z",
  "mergeable": "UNKNOWN"
}