Skip to content

feat(gradebook): gradebook overhaul - weights, external assessments, CSV import#8441

Open
LWS49 wants to merge 3 commits into
lws49/feat-gradebook-weighted-viewfrom
lws49/feat-ext-assessments
Open

feat(gradebook): gradebook overhaul - weights, external assessments, CSV import#8441
LWS49 wants to merge 3 commits into
lws49/feat-gradebook-weighted-viewfrom
lws49/feat-ext-assessments

Conversation

@LWS49

@LWS49 LWS49 commented Jun 15, 2026

Copy link
Copy Markdown
Collaborator

Summary

This PR ships the complete gradebook overhaul: a new gradebook page with column picker and CSV export; a weighted view with configurable per-tab weights, keep-highest rules, and projected totals; an externally-graded assessments system (add/rename/delete columns, per-student inline grade editing); and a CSV bulk import wizard that lets professors define component metadata, download a template, upload filled marks, verify a preview, resolve grade conflicts, and commit in one flow.

Externals are surfaced as a synthetic "External Assessments" category in the gradebook API response: the backend assembles them from course_external_assessments and serializes them with negative IDs (distinguishing from real assessment IDs), avoiding any schema change to the existing categories/tabs structure. Weight contributions are stored in a new course_gradebook_contributions table that accepts both real assessment_id entries and an external_assessment_id slot; the weighted total is a canvas-style additive sum (no normalization cap, bonus allowed).

The CSV import uses a two-gate flow: preview (dry-run — resolves every identifier against the current roster, returns a sample + conflict list, writes nothing) followed by commit (re-validates inside one transaction, authoritative). Re-importing into an existing component upserts grades only — max marks and weightage are never touched. The binding key is the resolved course_user_id; the raw identifier string is snapshotted into imported_identifier for audit and reassigned-ID mismatch detection.

Design decisions

  • Synthetic negative-ID serialization for externals - The gradebook index view assembles external assessments into a virtual category with negative IDs rather than polluting the real assessment tree. This lets the frontend treat externals uniformly in the column picker, weighted table, and CSV export without a schema migration on existing categories/tabs.

  • Component metadata in the dialog, not the CSV - The import wizard collects name, weightage, and max marks in a dialog form; the CSV carries only Identifier + one grade column per component. This avoids professors having to format a structured header row with embedded metadata and keeps the CSV human-editable with a plain template.

  • preview + commit two-gate instead of a single upload - commit re-validates entirely server-side in one transaction and never trusts a client-sent conflict list. The client posts the same payload both times; the preview result is purely advisory. This prevents a race between preview and commit (roster changes, concurrent imports) from silently committing bad data.

  • Conflict UI is a design copy, not a shared component - ExternalGradeConflictPrompt/Table mirror the visual design of ExternalIdConflictPrompt/Table (user-invitations) but are independent components acting on grades. Sharing the component would couple two unrelated domains through a generic prop interface neither actually owns.

Regression prevention

Covers: gradebook load and column picker toggle, CSV export output shape, weighted total calculation (equal/custom/keep-highest), external assessment CRUD, per-student inline grade edit, import service (preview dry-run, fresh import, upsert keep/replace, blank cell handling, conflict detection, identifier mismatch, determinacy), import controller (authorization, malformed grades, duplicate components, email mode, conflicts in response), template CSV escaping, wizard step flow (no conflicts, unresolved error, conflict prompt keep + replace paths).

Manual testing confirmed: all 8 scenarios ran including both conflict resolution paths (Keep Existing + Replace), email-mode import, and the error path with an unresolved student ID.

Existing gradebook controller, external assessments controller, and contribution model specs updated to cover the new behaviour. No changes to existing assessment or submission write paths.

@LWS49 LWS49 force-pushed the lws49/feat-ext-assessments branch from 2220631 to ad01bc8 Compare June 15, 2026 09:06
@LWS49 LWS49 changed the title Lws49/feat ext assessments feat(gradebook): add course gradebook with weighted view and external assessments Jun 15, 2026
…links & sorting

Introduce the course gradebook: a frozen-column table of students × assessments
with a column picker, CSV export, and per-grade links into submissions.

Gradebook table
- Page with TanStack-backed table: pinned checkbox + Name columns, sticky
  header and Max Marks rows, frozen-column border seams that survive sticky
  scroll compositing.
- Column picker (assessments grouped by tab/category) and CSV export of the
  selected columns; empty-state hint when no data columns are chosen.
- External ID column, shown when any student has one.
- Grade cells link to the student's submission; a dismissible GradeLinkHint
  banner explains the affordance (persisted per-user via useDismissibleOnce).
- "Search students" global search.
- Default sort with name ascending, null/undefined at bottom of sort regardless of order

Shared table builder
- getColumnCanGlobalFilter is gated on column visibility ("search what you
  see"): hiding a column via the picker removes it from search, and the
  nullable-first-row type sniff is bypassed. Affects all TanStack tables.

Backend
- Gradebook controller, ability and course component; index JSON serializes
  students, assessments, submissions (with submissionId) and gamification.
- Submission grade query also selects the submission id for grade links.

- Remove the redundant ScoreAssessmentSummary download from Statistics.
@LWS49 LWS49 changed the title feat(gradebook): add course gradebook with weighted view and external assessments feat(gradebook): add external assessments Jun 15, 2026
@LWS49 LWS49 force-pushed the lws49/feat-gradebook-weighted-view branch 3 times, most recently from 4c71a02 to 1b32632 Compare June 16, 2026 05:49
Add weighted view built on top of gradebook:

- add tables course_gradebook_contributions and course_gradebook_assessment_contributions
- weighted table with equal/custom weight modes and per-assessment
  weight inputs, with a sum gate on custom weights
- points / percentage display toggle
- inline per-student assessment breakdown (row expand)
- projected-total hint
- gradebook_excluded column, serialization, and update-weights API echo
- per-assessment include/exclude in the configure-weights modal,
  seeding custom weights from included assessments only
- excluded assessments shown in the breakdown with no contribution
- add SegmentedSelect component for stylized selection that is not "on-off"
@LWS49 LWS49 force-pushed the lws49/feat-gradebook-weighted-view branch from 7d9fdb4 to ade1ddf Compare June 16, 2026 07:12
@LWS49 LWS49 force-pushed the lws49/feat-ext-assessments branch from ad01bc8 to 82c7316 Compare June 16, 2026 13:58
@LWS49 LWS49 changed the title feat(gradebook): add external assessments feat(gradebook): gradebook overhaul - weights, external assessments, CSV import Jun 16, 2026
- add ExternalAssessment/Grade models, CRUD + grade-set endpoints
  gated to teaching staff, scoped to the course
- extend gradebook contributions to weight an external contributor
  (XOR validation), serialized as a synthetic negative-id grouping
- CSV import wizard (define/upload/verify/conflict) with dry-run
  preview, upsert commit, and conflict/mismatch detection
- FE store, operations, and inline add/rename/delete/grade/max editing
  for external columns in the gradebook table
- surface external assessments in gradebook index JSON and route
  weight entries to external contributions
- add controller, model, service, and component specs
@LWS49 LWS49 force-pushed the lws49/feat-ext-assessments branch from c799dd5 to 2de895b Compare June 17, 2026 06:13
@LWS49 LWS49 force-pushed the lws49/feat-gradebook-weighted-view branch 2 times, most recently from 521efed to 2efbb02 Compare June 17, 2026 06:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant