Skip to content

feat(gradebook): weighted view with weights and exclusions#8436

Merged
LWS49 merged 1 commit into
lws49/feat-gradebook-exportfrom
lws49/feat-gradebook-weighted-view
Jun 29, 2026
Merged

feat(gradebook): weighted view with weights and exclusions#8436
LWS49 merged 1 commit into
lws49/feat-gradebook-exportfrom
lws49/feat-gradebook-weighted-view

Conversation

@LWS49

@LWS49 LWS49 commented Jun 12, 2026

Copy link
Copy Markdown
Collaborator

Summary

Adds an opt-in weighted gradebook view on top of the base gradebook. Course staff enable it through a new Gradebook settings toggle, then configure per-tab weights in either equal mode (every included assessment counts equally toward the tab subtotal) or custom mode (explicit per-assessment weights that must sum to the tab total). Totals are additive: each tab subtotal is scaled by its weight and the gradebook Total is the literal sum of those subtotals. Staff can exclude individual assessments from totals . A per-student breakdown popover, a points/percentage display toggle, and a projected-total hint complete the view.

Design decisions

  • Weighted view is opt-in per course via a settings toggle - courses that don't want weighting see the existing gradebook unchanged.
  • Custom-mode weights are validated server-side to sum (at 2 decimal places) to the tab total, inside a transaction that rolls back on mismatch - prevents partially-saved weight sets that don't add up.
  • Exclusion flags are rewritten across the whole tab on every save (excluded ids to true, the rest to false) - avoids stale exclusions when modes change or assessments move.

Regression prevention

  • Backend specs: the tab weight-update pipeline (equal/custom modes, custom sum gate, exclusions, drop-lowest, unknown tab/assessment, transactional rollback), gradebook controller (index JSON plus update_weights authorization and error paths), gradebook ability, and the gradebook settings component.
  • Frontend tests: computeWeighted (equal/custom math, exclusions, drop-lowest), GradebookWeightedTable, the configure-weights prompt, weighted column tree, projected-total and weighted-view hints, the store, and the gradebook settings form.
  • Manual testing (confirmed by author): enabling and disabling the view, equal and custom weight configuration including the sum gate, per-assessment exclusion, points/percentage toggle, breakdown row expansion, and permission gating for read vs manage.
  • Backward compatible: new columns default to non-weighted behaviour (tab weight 0, equal mode, not excluded) and the weighted view stays hidden unless explicitly enabled, so existing gradebook behaviour is unchanged.

@LWS49 LWS49 changed the title Lws49/feat gradebook weighted view feat(gradebook): weighted view with weights, exclusions & drop-lowest Jun 12, 2026
@LWS49 LWS49 force-pushed the lws49/feat-gradebook-weighted-view branch from a8b79ab to 8d139e0 Compare June 12, 2026 03:53
@LWS49 LWS49 force-pushed the lws49/feat-gradebook-export branch 5 times, most recently from 42f0dfc to 1a7691c Compare June 12, 2026 06:04
@LWS49 LWS49 force-pushed the lws49/feat-gradebook-weighted-view branch from 8d139e0 to c1ea96f Compare June 12, 2026 06:09
@LWS49 LWS49 force-pushed the lws49/feat-gradebook-export branch from 1a7691c to 0c9f4e7 Compare June 12, 2026 06:13
@LWS49 LWS49 force-pushed the lws49/feat-gradebook-weighted-view branch 12 times, most recently from 90115ff to 422b4af Compare June 15, 2026 05:58
@LWS49 LWS49 force-pushed the lws49/feat-gradebook-export branch from 0c9f4e7 to c8af6a7 Compare June 15, 2026 05:59
@LWS49 LWS49 force-pushed the lws49/feat-gradebook-weighted-view branch from 422b4af to 44284da Compare June 15, 2026 06:02
@LWS49 LWS49 changed the title feat(gradebook): weighted view with weights, exclusions & drop-lowest feat(gradebook): weighted view with weights and exclusions Jun 15, 2026
@LWS49 LWS49 force-pushed the lws49/feat-gradebook-export branch 5 times, most recently from 3d7baf6 to f3cc59a Compare June 15, 2026 09:43
@LWS49 LWS49 force-pushed the lws49/feat-gradebook-weighted-view branch from 44284da to 2d24331 Compare June 15, 2026 09:51
@LWS49 LWS49 closed this Jun 26, 2026
@LWS49 LWS49 force-pushed the lws49/feat-gradebook-weighted-view branch from 5ad6ad2 to eda6ef5 Compare June 26, 2026 06:15
@LWS49 LWS49 reopened this Jun 26, 2026
@LWS49 LWS49 force-pushed the lws49/feat-gradebook-weighted-view branch 6 times, most recently from 16ea8b5 to 1c5b845 Compare June 26, 2026 08:51
@LWS49 LWS49 force-pushed the lws49/feat-gradebook-weighted-view branch 4 times, most recently from fea40ba to 2fb3c20 Compare June 29, 2026 08:20
Comment thread app/models/course/gradebook/tab_contribution.rb
@LWS49 LWS49 force-pushed the lws49/feat-gradebook-weighted-view branch from 2fb3c20 to a1a8b69 Compare June 29, 2026 08:46

