Skip to content

feat(gradebook): add gradebook page with column picker, CSV export, and statistics bridge#8400

Open
LWS49 wants to merge 1 commit into
masterfrom
lws49/feat-gradebook-export
Open

feat(gradebook): add gradebook page with column picker, CSV export, and statistics bridge#8400
LWS49 wants to merge 1 commit into
masterfrom
lws49/feat-gradebook-export

Conversation

@LWS49

@LWS49 LWS49 commented May 21, 2026

Copy link
Copy Markdown
Collaborator

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 state
  • GradebookIndex.test.tsx - covers loading state, table render, empty student state, column picker toggle, and error toast on fetch failure
  • GradebookTable.test.tsx - covers grade cell rendering, unsubmitted placeholder, column visibility, CSV export header/row content, and localStorage persistence across remounts
  • Backend: gradebook_controller_spec.rb (authorization, index JSON shape), assessment_spec.rb and submission_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.

image

@LWS49 LWS49 force-pushed the lws49/feat-gradebook-export branch 4 times, most recently from 3525c59 to 702f85e Compare May 21, 2026 11:43
@LWS49 LWS49 marked this pull request as ready for review May 21, 2026 11:53
@LWS49 LWS49 force-pushed the lws49/feat-gradebook-export branch 9 times, most recently from fa7a036 to 031c2e1 Compare May 25, 2026 05:53
@LWS49 LWS49 changed the base branch from master to lws49/feat-table-column-picker May 26, 2026 08:07
@LWS49 LWS49 force-pushed the lws49/feat-table-column-picker branch 4 times, most recently from 0bb52e5 to 8a7d3be Compare May 29, 2026 04:27
@LWS49 LWS49 force-pushed the lws49/feat-gradebook-export branch 4 times, most recently from cd70de2 to 64927b1 Compare May 29, 2026 09:08
Base automatically changed from lws49/feat-table-column-picker to master June 1, 2026 10:12
@LWS49 LWS49 force-pushed the lws49/feat-gradebook-export branch 3 times, most recently from 217cebd to 33def9e Compare June 4, 2026 02:22
@LWS49 LWS49 changed the title feat(gradebook): add gradebook with column picker and CSV export feat(gradebook): add gradebook page with column picker and CSV export Jun 4, 2026
@LWS49 LWS49 force-pushed the lws49/feat-gradebook-export branch from 33def9e to 7eb8f5d Compare June 4, 2026 02:36
@LWS49 LWS49 changed the title feat(gradebook): add gradebook page with column picker and CSV export feat(gradebook): add gradebook page with column picker, CSV export, and statistics bridge Jun 4, 2026
@LWS49 LWS49 force-pushed the lws49/feat-gradebook-export branch from 3517914 to b3e0042 Compare June 4, 2026 04:16
@LWS49 LWS49 closed this Jun 8, 2026
@LWS49 LWS49 reopened this Jun 8, 2026
@LWS49 LWS49 force-pushed the lws49/feat-gradebook-export branch 19 times, most recently from 3d7baf6 to f3cc59a Compare June 15, 2026 09:43
…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 force-pushed the lws49/feat-gradebook-export branch from f3cc59a to da2242e Compare June 17, 2026 06:43
return (
<Page title={t(translations.statistics)} unpadded>
<Box className="max-w-full border-b border-divider">
<Box

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 && {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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'),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We migrated sidebar titles from BE to FE 6 months ago:

#8172

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the purpose of this line?

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +36 to +40
def fetch_students
current_course.levels.to_a
current_course.course_users.students.without_phantom_users.
calculated(:experience_points).includes(:user).to_a
end
Comment on lines +230 to +235
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) {
Comment on lines +12 to +13
admin = User::Email.find_by_email('test@example.org').user
User.stamper = admin
Comment on lines +12 to +13
admin = User::Email.find_by_email('test@example.org').user
User.stamper = admin
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.

3 participants