[UnitTests] Update course matrix to use courseIds

This commit is contained in:
Aaro Koivukangas
2026-09-21 15:24:21 +03:00
committed by Aaro Koivukangas
parent ca54fba5be
commit 86900b3ad4
3 changed files with 44 additions and 40 deletions
@@ -20,8 +20,10 @@ export const CourseMatrixExport = ({
const { getTextIn } = useLanguage()
const getXlsx = () => {
const courseNameByCode = new Map(courses.map(({ course }) => [course.code, getTextIn(course.name) ?? '']))
const { completedCoursesRows, courseCounterRows } = calculateExcelData(students, courseNameByCode)
const courseInfoById = new Map(
courses.map(({ course }) => [course.id, { code: course.code, name: getTextIn(course.name) ?? '' }])
)
const { completedCoursesRows, courseCounterRows } = calculateExcelData(students, courseInfoById)
// Columns: Student number, Name, student's attainments in the format: "course name (course code)"
const completedCoursesSheet = utils.aoa_to_sheet([['Student number', 'Name'], ...completedCoursesRows])
@@ -21,41 +21,43 @@ export type CourseMatrixAggregate = {
/**
* Passed completions of the student that are included in one of the student's study plans (HOPS).
* Returns the latest completion per course code, sorted by course code.
* Returns the latest completion per course id, sorted by course id.
*/
export const getHopsCourses = (student: FormattedStudent): StudentCourse[] => {
const hopsCourseCodes = new Set(student.studyplans?.flatMap(plan => plan.included_courses ?? []) ?? [])
const passedCoursesInHops = student.courses.filter(course => course.passed && hopsCourseCodes.has(course.course_code))
const hopsCourseIds = new Set(student.studyplans?.flatMap(plan => plan.included_courses ?? []) ?? [])
const passedCoursesInHops = student.courses.filter(course => course.passed && hopsCourseIds.has(course.course_id))
const latestByCode = new Map<string, StudentCourse>()
const latestById = new Map<string, StudentCourse>()
for (const course of passedCoursesInHops) {
if (!course.course_code) continue
if (!course.course_id) continue
const existing = latestByCode.get(course.course_code)
const existing = latestById.get(course.course_id)
if (!existing || new Date(course.date).getTime() > new Date(existing.date).getTime()) {
latestByCode.set(course.course_code, course)
latestById.set(course.course_id, course)
}
}
return [...latestByCode.values()].sort((a, b) => a.course_code.localeCompare(b.course_code))
return [...latestById.values()].sort((a, b) => a.course_id.localeCompare(b.course_id))
}
export const calculateExcelData = (students: FormattedStudent[], courseNameByCode: Map<string, string>) => {
const counters = new Map<string, { name: string; credits: number; students: Set<string> }>()
export const calculateExcelData = (
students: FormattedStudent[],
courseInfoById: Map<string, { code: string; name: string }>
) => {
// Keyed by course_id internally so different courses are never accidentally merged;
// code/name are only for display.
const counters = new Map<string, { code: string; name: string; credits: number; students: Set<string> }>()
const rows = students.map(student => {
const courses = getHopsCourses(student).map(course => {
const code = course.course_code
const counter = counters.get(code) ?? {
name: courseNameByCode.get(code) ?? '',
credits: 0,
students: new Set(),
}
counters.set(code, counter)
const id = course.course_id
const { code, name } = courseInfoById.get(id) ?? { code: id, name: '' }
const counter = counters.get(id) ?? { code, name, credits: 0, students: new Set() }
counters.set(id, counter)
counter.credits += course.credits ?? 0
counter.students.add(student.studentNumber)
return { code, name: courseNameByCode.get(code) ?? '', credits: course.credits ?? 0 }
return { code, name, credits: course.credits ?? 0 }
})
return { studentNumber: student.studentNumber, studentName: student.name, courses }
@@ -67,8 +69,8 @@ export const calculateExcelData = (students: FormattedStudent[], courseNameByCod
...courses.map(course => `${course.name} (${course.code})`),
])
const courseCounterRows = [...counters.entries()]
.map(([code, { name, credits, students }]) => [code, name, students.size.toString(), credits.toString()])
const courseCounterRows = [...counters.values()]
.map(({ code, name, credits, students }) => [code, name, students.size.toString(), credits.toString()])
.sort(([_a, __a, aStudents], [_b, __b, bStudents]) => parseInt(bStudents) - parseInt(aStudents))
return { completedCoursesRows, courseCounterRows }
@@ -6,12 +6,12 @@ import { CreditTypeCode } from '@oodikone/shared/types'
import { createCourse as createBaseCourse, createStudent, createStudyPlan } from '@oodikone/shared/test/utils'
const createCourse = (
course_code: string,
course_id: string,
credits: number,
passed: boolean,
date: Date,
credittypecode: CreditTypeCode = passed ? CreditTypeCode.PASSED : CreditTypeCode.FAILED
) => createBaseCourse({ course_code, date, passed, grade: passed ? '5' : 'Hyl.', credits, credittypecode })
) => createBaseCourse({ course_id, date, passed, grade: passed ? '5' : 'Hyl.', credits, credittypecode })
const createHops = (includedCourses: string[]) => createStudyPlan({ included_courses: includedCourses })
@@ -29,7 +29,7 @@ void describe('getHopsCourses', () => {
const result = getHopsCourses(student)
assert.equal(result.length, 1)
assert.equal(result[0].course_code, 'A')
assert.equal(result[0].course_id, 'A')
})
void it('returns an empty list when the student has no HOPS', () => {
@@ -55,28 +55,28 @@ void describe('getHopsCourses', () => {
})
void describe('calculateExcelData', () => {
void it('builds completed-course rows and aggregates credits per course', () => {
void it('builds completed-course rows and aggregates credits per course, displaying code rather than id', () => {
const students = [
createStudent({
studentNumber: '1',
courses: [createCourse('A', 5, true, new Date('2024-09-01'))],
studyplans: [createHops(['A'])],
courses: [createCourse('course-a', 5, true, new Date('2024-09-01'))],
studyplans: [createHops(['course-a'])],
}),
createStudent({
studentNumber: '2',
courses: [
createCourse('A', 5, true, new Date('2024-09-01')),
createCourse('B', 3, true, new Date('2024-10-01')),
createCourse('course-a', 5, true, new Date('2024-09-01')),
createCourse('course-b', 3, true, new Date('2024-10-01')),
],
studyplans: [createHops(['A', 'B'])],
studyplans: [createHops(['course-a', 'course-b'])],
}),
]
const courseNameByCode = new Map([
['A', 'Course A'],
['B', 'Course B'],
const courseInfoById = new Map([
['course-a', { code: 'A', name: 'Course A' }],
['course-b', { code: 'B', name: 'Course B' }],
])
const data = calculateExcelData(students, courseNameByCode)
const data = calculateExcelData(students, courseInfoById)
assert.deepEqual(data.completedCoursesRows, [
['1', 'Testi Opiskelija', 'Course A (A)'],
@@ -88,18 +88,18 @@ void describe('calculateExcelData', () => {
])
})
void it('uses an empty name for courses missing from the course map', () => {
void it('falls back to id as the code for courses missing from the course map', () => {
const students = [
createStudent({
studentNumber: '1',
courses: [createCourse('RANDOMCODE', 5, true, new Date('2024-09-01'))],
studyplans: [createHops(['RANDOMCODE'])],
courses: [createCourse('unknown-course-id', 5, true, new Date('2024-09-01'))],
studyplans: [createHops(['unknown-course-id'])],
}),
]
const data = calculateExcelData(students, new Map())
assert.deepEqual(data.completedCoursesRows, [['1', 'Testi Opiskelija', ' (RANDOMCODE)']])
assert.deepEqual(data.courseCounterRows, [['RANDOMCODE', '', '1', '5']])
assert.deepEqual(data.completedCoursesRows, [['1', 'Testi Opiskelija', ' (unknown-course-id)']])
assert.deepEqual(data.courseCounterRows, [['unknown-course-id', '', '1', '5']])
})
})