feat(gradebook): add Level contribution to weighted gradebook#8449
feat(gradebook): add Level contribution to weighted gradebook#8449LWS49 wants to merge 11 commits into
Conversation
…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.
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"
7aadba7 to
5f6ba3b
Compare
521efed to
2efbb02
Compare
- map student Level to grade-points via a safe parsed arithmetic formula - fold the Level term into weighted Total and per-student breakdown - add Configure Contributions controls: formula, weight, max level, show - persist a singleton LevelConfig per course (new table + migration) - surface a Level column in the weighted table, toggleable via column picker - warn when a contribution falls outside the advisory weight budget
5f6ba3b to
dcd91bf
Compare
| end | ||
|
|
||
| def compute_student_level_contributions | ||
| return {} unless @level_config&.enabled |
There was a problem hiding this comment.
[Correctable] Layout/EmptyLineAfterGuardClause: Add empty line after guard clause.
| class Course::GradebookController < Course::ComponentController | ||
| before_action :authorize_read_gradebook! | ||
|
|
||
| def index |
There was a problem hiding this comment.
Metrics/AbcSize: Assignment Branch Condition size for index is too high. [<12, 19, 3> 22.67/20]
| @@ -0,0 +1,144 @@ | |||
| # frozen_string_literal: true | |||
| class Course::GradebookController < Course::ComponentController | |||
There was a problem hiding this comment.
Metrics/ClassLength: Class has too many lines. [123/100]
| when '+' then l + r | ||
| when '-' then l - r | ||
| when '*' then l * r | ||
| when '/' then r.zero? ? 0.0 : l / r |
There was a problem hiding this comment.
[Correctable] Style/NumericPredicate: Use r == 0 instead of r.zero?.
| errors.add(:formula_ast, 'has an invalid structure') unless self.class.valid_ast?(formula_ast) | ||
| end | ||
|
|
||
| def evaluate_node(node, level) |
There was a problem hiding this comment.
Metrics/AbcSize: Assignment Branch Condition size for evaluate_node is too high. [<5, 32, 16> 36.12/20]
Metrics/CyclomaticComplexity: Cyclomatic complexity for evaluate_node is too high. [16/7]
Metrics/MethodLength: Method has too many lines. [25/15]
| end | ||
|
|
||
| it 'rejects a node with a missing required key' do | ||
| expect(described_class.valid_ast?({ 'type' => 'binop', 'op' => '+', 'left' => { 'type' => 'num', 'value' => 1 } })).to be false |
There was a problem hiding this comment.
[Correctable] Layout/LineLength: Line is too long. [137/120]
| expect(described_class.valid_ast?(deep)).to be false | ||
| end | ||
|
|
||
| it 'rejects a node with a missing required key' do |
There was a problem hiding this comment.
[Correctable] Layout/IndentationConsistency: Inconsistent indentation detected.
| expect(described_class.valid_ast?(nil)).to be false | ||
| end | ||
|
|
||
| it 'rejects a tree exceeding depth 20' do |
There was a problem hiding this comment.
[Correctable] Layout/IndentationConsistency: Inconsistent indentation detected.
| expect(described_class.valid_ast?({ 'type' => 'evil', 'payload' => 'x' })).to be false | ||
| end | ||
|
|
||
| it 'rejects a non-hash' do |
There was a problem hiding this comment.
[Correctable] Layout/IndentationConsistency: Inconsistent indentation detected.
| expect(described_class.valid_ast?(node)).to be false | ||
| end | ||
|
|
||
| it 'rejects an unknown type' do |
There was a problem hiding this comment.
[Correctable] Layout/IndentationConsistency: Inconsistent indentation detected.
16ea8b5 to
1c5b845
Compare
Summary
Adds an optional Level contribution to the weighted gradebook, sitting alongside the existing assessment-tab weights. An instructor enters an arithmetic formula (e.g.
level * 2ormin(level, 5)) that maps each student's course Level to grade-points, which then folds into the student's weighted Total. The formula, weight budget, effective max level, and column visibility are configured in the Configure Contributions dialog and persisted per course. A new Level column surfaces each student's contribution in the weighted table.Design decisions
eval- a hostile or malformed string can only ever produce a parse error, never run as code. The grammar is restricted to numbers, thelevel/maxLevelvariables, the four operators, andfloor/ceil/round/min/max.course_idunique) upserted on save, not a row-per-edit, since there is exactly one Level contribution per course.-1), keeping it disjoint from real positive assessment ids so it flows through the same compute and rendering paths without a parallel code path.weightis treated as an advisory budget: contributions outside[0, weight]drive a dialog warning rather than being clamped, leaving the instructor in control of the formula.Regression prevention
levelFormula.test.ts); Level folding into student totals and breakdown, null/disabled handling, and out-of-range detection (computeWeighted.test.ts); store hydration and update oflevelContribution/courseMaxLevel(store.test.ts); dialog enable/configure/warning behaviour (ConfigureWeightsPrompt.test.tsx); Level column rendering (GradebookWeightedTable.test.tsx);LevelConfigvalidations andupsert_forsingleton behaviour (level_config_spec.rb); controller persistence and serialization (gradebook_controller_spec.rb).showtoggle controlling column visibility independent ofenabled; weight-only saves (nolevelContributionparam) leaving the singleton config untouched.Adding screenshots: the Configure Contributions dialog showing the Level section (formula input + weight + out-of-range warning), and the weighted table with the Level column populated.