feat(gradebook): add gradebook page with column picker, CSV export, and statistics bridge#8400
feat(gradebook): add gradebook page with column picker, CSV export, and statistics bridge#8400LWS49 wants to merge 1 commit into
Conversation
3525c59 to
702f85e
Compare
fa7a036 to
031c2e1
Compare
0bb52e5 to
8a7d3be
Compare
cd70de2 to
64927b1
Compare
217cebd to
33def9e
Compare
33def9e to
7eb8f5d
Compare
3517914 to
b3e0042
Compare
3d7baf6 to
f3cc59a
Compare
…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.
f3cc59a to
da2242e
Compare
| return ( | ||
| <Page title={t(translations.statistics)} unpadded> | ||
| <Box className="max-w-full border-b border-divider"> | ||
| <Box |
There was a problem hiding this comment.
Tailwind class names are preferred over sx attribute styling.
| column.sortProps!.sort!(rowA.original, rowB.original) | ||
| : 'alphanumeric', | ||
| sortUndefined: column.sortProps?.undefinedPriority ?? false, | ||
| ...(column.sortProps?.descFirst !== undefined && { |
There was a problem hiding this comment.
Is there a reason why we are introducing this, over reversing the sign of the sorting function?
|
|
||
| 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.
Was this an issue reported by anyone?
I'm not sure this is a good idea, since it would potentially be breaking for any scripts parsing CSVs downloaded from Coursemology.
| { | ||
| key: self.class.key, | ||
| icon: :gradebook, | ||
| title: I18n.t('course.gradebook.component.sidebar_title'), |
There was a problem hiding this comment.
We migrated sidebar titles from BE to FE 6 months ago:
Your code should not be following this patttern.
This also means that the additions to locales/{en, ko, zh} yml files should be removed.
| end | ||
|
|
||
| def fetch_students | ||
| current_course.levels.to_a |
There was a problem hiding this comment.
What is the purpose of this line?
There was a problem hiding this comment.
Pull request overview
Adds a new staff-facing Gradebook feature to consolidate per-student grades across published assessments (with column picking, CSV export, and gamification-aware columns), and updates the Statistics → Assessments view to point users to Gradebook while removing the legacy “Score Summary” download pipeline.
Changes:
- Introduces
/courses/:id/gradebook(Rails controller + JSON view + client router/page/store/types) with a TanStack-backed table, column picker, per-user localStorage persistence, and CSV export. - Enhances the shared table builder (sorting persistence, “search what you see”, CSV BOM for Excel UTF-8, column picker prompt UX updates).
- Removes the legacy Assessment Score Summary download feature end-to-end (frontend + backend + locales + specs).
Reviewed changes
Copilot reviewed 82 out of 82 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| spec/services/course/statistics/assessment_score_summary_download_service_spec.rb | Removes specs for deprecated score summary download service. |
| spec/models/course/assessment/submission_spec.rb | Adds spec coverage for new Submission.grade_summary query. |
| spec/models/course/assessment_spec.rb | Adds spec coverage for new Assessment.max_grades query. |
| spec/controllers/course/statistics/aggregate_controller_spec.rb | Adds coverage for gradebookEnabled flag in statistics payload. |
| spec/controllers/course/gradebook_controller_spec.rb | Adds controller specs for gradebook authorization and JSON shape. |
| lib/tasks/coursemology/seed_gradebook.rake | Adds rake task to seed a demo gradebook course (20 students). |
| lib/tasks/coursemology/seed_600_gradebook.rake | Adds rake task to seed a large demo gradebook course (600 students). |
| config/routes.rb | Adds gradebook route; removes score summary download route. |
| config/locales/zh/csv.yml | Removes score summary CSV header translations (deprecated feature). |
| config/locales/zh/course/gradebook.yml | Adds server-side gradebook component sidebar title (zh). |
| config/locales/ko/csv.yml | Removes score summary CSV header translations (deprecated feature). |
| config/locales/ko/course/gradebook.yml | Adds server-side gradebook component sidebar title (ko). |
| config/locales/en/csv.yml | Removes score summary CSV header translations (deprecated feature). |
| config/locales/en/course/gradebook.yml | Adds server-side gradebook component sidebar title (en). |
| client/locales/zh.json | Adds Gradebook + subtitle translations; updates table picker prompt keys. |
| client/locales/ko.json | Adds Gradebook + subtitle translations; updates table picker prompt keys. |
| client/locales/en.json | Adds Gradebook + subtitle translations; updates table picker prompt keys. |
| client/app/types/course/gradebook.ts | Introduces shared TS types for gradebook payload. |
| client/app/types/course/courses.ts | Extends course layout type with userId for per-user persistence. |
| client/app/store.ts | Registers new gradebook reducer. |
| client/app/routers/course/index.tsx | Adds gradebook route to the course router. |
| client/app/routers/course/gradebook.tsx | Adds lazy-loaded gradebook route definition + handle. |
| client/app/lib/translations/table.ts | Adds totalXp column translation. |
| client/app/lib/hooks/useDismissibleOnce.ts | Adds hook for per-user “dismiss once” UI persisted in localStorage. |
| client/app/lib/hooks/tests/useDismissibleOnce.test.tsx | Adds tests for useDismissibleOnce. |
| client/app/lib/helpers/url-builders.js | Adds getCourseGradebookURL. |
| client/app/lib/constants/icons.ts | Adds gradebook icon mapping for course sidebar component. |
| client/app/lib/components/table/utils.ts | Prepends UTF-8 BOM for CSV downloads (Excel compatibility). |
| client/app/lib/components/table/TanStackTableBuilder/useTanStackTableBuilder.tsx | Adds sort persistence, “search visible columns”, CSV button toggling, and per-user namespacing. |
| client/app/lib/components/table/TanStackTableBuilder/columnsBuilder.ts | Supports sortDescFirst via descFirst in column templates. |
| client/app/lib/components/table/MuiTableAdapter/MuiColumnPickerPrompt.tsx | Moves “no data columns” hint into the prompt footer. |
| client/app/lib/components/table/builder/featureTemplates.ts | Adds csv download + sorting template options. |
| client/app/lib/components/table/builder/ColumnTemplate.ts | Adds descFirst sort option. |
| client/app/lib/components/table/adapters/Toolbar.ts | Updates comment to match “Select Columns” behavior. |
| client/app/lib/components/table/tests/utils.test.ts | Adds tests for CSV BOM behavior. |
| client/app/lib/components/table/tests/useTanStackTableBuilder.test.tsx | Adds regression tests for search visibility + sort reconciliation. |
| client/app/lib/components/table/tests/csvGenerator.test.ts | Adds coverage for exporting only selected rows. |
| client/app/lib/components/core/dialogs/Prompt.tsx | Adds footer slot used by column picker prompt. |
| client/app/lib/components/core/buttons/DownloadButton.tsx | Adjusts MUI Typography color styling. |
| client/app/bundles/users/types.ts | Adds SET_CURRENT_USER_ID action type for user id hydration. |
| client/app/bundles/users/store.ts | Implements setCurrentUserId action/reducer handling. |
| client/app/bundles/course/user-invitations/components/tables/InvitationResultPrimaryTable.tsx | Removes unused constant import. |
| client/app/bundles/course/user-invitations/components/tables/InvitationResultExistingTable.tsx | Removes unused constant import. |
| client/app/bundles/course/translations.ts | Adds gradebook component title translation id. |
| client/app/bundles/course/statistics/types.ts | Adds gradebookEnabled to assessments statistics payload type. |
| client/app/bundles/course/statistics/pages/StatisticsIndex/index.tsx | Minor MUI styling refactor for divider border. |
| client/app/bundles/course/statistics/pages/StatisticsIndex/assessments/AssessmentsStatisticsTable.tsx | Adds gradebook bridge subtitle + removes score summary selection/download UI. |
| client/app/bundles/course/statistics/pages/StatisticsIndex/assessments/AssessmentsStatistics.tsx | Passes gradebookEnabled into table. |
| client/app/bundles/course/statistics/pages/StatisticsIndex/assessments/AssessmentsScoreSummaryDownload.tsx | Removes deprecated score summary download component. |
| client/app/bundles/course/statistics/pages/StatisticsIndex/assessments/tests/AssessmentsStatisticsTable.test.tsx | Adds tests for bridge subtitle and ensures row selection stays off. |
| client/app/bundles/course/statistics/operations.ts | Removes score summary download operation. |
| client/app/bundles/course/gradebook/types.ts | Re-exports shared gradebook TS types for bundle usage. |
| client/app/bundles/course/gradebook/store.ts | Adds gradebook Redux slice + action. |
| client/app/bundles/course/gradebook/selectors.ts | Adds selectors for gradebook Redux slice. |
| client/app/bundles/course/gradebook/pages/GradebookIndex/index.tsx | Adds gradebook page container with loading/error/empty handling. |
| client/app/bundles/course/gradebook/operations.ts | Adds gradebook fetch operation. |
| client/app/bundles/course/gradebook/handles.ts | Adds route handle/crumb data for gradebook. |
| client/app/bundles/course/gradebook/constants.ts | Defines gradebook column id constants. |
| client/app/bundles/course/gradebook/components/GradeLinkHint.tsx | Adds dismissible hint about clickable grade links. |
| client/app/bundles/course/gradebook/components/GradebookTable.tsx | Implements gradebook table rendering, picker integration, selection + CSV export UX. |
| client/app/bundles/course/gradebook/components/GradebookColumnTree.tsx | Implements hierarchical column picker tree (student info / gamification / grades). |
| client/app/bundles/course/gradebook/components/buildAssessmentColumnIds.ts | Adds helpers for stable assessment column ids. |
| client/app/bundles/course/gradebook/tests/GradeLinkHint.test.tsx | Adds tests for dismissible grade link hint behavior. |
| client/app/bundles/course/gradebook/tests/GradebookTable.test.tsx | Adds comprehensive gradebook table tests (render, picker, search, sort, CSV, persistence). |
| client/app/bundles/course/gradebook/tests/GradebookIndex.test.tsx | Adds tests for gradebook index loading/error/empty behaviors. |
| client/app/bundles/course/gradebook/tests/GradebookColumnTree.test.tsx | Adds tests for column tree rendering and toggle propagation. |
| client/app/bundles/course/container/CourseLoader.ts | Hydrates authenticated userId into global store from course layout payload. |
| client/app/bundles/course/container/tests/CourseLoader.test.ts | Adds tests ensuring userId hydration behavior. |
| client/app/api/course/Statistics/CourseStatistics.ts | Removes deprecated downloadScoreSummary API method. |
| client/app/api/course/index.js | Registers new gradebook API client. |
| client/app/api/course/Gradebook.ts | Adds gradebook API client wrapper. |
| app/views/course/statistics/aggregate/all_assessments.json.jbuilder | Adds gradebookEnabled flag to statistics payload. |
| app/views/course/gradebook/index.json.jbuilder | Adds gradebook JSON payload view. |
| app/views/course/courses/sidebar.json.jbuilder | Adds userId to course layout payload for client-side persistence namespacing. |
| app/services/course/statistics/assessments_score_summary_download_service.rb | Removes deprecated score summary service. |
| app/models/course/assessment/submission.rb | Adds grade_summary SQL query for gradebook. |
| app/models/course/assessment.rb | Adds max_grades SQL query for gradebook max marks row/CSV. |
| app/models/components/course/gradebook_ability_component.rb | Adds CanCan ability for :read_gradebook. |
| app/jobs/course/statistics/assessments_score_summary_download_job.rb | Removes deprecated background job. |
| app/controllers/course/statistics/aggregate_controller.rb | Removes deprecated score summary download action. |
| app/controllers/course/gradebook_controller.rb | Adds gradebook controller and data loading logic. |
| app/controllers/components/course/gradebook_component.rb | Adds gradebook sidebar component integration. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| def fetch_students | ||
| current_course.levels.to_a | ||
| current_course.course_users.students.without_phantom_users. | ||
| calculated(:experience_points).includes(:user).to_a | ||
| end |
| const subs = submissionsByStudent.get(student.id) ?? []; | ||
| const grades: Partial<Record<number, number | null>> = {}; | ||
| const submissionIds: Partial<Record<number, number>> = {}; | ||
| assessments.forEach((a) => { | ||
| const sub = subs.find((s) => s.assessmentId === a.id); | ||
| if (sub != null) { |
| admin = User::Email.find_by_email('test@example.org').user | ||
| User.stamper = admin |
| admin = User::Email.find_by_email('test@example.org').user | ||
| User.stamper = admin |
Summary
Adds a new Gradebook page (
/courses/:id/gradebook) giving staff a consolidated view of student grades across all published assessments. The page renders a TanStack-backed table with per-student rows and per-assessment grade columns, supports a tree-structured column picker (student info, gamification, and grades branches), persists column visibility to localStorage per user per course, and exports the visible columns as CSV. Gamification columns (Level, XP) are hidden from the picker when the course has gamification disabled. The Statistics → Assessments tab gains a subtitle explaining it is only for course-level statistics to bridge the two views. The existing "Download Score Summary" button on that tab is removed in full (frontend component, operations, API method, Rails controller action, route, background job, service, and locale keys) since the gradebook supersedes it.Regression prevention
Tests added:
GradebookColumnTree.test.tsx- covers branch rendering, gamification gating, checkbox disabled state, parent toggle propagation, and mixed visibility indeterminate stateGradebookIndex.test.tsx- covers loading state, table render, empty student state, column picker toggle, and error toast on fetch failureGradebookTable.test.tsx- covers grade cell rendering, unsubmitted placeholder, column visibility, CSV export header/row content, and localStorage persistence across remountsgradebook_controller_spec.rb(authorization, index JSON shape),assessment_spec.rbandsubmission_spec.rb(new query methods),aggregate_controller_spec.rb(gradebookEnabled flag)Manual testing: All scenarios confirmed - gradebook page loads with students and grades, column picker toggles persist and reflect in CSV, gamification columns absent when disabled, empty state renders cleanly, score summary button gone.
Backward compat: Score summary download removed for all users on upgrade; no migration needed. All other Statistics features unchanged.