def define_permissions
can :read_gradebook, Course, id: course.id if course_user&.staff?
can :read_gradebook, Course, id: course.id if course_user&.manager_or_owner?

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.

I see you have a separate PR open to restrict gradebook access to manager/owner.

However this is already doing that, does that mean the other PR is redundant now?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

yep, will retire that PR accordingly.

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 an opt-in weighted gradebook view (with per-tab weights, per-assessment custom weights, and exclusions) while keeping the existing “all assessments” gradebook unchanged unless enabled via a new course admin setting. The PR introduces new backend persistence + endpoints for weights/settings and a full frontend weighted-table UI with configuration dialog and supporting tests/translations.

Changes:

  • Backend: add gradebook weighting persistence (tab + assessment contribution tables/models) and APIs (index JSON enrichment + PATCH /gradebook/weights) with authorization gates.
  • Frontend: add a new Weighted total gradebook view (toggleable via tabs), configuration UI, and supporting shared UI components (e.g., SegmentedSwitch).
  • Admin: add a new Gradebook settings page + routes to enable/disable weighted view.

Reviewed changes

Copilot reviewed 63 out of 63 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
spec/models/course/settings/gradebook_component_spec.rb Adds specs for the new weighted_view_enabled course setting.
spec/models/course/gradebook/contribution_spec.rb Adds model specs for tab-level gradebook contributions and bulk update behavior.
spec/models/course/gradebook/assessment_contribution_spec.rb Adds model specs for per-assessment gradebook contribution rows.
spec/models/course/gradebook_ability_spec.rb Adds ability specs for reading gradebook and managing weights/settings.
spec/factories/course_gradebook_contributions.rb Introduces a factory for tab-level gradebook contributions.
spec/factories/course_gradebook_assessment_contributions.rb Introduces a factory for assessment-level gradebook contributions.
spec/controllers/course/gradebook_controller_spec.rb Expands controller specs for authorization, weighted fields, and update_weights.
spec/controllers/course/admin/gradebook_settings_controller_spec.rb Adds controller specs for the new gradebook settings admin endpoints.
db/schema.rb Updates schema to include new gradebook contribution tables and FKs.
db/migrate/20260611000000_create_gradebook_contribution_tables.rb Adds migration creating tab + assessment contribution tables.
config/routes.rb Adds routes for gradebook settings and PATCH /gradebook/weights.
config/locales/zh/activerecord/errors.yml Adds i18n for custom-weight mismatch validation.
config/locales/ko/activerecord/errors.yml Adds i18n for custom-weight mismatch validation.
config/locales/en/activerecord/errors.yml Adds i18n for custom-weight mismatch validation.
client/locales/zh.json Adds client strings for weighted gradebook UI + settings.
client/locales/ko.json Adds client strings for weighted gradebook UI + settings.
client/locales/en.json Adds client strings for weighted gradebook UI + settings.
client/app/types/course/gradebook.ts Extends gradebook payload types with weighting fields + update payload.
client/app/types/course/admin/gradebook.ts Adds types for gradebook settings admin API payloads.
client/app/routers/course/admin.tsx Adds admin route for the new Gradebook settings page.
client/app/lib/components/wrappers/mocks/I18nProvider.tsx Adds a synchronous I18nProvider mock for deterministic tests.
client/app/lib/components/core/buttons/SegmentedSwitch.tsx Introduces a new compact segmented switch control with keyboard support.
client/app/lib/components/core/buttons/test/SegmentedSwitch.test.tsx Adds tests for SegmentedSwitch behavior and accessibility roles.
client/app/bundles/course/gradebook/types.ts Re-exports UpdateWeightsPayload for gradebook bundle use.
client/app/bundles/course/gradebook/store.ts Adds weighted-view flags and reducer support for updating weights/exclusions.
client/app/bundles/course/gradebook/selectors.ts Adds selectors for weighted view enabled + manage-weights capability.
client/app/bundles/course/gradebook/pages/GradebookIndex/index.tsx Adds view toggle (All vs Weighted), weighted hint, and weighted table rendering.
client/app/bundles/course/gradebook/operations.ts Adds updateGradebookWeights operation calling the new API endpoint.
client/app/bundles/course/gradebook/computeWeighted.ts Implements weighted subtotal/total math, breakdowns, and default weight resolution.
client/app/bundles/course/gradebook/components/WeightedViewHint.tsx Adds one-time dismissible hint prompting managers to enable weighted view.
client/app/bundles/course/gradebook/components/WeightedGradebookTable.tsx Adds the main weighted gradebook table UI (display mode, breakdowns, CSV, etc.).
client/app/bundles/course/gradebook/components/WeightedGradebookColumnTree.tsx Adds column picker tree for the weighted table (student-info group only).
client/app/bundles/course/gradebook/components/ProjectedTotalHint.tsx Adds one-time projected-total policy hint + shared tooltip translation.
client/app/bundles/course/gradebook/components/ConfigureWeightsPrompt.tsx Adds UI to configure tab weights, modes, custom weights, and exclusions.
client/app/bundles/course/gradebook/tests/WeightedViewHint.test.tsx Tests the weighted-view discovery hint and dismissal persistence.
client/app/bundles/course/gradebook/tests/WeightedGradebookColumnTree.test.tsx Tests the weighted column picker tree behavior.
client/app/bundles/course/gradebook/tests/store.test.ts Tests reducer behavior for updating weights, exclusions, and modes.
client/app/bundles/course/gradebook/tests/ProjectedTotalHint.test.tsx Tests the projected-total hint and dismissal persistence.
client/app/bundles/course/gradebook/tests/GradebookIndex.test.tsx Tests view toggle rendering and hint visibility rules.
client/app/bundles/course/gradebook/tests/ConfigureWeightsPrompt.test.tsx Tests configure-weights dialog behavior (modes, sums, exclusions, save payload).
client/app/bundles/course/gradebook/tests/computeWeighted.test.ts Tests weighted math, defaults, exclusions, and breakdown computations.
client/app/bundles/course/admin/pages/GradebookSettings/translations.ts Adds translations for the admin gradebook settings page.
client/app/bundles/course/admin/pages/GradebookSettings/operations.ts Adds admin API operations to fetch/update weighted-view setting.
client/app/bundles/course/admin/pages/GradebookSettings/index.tsx Adds the admin Gradebook settings page container with preload + submit.
client/app/bundles/course/admin/pages/GradebookSettings/GradebookSettingsForm.tsx Adds the admin form for toggling weighted view.
client/app/bundles/course/admin/pages/GradebookSettings/tests/GradebookSettings.test.tsx Adds UI tests for the admin setting toggle behavior.
client/app/api/course/Gradebook.ts Adds updateWeights client API method for PATCH /gradebook/weights.
client/app/api/course/Admin/index.ts Registers the new admin gradebook API client.
client/app/api/course/Admin/Gradebook.ts Implements the admin gradebook settings API client.
app/views/course/gradebook/index.json.jbuilder Adds weighted-view flags + serialized weights/exclusions when enabled.
app/views/course/admin/gradebook_settings/edit.json.jbuilder Renders gradebook settings JSON (weightedViewEnabled).
app/models/course/settings/gradebook_component.rb Adds course setting accessors for weighted view enabled state.
app/models/course/gradebook/contribution.rb Adds tab contribution model, enums, validations, and transactional bulk update.
app/models/course/gradebook/assessment_contribution.rb Adds assessment contribution model with weight/excluded validations.
app/models/course/gradebook.rb Adds table name prefix module for gradebook namespace models.
app/models/course/assessment/tab.rb Adds association from tab to gradebook contribution.
app/models/course/assessment.rb Adds association from assessment to gradebook assessment contribution.
app/models/course.rb Adds association from course to gradebook contributions.
app/models/components/course/gradebook_ability_component.rb Tightens/defines abilities for reading gradebook and managing weights/settings.
app/controllers/course/gradebook_controller.rb Adds weighted fields to index and adds update_weights endpoint.
app/controllers/course/admin/gradebook_settings_controller.rb Adds admin controller to edit/update gradebook settings.
app/controllers/components/course/gradebook_component.rb Adds a settings sidebar item for gradebook settings when authorized.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread spec/controllers/course/gradebook_controller_spec.rb Outdated
Comment thread spec/controllers/course/gradebook_controller_spec.rb Outdated
Comment thread spec/controllers/course/gradebook_controller_spec.rb Outdated

