mirror of
https://github.com/UniversityOfHelsinkiCS/oodikone.git
synced 2026-10-12 19:03:39 +03:00
Merge pull request #4623 from UniversityOfHelsinkiCS/type-bottlenecks
Type bottlenecks
This commit is contained in:
@@ -146,7 +146,7 @@ jobs:
|
|||||||
version: ${{ steps.extract_version.outputs.VERSION }}
|
version: ${{ steps.extract_version.outputs.VERSION }}
|
||||||
|
|
||||||
report_success:
|
report_success:
|
||||||
name: 'Report deployment success to oodikone notifications'
|
name: 'Report deployment success to the "oodikone_notifications" channel'
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs:
|
needs:
|
||||||
- release_frontend
|
- release_frontend
|
||||||
@@ -158,7 +158,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- name: 'Send notification to slack'
|
- name: 'Send notification to Slack'
|
||||||
uses: UniversityOfHelsinkiCS/slackbot-action@v1.5
|
uses: UniversityOfHelsinkiCS/slackbot-action@v1.5
|
||||||
with:
|
with:
|
||||||
webhook-url: ${{ secrets.WEBHOOK_URL }}
|
webhook-url: ${{ secrets.WEBHOOK_URL }}
|
||||||
@@ -167,7 +167,7 @@ jobs:
|
|||||||
deployment-target: production
|
deployment-target: production
|
||||||
|
|
||||||
report_failure:
|
report_failure:
|
||||||
name: 'Report deployment failure to oodikone notifications'
|
name: 'Report deployment failure to the "oodikone_notifications" channel'
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs:
|
needs:
|
||||||
- release_frontend
|
- release_frontend
|
||||||
@@ -179,7 +179,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- name: 'Send notification to slack'
|
- name: 'Send notification to Slack'
|
||||||
uses: UniversityOfHelsinkiCS/slackbot-action@v1.5
|
uses: UniversityOfHelsinkiCS/slackbot-action@v1.5
|
||||||
with:
|
with:
|
||||||
webhook-url: ${{ secrets.WEBHOOK_URL }}
|
webhook-url: ${{ secrets.WEBHOOK_URL }}
|
||||||
|
|||||||
@@ -5,16 +5,16 @@ on:
|
|||||||
branches:
|
branches:
|
||||||
- master
|
- master
|
||||||
paths:
|
paths:
|
||||||
- '.github/workflows/test.yaml'
|
|
||||||
- '.github/workflows/staging.yaml'
|
|
||||||
- '.github/actions/**'
|
- '.github/actions/**'
|
||||||
|
- '.github/workflows/staging.yaml'
|
||||||
|
- '.github/workflows/test.yaml'
|
||||||
- 'cypress/**'
|
- 'cypress/**'
|
||||||
- 'cypress.config.json'
|
- 'cypress.config.json'
|
||||||
- 'services/**'
|
- 'services/**'
|
||||||
- '!services/backend/src/rapodiff/**'
|
- '!services/backend/src/rapodiff/**'
|
||||||
|
- 'updater/sis-updater-worker/src/db/migrations/**'
|
||||||
- 'docker-compose.ci.yml'
|
- 'docker-compose.ci.yml'
|
||||||
- 'package*.json'
|
- 'package*.json'
|
||||||
- 'updater/sis-updater-worker/src/db/migrations/**'
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
test:
|
test:
|
||||||
@@ -90,7 +90,7 @@ jobs:
|
|||||||
password: ${{ secrets.QUAY_IO_TOKEN }}
|
password: ${{ secrets.QUAY_IO_TOKEN }}
|
||||||
|
|
||||||
report_success:
|
report_success:
|
||||||
name: 'Report deployment success to oodikone notifications'
|
name: 'Report deployment success to the "oodikone_notifications" channel'
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs:
|
needs:
|
||||||
- release_frontend
|
- release_frontend
|
||||||
@@ -100,7 +100,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- name: 'Send notification to slack'
|
- name: 'Send notification to Slack'
|
||||||
uses: UniversityOfHelsinkiCS/slackbot-action@v1.5
|
uses: UniversityOfHelsinkiCS/slackbot-action@v1.5
|
||||||
with:
|
with:
|
||||||
webhook-url: ${{ secrets.WEBHOOK_URL }}
|
webhook-url: ${{ secrets.WEBHOOK_URL }}
|
||||||
@@ -109,7 +109,7 @@ jobs:
|
|||||||
deployment-target: staging
|
deployment-target: staging
|
||||||
|
|
||||||
report_failure:
|
report_failure:
|
||||||
name: 'Report deployment failure to oodikone notifications'
|
name: 'Report deployment failure to the "oodikone_notifications" channel'
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs:
|
needs:
|
||||||
- release_frontend
|
- release_frontend
|
||||||
@@ -119,7 +119,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- name: 'Send notification to slack'
|
- name: 'Send notification to Slack'
|
||||||
uses: UniversityOfHelsinkiCS/slackbot-action@v1.5
|
uses: UniversityOfHelsinkiCS/slackbot-action@v1.5
|
||||||
with:
|
with:
|
||||||
webhook-url: ${{ secrets.WEBHOOK_URL }}
|
webhook-url: ${{ secrets.WEBHOOK_URL }}
|
||||||
|
|||||||
@@ -96,7 +96,7 @@ router.post('/v2/populationstatistics/courses', async (req: PostPopulationStatis
|
|||||||
: req.body.selectedStudentsByYear[year]
|
: req.body.selectedStudentsByYear[year]
|
||||||
}
|
}
|
||||||
const newMonths = (upperYearBound - Number(year)) * 12
|
const newMonths = (upperYearBound - Number(year)) * 12
|
||||||
const query = { ...req.body, year, months: newMonths }
|
const query = { ...req.body, year, months: newMonths, selectedStudents: req.body.selectedStudents as string[] }
|
||||||
const coursestatistics = bottlenecksOf(query, null, encrypted)
|
const coursestatistics = bottlenecksOf(query, null, encrypted)
|
||||||
return coursestatistics
|
return coursestatistics
|
||||||
})
|
})
|
||||||
@@ -114,13 +114,14 @@ router.post('/v2/populationstatistics/courses', async (req: PostPopulationStatis
|
|||||||
req.body.selectedStudents = req.body.selectedStudents.map(decrypt)
|
req.body.selectedStudents = req.body.selectedStudents.map(decrypt)
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await bottlenecksOf(req.body, null, encrypted)
|
const query = { ...req.body, selectedStudents: req.body.selectedStudents as string[] }
|
||||||
if (result.error) {
|
try {
|
||||||
Sentry.captureException(new Error(result.error))
|
const result = await bottlenecksOf(query, null, encrypted)
|
||||||
return res.status(400).json(result)
|
return res.json(result)
|
||||||
|
} catch (error: any) {
|
||||||
|
Sentry.captureException(new Error(error.message))
|
||||||
|
return res.status(400).end()
|
||||||
}
|
}
|
||||||
|
|
||||||
return res.json(result)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
interface PostPopulationStatisticsByTagRequest extends Request {
|
interface PostPopulationStatisticsByTagRequest extends Request {
|
||||||
@@ -135,8 +136,7 @@ router.post(
|
|||||||
const { roles, studentsUserCanAccess } = req.user
|
const { roles, studentsUserCanAccess } = req.user
|
||||||
const { tag } = req.body
|
const { tag } = req.body
|
||||||
if (!tag) {
|
if (!tag) {
|
||||||
res.status(400).json({ error: 'The body should have a tag defined' })
|
return res.status(400).json({ error: 'The body should have a tag defined' })
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const studentNumbers = await findByTag(tag)
|
const studentNumbers = await findByTag(tag)
|
||||||
@@ -144,24 +144,22 @@ router.post(
|
|||||||
? studentNumbers
|
? studentNumbers
|
||||||
: intersection(studentNumbers, studentsUserCanAccess)
|
: intersection(studentNumbers, studentsUserCanAccess)
|
||||||
|
|
||||||
const result = await bottlenecksOf(
|
try {
|
||||||
{
|
const result = await bottlenecksOf(
|
||||||
year: 1900,
|
{
|
||||||
studyRights: [],
|
year: '1900',
|
||||||
semesters: ['FALL', 'SPRING'],
|
studyRights: [],
|
||||||
months: 10000,
|
semesters: ['FALL', 'SPRING'],
|
||||||
tag,
|
months: 10000,
|
||||||
},
|
tag,
|
||||||
studentNumberList
|
},
|
||||||
)
|
studentNumberList
|
||||||
|
)
|
||||||
if (result.error) {
|
return res.json(result)
|
||||||
Sentry.captureException(new Error(result.error))
|
} catch (error: any) {
|
||||||
res.status(400).end()
|
Sentry.captureException(new Error(error.message))
|
||||||
return
|
return res.status(400).end()
|
||||||
}
|
}
|
||||||
|
|
||||||
res.json(result)
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -190,22 +188,22 @@ router.post(
|
|||||||
const studentNumbers = hasFullAccessToStudentData(roles)
|
const studentNumbers = hasFullAccessToStudentData(roles)
|
||||||
? studentnumberlist
|
? studentnumberlist
|
||||||
: intersection(studentnumberlist, studentsUserCanAccess)
|
: intersection(studentnumberlist, studentsUserCanAccess)
|
||||||
const result = await bottlenecksOf(
|
|
||||||
{
|
|
||||||
year: query?.year ?? 1900,
|
|
||||||
studyRights: query?.studyRights ?? [],
|
|
||||||
semesters: query?.semesters ?? ['FALL', 'SPRING'],
|
|
||||||
months: query?.months ?? 10000,
|
|
||||||
},
|
|
||||||
studentNumbers
|
|
||||||
)
|
|
||||||
if (result.error) {
|
|
||||||
Sentry.captureException(new Error(result.error))
|
|
||||||
res.status(400).json(result)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
res.json(result)
|
try {
|
||||||
|
const result = await bottlenecksOf(
|
||||||
|
{
|
||||||
|
year: query?.year ?? 1900,
|
||||||
|
studyRights: query?.studyRights ?? [],
|
||||||
|
semesters: query?.semesters ?? ['FALL', 'SPRING'],
|
||||||
|
months: query?.months ?? 10000,
|
||||||
|
},
|
||||||
|
studentNumbers
|
||||||
|
)
|
||||||
|
return res.json(result)
|
||||||
|
} catch (error: any) {
|
||||||
|
Sentry.captureException(new Error(error.message))
|
||||||
|
return res.status(400).end()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -53,11 +53,59 @@ type DynamicEnrollments = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type CourseStatistics = {
|
||||||
|
stats: {
|
||||||
|
students: number
|
||||||
|
passed: number
|
||||||
|
failed: number
|
||||||
|
failedMany: number
|
||||||
|
retryPassed: number
|
||||||
|
attempts: number
|
||||||
|
improvedPassedGrade: number
|
||||||
|
percentage: number | undefined
|
||||||
|
passedOfPopulation: number | undefined
|
||||||
|
triedOfPopulation: number | undefined
|
||||||
|
perStudent: number | undefined
|
||||||
|
passingSemesters: Record<string, number>
|
||||||
|
passingSemestersCumulative?: Record<string, number>
|
||||||
|
totalStudents?: number
|
||||||
|
totalEnrolledNoGrade?: number
|
||||||
|
percentageWithEnrollments?: number
|
||||||
|
}
|
||||||
|
students: {
|
||||||
|
all: Record<string, boolean>
|
||||||
|
passed: Record<string, boolean>
|
||||||
|
failed: Record<string, boolean>
|
||||||
|
retryPassed: Record<string, boolean>
|
||||||
|
failedMany: Record<string, boolean>
|
||||||
|
improvedPassedGrade: Record<string, boolean>
|
||||||
|
markedToSemester: Record<string, boolean>
|
||||||
|
enrolledNoGrade: Record<string, boolean>
|
||||||
|
}
|
||||||
|
course: {
|
||||||
|
code: string
|
||||||
|
name: Name
|
||||||
|
disciplines: object
|
||||||
|
coursetypes: Record<string, Name>
|
||||||
|
substitutions: string[]
|
||||||
|
}
|
||||||
|
grades: {
|
||||||
|
[grade: string]: {
|
||||||
|
count: number
|
||||||
|
status: {
|
||||||
|
passingGrade: boolean
|
||||||
|
improvedGrade: boolean
|
||||||
|
failingGrade: boolean
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export class CourseStatsCounter {
|
export class CourseStatsCounter {
|
||||||
private studentsInTotal: number
|
private studentsInTotal: number
|
||||||
private course: {
|
private course: {
|
||||||
code: string
|
code: string
|
||||||
name: string
|
name: Name
|
||||||
disciplines: object
|
disciplines: object
|
||||||
coursetypes: Record<string, Name>
|
coursetypes: Record<string, Name>
|
||||||
substitutions: string[]
|
substitutions: string[]
|
||||||
@@ -102,7 +150,7 @@ export class CourseStatsCounter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(code: string, name: string, studentsInTotal: number) {
|
constructor(code: string, name: Name, studentsInTotal: number) {
|
||||||
this.studentsInTotal = studentsInTotal
|
this.studentsInTotal = studentsInTotal
|
||||||
this.course = {
|
this.course = {
|
||||||
code,
|
code,
|
||||||
@@ -289,6 +337,6 @@ export class CourseStatsCounter {
|
|||||||
stats.totalEnrolledNoGrade = lengthOf(filteredEnrolledNoGrade)
|
stats.totalEnrolledNoGrade = lengthOf(filteredEnrolledNoGrade)
|
||||||
stats.percentageWithEnrollments = percentageOf(stats.passed, stats.totalStudents)
|
stats.percentageWithEnrollments = percentageOf(stats.passed, stats.totalStudents)
|
||||||
|
|
||||||
return { stats, students, course: this.course, grades: this.grades }
|
return { stats, students, course: this.course, grades: this.grades } as CourseStatistics
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+45
-29
@@ -1,15 +1,29 @@
|
|||||||
const { keyBy } = require('lodash')
|
import { keyBy } from 'lodash'
|
||||||
|
|
||||||
const { getPassingSemester } = require('../../util/semester')
|
import { Name } from '../../types'
|
||||||
const { CourseStatsCounter } = require('../courses/courseStatsCounter')
|
import { getPassingSemester } from '../../util/semester'
|
||||||
const { encrypt } = require('../encrypt')
|
import { CourseStatistics, CourseStatsCounter } from '../courses/courseStatsCounter'
|
||||||
const { dateMonthsFromNow, findCourses, findCourseEnrollments, parseCreditInfo, parseQueryParams } = require('./shared')
|
import { encrypt } from '../encrypt'
|
||||||
const { getStudentNumbersWithAllStudyRightElements } = require('./studentNumbersWithAllElements')
|
import {
|
||||||
|
dateMonthsFromNow,
|
||||||
|
findCourses,
|
||||||
|
findCourseEnrollments,
|
||||||
|
Params,
|
||||||
|
parseCreditInfo,
|
||||||
|
parseQueryParams,
|
||||||
|
Query,
|
||||||
|
QueryResult,
|
||||||
|
} from './shared'
|
||||||
|
import { getStudentNumbersWithAllStudyRightElements } from './studentNumbersWithAllElements'
|
||||||
|
|
||||||
const getStudentsAndCourses = async (params, selectedStudents, studentNumbers, courseCodes) => {
|
const getStudentsAndCourses = async (
|
||||||
|
params: Params,
|
||||||
|
selectedStudents: string[] | undefined,
|
||||||
|
studentNumbers: string[] | null,
|
||||||
|
courseCodes: string[] | undefined
|
||||||
|
) => {
|
||||||
if (!studentNumbers) {
|
if (!studentNumbers) {
|
||||||
const { months, studyRights, startDate, endDate, exchangeStudents, nondegreeStudents, transferredStudents, tag } =
|
const { months, studyRights, startDate, endDate, exchangeStudents, nondegreeStudents, transferredStudents } = params
|
||||||
params
|
|
||||||
const studentnumbers =
|
const studentnumbers =
|
||||||
selectedStudents ||
|
selectedStudents ||
|
||||||
(await getStudentNumbersWithAllStudyRightElements({
|
(await getStudentNumbersWithAllStudyRightElements({
|
||||||
@@ -19,9 +33,6 @@ const getStudentsAndCourses = async (params, selectedStudents, studentNumbers, c
|
|||||||
exchangeStudents,
|
exchangeStudents,
|
||||||
nondegreeStudents,
|
nondegreeStudents,
|
||||||
transferredOutStudents: transferredStudents,
|
transferredOutStudents: transferredStudents,
|
||||||
tag,
|
|
||||||
transferredToStudents: true,
|
|
||||||
graduatedStudents: true,
|
|
||||||
}))
|
}))
|
||||||
|
|
||||||
const allStudents = studentnumbers.length
|
const allStudents = studentnumbers.length
|
||||||
@@ -48,8 +59,15 @@ const getStudentsAndCourses = async (params, selectedStudents, studentNumbers, c
|
|||||||
return [allStudents, courses, courseEnrollments]
|
return [allStudents, courses, courseEnrollments]
|
||||||
}
|
}
|
||||||
|
|
||||||
const bottlenecksOf = async (query, studentNumbers, encryptData = false) => {
|
type Bottlenecks = {
|
||||||
const encryptStudentNumbers = bottlenecks => {
|
disciplines: Record<string, string>
|
||||||
|
coursetypes: Record<string, Name>
|
||||||
|
coursestatistics: CourseStatistics[]
|
||||||
|
allStudents: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export const bottlenecksOf = async (query: Query, studentNumbers: string[] | null, encryptData: boolean = false) => {
|
||||||
|
const encryptStudentNumbers = (bottlenecks: Bottlenecks) => {
|
||||||
for (const course of Object.keys(bottlenecks.coursestatistics)) {
|
for (const course of Object.keys(bottlenecks.coursestatistics)) {
|
||||||
const encryptedStudentStats = {}
|
const encryptedStudentStats = {}
|
||||||
for (const data of Object.keys(bottlenecks.coursestatistics[course].students)) {
|
for (const data of Object.keys(bottlenecks.coursestatistics[course].students)) {
|
||||||
@@ -67,22 +85,22 @@ const bottlenecksOf = async (query, studentNumbers, encryptData = false) => {
|
|||||||
const params = parseQueryParams(query)
|
const params = parseQueryParams(query)
|
||||||
const allStudentsByYears = query?.selectedStudentsByYear
|
const allStudentsByYears = query?.selectedStudentsByYear
|
||||||
? Object.keys(query.selectedStudentsByYear).reduce(
|
? Object.keys(query.selectedStudentsByYear).reduce(
|
||||||
(res, year) => [...res, ...query.selectedStudentsByYear[year]],
|
(res, year) => [...res, ...query.selectedStudentsByYear![year]],
|
||||||
[]
|
[] as string[]
|
||||||
)
|
)
|
||||||
: []
|
: []
|
||||||
|
|
||||||
// To fix failed and enrolled, no grade filter options some not so clean and nice solutions were added
|
// To fix failed and enrolled, no grade filter options some not so clean and nice solutions were added
|
||||||
// Get the data with actual 1. courses and filtered students. 2. all students by year, if provided.
|
// Get the data with actual 1. courses and filtered students. 2. all students by year, if provided.
|
||||||
const [[allStudents, courses, courseEnrollements], [, allCourses]] = await Promise.all([
|
const [[allStudents, courses, courseEnrollements], [, allCourses]] = (await Promise.all([
|
||||||
getStudentsAndCourses(params, query.selectedStudents, studentNumbers, query.courses),
|
getStudentsAndCourses(params, query.selectedStudents, studentNumbers, query.courses),
|
||||||
getStudentsAndCourses(params, allStudentsByYears, null, query.courses),
|
getStudentsAndCourses(params, allStudentsByYears, null, query.courses),
|
||||||
])
|
])) as [[number, QueryResult, QueryResult], [number, QueryResult]]
|
||||||
|
|
||||||
// Get the substitution codes for the fetch data by selected students
|
// Get the substitution codes for the fetch data by selected students
|
||||||
const substitutionCodes = Object.entries(courses).reduce(
|
const substitutionCodes = Object.entries(courses).reduce(
|
||||||
(codes, [, course]) => [...codes, ...(course?.substitutions || [])],
|
(codes, [, course]) => [...codes, ...(course?.substitutions || [])],
|
||||||
[]
|
[] as string[]
|
||||||
)
|
)
|
||||||
const codes = Object.keys(keyBy(courses, 'code')).map(code => code)
|
const codes = Object.keys(keyBy(courses, 'code')).map(code => code)
|
||||||
// Filter substitution courses for fetched courses -> by this we avoid the situation in which there
|
// Filter substitution courses for fetched courses -> by this we avoid the situation in which there
|
||||||
@@ -90,12 +108,14 @@ const bottlenecksOf = async (query, studentNumbers, encryptData = false) => {
|
|||||||
const substitutionCourses = allCourses.filter(
|
const substitutionCourses = allCourses.filter(
|
||||||
course => substitutionCodes.includes(course.code) && !codes.includes(course.code)
|
course => substitutionCodes.includes(course.code) && !codes.includes(course.code)
|
||||||
)
|
)
|
||||||
const bottlenecks = {
|
const bottlenecks: Bottlenecks = {
|
||||||
disciplines: {},
|
disciplines: {},
|
||||||
coursetypes: {},
|
coursetypes: {},
|
||||||
|
coursestatistics: [],
|
||||||
|
allStudents: 0,
|
||||||
}
|
}
|
||||||
|
|
||||||
const stats = {}
|
const stats = {} as Record<string, CourseStatsCounter>
|
||||||
const startYear = parseInt(query.year, 10)
|
const startYear = parseInt(query.year, 10)
|
||||||
let coursesToLoop = courses.concat(substitutionCourses)
|
let coursesToLoop = courses.concat(substitutionCourses)
|
||||||
const courseCodes = coursesToLoop.map(course => course.code)
|
const courseCodes = coursesToLoop.map(course => course.code)
|
||||||
@@ -110,7 +130,6 @@ const bottlenecksOf = async (query, studentNumbers, encryptData = false) => {
|
|||||||
|
|
||||||
const coursesByCode = keyBy(coursesToLoop, 'code')
|
const coursesByCode = keyBy(coursesToLoop, 'code')
|
||||||
for (const course of coursesToLoop) {
|
for (const course of coursesToLoop) {
|
||||||
const { course_type } = course
|
|
||||||
let mainCourse = course
|
let mainCourse = course
|
||||||
|
|
||||||
if (course.main_course_code && course.main_course_code !== course.code) {
|
if (course.main_course_code && course.main_course_code !== course.code) {
|
||||||
@@ -123,15 +142,16 @@ const bottlenecksOf = async (query, studentNumbers, encryptData = false) => {
|
|||||||
if (!stats[mainCourse.code]) {
|
if (!stats[mainCourse.code]) {
|
||||||
stats[mainCourse.code] = new CourseStatsCounter(mainCourse.code, mainCourse.name, allStudents)
|
stats[mainCourse.code] = new CourseStatsCounter(mainCourse.code, mainCourse.name, allStudents)
|
||||||
}
|
}
|
||||||
|
|
||||||
const coursestats = stats[mainCourse.code]
|
const coursestats = stats[mainCourse.code]
|
||||||
coursestats.addCourseType(course_type.coursetypecode, course_type.name)
|
coursestats.addCourseType(course.coursetypecode, course.name)
|
||||||
coursestats.addCourseSubstitutions(course.substitutions)
|
coursestats.addCourseSubstitutions(course.substitutions)
|
||||||
bottlenecks.coursetypes[course_type.coursetypecode] = course_type.name
|
bottlenecks.coursetypes[course.coursetypecode] = course.name
|
||||||
if (course.enrollments) {
|
if (course.enrollments) {
|
||||||
course.enrollments.forEach(({ studentnumber, state, enrollment_date_time }) => {
|
course.enrollments.forEach(({ studentnumber, state, enrollment_date_time }) => {
|
||||||
if ((query?.selectedStudents && query?.selectedStudents.includes(studentnumber)) || !query?.selectedStudents) {
|
if ((query?.selectedStudents && query?.selectedStudents.includes(studentnumber)) || !query?.selectedStudents) {
|
||||||
const semester = getPassingSemester(startYear, enrollment_date_time)
|
const semester = getPassingSemester(startYear, enrollment_date_time)
|
||||||
coursestats.markEnrollment(studentnumber, state, semester, enrollment_date_time)
|
coursestats.markEnrollment(studentnumber, state, semester)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -157,7 +177,3 @@ const bottlenecksOf = async (query, studentNumbers, encryptData = false) => {
|
|||||||
|
|
||||||
return bottlenecks
|
return bottlenecks
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
bottlenecksOf,
|
|
||||||
}
|
|
||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
formatStudentsForApi,
|
formatStudentsForApi,
|
||||||
getOptionsForStudents,
|
getOptionsForStudents,
|
||||||
parseQueryParams,
|
parseQueryParams,
|
||||||
|
Query as FormattedQueryParams,
|
||||||
} from './shared'
|
} from './shared'
|
||||||
import { getStudentNumbersWithAllStudyRightElements } from './studentNumbersWithAllElements'
|
import { getStudentNumbersWithAllStudyRightElements } from './studentNumbersWithAllElements'
|
||||||
|
|
||||||
@@ -34,7 +35,7 @@ export const optimizedStatisticsOf = async (query: Query, studentNumberList?: st
|
|||||||
}
|
}
|
||||||
|
|
||||||
const { studyRights, startDate, months, endDate, exchangeStudents, nondegreeStudents, transferredStudents, tag } =
|
const { studyRights, startDate, months, endDate, exchangeStudents, nondegreeStudents, transferredStudents, tag } =
|
||||||
parseQueryParams(formattedQueryParams)
|
parseQueryParams(formattedQueryParams as FormattedQueryParams)
|
||||||
|
|
||||||
const studentNumbers =
|
const studentNumbers =
|
||||||
studentNumberList ||
|
studentNumberList ||
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { Op, QueryTypes } from 'sequelize'
|
|||||||
|
|
||||||
import { dbConnections } from '../../database/connection'
|
import { dbConnections } from '../../database/connection'
|
||||||
import { Course, Credit, Enrollment, SISStudyRight, SISStudyRightElement, Student, Studyplan } from '../../models'
|
import { Course, Credit, Enrollment, SISStudyRight, SISStudyRightElement, Student, Studyplan } from '../../models'
|
||||||
import { TagStudent } from '../../models/kone'
|
import { Tag, TagStudent } from '../../models/kone'
|
||||||
import { Criteria, DegreeProgrammeType, Name, ParsedCourse } from '../../types'
|
import { Criteria, DegreeProgrammeType, Name, ParsedCourse } from '../../types'
|
||||||
import { SemesterStart } from '../../util/semester'
|
import { SemesterStart } from '../../util/semester'
|
||||||
import { hasTransferredFromOrToProgramme } from '../studyProgramme/studyProgrammeHelpers'
|
import { hasTransferredFromOrToProgramme } from '../studyProgramme/studyProgrammeHelpers'
|
||||||
@@ -286,7 +286,31 @@ export const dateMonthsFromNow = (date: string, months: number) => {
|
|||||||
return new Date(moment(date).add(months, 'months').format('YYYY-MM-DD'))
|
return new Date(moment(date).add(months, 'months').format('YYYY-MM-DD'))
|
||||||
}
|
}
|
||||||
|
|
||||||
export const parseQueryParams = query => {
|
export type Query = {
|
||||||
|
year: string
|
||||||
|
months: number
|
||||||
|
semesters: string[]
|
||||||
|
studyRights: string | string[]
|
||||||
|
selectedStudents?: string[]
|
||||||
|
selectedStudentsByYear?: Record<string, string[]>
|
||||||
|
studentStatuses?: string[]
|
||||||
|
years?: string[]
|
||||||
|
tag?: string
|
||||||
|
courses?: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Params = {
|
||||||
|
startDate: string
|
||||||
|
endDate: string
|
||||||
|
exchangeStudents: boolean
|
||||||
|
nondegreeStudents: boolean
|
||||||
|
transferredStudents: boolean
|
||||||
|
studyRights: string[]
|
||||||
|
months: number
|
||||||
|
tag?: Tag
|
||||||
|
}
|
||||||
|
|
||||||
|
export const parseQueryParams = (query: Query) => {
|
||||||
const { semesters, studentStatuses, studyRights, months, year, tag } = query
|
const { semesters, studentStatuses, studyRights, months, year, tag } = query
|
||||||
const hasFall = semesters.includes('FALL')
|
const hasFall = semesters.includes('FALL')
|
||||||
const hasSpring = semesters.includes('SPRING')
|
const hasSpring = semesters.includes('SPRING')
|
||||||
@@ -313,7 +337,7 @@ export const parseQueryParams = query => {
|
|||||||
startDate,
|
startDate,
|
||||||
endDate,
|
endDate,
|
||||||
tag,
|
tag,
|
||||||
}
|
} as Params
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getOptionsForStudents = async (
|
export const getOptionsForStudents = async (
|
||||||
@@ -462,10 +486,21 @@ const getCourseCodes = async (courses: string[]) => {
|
|||||||
return courseCodes
|
return courseCodes
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type QueryResult = Array<{
|
||||||
|
code: string
|
||||||
|
name: Name
|
||||||
|
coursetypecode: string
|
||||||
|
substitutions: string[]
|
||||||
|
main_course_code: string
|
||||||
|
course_type: Name
|
||||||
|
credits: Array<Pick<Credit, 'grade' | 'student_studentnumber' | 'attainment_date' | 'credittypecode' | 'course_code'>>
|
||||||
|
enrollments: Array<Pick<Enrollment, 'studentnumber' | 'state' | 'enrollment_date_time'>> | null
|
||||||
|
}>
|
||||||
|
|
||||||
// This duplicate code is added here to ensure that we get the enrollments in cases no credits found for the selected students
|
// This duplicate code is added here to ensure that we get the enrollments in cases no credits found for the selected students
|
||||||
export const findCourseEnrollments = async (studentNumbers: string[], beforeDate: Date, courses: string[] = []) => {
|
export const findCourseEnrollments = async (studentNumbers: string[], beforeDate: Date, courses: string[] = []) => {
|
||||||
const courseCodes = await getCourseCodes(courses)
|
const courseCodes = await getCourseCodes(courses)
|
||||||
const res = await sequelize.query(
|
const result: QueryResult = await sequelize.query(
|
||||||
`
|
`
|
||||||
SELECT DISTINCT ON (course.id)
|
SELECT DISTINCT ON (course.id)
|
||||||
course.code,
|
course.code,
|
||||||
@@ -502,12 +537,12 @@ export const findCourseEnrollments = async (studentNumbers: string[], beforeDate
|
|||||||
type: QueryTypes.SELECT,
|
type: QueryTypes.SELECT,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
return res
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
export const findCourses = async (studentNumbers: string[], beforeDate: Date, courses: string[] = []) => {
|
export const findCourses = async (studentNumbers: string[], beforeDate: Date, courses: string[] = []) => {
|
||||||
const courseCodes = await getCourseCodes(courses)
|
const courseCodes = await getCourseCodes(courses)
|
||||||
const res = await sequelize.query(
|
const result: QueryResult = await sequelize.query(
|
||||||
`
|
`
|
||||||
SELECT DISTINCT ON (course.id)
|
SELECT DISTINCT ON (course.id)
|
||||||
course.code,
|
course.code,
|
||||||
@@ -559,15 +594,18 @@ export const findCourses = async (studentNumbers: string[], beforeDate: Date, co
|
|||||||
type: QueryTypes.SELECT,
|
type: QueryTypes.SELECT,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
return result
|
||||||
return res
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const parseCreditInfo = (credit: Credit) => ({
|
export const parseCreditInfo = (
|
||||||
studentnumber: credit.student_studentnumber,
|
credit: Pick<Credit, 'grade' | 'student_studentnumber' | 'attainment_date' | 'credittypecode' | 'course_code'>
|
||||||
grade: credit.grade,
|
) => {
|
||||||
passingGrade: Credit.passed(credit),
|
return {
|
||||||
failingGrade: Credit.failed(credit),
|
studentnumber: credit.student_studentnumber,
|
||||||
improvedGrade: Credit.improved(credit),
|
grade: credit.grade,
|
||||||
date: credit.attainment_date,
|
passingGrade: Credit.passed(credit),
|
||||||
})
|
failingGrade: Credit.failed(credit),
|
||||||
|
improvedGrade: Credit.improved(credit),
|
||||||
|
date: credit.attainment_date,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user