[Course rework] finishing touches, mainly programme courses

This commit is contained in:
joonas-a
2026-09-21 15:24:21 +03:00
committed by Aaro Koivukangas
parent 359e23ef84
commit eabe1406be
10 changed files with 292 additions and 279 deletions
+5 -1
View File
@@ -84,6 +84,10 @@ export class CourseModel extends Model implements Course {
@HasMany(() => EnrollmentModel, { foreignKey: 'course_id' })
declare enrollments: Course['enrollments']
@BelongsToMany(() => OrganizationModel, () => CourseProviderModel, 'coursecode')
@BelongsToMany(() => OrganizationModel, {
through: () => CourseProviderModel,
foreignKey: 'coursecode',
sourceKey: 'groupId',
})
declare organizations: Course['organizations']
}
@@ -13,6 +13,7 @@ import { OrganizationModel } from './organization'
export class CourseProviderModel extends Model implements CourseProvider {
@ForeignKey(() => CourseModel)
@Column(DataType.STRING)
// TODO: RENAME: this is courseGroupId, not code
declare coursecode: CourseProvider['coursecode']
@Column(DataType.JSONB)
@@ -20,6 +21,7 @@ export class CourseProviderModel extends Model implements CourseProvider {
@ForeignKey(() => OrganizationModel)
@Column(DataType.STRING)
// TODO: RENAME: this is organizationId, not code
declare organizationcode: CourseProvider['organizationcode']
@CreatedAt
+5 -1
View File
@@ -45,7 +45,11 @@ export class OrganizationModel extends Model implements Organization {
@HasMany(() => SISStudyRightModel, { foreignKey: 'facultyCode', sourceKey: 'code' })
declare SISStudyRights: Organization['SISStudyRights']
@BelongsToMany(() => CourseModel, () => CourseProviderModel, 'organizationcode')
@BelongsToMany(() => CourseModel, {
through: () => CourseProviderModel,
foreignKey: 'organizationcode',
targetKey: 'groupId',
})
declare courses: Organization['courses']
@CreatedAt
@@ -1,7 +1,7 @@
import { memoize } from 'lodash-es'
import { Op, QueryTypes } from 'sequelize'
import { Name, EnrollmentState } from '@oodikone/shared/types'
import { EnrollmentState } from '@oodikone/shared/types'
import { enrollmentTimeDateThresholdAcademicYear } from '@oodikone/shared/util'
import { dateIsBetween } from '@oodikone/shared/util/datetime'
import { dbConnections } from '../../database/connection'
@@ -9,9 +9,9 @@ import { CourseModel, CreditModel, EnrollmentModel, OrganizationModel, Programme
import logger from '../../util/logger'
export const getAllProgrammeCourses = async (providerCode: string) => {
const courses: Pick<CourseModel, 'id' | 'code' | 'name' | 'substitution_groups'>[] = await CourseModel.findAll({
const courses: Pick<CourseModel, 'id' | 'groupId' | 'code' | 'name'>[] = await CourseModel.findAll({
raw: true,
attributes: ['id', 'code', 'name', 'substitution_groups'],
attributes: ['id', 'groupId', 'code', 'name'],
include: [
{
model: OrganizationModel,
@@ -29,198 +29,210 @@ export const getAllProgrammeCourses = async (providerCode: string) => {
return courses
}
export const getCoursesByGroupIds = async (groupIds: string[]) => {
if (groupIds.length === 0) return []
return (
await CourseModel.findAll({
raw: true,
attributes: ['id'],
where: { groupId: { [Op.in]: groupIds } },
})
).map(course => course.id)
}
export const getPrimaryCoursesByGroupIds = async (
groupIds: string[]
): Promise<Pick<CourseModel, 'code' | 'name' | 'groupId'>[]> => {
if (groupIds.length === 0) return []
return CourseModel.findAll({
raw: true,
attributes: ['code', 'name', 'groupId'],
where: { groupId: { [Op.in]: groupIds }, isPrimary: true },
})
}
type NotCompletedCourseRow = {
groupId: string
isStudyModule: boolean
allNotPassed: number
}
export const getNotCompletedForProgrammeCourses = async (
from: Date,
to: Date,
programmeCourses: string[],
programmeCourseIds: string[],
yearRanges: Array<{ year: number; from: Date; to: Date }>,
start: Date,
end: Date
) => {
): Promise<Map<number, NotCompletedCourseRow[]>> => {
const emptyResult = new Map<number, NotCompletedCourseRow[]>(yearRanges.map(({ year }) => [year, []]))
try {
const enrollments: Array<
Pick<EnrollmentModel, 'studentnumber' | 'course_code' | 'enrollment_date_time' | 'course'>
> = await EnrollmentModel.findAll({
raw: true,
nest: true,
attributes: ['studentnumber', 'course_code', 'enrollment_date_time'],
include: {
model: CourseModel,
attributes: ['is_study_module'],
},
where: {
course_code: {
[Op.in]: programmeCourses,
const [enrollments, credits]: [
Pick<EnrollmentModel, 'studentnumber' | 'enrollment_date_time' | 'course'>[],
Pick<CreditModel, 'student_studentnumber' | 'credittypecode' | 'isStudyModule' | 'attainment_date' | 'course'>[],
] = await Promise.all([
EnrollmentModel.findAll({
raw: true,
nest: true,
attributes: ['studentnumber', 'enrollment_date_time'],
include: {
model: CourseModel,
attributes: ['isStudyModule', 'groupId'],
},
enrollment_date_time: {
[Op.and]: {
where: {
course_id: {
[Op.in]: programmeCourseIds,
},
enrollment_date_time: {
[Op.and]: {
[Op.between]: [start, end],
[Op.gte]: enrollmentTimeDateThresholdAcademicYear, // This has to be Academic year to match with Course statistics which doesn't display enrollments for ...-2021
},
},
state: EnrollmentState.ENROLLED,
},
order: [['enrollment_date_time', 'DESC']],
}),
CreditModel.findAll({
raw: true,
nest: true,
attributes: ['student_studentnumber', 'credittypecode', 'isStudyModule', 'attainment_date'],
include: {
model: CourseModel,
attributes: ['groupId'],
},
where: {
course_id: {
[Op.in]: programmeCourseIds,
},
attainment_date: {
[Op.between]: [start, end],
[Op.gte]: enrollmentTimeDateThresholdAcademicYear, // This has to be Academic year to match with Course statistics which doesn't display enrollments for ...-2021
},
},
state: EnrollmentState.ENROLLED,
},
order: [['enrollment_date_time', 'DESC']],
})
order: [
['attainment_date', 'DESC'],
['credittypecode', 'ASC'],
], // Passed credits for a student have to be handled first (only PASSED, and FAILED matter)
}),
])
// TODO: This is being called for each year, could be optimized to call only once
const credits: Array<
Pick<
CreditModel,
'course_code' | 'student_studentnumber' | 'credittypecode' | 'isStudyModule' | 'attainment_date'
>
> = await CreditModel.findAll({
raw: true,
attributes: ['course_code', 'student_studentnumber', 'credittypecode', 'isStudyModule', 'attainment_date'],
where: {
course_code: {
[Op.in]: programmeCourses,
},
attainment_date: {
[Op.between]: [start, end],
},
},
order: [
['attainment_date', 'DESC'],
['credittypecode', 'ASC'],
], // Passed credits for a student have to be handled first (only PASSED, and FAILED matter)
})
const creditKey = (studentNumber: string, courseGroupId: string) => `${studentNumber}::${courseGroupId}`
const enrolledStudentsByCourseCode = new Map<string, Set<string>>()
const everPassedByStudentCourse = new Set<string>()
const latestFailedDateByStudentCourse = new Map<string, Date>()
// Pick only this (between to, from) year's credits
const filteredCredits = credits.filter(credit => dateIsBetween(credit.attainment_date, from, to))
for (const credit of credits) {
const groupId = credit.course.groupId
const filteredEnrollments = enrollments
// Only include the first enrollment for a course for a student.
// This assumes that enrollments are sorted in DESC order by enrollment_date_time
.filter(({ course_code: courseCode, studentnumber: studentNumber }) => {
const courseEnrollments = enrolledStudentsByCourseCode.get(courseCode)
const hasEnrolled = courseEnrollments?.has(studentNumber) ?? false
if (!courseEnrollments) enrolledStudentsByCourseCode.set(courseCode, new Set<string>())
if (!hasEnrolled) enrolledStudentsByCourseCode.get(courseCode)!.add(studentNumber)
return !hasEnrolled
})
.filter(enrollment => dateIsBetween(enrollment.enrollment_date_time, from, to))
const courseCodeToDetails = (
await CourseModel.findAll({
raw: true,
attributes: ['code', 'name', 'groupId'],
where: {
code: programmeCourses,
},
})
).reduce<Map<string, { name: Name; groupId: string }>>((acc, val) => {
acc.set(val.code, { name: val.name, groupId: val.groupId })
return acc
}, new Map())
const creditCourses = filteredCredits.map(credit => ({
code: credit.course_code,
studentNumber: credit.student_studentnumber,
creditTypeCode: credit.credittypecode,
courseName: courseCodeToDetails.get(credit.course_code)!.name,
groupId: courseCodeToDetails.get(credit.course_code)!.groupId,
isStudyModule: credit.isStudyModule,
attainmentDate: credit.attainment_date,
}))
const passedByCourseCodes = new Map<string, Set<string>>()
const failedByCourseCodes = new Map<string, Set<string>>()
const notCompletedByCourseCodes = new Map<string, Set<string>>()
const courses = new Map<string, { name: Name; groupId: string; isStudyModule: boolean }>()
const studentHasPassedCourse = (studentNumber: string, courseCode: string): boolean =>
(passedByCourseCodes.get(courseCode)?.has(studentNumber) ?? false) ||
!!credits.find(
credit =>
credit.student_studentnumber === studentNumber &&
credit.course_code === courseCode &&
CreditModel.passed(credit)
)
/** Student has already been counted as failed or there is a newer failed credit */
const studentHasFailedCourse = (
studentNumber: string,
courseCode: string,
attainmentDate: Date = new Date(0)
): boolean =>
(failedByCourseCodes.get(courseCode)?.has(studentNumber) ?? false) ||
!!credits.find(
credit =>
credit.student_studentnumber === studentNumber &&
credit.course_code === courseCode &&
CreditModel.failed(credit) &&
credit.attainment_date > attainmentDate
)
const studentHasCredit = (studentNumber: string, courseCode: string): boolean =>
studentHasPassedCourse(studentNumber, courseCode) || studentHasFailedCourse(studentNumber, courseCode)
for (const {
code: courseCode,
courseName,
groupId,
creditTypeCode,
isStudyModule,
studentNumber,
attainmentDate,
} of creditCourses) {
if (!courses.has(courseCode)) {
courses.set(courseCode, {
isStudyModule,
name: courseName,
groupId,
})
passedByCourseCodes.set(courseCode, new Set())
failedByCourseCodes.set(courseCode, new Set())
notCompletedByCourseCodes.set(courseCode, new Set())
if (CreditModel.passed(credit)) {
everPassedByStudentCourse.add(creditKey(credit.student_studentnumber, groupId))
}
if (CreditModel.passed({ credittypecode: creditTypeCode })) {
passedByCourseCodes.get(courseCode)!.add(studentNumber)
}
if (
CreditModel.failed({ credittypecode: creditTypeCode }) &&
!studentHasPassedCourse(studentNumber, courseCode) &&
!studentHasFailedCourse(studentNumber, courseCode, attainmentDate)
) {
failedByCourseCodes.get(courseCode)!.add(studentNumber)
if (CreditModel.failed(credit)) {
const mapKey = creditKey(credit.student_studentnumber, groupId)
const latestFailedDate = latestFailedDateByStudentCourse.get(mapKey)
if (!latestFailedDate || credit.attainment_date > latestFailedDate) {
latestFailedDateByStudentCourse.set(mapKey, credit.attainment_date)
}
}
}
// Add course details to courseMap if there are only enrollments
for (const { studentnumber: studentNumber, course_code: courseCode, course } of filteredEnrollments) {
if (!courses.has(courseCode)) {
courses.set(courseCode, {
isStudyModule: course.is_study_module,
name: courseCodeToDetails.get(courseCode)!.name,
groupId: courseCodeToDetails.get(courseCode)!.groupId,
})
passedByCourseCodes.set(courseCode, new Set())
failedByCourseCodes.set(courseCode, new Set())
notCompletedByCourseCodes.set(courseCode, new Set())
const enrolledStudentsByCourseGroupId = new Map<string, Set<string>>()
// Only include the latest enrollment for a course per student per given time range.
// This assumes that enrollments are sorted in DESC order by enrollment_date_time
const dedupedEnrollments = enrollments.filter(({ course, studentnumber: studentNumber }) => {
const groupId = course.groupId
const courseEnrollments = enrolledStudentsByCourseGroupId.get(groupId)
const hasEnrolled = !!courseEnrollments?.has(studentNumber)
if (!courseEnrollments) enrolledStudentsByCourseGroupId.set(groupId, new Set<string>())
if (!hasEnrolled) enrolledStudentsByCourseGroupId.get(groupId)!.add(studentNumber)
return !hasEnrolled
})
const result = new Map<number, NotCompletedCourseRow[]>()
for (const { year, from, to } of yearRanges) {
const filteredCredits = credits.filter(credit => dateIsBetween(credit.attainment_date, from, to))
const filteredEnrollments = dedupedEnrollments.filter(enrollment =>
dateIsBetween(enrollment.enrollment_date_time, from, to)
)
// Keys are groupIds
const failedByCourse = new Map<string, Set<string>>()
const notCompletedByCourse = new Map<string, Set<string>>()
const courses = new Map<string, { isStudyModule: boolean }>()
const studentHasPassedCourse = (studentNumber: string, courseGroupId: string): boolean =>
everPassedByStudentCourse.has(creditKey(studentNumber, courseGroupId))
// Whether there is a newer failed credit
const studentHasFailedCourse = (
studentNumber: string,
courseGroupId: string,
attainmentDate: Date = new Date(0)
): boolean => {
const latestFailedDate = latestFailedDateByStudentCourse.get(creditKey(studentNumber, courseGroupId))
return !!latestFailedDate && latestFailedDate > attainmentDate
}
if (!studentHasCredit(studentNumber, courseCode)) {
notCompletedByCourseCodes.get(courseCode)!.add(studentNumber)
}
}
const studentHasCredit = (studentNumber: string, courseGroupId: string): boolean =>
studentHasPassedCourse(studentNumber, courseGroupId) || studentHasFailedCourse(studentNumber, courseGroupId)
return [...courses.entries()].map(([code, { isStudyModule, name, groupId }]) => {
return {
code,
name,
groupId,
const ensureCourseExists = (groupId: string, isStudyModule: boolean) => {
if (!courses.has(groupId)) {
courses.set(groupId, {
isStudyModule,
})
failedByCourse.set(groupId, new Set())
notCompletedByCourse.set(groupId, new Set())
}
}
for (const {
credittypecode: creditTypeCode,
isStudyModule,
allNotPassed: (notCompletedByCourseCodes.get(code)?.size ?? 0) + (failedByCourseCodes.get(code)?.size ?? 0),
student_studentnumber: studentNumber,
attainment_date: attainmentDate,
course,
} of filteredCredits) {
const groupId = course.groupId
ensureCourseExists(groupId, isStudyModule)
if (
CreditModel.failed({ credittypecode: creditTypeCode }) &&
!studentHasPassedCourse(studentNumber, groupId) &&
!studentHasFailedCourse(studentNumber, groupId, attainmentDate)
) {
failedByCourse.get(groupId)!.add(studentNumber)
}
}
})
// Add course details to courseMap if there are only enrollments
for (const { studentnumber: studentNumber, course } of filteredEnrollments) {
const groupId = course.groupId
ensureCourseExists(groupId, course.isStudyModule)
if (!studentHasCredit(studentNumber, groupId)) {
notCompletedByCourse.get(groupId)!.add(studentNumber)
}
}
result.set(
year,
[...courses.entries()].map(([groupId, { isStudyModule }]) => ({
groupId,
isStudyModule,
allNotPassed: (notCompletedByCourse.get(groupId)?.size ?? 0) + (failedByCourse.get(groupId)?.size ?? 0),
}))
)
}
return result
} catch (error) {
logger.error(`getNotCompletedForProgrammeCourses failed ${error}`)
return []
return emptyResult
}
}
@@ -59,7 +59,7 @@ const { sequelize } = dbConnections
/**
NOTE:
1. There are some attainments that are linked to a valid study right, but the attainmentdate
is outside the study right's validity period. (maybe human error when the credits were registered)
is outside the study right's validity period.
2. There are some attainments with no attached study right at all.
3. Possibly some other edge cases.
@@ -69,24 +69,18 @@ NOTE:
degree student -> has any degree (bsc / msc) study right in the university
openuni -> only if no other study rights
**/
export const getProgrammeCourseAggregates = async (params: {
courseCodes: string[]
from: Date
export const getProgrammeCourseAggregates = async (
courseIds: string[],
from: Date,
to: Date
}): Promise<ProgrammeCourseCreditRow[]> => {
const { courseCodes, from, to } = params
if (courseCodes.length === 0) {
return []
}
): Promise<ProgrammeCourseCreditRow[]> => {
if (!courseIds.length) return []
try {
return await sequelize.query<ProgrammeCourseCreditRow>(
`
WITH filtered_credits AS (
SELECT
cr.course_code AS "courseCode",
co.name AS "courseName",
co.group_id AS "groupId",
cr.student_studentnumber AS "studentNumber",
cr.attainment_date AS "attainmentDate",
@@ -94,15 +88,14 @@ export const getProgrammeCourseAggregates = async (params: {
cr."isStudyModule" AS "isStudyModule"
FROM
credit cr
INNER JOIN course co ON co.code = cr.course_code
INNER JOIN course co ON co.id = cr.course_id
WHERE
cr.course_code IN (:courseCodes)
cr.course_id IN (:courseIds)
AND co.group_id is NOT NULL
AND cr.attainment_date BETWEEN :from AND :to
AND cr.credittypecode IN (:creditTypes)
)
SELECT
fc."courseCode",
fc."courseName",
fc."groupId",
fc."studentNumber",
fc."attainmentDate",
@@ -177,7 +170,7 @@ export const getProgrammeCourseAggregates = async (params: {
{
type: QueryTypes.SELECT,
replacements: {
courseCodes,
courseIds,
from,
to,
creditTypes: [CreditTypeCode.PASSED, CreditTypeCode.APPROVED],
@@ -196,23 +189,17 @@ export const getProgrammeCourseAggregates = async (params: {
}
}
export const getTransferCourseAggregates = async (params: {
courseCodes: string[]
from: Date
export const getTransferCourseAggregates = async (
courseIds: string[],
from: Date,
to: Date
}): Promise<TransferCourseCreditRow[]> => {
const { courseCodes, from, to } = params
if (courseCodes.length === 0) {
return []
}
): Promise<TransferCourseCreditRow[]> => {
if (!courseIds.length) return []
try {
return await sequelize.query<TransferCourseCreditRow>(
`
SELECT
cr.course_code AS "courseCode",
co.name AS "courseName",
co.group_id AS "groupId",
cr.student_studentnumber AS "studentNumber",
cr.attainment_date AS "attainmentDate",
@@ -220,16 +207,17 @@ export const getTransferCourseAggregates = async (params: {
cr."isStudyModule" AS "isStudyModule"
FROM
credit cr
INNER JOIN course co ON co.code = cr.course_code
INNER JOIN course co ON co.id = cr.course_id
WHERE
cr.course_code IN (:courseCodes)
cr.course_id IN (:courseIds)
AND co.group_id IS NOT NULL
AND cr.attainment_date BETWEEN :from AND :to
AND cr.credittypecode = :creditType
`,
{
type: QueryTypes.SELECT,
replacements: {
courseCodes,
courseIds,
from,
to,
creditType: CreditTypeCode.APPROVED,
@@ -1,10 +1,17 @@
import { Name, StudyProgrammeCourse, YearType } from '@oodikone/shared/types'
import { StudyProgrammeCourse, YearType } from '@oodikone/shared/types'
import { mapToProviders, range, yearCodeToYear } from '@oodikone/shared/util'
import { createEmptyStats, YearStats } from '@oodikone/shared/util/studyProgramme'
import { now } from '../../util/clock'
import { getAllProgrammeCourses, getCurrentStudyYearStartDate, getNotCompletedForProgrammeCourses } from '.'
import {
getAllProgrammeCourses,
getCoursesByGroupIds,
getCurrentStudyYearStartDate,
getNotCompletedForProgrammeCourses,
getPrimaryCoursesByGroupIds,
} from '.'
import { getStudentHetuStateMap, getProgrammeCourseAggregates, getTransferCourseAggregates } from './studentGetters'
import logger from '../../util/logger'
const START_YEAR = 2017
const JULY = 6
@@ -18,22 +25,12 @@ const getFrom = (yearType: YearType, year: number) =>
const getTo = (yearType: YearType, year: number) =>
yearType === 'ACADEMIC_YEAR' ? new Date(year + 1, JULY, 31, 23, 59, 59) : new Date(year, 11, 31, 23, 59, 59)
const getAllStudyProgrammeCourses = async (studyProgramme: string) => {
const getAllStudyProgrammeCourseGroupIds = async (studyProgramme: string): Promise<string[]> => {
const providerCode = mapToProviders([studyProgramme])[0]
if (!providerCode) return []
const normalCourses = await getAllProgrammeCourses(providerCode)
const courseCodes = new Set<string>()
for (const course of normalCourses) {
courseCodes.add(course.code)
// We want all AY course codes variants, even from partially completed susbtitution groups
for (const code of course?.substitution_groups?.flat() ?? []) {
if (code === 'AY' + course.code) courseCodes.add(code)
}
}
return [...courseCodes]
return [...new Set(normalCourses.map(course => course.groupId))]
}
const getYearKey = (date: Date, isAcademicYear: boolean) => {
@@ -58,9 +55,7 @@ type YearAccumulator = {
}
type CourseAccumulator = {
code: string
groupId: string
name: Name
isStudyModule: boolean
years: Map<number, YearAccumulator>
}
@@ -81,26 +76,18 @@ const createYearAccumulator = (isStudyModule: boolean): YearAccumulator => ({
const ensureCourse = (
courseMap: Map<string, CourseAccumulator>,
code: string,
groupId: string,
name: Name,
isStudyModule: boolean
): CourseAccumulator => {
if (!courseMap.has(code)) {
courseMap.set(code, {
code,
if (!courseMap.has(groupId)) {
courseMap.set(groupId, {
groupId,
name,
isStudyModule,
years: new Map(),
})
}
const course = courseMap.get(code)!
course.isStudyModule = course.isStudyModule || isStudyModule
course.name = course.name ?? name
course.groupId = course.groupId ?? groupId
return course
return courseMap.get(groupId)!
}
const ensureYear = (course: CourseAccumulator, year: number, isStudyModule: boolean) => {
@@ -158,30 +145,35 @@ export const getStudyProgrammeCoursesForStudyTrack = async (
return []
}
const mainProgrammeCourses = await getAllStudyProgrammeCourses(studyProgramme)
const secondProgrammeCourses = combinedProgramme ? await getAllStudyProgrammeCourses(combinedProgramme) : []
const programmeCourses = [...new Set([...mainProgrammeCourses, ...secondProgrammeCourses])]
const [mainProgrammeGroupIds, secondProgrammeGroupIds] = await Promise.all([
getAllStudyProgrammeCourseGroupIds(studyProgramme),
combinedProgramme ? getAllStudyProgrammeCourseGroupIds(combinedProgramme) : [],
])
if (programmeCourses.length === 0) {
return []
}
const programmeGroupIds = [...new Set([...mainProgrammeGroupIds, ...secondProgrammeGroupIds])]
if (!programmeGroupIds.length) return []
const programmeCourseIds = [...new Set(await getCoursesByGroupIds(programmeGroupIds))]
const firstYear = yearRange[0]
const lastYear = yearRange[yearRange.length - 1]
const from = getFrom(academicYear, firstYear)
const to = getTo(academicYear, lastYear)
const notCompletedByYearPromise = getNotCompletedForProgrammeCourses(
programmeCourseIds,
yearRange.map(year => ({ year, from: getFrom(academicYear, year), to: getTo(academicYear, year) })),
from,
to
)
const primaryByGroupIdPromise = getPrimaryCoursesByGroupIds(programmeGroupIds).then(
rows => new Map(rows.map(({ groupId, code, name }) => [groupId, { code, name }]))
)
const [programmeCredits, transferCredits] = await Promise.all([
getProgrammeCourseAggregates({
courseCodes: programmeCourses,
from,
to,
}),
getTransferCourseAggregates({
courseCodes: programmeCourses,
from,
to,
}),
getProgrammeCourseAggregates(programmeCourseIds, from, to),
getTransferCourseAggregates(programmeCourseIds, from, to),
])
const openUniStudentHetuMap = await getStudentHetuStateMap(
@@ -198,7 +190,8 @@ export const getStudyProgrammeCoursesForStudyTrack = async (
if (!yearRange.includes(year)) continue
maxYear = Math.max(maxYear, year)
const course = ensureCourse(courseMap, row.courseCode, row.groupId, row.courseName, row.isStudyModule)
const course = ensureCourse(courseMap, row.groupId, row.isStudyModule)
const yearAccumulator = ensureYear(course, year, row.isStudyModule)
const { stats } = yearAccumulator
@@ -246,54 +239,56 @@ export const getStudyProgrammeCoursesForStudyTrack = async (
if (!yearRange.includes(year)) continue
maxYear = Math.max(maxYear, year)
const course = ensureCourse(courseMap, row.courseCode, row.groupId, row.courseName, row.isStudyModule)
const course = ensureCourse(courseMap, row.groupId, row.isStudyModule)
const yearAccumulator = ensureYear(course, year, row.isStudyModule)
yearAccumulator.transferStudents.add(row.studentNumber)
yearAccumulator.stats.transferStudentsCredits += row.credits
}
const notCompletedResults = await Promise.all(
yearRange.map(year => {
const yearFrom = getFrom(academicYear, year)
const yearTo = getTo(academicYear, year)
return getNotCompletedForProgrammeCourses(yearFrom, yearTo, programmeCourses, from, to)
})
)
const notCompletedByYear = await notCompletedByYearPromise
notCompletedResults.forEach((results, index) => {
const year = yearRange[index]
if (!results) return
for (const year of yearRange) {
const results = notCompletedByYear.get(year)
if (!results) continue
if (results.length > 0) {
maxYear = Math.max(maxYear, year)
}
for (const result of results) {
const course = ensureCourse(courseMap, result.code, result.groupId, result.name, result.isStudyModule ?? false)
const course = ensureCourse(courseMap, result.groupId, result.isStudyModule ?? false)
const yearAccumulator = ensureYear(course, year, result.isStudyModule ?? false)
yearAccumulator.stats.allNotPassed += result.allNotPassed
}
})
}
if (maxYear === 0) {
maxYear = lastYear
}
const primaryByGroupId = await primaryByGroupIdPromise
const coursesRecord: Record<string, StudyProgrammeCourse> = {}
for (const [code, course] of courseMap.entries()) {
// Code and name for each CU group is fetched from the "primary" instance
for (const [groupId, course] of courseMap.entries()) {
const years: Record<number, YearStats> = {}
for (const [year, yearAccumulator] of course.years.entries()) {
if (year > maxYear) continue
years[year] = finalizeYearStats(yearAccumulator)
}
if (Object.keys(years).length === 0) {
if (!Object.keys(years).length) continue
const primary = primaryByGroupId.get(groupId)
if (!primary?.code || !primary?.name) {
logger.warn(`Warning: Programme courses: No primary course for groupId: ${groupId ?? '(groupId is undefined)'}`)
continue
}
coursesRecord[code] = {
code,
groupId: course.groupId,
name: course.name,
coursesRecord[groupId] = {
code: primary.code,
groupId,
name: primary.name,
isStudyModule: course.isStudyModule,
years,
}
@@ -151,7 +151,7 @@ const getActiveTeachers = async (providers: string[], startSemester: number, end
JOIN credit_teachers ON teacher.id = credit_teachers.teacher_id
JOIN credit ON credit.id = credit_teachers.credit_id
JOIN course ON credit.course_id = course.id
JOIN course_providers ON course.id = course_providers.coursecode
JOIN course_providers ON course.group_id = course_providers.coursecode
JOIN organization ON organization.id = course_providers.organizationcode
WHERE credit.semestercode BETWEEN :startSemester AND :endSemester
AND organization.code IN (:providers)
+2
View File
@@ -1,6 +1,8 @@
export type CourseProvider = {
// TODO: RENAME: this is courseGroupId, not code
coursecode: string
shares: Array<{ share: number; startDate?: string; endDate?: string }> | null
// TODO: RENAME: this is organizationId, not code
organizationcode: string
createdAt: Date
updatedAt: Date