Skip to content

Commit

Permalink
Programme modules reworked
Browse files Browse the repository at this point in the history
First publishable version of the programme modules rework.
Also minor other fixes like refactorings and logging improved.
  • Loading branch information
ShootingStar91 committed Sep 20, 2023
1 parent 46f8c36 commit 011d70d
Show file tree
Hide file tree
Showing 26 changed files with 400 additions and 135 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
module.exports = {
up: async (queryInterface, Sequelize) => {
await queryInterface.sequelize.query('TRUNCATE TABLE progress_criteria')
await queryInterface.sequelize.query('TRUNCATE TABLE excluded_courses')
await queryInterface.removeConstraint('excluded_courses', 'excluded_courses_programme_code_course_code_key')
await queryInterface.addColumn('progress_criteria', 'curriculum_version', {
type: Sequelize.STRING,
})
await queryInterface.addColumn('excluded_courses', 'curriculum_version', {
type: Sequelize.STRING,
})
},
down: async queryInterface => {
await queryInterface.removeColumn('progress_criteria', 'curriculum_version')
await queryInterface.removeColumn('excluded_courses', 'curriculum_version')
},
}
3 changes: 3 additions & 0 deletions services/backend/src/models/excludedCourse.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ ExcludedCourse.init(
course_code: {
type: STRING,
},
curriculum_version: {
type: STRING,
},
created_at: {
type: DATE,
},
Expand Down
6 changes: 6 additions & 0 deletions services/backend/src/models/models_kone.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ const ExcludedCourse = sequelizeKone.define(
programme_code: {
type: Sequelize.STRING,
},
curriculum_version: {
type: STRING,
},
course_code: {
type: Sequelize.STRING,
},
Expand Down Expand Up @@ -123,6 +126,9 @@ const ProgressCriteria = sequelizeKone.define(
primaryKey: true,
type: STRING,
},
curriculumVersion: {
type: STRING,
},
coursesYearOne: {
type: Sequelize.ARRAY(Sequelize.STRING),
},
Expand Down
5 changes: 4 additions & 1 deletion services/backend/src/models/programmeModule.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
const { Model, STRING, DATE, INTEGER, JSONB } = require('sequelize')
const { Model, STRING, DATE, INTEGER, JSONB, ARRAY, TEXT } = require('sequelize')
const { dbConnections } = require('../database/connection')

class ProgrammeModule extends Model {}
Expand Down Expand Up @@ -40,6 +40,9 @@ ProgrammeModule.init(
valid_to: {
type: DATE,
},
curriculum_period_ids: {
type: ARRAY(TEXT),
},
createdAt: {
type: DATE,
},
Expand Down
27 changes: 20 additions & 7 deletions services/backend/src/routes/programmeModules.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
const router = require('express').Router()
const getCoursesAndModules = require('../services/programmeModules')
const { getCoursesAndModules, getCurriculumVersions } = require('../services/programmeModules')
const { addExcludedCourses, removeExcludedCourses } = require('../services/excludedCourses')

router.get('/v3/programme_modules/:code', async (req, res) => {
Expand All @@ -18,9 +18,8 @@ router.get('/v3/programme_modules/:code/modules', async (req, res) => {
})

router.delete('/v3/programme_modules', async (req, res) => {
const { programmecode, ids } = req.body
await removeExcludedCourses(ids)
const result = await getCoursesAndModules(programmecode)
const { programmeCode, curriculumVersion, courseCodes } = req.body
const result = await removeExcludedCourses({ programmeCode, curriculumVersion, courseCodes })
if (!result) {
res.status(400).end()
return
Expand All @@ -29,14 +28,28 @@ router.delete('/v3/programme_modules', async (req, res) => {
})

router.post('/v3/programme_modules/:programmecode/', async (req, res) => {
const { programmecode, excludeFromProgramme, coursecodes } = req.body
await addExcludedCourses(excludeFromProgramme, coursecodes)
const result = await getCoursesAndModules(programmecode)
const { excludeFromProgramme, coursecodes, curriculum } = req.body
const result = await addExcludedCourses(excludeFromProgramme, coursecodes, curriculum.join(','))
if (!result) {
res.status(400).end()
return
}
res.json(result)
})

router.get('/v3/programme_modules/get_curriculum_options/:code', async (req, res) => {
const { code } = req.params
const result = await getCurriculumVersions(code)
res.json(result)
})

router.get('/v3/programme_modules/get_curriculum/:code/:period_ids', async (req, res) => {
const { code, period_ids } = req.params
const result = await getCoursesAndModules(code, period_ids.replace(' ', '').split(','))
res.json({
defaultProgrammeCourses: result.defaultProgrammeCourses.courses,
secondProgrammeCourses: result.secondProgrammeCourses?.courses,
})
})

module.exports = router
14 changes: 8 additions & 6 deletions services/backend/src/services/excludedCourses.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,18 @@ const Sequelize = require('sequelize')
const { ExcludedCourse } = require('../models/models_kone')
const { Op } = Sequelize

const addExcludedCourses = async (programmecode, coursecodes) => {
return ExcludedCourse.bulkCreate(coursecodes.map(c => ({ programme_code: programmecode, course_code: c })))
const addExcludedCourses = async (programmecode, coursecodes, curriculum) => {
return ExcludedCourse.bulkCreate(
coursecodes.map(c => ({ programme_code: programmecode, curriculum_version: curriculum, course_code: c }))
)
}

const removeExcludedCourses = async ids => {
const removeExcludedCourses = async ({ programmeCode, curriculumVersion, courseCodes }) => {
return ExcludedCourse.destroy({
where: {
id: {
[Op.or]: ids,
},
programme_code: programmeCode,
curriculum_version: curriculumVersion,
course_code: { [Op.in]: courseCodes },
},
})
}
Expand Down
56 changes: 43 additions & 13 deletions services/backend/src/services/programmeModules.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,25 +4,43 @@ const { Op } = Sequelize
const logger = require('../util/logger')
const { combinedStudyprogrammes } = require('./studyprogrammeHelpers.js')
const { dbConnections } = require('../database/connection')
const { ProgrammeModule } = require('../models')

const recursivelyGetModuleAndChildren = async (code, type, start = '1900-1-1', end = '2100-1-1') => {
const getCurriculumVersions = async code => {
try {
const result = await ProgrammeModule.findAll({ where: { code } })
if (!result) return
const modified = result.map(r => ({
...r,
curriculum_period_ids: r.curriculum_period_ids.map(id => parseInt(id.substring(6)) + 1949),
}))
return modified
} catch (e) {
logger.error(`Error when searching curriculum versions for code: ${code}`)
logger.error(e)
return []
}
}

const recursivelyGetModuleAndChildren = async (code, curriculum_period_ids) => {
const connection = dbConnections.sequelize
try {
const [result] = await connection.query(
`WITH RECURSIVE children as (
SELECT DISTINCT pm.*, 0 AS module_order, NULL::jsonb AS parent_name, NULL AS parent_code, NULL as parent_id FROM programme_modules pm
WHERE pm.code = ?
WHERE pm.code = ? AND ARRAY[?]::text[] && curriculum_period_ids
UNION ALL
SELECT pm.*, c.order AS module_order, c.name AS parent_name, c.code AS parent_code, c.id as parent_id
FROM children c, programme_modules pm, programme_module_children pmc
WHERE c.id = pmc.parent_id AND pm.group_id = pmc.child_id
WHERE c.id = pmc.parent_id AND pm.group_id = pmc.child_id AND (ARRAY[?]::text[] && pm.curriculum_period_ids OR pm.type = 'course' OR pm.code is null)
GROUP BY pm.id, c.name, c.code, c.order, c.id
) SELECT * FROM children WHERE type = ? AND (valid_to > ? OR valid_to IS NULL) AND (valid_from < ? OR valid_from IS NULL)`,
{ replacements: [code, type, start, end] }
) SELECT * FROM children`,
{ replacements: [code, curriculum_period_ids, curriculum_period_ids] }
)
return result
} catch (e) {
logger.error(`Error when searching modules and children with code: ${code}`)
logger.error(e)
return []
}
}
Expand All @@ -35,29 +53,41 @@ const modifyParent = (course, moduleMap) => {
parent = moduleMap[parent.parent_id]
}

let skip = 0
const skip = 0
const parentsWithCode = parents.filter(p => p.code)
if (parentsWithCode.length > 0) {
parent = parentsWithCode[skip >= parentsWithCode.length ? parentsWithCode.length - 1 : skip]
} else {
parent = parents.find(m => m.code)
}
if (!parent) {
return { faulty: true }
}
return { ...course, parent_id: parent.id, parent_code: parent.code, parent_name: parent.name }
}

const getCoursesAndModulesForProgramme = async code => {
const courses = await recursivelyGetModuleAndChildren(code, 'course')
const modules = await recursivelyGetModuleAndChildren(code, 'module')
const getCoursesAndModulesForProgramme = async (code, period_ids) => {
if (!period_ids) return {}
const result = await recursivelyGetModuleAndChildren(
code,
period_ids.map(id => `hy-lv-${id - 1949}`)
)
const courses = result.filter(r => r.type === 'course')
const modules = result.filter(r => r.type === 'module')
const excludedCourses = await ExcludedCourse.findAll({
where: {
programme_code: {
[Op.eq]: code,
},
curriculum_version: {
[Op.eq]: period_ids.join(','),
},
},
})
const modulesMap = modules.reduce((obj, cur) => ({ ...obj, [cur.id]: cur }), {})
const modifiedCourses = courses
.map(c => modifyParent(c, modulesMap, code))
.filter(c => !c.faulty)
.filter(
(course1, index, array) =>
array.findIndex(course2 => course1.code === course2.code && course1.parent_code === course2.parent_code) ===
Expand All @@ -67,11 +97,11 @@ const getCoursesAndModulesForProgramme = async code => {
return { courses: labelProgammes(modifiedCourses, excludedCourses), modules }
}

const getCoursesAndModules = async code => {
const defaultProgrammeCourses = await getCoursesAndModulesForProgramme(code)
const getCoursesAndModules = async (code, period_ids) => {
const defaultProgrammeCourses = await getCoursesAndModulesForProgramme(code, period_ids)
if (Object.keys(combinedStudyprogrammes).includes(code)) {
const secondProgramme = combinedStudyprogrammes[code]
const secondProgrammeCourses = await getCoursesAndModulesForProgramme(secondProgramme)
const secondProgrammeCourses = await getCoursesAndModulesForProgramme(secondProgramme, period_ids)
return { defaultProgrammeCourses, secondProgrammeCourses }
}
return { defaultProgrammeCourses, secondProgrammeCourses: { courses: [], modules: [] } }
Expand All @@ -90,4 +120,4 @@ const labelProgammes = (modules, excludedCourses) => {
})
}

module.exports = getCoursesAndModules
module.exports = { getCoursesAndModules, getCurriculumVersions }
1 change: 1 addition & 0 deletions services/backend/src/services/studyprogrammeGraduations.js
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,7 @@ const getGraduationStatsForStudytrack = async ({ studyprogramme, combinedProgram
isAcademicYear,
includeAllSpecials,
}

const thesis = await getThesisStats(queryParameters)
const thesisSecondProgramme = await getThesisStats(combinedQueryParameters)

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React, { useState, useEffect, useCallback } from 'react'
import { connect, useSelector } from 'react-redux'
import { connect } from 'react-redux'
import { Table, Input, Tab, Icon } from 'semantic-ui-react'
import { orderBy, debounce } from 'lodash'
import { withRouter } from 'react-router-dom'
Expand Down Expand Up @@ -103,7 +103,7 @@ const PopulationCourseStats = props => {
const [modules, setModules] = useState([])
const [state, setState] = useState(initialState(props))
const [expandedGroups, setExpandedGroups] = useState(new Set())
const mandatoryCourses = useSelector(({ populationMandatoryCourses }) => populationMandatoryCourses.data)
const { mandatoryCourses } = props
const { handleTabChange } = useTabChangeAnalytics()

const courseStatistics = useDelayedMemo(
Expand Down Expand Up @@ -133,6 +133,7 @@ const PopulationCourseStats = props => {
courses: { coursestatistics },
language,
} = props

const courseCodeFilter = ({ course }) => {
if (!codeFilter) return true

Expand Down Expand Up @@ -207,7 +208,7 @@ const PopulationCourseStats = props => {
module,
courses,
})),
item => item.module.order
item => item.module.code
)
)
}, [state.studentAmountLimit, props.courses.coursestatistics, state.codeFilter, state.nameFilter, mandatoryCourses])
Expand Down
Loading

0 comments on commit 011d70d

Please sign in to comment.