feat(gradebook): external assessment foundation - model, serialization, and inline grade entry#8470
Conversation
cbf61fa to
af0e2bd
Compare
177439e to
c563f33
Compare
af0e2bd to
b6b0aaf
Compare
c563f33 to
e963677
Compare
b6b0aaf to
bd9256e
Compare
e963677 to
dd2eeeb
Compare
bd9256e to
d6d17fa
Compare
There was a problem hiding this comment.
Pull request overview
This PR lays the groundwork for External Assessments in the gradebook (professor-created columns distinct from native Course::Assessments), including backend data modelling, gradebook JSON serialization for rendering externals as columns, and inline grade entry with an optimistic save/rollback flow. It also integrates externals into the weighted gradebook structure via a synthetic category/tab representation and external weight contributions.
Changes:
- Added external-assessment backend models + migration/schema updates (external assessments, per-student external grades, and gradebook contribution weights).
- Extended gradebook JSON payload to emit externals as synthetic category/tabs/assessment leaves, and emit external grade “submissions”.
- Implemented frontend rendering + inline editing for external-grade cells, plus bounded-grade handling in weighted computations.
Reviewed changes
Copilot reviewed 41 out of 41 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| spec/models/course/gradebook/external_contribution_spec.rb | Tests ExternalContribution validations, dependent destroy, and bulk upsert behavior. |
| spec/models/course/external_assessment_spec.rb | Tests ExternalAssessment associations, validations, defaults, and transactional creation. |
| spec/models/course/external_assessment_grade_spec.rb | Tests ExternalAssessmentGrade uniqueness and numeric/null grade validation. |
| spec/factories/course_gradebook_external_contributions.rb | Adds factory for ExternalContribution. |
| spec/factories/course_external_assessments.rb | Adds factories for ExternalAssessment and ExternalAssessmentGrade. |
| spec/controllers/course/gradebook_controller_spec.rb | Verifies gradebook index payload includes externals and that weights persist to ExternalContribution. |
| spec/controllers/course/external_assessments_controller_spec.rb | Verifies external-grade write endpoint behavior + authorization. |
| db/schema.rb | Reflects new external assessment/grade/contribution tables and keys. |
| db/migrate/20260624000000_create_course_external_assessment_tables.rb | Migration squashing external assessment tables + contributions with decimal(5,2). |
| config/routes.rb | Adds nested gradebook route for external grade writes. |
| config/locales/en/activerecord/errors.yml | Adds translated validation error for “exactly one contributor” rule. |
| config/locales/zh/activerecord/errors.yml | Same as above (ZH). |
| config/locales/ko/activerecord/errors.yml | Same as above (KO). |
| client/locales/en.json | Adds UI strings for external badge and grade save toast/errors. |
| client/locales/zh.json | Same as above (ZH). |
| client/locales/ko.json | Same as above (KO). |
| client/app/types/course/gradebook.ts | Extends gradebook types for externals and optional submissionId. |
| client/app/lib/components/table/utils.ts | Prepends UTF-8 BOM for CSV download to improve Excel UTF-8 detection. |
| client/app/lib/components/table/TanStackTableBuilder/columnsBuilder.ts | Adds sortDescFirst support when provided by column template. |
| client/app/lib/components/table/builder/ColumnTemplate.ts | Adds descFirst sorting option to template type. |
| client/app/bundles/course/statistics/pages/StatisticsIndex/index.tsx | Minor styling adjustment (border via sx). |
| client/app/bundles/course/gradebook/store.ts | Adds reducer/action to upsert external grades into submissions. |
| client/app/bundles/course/gradebook/selectors.ts | Adds selector for external assessments. |
| client/app/bundles/course/gradebook/operations.ts | Adds optimistic setExternalGrade operation calling new API endpoint. |
| client/app/bundles/course/gradebook/computeWeighted.ts | Applies external grade floor/cap at compute time and uses bounded ratio for percent display. |
| client/app/bundles/course/gradebook/components/WeightedGradebookTable.tsx | Uses bounded ratio for percent breakdown display. |
| client/app/bundles/course/gradebook/components/GradebookTable.tsx | Renders external columns distinctly and adds inline-edit external grade cell + toast feedback. |
| client/app/bundles/course/gradebook/components/buildAssessmentColumnIds.ts | Allows negative assessment IDs in column-id parsing. |
| client/app/bundles/course/gradebook/tests/store.test.ts | Tests external grade upsert/clear reducer behavior. |
| client/app/bundles/course/gradebook/tests/computeWeighted.test.ts | Tests floor/cap grade bounding behavior in equal/custom/breakdown flows. |
| client/app/bundles/course/gradebook/tests/buildAssessmentColumnIds.test.ts | Tests round-tripping of negative assessment IDs in column IDs. |
| client/app/api/course/Gradebook.ts | Adds setExternalGrade API call. |
| app/views/course/gradebook/index.json.jbuilder | Emits synthetic external category/tabs/assessments and external grade submissions. |
| app/views/course/external_assessments/update_grade.json.jbuilder | Serializes external-grade write response for optimistic reconcile. |
| app/models/course/gradebook/external_contribution.rb | New model: external weight contributions + bulk update implementation. |
| app/models/course/external_assessment.rb | New model: external assessment + synthetic IDs and transactional creation helper. |
| app/models/course/external_assessment_grade.rb | New model: per-student external grade storage. |
| app/models/course.rb | Adds associations for external assessments and their contributions. |
| app/models/course_user.rb | Adds association for external assessment grades. |
| app/controllers/course/gradebook_controller.rb | Loads externals for index and persists external + tab weights atomically. |
| app/controllers/course/external_assessments_controller.rb | New endpoint to upsert/clear an external grade for a student. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| const [localValue, setLocalValue] = useState<number | null | undefined>( | ||
| value, | ||
| ); | ||
|
|
There was a problem hiding this comment.
agreed, will update accordingly.
| <span | ||
| onClick={() => { | ||
| setText(localValue == null ? '' : String(localValue)); | ||
| setEditing(true); | ||
| }} | ||
| role="button" | ||
| style={{ cursor: 'pointer', display: 'inline-block', minWidth: 24 }} | ||
| tabIndex={0} | ||
| > | ||
| {localValue == null ? '—' : localValue} | ||
| </span> |
There was a problem hiding this comment.
agreed, will update accordingly.
| const submissionIds: Partial<Record<number, number>> = {}; | ||
| assessments.forEach((a) => { | ||
| const sub = subs?.get(a.id); | ||
| const sub = subs.find((s) => s.assessmentId === a.id); | ||
| if (sub != null) { | ||
| grades[a.id] = sub.grade; |
| interface SortingProps<D> { | ||
| sort?: (datumA: D, datumB: D) => number; | ||
| undefinedPriority?: false | 'first' | 'last'; | ||
| descFirst?: boolean; |
There was a problem hiding this comment.
This appears to be unused. I thought we removed this from an earlier PR?
There was a problem hiding this comment.
My bad. I removed descFirst from the tables, but forgot to remove them from the components setup. removing it from both ColumnTemplate.ts and columnsBuilder.ts, there should be no more remnants of descFirst now.
|
|
||
| const DEFAULT_CSV_FILENAME = 'data' as const; | ||
|
|
||
| // Prepend UTF-8 BOM so Excel on macOS/Windows detects UTF-8 encoding instead |
There was a problem hiding this comment.
I remember being against this functionality when reviewing a previous PR, but I see that our backend actually has some code handling BOM logic, so I'm inclined to keep it in for now to keep things moving.
There was a problem hiding this comment.
right, noted. will keep this then.
a431979 to
f2b0246
Compare
…ation, inline grade entry) Foundation layer for external assessments: data model + migrations, gradebook read serialization, and inline grade entry (update_grade). Management UI (panel, add/edit/delete), weight integration, and CSV import are introduced in later PRs of the stack.
f2b0246 to
251439f
Compare
Summary
Adds the foundation for external assessments, the only gradebook columns a professor can create, on top of the weighted gradebook view (#8436). This PR introduces the data model (external assessment + external grade), serializes externals into the gradebook payload so they render as columns in the "All assessments" view, and makes those cells inline-editable with an optimistic save. Grades store at
decimal(5,2)so two-decimal imports (Canvas, SoftMark) round-trip without silent rounding, and each external carries optional floor/cap bound flags. The management panel and CSV import wizard are deferred to later PRs in this stack; this PR is model, read serialization, and single-cell write only. The grade-write endpoint (#grades) is also tightened from teaching assistant to manager.Design decisions
decimal(5,2)while native grade columns staydecimal(4,1)- externals are fed by Canvas/SoftMark which emit two decimals, whereas native grading is Coursemology-native where one decimal is fine. Widening every grade column would be a larger, separate change.effectiveGrade), never mutating the stored grade - the raw "All assessments" view always shows the true entered value, so toggling a flag re-interprets rather than rewrites data.Regression prevention
Covers: the external assessment and external grade models (validations, bounds, two-decimal precision), the synthetic-category contribution integration, the gradebook serializer's external payload, inline grade entry (optimistic apply and rollback on failure), the
setExternalGradeoperation, and external assessment column-id building. Migrations (20260615create external tables,20260616link externals to contributions,20260622bounds,20260623decimal(5,2)precision) are reversible. The#gradesauthorization change is asserted in the gradebook controller spec. No change to existing gradebook behavior when a course has no externals. Model and controller specs run on CI.