Skip to content

feat(gradebook): CSV grade import for external assessments#8465

Open
LWS49 wants to merge 1 commit into
lws49/feat-ext-assessments-pr3-validationfrom
lws49/feat-ext-assessments-pr4-import
Open

feat(gradebook): CSV grade import for external assessments#8465
LWS49 wants to merge 1 commit into
lws49/feat-ext-assessments-pr3-validationfrom
lws49/feat-ext-assessments-pr4-import

Conversation

@LWS49

@LWS49 LWS49 commented Jun 28, 2026

Copy link
Copy Markdown
Collaborator

Summary

Adds CSV grade import for external assessments, reached through "Import CSV" in the manage panel. A three-step wizard defines the components, confirms the required headers and file, then previews the resolved rows before writing. Students are matched by External ID (Coursemology's term) or email, keyed by column position; headers are accepted in any order with duplicate-header reporting and near-miss ("did you mean") suggestions. The Verify step pairs each CSV identifier with its resolved student name, flags out-of-range cells as a non-blocking advisory, and blocks only on unresolved identifiers or non-numeric cells. Grade conflicts are shown as a per-student change matrix (old to new, unchanged cells dropped), and writes are batched with student emails preloaded to avoid N+1s.

Design decisions

  • The wizard is a thin renderer of backend-decided state: out-of-range detection, structured conflict rows, header suggestions, and reassigned-identifier detection are computed in the import service and emitted in the preview payload - this keeps validation in one place and the wizard presentation-only.
  • Import weightage is zeroed server-side unless the weighted view is enabled, placed in the controller rather than the service - the service stays a pure writer, matching where the manual-add path enforces the same rule; the hidden frontend field was previously the only guard, so a stale request could write a nonzero weight into a weighting-off course.
  • Conflicts are shown as a per-student change matrix with unchanged cells dropped - the prior flat list re-counted cells identical at two decimals; the matrix shows only real changes (old to new) grouped by the student they affect.
  • Identifier-first display (the CSV value plus a resolved-name column) replaces name-only rows - under an identifier-mode header, showing only the resolved name read as a mismatch; pairing the two confirms the match.

Regression prevention

Covers (backend): header order tolerance, duplicate-header reporting, near-miss header suggestion, identifier resolution and reassigned-identifier detection, out-of-range computation, structured conflict rows, the weightage-zeroing guard, the email-preload N+1 fix, and batched writes (import service and imports controller specs). Covers (frontend): the full wizard (define / template / upload / verify, did-you-mean, loading), the conflict change-matrix table (struck old value, arrow, bold new value, blank sides shown as a dash, new-fill vs existing fallback), and the template builder. Adds the imports controller route; no schema change.

Introduce the full external-assessment CSV import feature as a single
slice on top of grade validation (pr3).

Backend:
- Course::Gradebook::ExternalAssessmentImportService — header-order
  tolerance, duplicate-header/identifier guards, reassigned-identifier
  detection, out-of-range flagging, and batched (bulk insert + upsert)
  grade writes.
- external_assessment_imports controller (preview + create/commit),
  create/preview jbuilders, and import routes.

Frontend:
- ImportExternalAssessmentsWizard (upload → define → verify), with the
  ExternalGradeConflict prompt/table change-matrix and buildTemplate.
- previewImport/commitImport operations + import API endpoints + types.
- ManageExternalAssessmentsPanel gains the Import CSV entry point.

Consolidates the previously planned pr4a (correctness) and pr4b (ux)
into one PR: the import wizard and service are edited by both themes at
the hunk level, so they do not separate into independently-reviewable
slices.

Fixes a pre-existing broken assertion in the import controller spec
(course_externalassessments -> course_external_assessments) that
prevented the replace-on-conflict test from running.
@LWS49 LWS49 force-pushed the lws49/feat-ext-assessments-pr3-validation branch from c75dd03 to 32c59d0 Compare June 29, 2026 04:43
@LWS49 LWS49 force-pushed the lws49/feat-ext-assessments-pr4-import branch from 6f62578 to 245d005 Compare June 29, 2026 04:43
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