Central Station / YPM-FEATURE-ASSIGNMENT-TYPE-VISIBILITY

AssignmentType visibility

features/assignment-type-visibility.md · Updated 2026-05-05
GET /api/tickets/YPM-FEATURE-ASSIGNMENT-TYPE-VISIBILITY

Summary

Org-scoped sharing, dashboard scoped to used types, hide system types, no standalone management page — inline create-new in the picker

0Questions 0Links 0Comments 0PRs
Spec body Markdown
# AssignmentType visibility

Tighten what teachers and students see on their dashboards and in the AssignmentType picker, and hide system AssignmentTypes (currently FREE_WRITE) that exist for plumbing reasons but shouldn't appear in any user surface.

## Problem

After the [assignments-unification](assignments-unification.md) refactor, every teacher in the system can see every AssignmentType ever created — including the FREE_WRITE template that exists only to hold orphaned student documents. Two pains fall out of that:

- **Teacher dashboards are noisy.** A teacher's "my assignments" view today is unbounded by which classes the teacher actually teaches. Across an org with multiple teachers and years of accumulated AssignmentTypes, the dashboard becomes useless as a working surface.
- **System types leak into UX.** FREE_WRITE shows up in AssignmentType pickers and dashboards as a real option, even though no one should ever assign it directly — it's machinery for orphaned-doc handling.

There's also no current way to deliberately hide a Class itself from dashboards (e.g. archived classes), but `Class.isArchived` already exists and is just unused. Out of scope here, noted for completeness.

## Goals

- A teacher's dashboard shows only AssignmentTypes the teacher has actually used (i.e. has at least one Assignment instance for in a class they teach).
- A teacher's AssignmentType picker (when creating an Assignment, or when starting a new AssignmentType from a similar one) shows every AssignmentType in the teacher's org, plus the teacher's own creations. No system types.
- Creating a new AssignmentType happens inline from the picker — no standalone AssignmentType library or management page.
- Any AssignmentType created by a teacher is visible to all teachers in that org by default — no per-teacher grants, no whitelist.
- Students see only assignments tied to a class they're enrolled in (mostly already true; verify and lock in).
- System AssignmentTypes (FREE_WRITE today, more in the future) are hidden from every user-facing surface.
- Migration day: nobody loses access to an AssignmentType they could see before.

## Non-goals

