Skip to content

Commit

Permalink
[Class statistics] Use new study rights to determine the students of …
Browse files Browse the repository at this point in the history
…one year's class
  • Loading branch information
valtterikantanen committed Aug 26, 2024
1 parent 2d866b5 commit 56feb95
Show file tree
Hide file tree
Showing 6 changed files with 60 additions and 243 deletions.
3 changes: 2 additions & 1 deletion services/backend/src/routes/population.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ import { rootOrgId } from '../config'
import { maxYearsToCreatePopulationFrom, getCourseProvidersForCourses } from '../services/courses'
import { encrypt, decrypt } from '../services/encrypt'
import { getDegreeProgrammesOfOrganization, ProgrammesOfOrganization } from '../services/faculty/faculty'
import { bottlenecksOf, optimizedStatisticsOf } from '../services/populations'
import { bottlenecksOf } from '../services/populations/bottlenecksOf'
import { optimizedStatisticsOf } from '../services/populations/optimizedStatisticsOf'
import { populationStudentsMerger, populationCourseStatsMerger } from '../services/statMerger'
import { findByTag, findByCourseAndSemesters } from '../services/students'
import { Unarray, Unification, UnifyStatus } from '../types'
Expand Down
3 changes: 0 additions & 3 deletions services/backend/src/services/populations/index.ts

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import moment from 'moment-timezone'