def update_weights
authorize! :manage_gradebook_weights, current_course
updates = update_weights_params[:weights].map { |entry| parse_weight_entry(entry) }

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Guarded: updates = (update_weights_params[:weights] || []).map { ... }. An omitted weights param is now a 200 no-op instead of NoMethodError -> 500.

Comment thread app/controllers/course/gradebook_controller.rb Outdated
Comment thread app/controllers/course/gradebook_controller.rb Outdated
Comment on lines +108 to +114
def self.validate_custom_assessment_weights_sum!(tab, entry, included_sum, included_any)
return unless included_any
return unless (included_sum * 100).round != (entry[:weight] * 100).round

tab.errors.add(:base, :custom_weights_mismatch)
raise ActiveRecord::RecordInvalid, tab
end

@LWS49 LWS49 Jun 29, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Left as-is. (included_sum * 100).round != (entry[:weight] * 100).round is an integer-cents comparison, which is robust against 2dp values in [0, 100]. And now that weights are rounded to 2dp at the parse boundary (see the parse_weight_entry change), the values reaching this gate are already clean 2dp. No float divergence left to guard against, so changing the model here would be a no-op cosmetic diff.

@adi-herwana-nus adi-herwana-nus 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.

Comments on frontend testing:

Screenshot 2026-06-29 at 16 38 09

