Skip to content

Commit 0ea5b07

Browse files
committed
fix: implement fixes based on comments
1 parent 42b6c76 commit 0ea5b07

16 files changed

Lines changed: 49 additions & 68 deletions

File tree

.circleci/config.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@ commands:
119119
command: yarn build:test
120120
environment:
121121
AVAILABLE_CPUS: 4
122+
NODE_OPTIONS: --max-old-space-size=4096
122123

123124
- save_cache:
124125
paths:

app/controllers/components/course/gradebook_component.rb

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ def sidebar_items
1313
{
1414
key: self.class.key,
1515
icon: :gradebook,
16-
title: I18n.t('course.gradebook.component.sidebar_title'),
1716
type: :normal,
1817
weight: 9,
1918
path: course_gradebook_path(current_course)

app/controllers/course/gradebook_controller.rb

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# frozen_string_literal: true
22
class Course::GradebookController < Course::ComponentController
33
before_action :authorize_read_gradebook!
4+
before_action :preload_levels, only: [:index]
45

56
def index
67
respond_to do |format|
@@ -34,9 +35,8 @@ def fetch_categories_and_tabs
3435
end
3536

3637
def fetch_students
37-
current_course.levels.to_a
3838
current_course.course_users.students.without_phantom_users.
39-
calculated(:experience_points).includes(:user).to_a
39+
calculated(:experience_points).includes(user: :emails).to_a
4040
end
4141

4242
def fetch_published_assessments
@@ -46,4 +46,11 @@ def fetch_published_assessments
4646
joins(tab: :category).
4747
reorder('course_assessment_categories.weight, course_assessment_tabs.weight, course_assessments.id')
4848
end
49+
50+
# Pre-loads course levels to avoid N+1 queries when each course_user.level_number is rendered.
51+
# level_number is derived, not an association (it buckets EXP against course.levels), so it
52+
# can't be added to fetch_students' includes. See Course::LevelsConcern#level_for.
53+
def preload_levels
54+
current_course.levels.to_a
55+
end
4956
end

client/app/bundles/course/gradebook/__tests__/GradebookTable.test.tsx

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -701,7 +701,9 @@ describe('GradebookTable', () => {
701701
const user = userEvent.setup();
702702
renderGrades(mixedStudents, mixedSubmissions);
703703
await screen.findByText('Alice');
704-
await user.click(screen.getByRole('button', { name: /quiz 1/i }));
704+
// Grade columns sort desc-first, so click twice to reach ascending.
705+
await user.click(screen.getByRole('button', { name: /quiz 1/i })); // desc
706+
await user.click(screen.getByRole('button', { name: /quiz 1/i })); // asc
705707
// Ascending: Bob(3), Alice(8), then the two missing rows last.
706708
await waitFor(() => expectInOrder(['Bob', 'Alice']));
707709
expectInOrder(['Alice', 'Carol']);
@@ -712,7 +714,7 @@ describe('GradebookTable', () => {
712714
const user = userEvent.setup();
713715
renderGrades(mixedStudents, mixedSubmissions);
714716
await screen.findByText('Alice');
715-
await user.click(screen.getByRole('button', { name: /quiz 1/i })); // asc
717+
// Grade columns sort desc-first, so a single click yields descending.
716718
await user.click(screen.getByRole('button', { name: /quiz 1/i })); // desc
717719
// Descending: Alice(8), Bob(3), then the two missing rows still last.
718720
await waitFor(() => expectInOrder(['Alice', 'Bob']));
@@ -730,7 +732,9 @@ describe('GradebookTable', () => {
730732
],
731733
);
732734
await screen.findByText('Alice');
733-
await user.click(screen.getByRole('button', { name: /quiz 1/i }));
735+
// Grade columns sort desc-first, so click twice to reach ascending.
736+
await user.click(screen.getByRole('button', { name: /quiz 1/i })); // desc
737+
await user.click(screen.getByRole('button', { name: /quiz 1/i })); // asc
734738
// Numeric ascending: 9 (Alice) before 10 (Bob). Lexical would reverse this.
735739
await waitFor(() => expectInOrder(['Alice', 'Bob']));
736740
});

client/app/bundles/course/gradebook/components/GradebookTable.tsx

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -212,26 +212,26 @@ const GradebookTable = ({
212212
const { t } = useTranslation();
213213

214214
const submissionsByStudent = useMemo(() => {
215-
const map = new Map<number, SubmissionData[]>();
215+
const map = new Map<number, Map<number, SubmissionData>>();
216216
submissions.forEach((s) => {
217-
const existing = map.get(s.studentId);
218-
if (existing) {
219-
existing.push(s);
220-
} else {
221-
map.set(s.studentId, [s]);
217+
let byAssessment = map.get(s.studentId);
218+
if (!byAssessment) {
219+
byAssessment = new Map<number, SubmissionData>();
220+
map.set(s.studentId, byAssessment);
222221
}
222+
byAssessment.set(s.assessmentId, s);
223223
});
224224
return map;
225225
}, [submissions]);
226226

227227
const rows = useMemo<GradebookRow[]>(
228228
() =>
229229
students.map((student) => {
230-
const subs = submissionsByStudent.get(student.id) ?? [];
230+
const subs = submissionsByStudent.get(student.id);
231231
const grades: Partial<Record<number, number | null>> = {};
232232
const submissionIds: Partial<Record<number, number>> = {};
233233
assessments.forEach((a) => {
234-
const sub = subs.find((s) => s.assessmentId === a.id);
234+
const sub = subs?.get(a.id);
235235
if (sub != null) {
236236
grades[a.id] = sub.grade;
237237
submissionIds[a.id] = sub.submissionId;
@@ -321,7 +321,6 @@ const GradebookTable = ({
321321
sortable: true,
322322
sortProps: {
323323
undefinedPriority: 'last',
324-
descFirst: false,
325324
sort: (a, b) => {
326325
const aGrade = a.grades[asn.id];
327326
const bGrade = b.grades[asn.id];

client/app/bundles/course/statistics/pages/StatisticsIndex/index.tsx

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -115,10 +115,7 @@ const StatisticsIndex: FC = () => {
115115

116116
return (
117117
<Page title={t(translations.statistics)} unpadded>
118-
<Box
119-
className="max-w-full"
120-
sx={{ borderBottom: 1, borderColor: 'divider' }}
121-
>
118+
<Box className="max-w-full border-b border-divider">
122119
<Tabs
123120
aria-label="Statistics Index Tabs"
124121
onChange={(_, value) => {

client/app/lib/components/table/TanStackTableBuilder/columnsBuilder.ts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -61,9 +61,6 @@ const buildTanStackColumns = <D extends Data>(
6161
column.sortProps!.sort!(rowA.original, rowB.original)
6262
: 'alphanumeric',
6363
sortUndefined: column.sortProps?.undefinedPriority ?? false,
64-
...(column.sortProps?.descFirst !== undefined && {
65-
sortDescFirst: column.sortProps.descFirst,
66-
}),
6764
filterFn:
6865
column.filterProps?.shouldInclude &&
6966
Object.assign(

client/app/lib/components/table/__tests__/useTanStackTableBuilder.test.tsx

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1049,17 +1049,12 @@ describe('useTanStackTableBuilder sort reconciliation', () => {
10491049
cell: (r) => r.label,
10501050
sortable: true,
10511051
},
1052-
// descFirst:false so the first click sorts score ascending → Bob(1),
1053-
// Alice(9). TanStack otherwise derives desc-first from the numeric
1054-
// first value, yielding Alice,Bob — which is identical to the
1055-
// label-asc reset order and would silently defeat this test.
10561052
{
10571053
id: 'score',
10581054
of: 'score',
10591055
title: 'Score',
10601056
cell: (r) => r.score,
10611057
sortable: true,
1062-
sortProps: { descFirst: false },
10631058
},
10641059
]
10651060
: [
@@ -1086,13 +1081,20 @@ describe('useTanStackTableBuilder sort reconciliation', () => {
10861081
// Initial sort is label asc → Alice, Bob.
10871082
expect(names(result)).toEqual(['Alice', 'Bob']);
10881083

1089-
// User sorts by the score column (asc) → Bob (1), Alice (9).
1090-
act(() => {
1091-
const scoreHeader = result.current.header!.headers[1];
1092-
result.current.header!.forEach(scoreHeader, 1).sorting!.onClickSort!(
1093-
{} as never,
1094-
);
1095-
});
1084+
// Score is numeric, so TanStack sorts it desc-first. The first click gives
1085+
// desc (Alice 9, Bob 1) — identical to the label-asc reset order — so click
1086+
// again to reach asc (Bob 1, Alice 9), an order distinct from the reset that
1087+
// proves the score column is the active sort before it is removed.
1088+
const clickScoreSort = (): void => {
1089+
act(() => {
1090+
const scoreHeader = result.current.header!.headers[1];
1091+
result.current.header!.forEach(scoreHeader, 1).sorting!.onClickSort!(
1092+
{} as never,
1093+
);
1094+
});
1095+
};
1096+
clickScoreSort(); // desc → Alice, Bob
1097+
clickScoreSort(); // asc → Bob, Alice
10961098
expect(names(result)).toEqual(['Bob', 'Alice']);
10971099

10981100
// Score column disappears (gamification turned off) → sort resets to label asc.

client/app/lib/components/table/__tests__/utils.test.ts

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -9,18 +9,6 @@ const mockDownloadFile = downloadFile as jest.Mock;
99
describe('downloadCsv', () => {
1010
beforeEach(() => mockDownloadFile.mockClear());
1111

12-
it('prepends a UTF-8 BOM so Excel detects UTF-8 encoding', () => {
13-
downloadCsv('a,b\n1,2');
14-
const content: string = mockDownloadFile.mock.calls[0][1];
15-
expect(content.charCodeAt(0)).toBe(0xfeff);
16-
});
17-
18-
it('preserves em dash characters after the BOM', () => {
19-
downloadCsv('Name,Score\nAlice — Test,10');
20-
const content: string = mockDownloadFile.mock.calls[0][1];
21-
expect(content).toContain('—');
22-
});
23-
2412
it('uses the provided filename with .csv extension', () => {
2513
downloadCsv('a,b', 'my-file');
2614
expect(mockDownloadFile.mock.calls[0][2]).toBe('my-file.csv');

client/app/lib/components/table/builder/ColumnTemplate.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ interface SearchingProps<D> {
1717
interface SortingProps<D> {
1818
sort?: (datumA: D, datumB: D) => number;
1919
undefinedPriority?: false | 'first' | 'last';
20-
descFirst?: boolean;
2120
}
2221

2322
interface ColumnTemplate<D extends Data> {

0 commit comments

Comments
 (0)