import { Criteria, Name } from '../../types'
import { getCriteria } from '../studyProgramme/studyProgrammeCriteria'
import { getStudentsIncludeCoursesBetween } from './getStudentsIncludeCoursesBetween'
Expand Down Expand Up @@ -37,21 +35,15 @@ export const optimizedStatisticsOf = async (query: Query, studentNumberList?: st
const { studyRights, startDate, months, endDate, exchangeStudents, nondegreeStudents, transferredStudents, tag } =
parseQueryParams(formattedQueryParams)

// db startdate is formatted to utc so need to change it when querying
const formattedStartDate = new Date(moment.tz(startDate, 'Europe/Helsinki').format()).toUTCString()

const studentNumbers =
studentNumberList ||
(await getStudentNumbersWithAllStudyRightElements({
studyRights,
startDate: formattedStartDate,
startDate,
endDate,
exchangeStudents,
nondegreeStudents,
transferredOutStudents: transferredStudents,
tag: null,
transferredToStudents: true,
graduatedStudents: true,
}))

const code = studyRights[0] || ''
Expand Down
23 changes: 8 additions & 15 deletions services/backend/src/services/populations/shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { Op, QueryTypes } from 'sequelize'
import { dbConnections } from '../../database/connection'
import { Course, Credit, SISStudyRight, SISStudyRightElement } from '../../models'
import { ElementDetailType, Criteria, Name } from '../../types'
import { SemesterEnd, SemesterStart } from '../../util/semester'
import { SemesterStart } from '../../util/semester'
import { getCurrentSemester } from '../semesters'

const { sequelize } = dbConnections
Expand Down Expand Up @@ -237,29 +237,22 @@ export const dateMonthsFromNow = (date: string, months: number) => {
return new Date(moment(date).add(months, 'months').format('YYYY-MM-DD'))
}

export const count = (column, count, distinct = false) => {
const countable = !distinct ? sequelize.col(column) : sequelize.fn('DISTINCT', sequelize.col(column))
return sequelize.where(sequelize.fn('COUNT', countable), {
[Op.eq]: count,
})
}

export const parseQueryParams = query => {
const { semesters, studentStatuses, studyRights, months, year, tag } = query
const hasFall = semesters.includes('FALL')
const hasSpring = semesters.includes('SPRING')

const startDate = hasFall
? `${year}-${SemesterStart.FALL}`
: `${moment(year, 'YYYY').add(1, 'years').format('YYYY')}-${SemesterStart.SPRING}`
? new Date(`${year}-${SemesterStart.FALL}`).toISOString()
: new Date(`${year + 1}-${SemesterStart.SPRING}`).toISOString()

const endDate = hasSpring
? `${moment(year, 'YYYY').add(1, 'years').format('YYYY')}-${SemesterEnd.SPRING}`
: `${year}-${SemesterEnd.FALL}`
? new Date(`${year + 1}-${SemesterStart.FALL}`).toISOString()
: new Date(`${year}-${SemesterStart.SPRING}`).toISOString()

const exchangeStudents = studentStatuses && studentStatuses.includes('EXCHANGE')
const nondegreeStudents = studentStatuses && studentStatuses.includes('NONDEGREE')
const transferredStudents = studentStatuses && studentStatuses.includes('TRANSFERRED')
const exchangeStudents = studentStatuses != null && studentStatuses.includes('EXCHANGE')
const nondegreeStudents = studentStatuses != null && studentStatuses.includes('NONDEGREE')
const transferredStudents = studentStatuses != null && studentStatuses.includes('TRANSFERRED')

return {
exchangeStudents,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,8 @@
import { sortBy } from 'lodash'
import { Op } from 'sequelize'

import { dbConnections } from '../../database/connection'
import { ElementDetail, Student, Studyright, StudyrightElement, Transfer } from '../../models'
import { TagStudent } from '../../models/kone'
import { ElementDetailType, ExtentCode, PriorityCode } from '../../types'
import { count } from './shared'

const { sequelize } = dbConnections
import { SISStudyRight, SISStudyRightElement } from '../../models'
import { ExtentCode } from '../../types'
import { hasTransferredFromOrToProgramme } from '../studyProgramme/studyProgrammeHelpers'

export const getStudentNumbersWithAllStudyRightElements = async ({
studyRights,
Expand All @@ -16,9 +11,6 @@ export const getStudentNumbersWithAllStudyRightElements = async ({
exchangeStudents,
nondegreeStudents,
transferredOutStudents,
tag,
transferredToStudents,
graduatedStudents,
}) => {
const filteredExtents = [ExtentCode.STUDIES_FOR_SECONDARY_SCHOOL_STUDENTS]
if (!exchangeStudents) {
Expand All @@ -33,219 +25,68 @@ export const getStudentNumbersWithAllStudyRightElements = async ({
ExtentCode.SPECIALIZATION_STUDIES,
ExtentCode.NON_DEGREE_PROGRAMME_FOR_SPECIAL_EDUCATION_TEACHERS,
ExtentCode.SPECIALIST_TRAINING_IN_MEDICINE_AND_DENTISTRY,
ExtentCode.NON_DEGREE_STUDIES
ExtentCode.NON_DEGREE_STUDIES,
ExtentCode.SUMMER_AND_WINTER_SCHOOL
)
}

const studyrightWhere = {
extentcode: {
[Op.notIn]: filteredExtents,
},
prioritycode: {
[Op.not]: PriorityCode.OPTION,
},
}

const studentWhere: Record<string, any> = {}
if (tag) {
const taggedStudentnumbers = await TagStudent.findAll({
attributes: ['studentnumber'],
where: {
tag_id: tag,
},
})
studentWhere.where = {
student_studentnumber: {
[Op.in]: taggedStudentnumbers.map(student => student.studentnumber),
},
}
}

const students = await Studyright.findAll({
attributes: ['student_studentnumber', 'graduated', 'enddate'],
include: {
model: StudyrightElement,
attributes: [],
required: true,
where: {
code: {
[Op.in]: studyRights,
},
},
include: [
{
model: ElementDetail,
attributes: [],
},
],
},
where: {
[Op.or]: [
{
'$studyright_elements->element_detail.type$': {
[Op.ne]: ElementDetailType.PROGRAMME,
const studyRightIds = (
await SISStudyRight.findAll({
attributes: ['id'],
include: {
model: SISStudyRightElement,
attributes: [],
where: {
code: {
[Op.in]: studyRights,
},
startDate: {
[Op.gte]: startDate,
[Op.lt]: endDate,
},
},
sequelize.where(
sequelize.fn(
'GREATEST',
sequelize.col('studyright_elements.startdate'),
sequelize.col('studyright.startdate')
),
{
[Op.between]: [startDate, endDate],
}
),
],
...studyrightWhere,
},
...studentWhere,
group: [sequelize.col('studyright.studyrightid')],
having: count('studyright_elements.code', studyRights.length, true),
raw: true,
})

const studentNumbers = [...new Set(students.map(student => student.student_studentnumber))]
const rights = await Studyright.findAll({
attributes: ['studyrightid'],
where: {
student_studentnumber: {
[Op.in]: studentNumbers,
},
},
include: {
attributes: [],
model: StudyrightElement,
where: {
code: {
[Op.in]: studyRights,
extentCode: {
[Op.notIn]: filteredExtents,
},
},
},
group: ['studyright.studyrightid'],
having: count('studyright_elements.id', studyRights.length, true),
raw: true,
})

// bit hacky solution, but this is used to filter out studentnumbers who have since changed studytracks
const allStudytracksForStudents = await StudyrightElement.findAll({
where: {
studyrightid: {
[Op.in]: rights.map(studyRight => studyRight.studyrightid),
raw: true,
})
).map(studyRight => studyRight.id)

const studentsStudyRights = (
await SISStudyRight.findAll({
attributes: ['studentNumber'],
include: {
model: SISStudyRightElement,
as: 'studyRightElements',
attributes: ['code', 'endDate', 'startDate', 'phase'],
},
},
include: {
model: ElementDetail,
where: {
type: {
[Op.eq]: ElementDetailType.MODULE,
id: {
[Op.in]: studyRightIds,
},
},
},
raw: true,
})

const studentNumberToSrElementMap = allStudytracksForStudents.reduce((obj, cur) => {
if (!obj[cur.studentnumber]) obj[cur.studentnumber] = []
obj[cur.studentnumber].push(cur)
return obj
}, {})

const formattedStudytracks = studentNumbers.reduce((acc, curr) => {
acc[curr] = studentNumberToSrElementMap[curr]
return acc
}, {})

// Take the newest studytrack primarily by latest starting date in the track, secondarily by the latest enddate
const filteredStudentnumbers = studentNumbers.filter(studentNumber => {
const newestStudytrack = sortBy(formattedStudytracks[studentNumber], ['startdate', 'enddate']).reverse()[0]
if (!newestStudytrack) return false
return studyRights.includes(newestStudytrack.code)
})

// Use the filtered list, if the search includes studytracks
// Then the studyrights length is > 1, which means that there is [studyright, studytrack].
// When searching only for studyprogramme, there is [studyright]
let studentNumberList = studyRights.length > 1 ? filteredStudentnumbers : studentNumbers

// fetch students that have transferred out of the programme and filter out these studentnumbers
if (!transferredOutStudents) {
const transfersOut = (
await Transfer.findAll({
attributes: ['studentnumber'],
where: {
sourcecode: {
[Op.in]: studyRights,
},
transferdate: {
[Op.gt]: startDate,
},
studentnumber: {
[Op.in]: studentNumberList,
},
},
raw: true,
})
).map(student => student.studentnumber)
})
).map(studyRight => studyRight.toJSON())

studentNumberList = studentNumberList.filter(studentNumber => !transfersOut.includes(studentNumber))
}
const studentNumbers = studentsStudyRights.map(student => student.studentNumber)

// fetch students that have transferred to the programme and filter out these studentnumbers
if (!transferredToStudents) {
const transfersTo = (
await Transfer.findAll({
attributes: ['studentnumber'],
where: {
targetcode: {
[Op.in]: studyRights,
},
transferdate: {
[Op.gt]: startDate,
},
studentnumber: {
[Op.in]: studentNumberList,
},
},
raw: true,
})
).map(transfer => transfer.studentnumber)
studentNumberList = studentNumberList.filter(studentNumber => !transfersTo.includes(studentNumber))
if (transferredOutStudents) {
return studentNumbers
}

// fetch students that have graduated from the programme and filter out these studentnumbers
if (!graduatedStudents) {
const graduated = (
await Student.findAll({
attributes: ['studentnumber'],
include: [
{
model: Studyright,
include: [
{
model: StudyrightElement,
required: true,
where: {
code: {
[Op.in]: studyRights,
},
},
},
],
where: {
graduated: 1,
},
},
],
where: {
studentnumber: {
[Op.in]: studentNumbers,
},
},
})
).map(student => student.studentnumber)
studentNumberList = studentNumberList.filter(studentNumber => !graduated.includes(studentNumber))
}
const transferredStudentNumbers = studentsStudyRights.reduce<string[]>((acc, student) => {
const [hasTransferredFromProgramme] = hasTransferredFromOrToProgramme(
student,
student.studyRightElements.find(element => studyRights.includes(element.code))!
)
if (hasTransferredFromProgramme) {
acc.push(student.studentNumber)
}
return acc
}, [])

return studentNumberList
return studentNumbers.filter(studentNumber => !transferredStudentNumbers.includes(studentNumber))
}
7 changes: 0 additions & 7 deletions services/backend/src/util/semester.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,6 @@ export enum SemesterStart {
FALL = '08-01',
}

// Sometimes start date is in UTC and sometimes not, add flexibility to this
// End date must be then also in UTC, otherwice students may land into two academic years
export enum SemesterEnd {
SPRING = '07-31T20:59:59.000Z',
FALL = '12-31T20:59:59.000Z',
}

export const getPassingSemester = (startYear: number, date: Date): string => {
const mDate = moment(date).add(1, 'day')
const year = mDate.year()
Expand Down

0 comments on commit 56feb95

Please sign in to comment.