first alert: "Want to set how much each item contributes to the student's total grade? Enable weighted total view in gradebook settings."

Second alert can probably be removed entirely.

Image

In the course settings, it should also be called "Weighted total" view to make things consistent everywhere.

Image

Looks like 0/0 is resolving to NaN and it's propagating through the grade computation, you should probably coerce it to 0.


Nitpick: I don't feel the button group (thing that selects between Points and Percentage) style is consistent with what we have on CM currently. See below for reference

Screenshot 2026-06-29 at 16 43 05

Though this is only UI change, so I'm OK to merge everything else first, then handle this at the end.

@LWS49

LWS49 commented Jun 29, 2026

Copy link
Copy Markdown
Collaborator Author

Inclined to keep GradeLinkHint for now: it is a one-time dismissable that helps guard against the gradebook expectation that "users can modify grades in gradebook".

Missed the case where the default assignment itself can have max marks of 0. Added guards against it.

Agree with your comments on keeping it consistent, changed course settings to say enable Weighted Total view. Capitalised the view names to stay consistent with codebase too.
image

image

Agreed on the button group. Thought toggle was the only 2-way switch, didn't know this existed. Post-merge, will create a PR to abstract out the button + use it here.

@LWS49 LWS49 force-pushed the lws49/feat-gradebook-weighted-view branch 3 times, most recently from b89ca5d to 6dd61fc Compare June 29, 2026 10:38
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 6dd61fc to 91969ac Compare June 29, 2026 12:12
@LWS49 LWS49 merged commit 2a4bcf2 into lws49/feat-gradebook-export Jun 29, 2026
1 of 8 checks passed
@LWS49 LWS49 deleted the lws49/feat-gradebook-weighted-view branch June 29, 2026 12:12
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