[Course statistics] Move minimum and maximum attainment date calculation to the backend

This commit is contained in:
Valtteri Kantanen
2025-01-10 13:02:46 +02:00
parent 0d210a607b
commit 39ce64edce
3 changed files with 43 additions and 77 deletions
+30 -9
View File
@@ -1,10 +1,10 @@
import crypto from 'crypto'
import { Request, Response, Router } from 'express'
import moment from 'moment'
import { serviceProvider } from '../config'
import { getCourseYearlyStats } from '../services/courses'
import { getCoursesByNameAndOrCode, getCoursesByCodes } from '../services/courses/courseFinders'
import { CourseWithSubsId } from '../types'
import { getFullStudyProgrammeRights, hasFullAccessToStudentData, validateParamLength } from '../util'
import logger from '../util/logger'
@@ -24,24 +24,45 @@ router.get('/v2/coursesmulti', async (req: GetCoursesRequest, res: Response) =>
return res.status(400).json({ error: 'Query parameter name or code is invalid' })
}
let results: { courses: CourseWithSubsId[] } = { courses: [] }
results = await getCoursesByNameAndOrCode(name, code)
const courses = await getCoursesByNameAndOrCode(name, code)
if (combineSubstitutions === 'false') {
const courseCodes = results.courses.map(course => course.code)
const courseCodes = courses.map(course => course.code)
const substitutions = [
...new Set(
results.courses
.flatMap(course => course.substitutions)
.filter(code => code !== null && !courseCodes.includes(code))
courses.flatMap(course => course.substitutions).filter(code => code !== null && !courseCodes.includes(code))
),
]
const substitutionDetails = await getCoursesByCodes(substitutions)
for (const substitution of substitutionDetails) {
results.courses.push(substitution.dataValues)
courses.push(substitution.toJSON())
}
}
res.json(results)
const mergedCourses = {}
for (const course of courses) {
const groupId = combineSubstitutions === 'true' ? course.subsId! : course.code
if (!(course.max_attainment_date && course.min_attainment_date)) {
continue
}
if (!mergedCourses[groupId]) {
mergedCourses[groupId] = {
...course,
substitutions: combineSubstitutions === 'true' ? course.substitutions : [],
}
} else {
const mergedCourse = mergedCourses[groupId]
if (moment(mergedCourse.max_attainment_date).isBefore(course.max_attainment_date)) {
mergedCourse.max_attainment_date = course.max_attainment_date
}
if (moment(mergedCourse.min_attainment_date).isAfter(course.min_attainment_date)) {
mergedCourse.min_attainment_date = course.min_attainment_date
}
}
}
res.json({ courses: Object.values(mergedCourses) })
})
interface GetCourseYearlyStatsRequest extends Request {
@@ -1,3 +1,4 @@
import { orderBy } from 'lodash'
import { Op } from 'sequelize'
import { Course } from '../../models'
@@ -52,7 +53,17 @@ const getRawCourses = async (name: string, code: string) => {
const getCourses = async (name: string, code: string) => {
const rawCourses = await getRawCourses(name, code)
const courses: CourseWithSubsId[] = rawCourses
.map(course => ({ ...course.dataValues }))
.map(course => ({
...course.toJSON(),
substitutions: orderBy(course.substitutions, [
substitution => {
if (/^A/.exec(substitution)) return 4 // open university codes come last
if (/^\d/.exec(substitution)) return 2 // old numeric codes come second
if (/^[A-Za-z]/.exec(substitution)) return 1 // new letter based codes come first
return 3 // unknown, comes before open uni?
},
]),
}))
.sort((a, b) => getSortRank(b.code) - getSortRank(a.code))
return courses
}
@@ -87,7 +98,7 @@ export const getCoursesByNameAndOrCode = async (name: string, code: string) => {
}
})
return { courses }
return courses
}
export const getCoursesByCodes = (codes: string[]) => {
@@ -1,66 +0,0 @@
import { createSelector } from '@reduxjs/toolkit'
import { orderBy } from 'lodash'
const sortSubstitutions = course => {
const result = orderBy(
course.substitutions,
[
a => {
if (a.match(/^A/)) return 4 // open university codes come last
if (a.match(/^\d/)) return 2 // old numeric codes come second
if (a.match(/^[A-Za-z]/)) return 1 // new letter based codes come first
return 3 // unknown, comes before open uni?
},
],
['asc']
)
return result
}
const filterCourseSearchResults = (courses, combineSubstitutions) => {
const mergedCourses = {}
courses.forEach(course => {
// Filter out courses made out of custom attainments (#4624)
if (/-\d{9}/.test(course.code)) {
return
}
const groupId = combineSubstitutions ? course.subsId : course.code
if (!(course.max_attainment_date && course.min_attainment_date)) {
return
}
if (!mergedCourses[groupId]) {
mergedCourses[groupId] = {
...course,
substitutions: combineSubstitutions ? sortSubstitutions(course) : [],
min_attainment_date: new Date(course.min_attainment_date),
max_attainment_date: new Date(course.max_attainment_date),
}
} else {
const mergedCourse = mergedCourses[groupId]
mergedCourse.min_attainment_date = new Date(
Math.min(mergedCourse.min_attainment_date, new Date(course.min_attainment_date))
)
mergedCourse.max_attainment_date = new Date(
Math.max(mergedCourse.max_attainment_date, new Date(course.max_attainment_date))
)
}
})
const result = Object.values(mergedCourses)
return result
}
const getCourseSearchSelector = state => state.courseSearch.data.courses
export const getCourseSearchResults = createSelector(
[getCourseSearchSelector, (state, combineSubstitutions) => combineSubstitutions],
(courses, combineSubstitutions) => {
if (!courses) {
return []
}
return filterCourseSearchResults(courses, combineSubstitutions)
}
)