mirror of
https://github.com/UniversityOfHelsinkiCS/oodikone.git
synced 2026-10-12 19:03:39 +03:00
[Backend][CourseStatistics] Change student count calculation logic to respect selected year
This commit is contained in:
@@ -56,7 +56,7 @@ export type CourseYearlyStatsResBody = Awaited<ReturnType<typeof getCourseYearly
|
||||
router.get<never, CanError<CourseYearlyStatsResBody>, CourseYearlyStatsReqBody, CourseYearlyStatsQuery>(
|
||||
'/courseyearlystats',
|
||||
async (req, res) => {
|
||||
const { codes, combineSubstitutions, separate } = req.query
|
||||
const { codes, combineSubstitutions, separate, fromYear, toYear } = req.query
|
||||
|
||||
const courseCodes = handleQueryArrays(codes)
|
||||
|
||||
@@ -82,7 +82,14 @@ router.get<never, CanError<CourseYearlyStatsResBody>, CourseYearlyStatsReqBody,
|
||||
const useCombineSubstitutions = combineSubstitutions !== 'false'
|
||||
const useSeparate = separate === 'true'
|
||||
|
||||
const results = await getCourseYearlyStats(courseCodes, useSeparate, anonymizationSalt, useCombineSubstitutions)
|
||||
const results = await getCourseYearlyStats(
|
||||
courseCodes,
|
||||
useSeparate,
|
||||
anonymizationSalt,
|
||||
useCombineSubstitutions,
|
||||
fromYear,
|
||||
toYear
|
||||
)
|
||||
return res.json(results)
|
||||
}
|
||||
)
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
} from '../../models'
|
||||
import { getIsOpen } from './helpers'
|
||||
|
||||
export const getCreditsForCourses = async (codeGroups: string[][], unification: Unification) => {
|
||||
export const getCreditsForCourses = async (codeGroups: string[][], unification: Unification, from: Date, to: Date) => {
|
||||
const allCourseCodes = codeGroups.flatMap(group => group)
|
||||
|
||||
// We need the credits grouped by student numbers so that we can check if a student has
|
||||
@@ -41,6 +41,7 @@ export const getCreditsForCourses = async (codeGroups: string[][], unification:
|
||||
},
|
||||
is_open: getIsOpen(unification),
|
||||
credittypecode: { [Op.not]: CreditTypeCode.IMPROVED }, // We do not care about improved grades
|
||||
attainment_date: { [Op.between]: [from, to] },
|
||||
},
|
||||
order: [['attainment_date', 'ASC']],
|
||||
include: [
|
||||
@@ -134,7 +135,12 @@ export const getStudentNumberToSrElementsMap = async (studentNumbers: string[])
|
||||
}, {})
|
||||
}
|
||||
|
||||
export const getEnrollmentsForCourses = async (codeGroups: string[][], unification: Unification) => {
|
||||
export const getEnrollmentsForCourses = async (
|
||||
codeGroups: string[][],
|
||||
unification: Unification,
|
||||
from: Date,
|
||||
to: Date
|
||||
) => {
|
||||
const allCourseCodes = codeGroups.flatMap(group => group)
|
||||
|
||||
const students = await StudentModel.findAll({
|
||||
@@ -160,8 +166,13 @@ export const getEnrollmentsForCourses = async (codeGroups: string[][], unificati
|
||||
course_code: {
|
||||
[Op.in]: allCourseCodes,
|
||||
},
|
||||
// Date when OK changed from Oodi to Sisu data, studyright_id is null before that date
|
||||
enrollment_date_time: { [Op.gte]: enrollmentTimeDateThreshold },
|
||||
enrollment_date_time: {
|
||||
[Op.and]: {
|
||||
[Op.between]: [from, to],
|
||||
// Date when OK changed from Oodi to Sisu data, studyright_id is null before that date
|
||||
[Op.gte]: enrollmentTimeDateThreshold,
|
||||
},
|
||||
},
|
||||
state: EnrollmentState.ENROLLED,
|
||||
is_open: getIsOpen(unification),
|
||||
},
|
||||
|
||||
@@ -80,7 +80,7 @@ const parseCredit = (
|
||||
studyRightElements.find(studyRightElement => studyRightElement.studyRightId === studyRightId) ??
|
||||
studyRightElements
|
||||
.filter(({ startDate, endDate }) => dateIsBetween(attainmentDate, startDate, endDate))
|
||||
.sort((a, b) => b.startDate.getTime() - a.startDate.getTime())
|
||||
.sort((a, b) => (b.endDate?.getTime() ?? 0) - (a.endDate?.getTime() ?? 0))
|
||||
.at(0) // The newest studyRightElement
|
||||
|
||||
const programme = programmeOfCredit
|
||||
@@ -184,7 +184,9 @@ const getYearlyStatsOfNew = async (
|
||||
unification: Unification,
|
||||
anonymizationSalt: string | null,
|
||||
combineSubstitutions: boolean,
|
||||
studentNumberToSrElementsMap: Record<string, SISStudyRightElementModel[]>
|
||||
studentNumberToSrElementsMap: Record<string, SISStudyRightElementModel[]>,
|
||||
from: Date,
|
||||
to: Date
|
||||
) => {
|
||||
// Includes main course code and substitutions (if enabled)
|
||||
const creditGroupCodes =
|
||||
@@ -209,8 +211,8 @@ const getYearlyStatsOfNew = async (
|
||||
const { semesters, years } = await getSemestersAndYears()
|
||||
|
||||
const [creditGroups, enrollmentGroups] = await Promise.all([
|
||||
getCreditsForCourses(filteredCreditGroupCodes, unification),
|
||||
getEnrollmentsForCourses(filteredCreditGroupCodes, unification),
|
||||
getCreditsForCourses(filteredCreditGroupCodes, unification, from, to),
|
||||
getEnrollmentsForCourses(filteredCreditGroupCodes, unification, from, to),
|
||||
])
|
||||
|
||||
const counter = new CourseYearlyStatsCounter()
|
||||
@@ -350,12 +352,21 @@ export const getCourseYearlyStats = async (
|
||||
courseCodes: string[],
|
||||
separate: boolean,
|
||||
anonymizationSalt: string | null,
|
||||
combineSubstitutions: boolean
|
||||
combineSubstitutions: boolean,
|
||||
fromYear = '1900',
|
||||
toYear: string = new Date().getFullYear().toString()
|
||||
) => {
|
||||
// Default to 1900 - currentYear+1 so that without parameters the api returns stats for all years
|
||||
const from = new Date(`${fromYear}-08-01`) // FALL
|
||||
const to = new Date(`${parseInt(toYear) + 1}-07-31`) // SPRING next year
|
||||
|
||||
const [credits, enrollments] = await Promise.all([
|
||||
CreditModel.findAll({
|
||||
attributes: ['student_studentnumber'],
|
||||
where: { course_code: { [Op.in]: courseCodes } },
|
||||
where: {
|
||||
course_code: { [Op.in]: courseCodes },
|
||||
attainment_date: { [Op.between]: [from, to] },
|
||||
},
|
||||
}),
|
||||
EnrollmentModel.findAll({
|
||||
attributes: ['studentnumber'],
|
||||
@@ -364,7 +375,12 @@ export const getCourseYearlyStats = async (
|
||||
[Op.in]: courseCodes,
|
||||
},
|
||||
state: EnrollmentState.ENROLLED,
|
||||
enrollment_date_time: { [Op.gte]: enrollmentTimeDateThreshold },
|
||||
enrollment_date_time: {
|
||||
[Op.and]: {
|
||||
[Op.between]: [from, to],
|
||||
[Op.gte]: enrollmentTimeDateThreshold,
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
])
|
||||
@@ -384,9 +400,9 @@ export const getCourseYearlyStats = async (
|
||||
const statsRegular = await Promise.all(
|
||||
courseCodes.map(async courseCode => {
|
||||
const course: Pick<CourseModel, 'code' | 'name' | 'substitution_groups'> | null = await CourseModel.findOne({
|
||||
raw: true,
|
||||
attributes: ['code', 'name', 'substitution_groups'],
|
||||
where: { code: courseCode },
|
||||
raw: true,
|
||||
})
|
||||
|
||||
if (!course) {
|
||||
@@ -402,7 +418,9 @@ export const getCourseYearlyStats = async (
|
||||
Unification.OPEN,
|
||||
anonymizationSalt,
|
||||
combineSubstitutions,
|
||||
studentNumberToSrElementsMap
|
||||
studentNumberToSrElementsMap,
|
||||
from,
|
||||
to
|
||||
),
|
||||
getYearlyStatsOfNew(
|
||||
course,
|
||||
@@ -411,7 +429,9 @@ export const getCourseYearlyStats = async (
|
||||
Unification.REGULAR,
|
||||
anonymizationSalt,
|
||||
combineSubstitutions,
|
||||
studentNumberToSrElementsMap
|
||||
studentNumberToSrElementsMap,
|
||||
from,
|
||||
to
|
||||
),
|
||||
getYearlyStatsOfNew(
|
||||
course,
|
||||
@@ -420,7 +440,9 @@ export const getCourseYearlyStats = async (
|
||||
Unification.UNIFY,
|
||||
anonymizationSalt,
|
||||
combineSubstitutions,
|
||||
studentNumberToSrElementsMap
|
||||
studentNumberToSrElementsMap,
|
||||
from,
|
||||
to
|
||||
),
|
||||
])
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import { Unarray } from '@oodikone/shared/types'
|
||||
import { CourseYearlyStatsResBody } from '../../../src/routes/courses'
|
||||
import { initTests, ResponseWithBody } from '../../utils'
|
||||
|
||||
void describe('Course yearly statistics', () => {
|
||||
void describe('Course yearly statistics (2016-2023)', () => {
|
||||
let app: Express
|
||||
beforeAll(async () => {
|
||||
app = await initTests()
|
||||
@@ -27,7 +27,7 @@ void describe('Course yearly statistics', () => {
|
||||
let body: Unarray<CourseYearlyStatsResBody>
|
||||
beforeAll(async () => {
|
||||
const res = (await request(app)
|
||||
.get('/courseyearlystats?codes=TKT10002&combineSubstitutions=false')
|
||||
.get('/courseyearlystats?codes=TKT10002&combineSubstitutions=false&fromYear=2016&toYear=2023')
|
||||
.set('shib-session-id', 'test')
|
||||
.set('uid', 'basic')
|
||||
.set('hygroupcn', 'grp-oodikone-basic-users')) as ResponseWithBody<CourseYearlyStatsResBody>
|
||||
@@ -41,7 +41,7 @@ void describe('Course yearly statistics', () => {
|
||||
)
|
||||
})
|
||||
|
||||
it('(statistic years)', () => {
|
||||
it('for statistic years', () => {
|
||||
assert.strictEqual(body.unifyStats!.statistics.length, 8, 'unifyStats years')
|
||||
assert.strictEqual(body.regularStats!.statistics.length, 8, 'regularStats years')
|
||||
assert.strictEqual(body.openStats!.statistics.length, 4, 'openStats years')
|
||||
@@ -52,121 +52,90 @@ void describe('Course yearly statistics', () => {
|
||||
assert.strictEqual(Object.keys(body.openStats!.facultyStats).length, 4, 'openStats years')
|
||||
})
|
||||
|
||||
describe('(statistic contents)', () => {
|
||||
it('- 2016-2017', () => {
|
||||
const stats2016 = body.unifyStats!.statistics.find(year => year.name === '2016-2017')!
|
||||
assert.deepStrictEqual(
|
||||
stats2016.students.studentNumbers,
|
||||
['484144'],
|
||||
'Incorrect students marked to 2016 stats (students)'
|
||||
)
|
||||
// TODO: Rewrite /courseyearlystats so that total = passed + failed + enrolledNoGrade
|
||||
it.each([
|
||||
[
|
||||
'2016-2017',
|
||||
{ total: 1, passed: 1, failed: 0, enrolledNoGrade: 0 },
|
||||
{ 5: 0, 4: 1, 3: 0, 2: 0, 1: 0, 0: 0, 'Hyl.': 0, 'Hyv.': 0 },
|
||||
],
|
||||
[
|
||||
'2017-2018',
|
||||
{ total: 23, passed: 19, failed: 4, enrolledNoGrade: 0 },
|
||||
{ 5: 11, 4: 5, 3: 2, 2: 1, 1: 0, 0: 0, 'Hyl.': 4, 'Hyv.': 0 },
|
||||
],
|
||||
[
|
||||
'2018-2019',
|
||||
{ total: 28, passed: 26, failed: 2, enrolledNoGrade: 0 },
|
||||
{ 5: 18, 4: 3, 3: 2, 2: 1, 1: 2, 0: 2, 'Hyl.': 0, 'Hyv.': 0 },
|
||||
],
|
||||
[
|
||||
'2019-2020',
|
||||
{ total: 28, passed: 28, failed: 0, enrolledNoGrade: 0 },
|
||||
{ 5: 17, 4: 5, 3: 1, 2: 4, 1: 1, 0: 0, 'Hyl.': 0, 'Hyv.': 0 },
|
||||
],
|
||||
[
|
||||
'2020-2021',
|
||||
{ total: 23, passed: 23, failed: 0, enrolledNoGrade: 0 },
|
||||
{ 5: 18, 4: 2, 3: 0, 2: 2, 1: 0, 0: 0, 'Hyl.': 0, 'Hyv.': 1 },
|
||||
],
|
||||
[
|
||||
'2021-2022',
|
||||
{ total: 15, passed: 15, failed: 0, enrolledNoGrade: 20 },
|
||||
{ 5: 13, 4: 2, 3: 0, 2: 0, 1: 0, 0: 0, 'Hyl.': 0, 'Hyv.': 0 },
|
||||
],
|
||||
[
|
||||
'2022-2023',
|
||||
{ total: 27, passed: 27, failed: 0, enrolledNoGrade: 34 },
|
||||
{ 5: 21, 4: 6, 3: 0, 2: 0, 1: 0, 0: 0, 'Hyl.': 0, 'Hyv.': 0 },
|
||||
],
|
||||
[
|
||||
'2023-2024',
|
||||
{ total: 1, passed: 1, failed: 0, enrolledNoGrade: 7 },
|
||||
{ 5: 1, 4: 0, 3: 0, 2: 0, 1: 0, 0: 0, 'Hyl.': 0, 'Hyv.': 0 },
|
||||
],
|
||||
])('for statistic content during ($0)', (year, categories, grades) => {
|
||||
const stats = body.unifyStats!.statistics.find(yearStats => yearStats.name === year)!
|
||||
assert('enrollments' in stats, 'Missing field enrollment in statsitics')
|
||||
|
||||
assert.deepStrictEqual(stats2016.attempts.grades['4'], ['484144'], 'Incorrect grades marked to 2016 stats (4)')
|
||||
if (categories) {
|
||||
assert.strictEqual(
|
||||
stats2016.attempts.categories.passed.length,
|
||||
1,
|
||||
'Incorrect students marked as passed to 2016 stats (passed)'
|
||||
stats.attempts.categories.failed.length,
|
||||
categories.failed,
|
||||
`Incorrect amount of failed students for ${year}`
|
||||
)
|
||||
assert.strictEqual(
|
||||
stats2016.attempts.categories.failed.length,
|
||||
0,
|
||||
'Incorrect students marked as failed to 2016 stats (failed)'
|
||||
)
|
||||
|
||||
assert('enrollments' in stats2016, 'Missing field enrollment in statsitics')
|
||||
assert.strictEqual(stats2016.enrollments.length, 0, 'Enrollments found when none should exist')
|
||||
assert.strictEqual(stats2016.allEnrollments.length, 0, 'Enrollments found when none should exist')
|
||||
})
|
||||
|
||||
it('- 2017-2018', () => {
|
||||
const stats2017 = body.unifyStats!.statistics.find(year => year.name === '2017-2018')!
|
||||
|
||||
assert.strictEqual(stats2017.attempts.grades['5'].length, 11, 'Incorrect grades marked to 2017 stats (5)')
|
||||
assert.strictEqual(stats2017.attempts.grades['4'].length, 5, 'Incorrect grades marked to 2017 stats, (4)')
|
||||
assert.strictEqual(stats2017.attempts.grades['3'].length, 2, 'Incorrect grades marked to 2017 stats (3)')
|
||||
assert.strictEqual(stats2017.attempts.grades['2'].length, 1, 'Incorrect grades marked to 2017 stats (2)')
|
||||
assert(!('1' in stats2017.attempts.grades), 'Incorrect grades marked to 2017 stats (1)')
|
||||
assert.strictEqual(stats2017.attempts.grades['Hyl.'].length, 4, 'Incorrect grades marked to 2017 stats (Hyl.)')
|
||||
|
||||
assert.strictEqual(stats2017.students.studentNumbers.length, 23, 'Incorrect amount of total students 2017')
|
||||
|
||||
assert.strictEqual(
|
||||
stats2017.attempts.categories.passed.length,
|
||||
11 + 5 + 2 + 1 + 0,
|
||||
'Incorrect students marked as passed to 2017 stats'
|
||||
stats.attempts.categories.passed.length,
|
||||
categories.passed,
|
||||
`Incorrect amount of passed students for ${year}`
|
||||
)
|
||||
assert.strictEqual(
|
||||
stats2017.attempts.categories.failed.length,
|
||||
4,
|
||||
'Incorrect students marked as failed to 2017 stats'
|
||||
stats.students.studentNumbers.length,
|
||||
categories.total,
|
||||
`Incorrect amount of total students for ${year}`
|
||||
)
|
||||
|
||||
// These should not exist before 21-22
|
||||
assert('enrollments' in stats2017, 'Missing field enrollment in statsitics')
|
||||
assert.strictEqual(stats2017.enrollments.length, 0, 'Enrollments found when none should exist')
|
||||
assert.strictEqual(stats2017.allEnrollments.length, 0, 'Enrollments found when none should exist')
|
||||
})
|
||||
|
||||
it('- 2018-2019', () => {
|
||||
const stats2018 = body.unifyStats!.statistics.find(year => year.name === '2018-2019')!
|
||||
|
||||
// There are more students that have passed this course later even though they have failed the course during 2018-2019
|
||||
assert.deepStrictEqual(
|
||||
stats2018.attempts.categories.failed,
|
||||
['454128', '546124'],
|
||||
'Incorrect students marked as failed 2018'
|
||||
assert.strictEqual(
|
||||
stats.enrollments.length,
|
||||
categories.enrolledNoGrade,
|
||||
`Incorrect amount of enrolled students for ${year}`
|
||||
)
|
||||
|
||||
assert.strictEqual(stats2018.attempts.grades['0'].length, 2)
|
||||
assert.strictEqual((stats2018.attempts.grades['Hyl.'] ?? []).length, 0)
|
||||
assert.strictEqual(stats2018.attempts.categories.failed.length, 2, 'Incorrect amount of failed students 2018')
|
||||
})
|
||||
|
||||
it('- 2020-2021', () => {
|
||||
const stats2020 = body.unifyStats!.statistics.find(year => year.name === '2020-2021')!
|
||||
|
||||
// These should not exist before 21-22
|
||||
assert('enrollments' in stats2020, 'Missing field enrollment in statsitics')
|
||||
assert.strictEqual(stats2020.enrollments.length, 0, 'Enrollments found when none should exist')
|
||||
assert.strictEqual(stats2020.allEnrollments.length, 0, 'Enrollments found when none should exist')
|
||||
})
|
||||
|
||||
it('- 2022-2023', () => {
|
||||
const stats2022 = body.unifyStats!.statistics.find(year => year.name === '2022-2023')!
|
||||
assert('enrollments' in stats2022, 'Missing field enrollment in statsitics')
|
||||
|
||||
assert.strictEqual(stats2022.students.studentNumbers.length, 27, 'Incorrect amount of total students 2022')
|
||||
|
||||
assert.strictEqual(stats2022.enrollments.length, 34, 'Enrollments found when none should exist')
|
||||
assert.strictEqual(stats2022.allEnrollments.length, 43, 'Enrollments found when none should exist')
|
||||
})
|
||||
|
||||
it('- 2023-2024', () => {
|
||||
const stats2023 = body.unifyStats!.statistics.find(year => year.name === '2023-2024')!
|
||||
assert.strictEqual(stats2023.students.studentNumbers.length, 1, 'Incorrect amount of total students 2023')
|
||||
|
||||
assert.deepStrictEqual(
|
||||
stats2023.attempts.categories.passed,
|
||||
['521757'],
|
||||
'Incorrect students marked as passed in 2023 stats'
|
||||
}
|
||||
if (grades) {
|
||||
Object.entries(grades).forEach(([grade, count]) =>
|
||||
assert.strictEqual(
|
||||
stats.attempts.grades[grade]?.length ?? 0,
|
||||
count,
|
||||
`Incorrect amount of students with grade ${grade}`
|
||||
)
|
||||
)
|
||||
assert.deepStrictEqual(
|
||||
stats2023.attempts.grades['5'],
|
||||
['521757'],
|
||||
"Correct student doesn't exist under correct grade"
|
||||
)
|
||||
|
||||
assert('enrollments' in stats2023, 'Missing field enrollment in statsitics')
|
||||
assert.strictEqual(stats2023.enrollments.length, 7, 'Enrollments found when none should exist')
|
||||
assert.strictEqual(stats2023.allEnrollments.length, 7, 'Enrollments found when none should exist')
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
describe('should work correctly in specific cases (TKT10002)', () => {
|
||||
let body: Unarray<CourseYearlyStatsResBody>
|
||||
beforeAll(async () => {
|
||||
const res = (await request(app)
|
||||
.get('/courseyearlystats?codes=TKT10002&combineSubstitutions=false')
|
||||
.get('/courseyearlystats?codes=TKT10002&combineSubstitutions=false&fromYear=2016&toYear=2023')
|
||||
.set('shib-session-id', 'test')
|
||||
.set('uid', 'basic')
|
||||
.set('hygroupcn', 'grp-oodikone-basic-users')) as ResponseWithBody<CourseYearlyStatsResBody>
|
||||
@@ -423,24 +392,24 @@ void describe('Course yearly statistics', () => {
|
||||
})
|
||||
|
||||
describe('should work correctly in specific cases (MAT21003)', () => {
|
||||
let body: Unarray<CourseYearlyStatsResBody>
|
||||
beforeAll(async () => {
|
||||
const res = (await request(app)
|
||||
.get('/courseyearlystats?codes=MAT21003&combineSubstitutions=false')
|
||||
.set('shib-session-id', 'test')
|
||||
.set('uid', 'basic')
|
||||
.set('hygroupcn', 'grp-oodikone-basic-users')) as ResponseWithBody<CourseYearlyStatsResBody>
|
||||
describe('should not count duplicate failed grades to different years when choosing a large timespan', () => {
|
||||
let body: Unarray<CourseYearlyStatsResBody>
|
||||
beforeAll(async () => {
|
||||
const res = (await request(app)
|
||||
.get('/courseyearlystats?codes=MAT21003&combineSubstitutions=false')
|
||||
.set('shib-session-id', 'test')
|
||||
.set('uid', 'basic')
|
||||
.set('hygroupcn', 'grp-oodikone-basic-users')) as ResponseWithBody<CourseYearlyStatsResBody>
|
||||
|
||||
assert.strictEqual(res.status, 200)
|
||||
assert.strictEqual(res.body.length, 1)
|
||||
body = res.body.at(0)!
|
||||
assert(
|
||||
'unifyStats' in body && 'regularStats' in body && 'openStats' in body,
|
||||
'All keys of courseyearlystats not defined'
|
||||
)
|
||||
})
|
||||
assert.strictEqual(res.status, 200)
|
||||
assert.strictEqual(res.body.length, 1)
|
||||
body = res.body.at(0)!
|
||||
assert(
|
||||
'unifyStats' in body && 'regularStats' in body && 'openStats' in body,
|
||||
'All keys of courseyearlystats not defined'
|
||||
)
|
||||
})
|
||||
|
||||
describe('should not count duplicate failed grades to different years', () => {
|
||||
it('- 2017-2018 should not include failed grade', () => {
|
||||
const year = body.unifyStats?.statistics.find(year => year.name === '2017-2018')
|
||||
assert.strictEqual(year?.attempts.categories.failed.length, 0, 'Failed stats should not include any students')
|
||||
@@ -463,3 +432,196 @@ void describe('Course yearly statistics', () => {
|
||||
it.todo('should return correct amount of students for multiple coursecodes without substitutions')
|
||||
it.todo('should return correct amount of students for multiple coursecodes with substitutions')
|
||||
})
|
||||
|
||||
describe('Course yearly statistics (smaller timeframes)', () => {
|
||||
let app: Express
|
||||
beforeAll(async () => {
|
||||
app = await initTests()
|
||||
})
|
||||
|
||||
it.each([
|
||||
['2017-2018', 2017, 2017],
|
||||
['2020-2023', 2020, 2022],
|
||||
['2016-2024', 2016, 2023],
|
||||
])('should include correct years for smaller timeframe (MAT21003 $0)', async (name, from, to) => {
|
||||
const res = (await request(app)
|
||||
.get(`/courseyearlystats?codes=MAT21003&combineSubstitutions=false&fromYear=${from}&toYear=${to}`)
|
||||
.set('shib-session-id', 'test')
|
||||
.set('uid', 'basic')
|
||||
.set('hygroupcn', 'grp-oodikone-basic-users')) as ResponseWithBody<CourseYearlyStatsResBody>
|
||||
|
||||
assert.strictEqual(res.status, 200)
|
||||
assert.strictEqual(res.body.length, 1)
|
||||
const body = res.body.at(0)!
|
||||
|
||||
const years: string[] = []
|
||||
for (let i = from; i <= to; i++) {
|
||||
years.push(`${i}-${i + 1}`)
|
||||
}
|
||||
assert.deepStrictEqual(
|
||||
body.unifyStats!.statistics.map(({ name }) => name),
|
||||
years,
|
||||
`${name} included incorrect years`
|
||||
)
|
||||
})
|
||||
|
||||
// TODO: Rewrite /courseyearlystats so that total = passed + failed + enrolledNoGrade
|
||||
it.each([
|
||||
[
|
||||
'2016-2017',
|
||||
{ total: 1, passed: 1, failed: 0, enrolledNoGrade: 0, extra: {} },
|
||||
{ 5: 0, 4: 1, 3: 0, 2: 0, 1: 0, 0: 0, 'Hyl.': 0, 'Hyv.': 0 },
|
||||
],
|
||||
[
|
||||
'2017-2018',
|
||||
{
|
||||
total: 26,
|
||||
passed: 19,
|
||||
failed: 7,
|
||||
enrolledNoGrade: 0,
|
||||
extra: { failed: ['455478', '457686', '474032', '497183', '509165', '547994', '550840'] },
|
||||
},
|
||||
{ 5: 11, 4: 5, 3: 2, 2: 1, 1: 0, 0: 0, 'Hyl.': 7, 'Hyv.': 0 },
|
||||
],
|
||||
[
|
||||
'2018-2019',
|
||||
{ total: 30, passed: 26, failed: 4, enrolledNoGrade: 0, extra: {} },
|
||||
{ 5: 18, 4: 3, 3: 2, 2: 1, 1: 2, 0: 3, 'Hyl.': 1, 'Hyv.': 0 },
|
||||
],
|
||||
[
|
||||
'2019-2020',
|
||||
{ total: 28, passed: 28, failed: 0, enrolledNoGrade: 0, extra: {} },
|
||||
{ 5: 17, 4: 5, 3: 1, 2: 4, 1: 1, 0: 0, 'Hyl.': 0, 'Hyv.': 0 },
|
||||
],
|
||||
[
|
||||
'2020-2021',
|
||||
{ total: 23, passed: 23, failed: 0, enrolledNoGrade: 0, extra: {} },
|
||||
{ 5: 18, 4: 2, 3: 0, 2: 2, 1: 0, 0: 0, 'Hyl.': 0, 'Hyv.': 1 },
|
||||
],
|
||||
[
|
||||
'2021-2022',
|
||||
{ total: 15, passed: 15, failed: 0, enrolledNoGrade: 20, extra: {} },
|
||||
{ 5: 13, 4: 2, 3: 0, 2: 0, 1: 0, 0: 0, 'Hyl.': 0, 'Hyv.': 0 },
|
||||
],
|
||||
[
|
||||
'2022-2023',
|
||||
{ total: 27, passed: 27, failed: 0, enrolledNoGrade: 34, extra: {} },
|
||||
{ 5: 21, 4: 6, 3: 0, 2: 0, 1: 0, 0: 0, 'Hyl.': 0, 'Hyv.': 0 },
|
||||
],
|
||||
[
|
||||
'2023-2024',
|
||||
{ total: 1, passed: 1, failed: 0, enrolledNoGrade: 7, extra: {} },
|
||||
{ 5: 1, 4: 0, 3: 0, 2: 0, 1: 0, 0: 0, 'Hyl.': 0, 'Hyv.': 0 },
|
||||
],
|
||||
])('should include correct stats for one academic year (TKT10002, $0)', async (year, categories, grades) => {
|
||||
const res = (await request(app)
|
||||
.get(
|
||||
`/courseyearlystats?codes=TKT10002&combineSubstitutions=false&fromYear=${year.split('-').at(0)}&toYear=${parseInt(year.split('-').at(1)!) - 1}`
|
||||
)
|
||||
.set('shib-session-id', 'test')
|
||||
.set('uid', 'basic')
|
||||
.set('hygroupcn', 'grp-oodikone-basic-users')) as ResponseWithBody<CourseYearlyStatsResBody>
|
||||
|
||||
assert.strictEqual(res.status, 200)
|
||||
assert.strictEqual(res.body.length, 1)
|
||||
const body = res.body.at(0)!
|
||||
assert(
|
||||
'unifyStats' in body && 'regularStats' in body && 'openStats' in body,
|
||||
'All keys of courseyearlystats not defined'
|
||||
)
|
||||
const stats = body.unifyStats!.statistics.find(yearStats => yearStats.name === year)!
|
||||
assert('enrollments' in stats, 'Missing field enrollment in statsitics')
|
||||
|
||||
if (Object.keys(categories.extra).length) {
|
||||
Object.keys(categories.extra).forEach(category => {
|
||||
assert.deepStrictEqual(
|
||||
stats.attempts.categories[category].sort(),
|
||||
categories.extra[category].sort(),
|
||||
`"${category}" included incorrect students`
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
if (categories) {
|
||||
assert.strictEqual(
|
||||
stats.attempts.categories.failed.length,
|
||||
categories.failed,
|
||||
`Incorrect amount of failed students for ${year}`
|
||||
)
|
||||
assert.strictEqual(
|
||||
stats.attempts.categories.passed.length,
|
||||
categories.passed,
|
||||
`Incorrect amount of passed students for ${year}`
|
||||
)
|
||||
assert.strictEqual(
|
||||
stats.students.studentNumbers.length,
|
||||
categories.total,
|
||||
`Incorrect amount of total students for ${year}`
|
||||
)
|
||||
assert.strictEqual(
|
||||
stats.enrollments.length,
|
||||
categories.enrolledNoGrade,
|
||||
`Incorrect amount of enrolled students for ${year}`
|
||||
)
|
||||
}
|
||||
if (grades) {
|
||||
Object.entries(grades).forEach(([grade, count]) =>
|
||||
assert.strictEqual(
|
||||
stats.attempts.grades[grade]?.length ?? 0,
|
||||
count,
|
||||
`Incorrect amount of students with grade ${grade}`
|
||||
)
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
describe('should count duplicate grades if an attainment falls out of the timespan (MAT21003)', () => {
|
||||
it('- 2017-2018 should include a failed grade', async () => {
|
||||
const res = (await request(app)
|
||||
.get('/courseyearlystats?codes=MAT21003&combineSubstitutions=false&fromYear=2017&toYear=2017')
|
||||
.set('shib-session-id', 'test')
|
||||
.set('uid', 'basic')
|
||||
.set('hygroupcn', 'grp-oodikone-basic-users')) as ResponseWithBody<CourseYearlyStatsResBody>
|
||||
|
||||
assert.strictEqual(res.status, 200)
|
||||
assert.strictEqual(res.body.length, 1)
|
||||
const body = res.body.at(0)!
|
||||
assert(
|
||||
'unifyStats' in body && 'regularStats' in body && 'openStats' in body,
|
||||
'All keys of courseyearlystats not defined'
|
||||
)
|
||||
const year = body.unifyStats!.statistics.find(year => year.name === '2017-2018')!
|
||||
|
||||
assert.deepStrictEqual(year.attempts.categories.failed, ['539036'], 'Failed stats should have include a students')
|
||||
assert.strictEqual(
|
||||
year.attempts.categories.failed.length,
|
||||
1,
|
||||
'Failed stats should have included a failed student'
|
||||
)
|
||||
})
|
||||
|
||||
it('- 2018-2019 should include a failed grade', async () => {
|
||||
const res = (await request(app)
|
||||
.get('/courseyearlystats?codes=MAT21003&combineSubstitutions=false&fromYear=2018&toYear=2018')
|
||||
.set('shib-session-id', 'test')
|
||||
.set('uid', 'basic')
|
||||
.set('hygroupcn', 'grp-oodikone-basic-users')) as ResponseWithBody<CourseYearlyStatsResBody>
|
||||
|
||||
assert.strictEqual(res.status, 200)
|
||||
assert.strictEqual(res.body.length, 1)
|
||||
const body = res.body.at(0)!
|
||||
assert(
|
||||
'unifyStats' in body && 'regularStats' in body && 'openStats' in body,
|
||||
'All keys of courseyearlystats not defined'
|
||||
)
|
||||
|
||||
const year = body.unifyStats?.statistics.find(year => year.name === '2018-2019')
|
||||
assert.deepStrictEqual(
|
||||
year?.attempts.categories.failed,
|
||||
['539036', '540698', '542927', '544688'],
|
||||
'Failed stats included the incorrect students'
|
||||
)
|
||||
assert.strictEqual(year?.attempts.categories.failed.length, 4, 'Failed stats should include only one student')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -14,4 +14,6 @@ export type CourseYearlyStatsQuery = {
|
||||
codes: string[]
|
||||
separate: string
|
||||
combineSubstitutions: string
|
||||
fromYear: string
|
||||
toYear: string
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user