- **Cross-org sharing.** Teacher A in Org X cannot see Teacher B in Org Y's AssignmentTypes. Org boundary is the visibility boundary.
- **Per-teacher grants / whitelisting.** No granular "Teacher A may use Teacher B's type but Teacher C may not." Org-wide is the only sharing layer.
- **Hiding a Class from dashboards.** `Class.isArchived` exists and could carry this in a follow-up, but isn't in scope here.
- **AssignmentType archival.** Soft-deleting old types is a separate problem.
- **Standalone AssignmentType management page.** Out of scope. The picker is the only entry point; create/edit happens inline from there.
- **Admin override surfaces** (org admin sees all teachers' types). Not needed for the pilot orgs; revisit as a separate spec if it comes up.

## Domain notes

Entities involved:

- **AssignmentType** — template owned by an org, optionally attributed to a teacher. Today has `ownerOrgId` and `ownerTeacherId` columns with a mutual-exclusion DB constraint (exactly one is set, or both NULL for system types).
- **Assignment** — instance of an AssignmentType in a Class. `Assignment.classId` + `Assignment.assignmentTypeId`.
- **Class** — container for students and teachers. Many-to-many to both.
- **TeacherProfile / StudentProfile** — role-specific projection of a Profile, scoped to an Organization.
- **System AssignmentType** — currently a single seeded row, `FREE_WRITE_ASSIGNMENT_TYPE_ID = "cfreewrite0000000000000000"`, both owners NULL. Holds orphaned documents created outside any class context.

The domain shift this spec asks for: **org scope replaces teacher-only scope.** Today an AssignmentType is either teacher-only (`ownerTeacherId` set) or org-wide (`ownerOrgId` set). After this change, every teacher-created type should be visible org-wide. Whether we keep `ownerTeacherId` as a pure attribution field, or drop it and add `creatorTeacherProfileId` separately, is an engineering call (see Open questions).

## UX sketch

### Teacher dashboard

```
+--------------------------------------------------------+
| My assignments                                         |
+--------------------------------------------------------+
| (only AssignmentTypes I have at least one Assignment   |
|  instance for, in a class I teach)                     |
|                                                        |
| - Persuasive Essay         [3 classes, 47 students]    |
| - Daily Pages              [2 classes, 31 students]    |
| - Research Paper           [1 class,  18 students]     |
+--------------------------------------------------------+
```

Empty state for a brand-new teacher who hasn't assigned anything yet:

```
+--------------------------------------------------------+
| My assignments                                         |
+--------------------------------------------------------+
| You haven't assigned anything yet.                     |
| [ Create or pick an assignment to get started ]        |
+--------------------------------------------------------+
```

That CTA opens the AssignmentType picker.

### AssignmentType picker (entry point: "Assign to a class")

The picker is the only place a teacher interacts with the AssignmentType library. There is no standalone "manage AssignmentTypes" page.

```
+--------------------------------------------------------+
| Pick an assignment                                     |
+--------------------------------------------------------+
| [ + Create new assignment type ]                       |
|                                                        |
| Used by you            (sorted: most recently used)    |
|   - Persuasive Essay                                   |
|   - Daily Pages                                        |
|                                                        |
| Available in your org  (sorted: alphabetical)          |
|   - Compare & Contrast        (created by Ms. Lee)     |
|   - Lab Report                (created by Mr. Patel)   |
|   - Senior Thesis             (created by you)         |
+--------------------------------------------------------+
```

Two sections — "Used by you" first (sorted by most recent Assignment instance the teacher created with that type), "Available in your org" below (alphabetical). System types do not appear in either section.

**Inline create flow.** "+ Create new assignment type" opens a modal with the AssignmentType editor (title, description, modules, etc. — same fields the existing AssignmentType creation flow already exposes). On save, the new AssignmentType is created (with `ownerOrgId = me.organizationId`, attribution to the creator) and is immediately selected for the Assignment being created. The picker doesn't need to be reopened — flow continues straight into "assign to which class."

### Student view

Unchanged in shape. Verify the student dashboard query already filters Assignments to those in classes the student is enrolled in (research suggests yes via `Class.students` many-to-many; engineering should confirm during implementation).

### Removed surfaces

- The "FREE_WRITE" entry that currently appears in teacher AssignmentType pickers must disappear. Orphaned documents continue to be backed by FREE_WRITE under the hood — this is a UX hide, not a functional removal.

## Data model implications

Two changes to `AssignmentType`:

1. **`isSystem: Boolean @default(false)`** — new column. Set to `true` for FREE_WRITE during migration. All user-facing queries (teacher dashboard, picker, student-facing surfaces) filter `isSystem = false`.
2. **Org scope is the only scope.** Every non-system AssignmentType has `ownerOrgId` set. The current `ownerTeacherId` field becomes pure attribution (rename to `creatorTeacherProfileId`) — it does not gate visibility. The existing `ownerOrgId XOR ownerTeacherId` mutual-exclusion constraint goes away; `ownerOrgId` is required for non-system types and `creatorTeacherProfileId` is optional (NULL for system types and for org-imported types without a creator).

Migration backfill: for every AssignmentType where `ownerOrgId` is NULL and `ownerTeacherId` is set, copy `ownerTeacherId.profile.organizationId` into `ownerOrgId`. After backfill, every non-system row has `ownerOrgId`.

### Backward-compatibility / migration

Yawp requires backward-compat data migrations. Expand-then-contract:

1. **Expand.** Add `isSystem` column with default `false`. Backfill `isSystem = true` for the FREE_WRITE row. Backfill `ownerOrgId` on every AssignmentType with NULL `ownerOrgId` but non-NULL `ownerTeacherId`, copying `ownerTeacherId.profile.organizationId`. Verify by query: every non-system AssignmentType has `ownerOrgId` set. Drop the `ownerOrgId XOR ownerTeacherId` mutual-exclusion constraint; replace with "non-system rows must have `ownerOrgId`."
2. **Code switch.** Update teacher dashboard, picker, and any other AssignmentType-listing surface to use the new filters. New AssignmentTypes created by teachers set both `ownerOrgId` and `ownerTeacherId`. Ship behind a feature flag (see Rollout). Old data is fully compatible with the new code paths.
3. **Contract (later, separate PR).** Rename `ownerTeacherId` → `creatorTeacherProfileId` to make the attribution-not-scope semantics explicit. Pure schema rename, no data movement. Schedule after the feature has been on default for two weeks.

No destructive changes. No risk of locking a teacher out of types they were already using — the migration only backfills.

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

- `packages/prisma/schema.prisma` — add `isSystem` to `AssignmentType`; possibly drop or relax `ownerOrgId`/`ownerTeacherId` constraint.
- `packages/prisma/migrations/<timestamp>_assignment_type_visibility/migration.sql` — new migration: add column, backfill, mark FREE_WRITE.
- `services/web-app/app/utils/assignment-types.ts` — FREE_WRITE constant lives here; any helper for "is this a system type?" goes here.
- The teacher AssignmentType picker route (last touched in commit `442c01c "teacher routes use owner-scoped AssignmentType picker; drop whitelist UI"`). Update the loader's where-clause to `ownerOrgId = me.organizationId AND isSystem = false`. Add an inline "create new" affordance that opens the existing AssignmentType editor in a modal and, on save, selects the newly-created type for the in-flight Assignment.
- The teacher dashboard route (whichever loader feeds "my assignments"). Update to filter to AssignmentTypes that have an Assignment in a Class the teacher teaches:
  ```
  AssignmentType where
    isSystem = false
    AND assignments.some(a => a.class.teachers.some(t => t.id = me.teacherProfileId))
  ```
- `packages/prisma/scripts/seed-overlay.ts` — if seed creates AssignmentTypes for test users, ensure the new shape is honored.
- E2E tests under `services/web-app/tests/e2e/` that hit the teacher dashboard or AssignmentType picker.

## Open questions

None blocking. Engineering should verify during implementation that the student dashboard already scopes Assignments to enrolled classes only — research suggests yes, but add a regression test if not already covered.

## Edge cases

- **Teacher with no classes.** Dashboard is empty. Picker shows org-wide types + their own creations. Works.
- **Brand-new org.** No org AssignmentTypes yet. Picker shows only "Create new." First teacher to create a type seeds the org's library.
- **Teacher leaves the org.** Their authored AssignmentTypes stay in the org (since visibility is org-scoped). If `ownerTeacherId` was kept as attribution, the UI label "(created by Ms. Lee)" still works post-departure as long as the Profile row stays. If the Profile is deleted (cascade), label gracefully falls back to org name or "(unknown)".
- **System type in flight.** FREE_WRITE is referenced by orphaned documents. Hiding from picker doesn't break those documents — they continue to render normally because the document → AssignmentType relation still resolves; it's just not listed in any user-facing chooser.
- **AssignmentType deletion.** A teacher can delete their own AssignmentType today. Under org-wide visibility, deletion semantics need a check: if other teachers in the org have used it (have Assignments referencing it), block deletion or cascade carefully. **Not in scope here** but flag for engineering — the existing delete path may already handle this; verify.
- **Pilot orgs at scale.** UA professor with 150+ students and likely many AssignmentTypes — the dashboard "AssignmentTypes I've used" query needs an index. `Assignment.assignmentTypeId` + `Assignment.classId` indexes should be sufficient; engineering to confirm query plan.

## Test plan

- **Unit / integration**
  - Picker loader returns: org AssignmentTypes (created by anyone in org, non-system) + the teacher's own; excludes `isSystem = true`; excludes other orgs.
  - Dashboard loader returns: AssignmentTypes the teacher has Assignments for in their classes; excludes types they created but never assigned; excludes `isSystem`; excludes types from classes they don't teach.
  - Student dashboard loader: assignments scoped to enrolled classes only (regression — confirm existing behavior is preserved).
- **Migration**
  - Pre-migration: assert FREE_WRITE row exists with both owners NULL, no `isSystem`.
  - Post-migration: assert FREE_WRITE has `isSystem = true`; assert every non-system AssignmentType has non-NULL `ownerOrgId`; assert no AssignmentType lost its `ownerTeacherId` value.
  - Use the same pattern as the assignments-unification preflight/postcheck (`packages/prisma/scripts/preflight-*` / `postcheck-*`).
- **E2E (Playwright)**
  - Teacher with assignments: dashboard shows only used types, picker shows org list, FREE_WRITE never appears.
  - Teacher with no assignments: dashboard empty state, picker still functional.
  - Student: dashboard shows only their classes' assignments. Verify a known assignment in another class is not visible.
- **Manual QA on preview**
  - Walk a teacher account through: dashboard, "Create new assignment" → picker, assign, return to dashboard, see it appear.
  - Sanity-check on a UA-scale org if possible (largest data we have).

## Rollout

Feature flag: `feature_assignment_type_visibility`. Default OFF.

1. Land schema migration first (additive — `isSystem` + backfills). No code behavior change yet.
2. Land code paths gated on the flag. Feature flag ON in staging; smoke-test.
3. Pilot order: UA professor first (highest-volume single user, fastest signal), then Washington, then Birmingham City, then default ON for all orgs.
4. Once defaulted ON for two weeks without issue, remove the flag 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 Not recorded

No repo sync metadata recorded yet.