mirror of
https://github.com/UniversityOfHelsinkiCS/oodikone.git
synced 2026-10-12 19:03:39 +03:00
[Kone] Course exclusion migration to group id step 1
This commit is contained in:
@@ -4,6 +4,7 @@ import { baseUrl, backendPort } from './config'
|
||||
import { initializeDatabaseConnection, dbConnections } from './database/connection'
|
||||
import { startCron } from './events'
|
||||
import routes from './routes'
|
||||
import { backfillExcludedCourseGroupIds } from './services/excludedCourses'
|
||||
import logger from './util/logger'
|
||||
import './worker/worker'
|
||||
|
||||
@@ -13,6 +14,7 @@ initializeDatabaseConnection()
|
||||
|
||||
dbConnections.on('connect', () => {
|
||||
logger.info('Connected to sis db successfully')
|
||||
backfillExcludedCourseGroupIds().catch(error => logger.error(error))
|
||||
})
|
||||
|
||||
dbConnections.on('error', () => {
|
||||
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
const { STRING } = require('sequelize')
|
||||
|
||||
// Migration step 1 of 2
|
||||
module.exports = {
|
||||
up: async ({ context: queryInterface }) => {
|
||||
await queryInterface.addColumn('excluded_courses', 'course_group_id', {
|
||||
type: STRING,
|
||||
})
|
||||
},
|
||||
|
||||
down: async ({ context: queryInterface }) => {
|
||||
await queryInterface.removeColumn('excluded_courses', 'course_group_id')
|
||||
},
|
||||
}
|
||||
@@ -19,6 +19,9 @@ export class ExcludedCourseModel extends Model implements ExcludedCourse {
|
||||
@Column(DataType.STRING)
|
||||
declare course_code: ExcludedCourse['course_code']
|
||||
|
||||
@Column(DataType.STRING)
|
||||
declare course_group_id: ExcludedCourse['course_group_id']
|
||||
|
||||
@Column(DataType.STRING)
|
||||
declare curriculum_version: ExcludedCourse['curriculum_version']
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import { hasFullAccessToStudentData } from '../util'
|
||||
const router = Router()
|
||||
|
||||
router.post<never, ExcludedCoursesResBody, ExcludedCoursesReqBody>('/', async (req, res) => {
|
||||
const { programmeCode, courseCodes, curriculumVersion } = req.body
|
||||
const { programmeCode, courseGroupIds, curriculumVersion } = req.body
|
||||
const { roles, programmeRights } = req.user
|
||||
|
||||
const hasFullAccess = hasFullAccessToStudentData(roles)
|
||||
@@ -15,7 +15,7 @@ router.post<never, ExcludedCoursesResBody, ExcludedCoursesReqBody>('/', async (r
|
||||
|
||||
if (!hasFullAccess && !hasAccessToProgramme) return res.status(403).end()
|
||||
|
||||
const result = await addExcludedCourses(courseCodes, curriculumVersion, programmeCode)
|
||||
const result = await addExcludedCourses(courseGroupIds, curriculumVersion, programmeCode)
|
||||
if (!result) {
|
||||
res.status(400).end()
|
||||
return
|
||||
@@ -24,7 +24,7 @@ router.post<never, ExcludedCoursesResBody, ExcludedCoursesReqBody>('/', async (r
|
||||
})
|
||||
|
||||
router.delete<never, ExcludedCoursesResBody, ExcludedCoursesReqBody>('/', async (req, res) => {
|
||||
const { programmeCode, courseCodes, curriculumVersion } = req.body
|
||||
const { programmeCode, courseGroupIds, curriculumVersion } = req.body
|
||||
const { roles, programmeRights } = req.user
|
||||
|
||||
const hasFullAccess = hasFullAccessToStudentData(roles)
|
||||
@@ -32,7 +32,7 @@ router.delete<never, ExcludedCoursesResBody, ExcludedCoursesReqBody>('/', async
|
||||
|
||||
if (!hasFullAccess && !hasAccessToProgramme) return res.status(403).end()
|
||||
|
||||
const result = await removeExcludedCourses(courseCodes, curriculumVersion, programmeCode)
|
||||
const result = await removeExcludedCourses(courseGroupIds, curriculumVersion, programmeCode)
|
||||
if (!result) {
|
||||
res.status(400).end()
|
||||
return
|
||||
|
||||
@@ -1,19 +1,38 @@
|
||||
import { Op } from 'sequelize'
|
||||
|
||||
import { CourseModel } from '../models'
|
||||
import { ExcludedCourseModel } from '../models/kone'
|
||||
import logger from '../util/logger'
|
||||
|
||||
export const addExcludedCourses = async (courseCodes: string[], curriculumVersion: string, programmeCode: string) => {
|
||||
return ExcludedCourseModel.bulkCreate(
|
||||
courseCodes.map(courseCode => ({
|
||||
export const addExcludedCourses = async (
|
||||
courseGroupIds: string[],
|
||||
curriculumVersion: string,
|
||||
programmeCode: string
|
||||
) => {
|
||||
const existing = await ExcludedCourseModel.findAll({
|
||||
attributes: ['course_group_id'],
|
||||
where: {
|
||||
programme_code: programmeCode,
|
||||
curriculum_version: curriculumVersion,
|
||||
course_code: courseCode,
|
||||
}))
|
||||
course_group_id: { [Op.in]: courseGroupIds },
|
||||
},
|
||||
raw: true,
|
||||
})
|
||||
const alreadyExcluded = new Set(existing.map(({ course_group_id }) => course_group_id))
|
||||
|
||||
return ExcludedCourseModel.bulkCreate(
|
||||
courseGroupIds
|
||||
.filter(courseGroupId => !alreadyExcluded.has(courseGroupId))
|
||||
.map(courseGroupId => ({
|
||||
programme_code: programmeCode,
|
||||
curriculum_version: curriculumVersion,
|
||||
course_group_id: courseGroupId,
|
||||
}))
|
||||
)
|
||||
}
|
||||
|
||||
export const removeExcludedCourses = async (
|
||||
courseCodes: string[],
|
||||
courseGroupIds: string[],
|
||||
curriculumVersion: string,
|
||||
programmeCode: string
|
||||
) => {
|
||||
@@ -21,7 +40,46 @@ export const removeExcludedCourses = async (
|
||||
where: {
|
||||
programme_code: programmeCode,
|
||||
curriculum_version: curriculumVersion,
|
||||
course_code: { [Op.in]: courseCodes },
|
||||
course_group_id: { [Op.in]: courseGroupIds },
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Fills course_group_id for rows that only have the legacy course_code. Safe to run repeatedly.
|
||||
* Rows whose code cannot be found in the course table are left untouched and reported.
|
||||
*/
|
||||
export const backfillExcludedCourseGroupIds = async () => {
|
||||
const legacyRows = await ExcludedCourseModel.findAll({
|
||||
attributes: ['course_code'],
|
||||
where: { course_group_id: null, course_code: { [Op.ne]: null } },
|
||||
group: ['course_code'],
|
||||
raw: true,
|
||||
})
|
||||
if (legacyRows.length === 0) return
|
||||
|
||||
const courseCodes = legacyRows.map(({ course_code }) => course_code!)
|
||||
const courses = await CourseModel.findAll({
|
||||
attributes: ['code', 'groupId'],
|
||||
where: { code: { [Op.in]: courseCodes } },
|
||||
raw: true,
|
||||
})
|
||||
const groupIdByCode = new Map(courses.map(({ code, groupId }) => [code, groupId]))
|
||||
|
||||
let updated = 0
|
||||
for (const courseCode of courseCodes) {
|
||||
const groupId = groupIdByCode.get(courseCode)
|
||||
if (!groupId) continue
|
||||
const [count] = await ExcludedCourseModel.update(
|
||||
{ course_group_id: groupId },
|
||||
{ where: { course_code: courseCode, course_group_id: null } }
|
||||
)
|
||||
updated += count
|
||||
}
|
||||
|
||||
const unmapped = courseCodes.filter(code => !groupIdByCode.has(code))
|
||||
logger.info(`Backfilled course_group_id for ${updated} excluded courses`)
|
||||
if (unmapped.length > 0) {
|
||||
logger.warn(`Could not find a course group id for excluded course codes: ${unmapped.join(', ')}`)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,7 +47,7 @@ export const getCurriculumOptions = async (code: string) => {
|
||||
}
|
||||
}
|
||||
|
||||
type ModuleWithChildren = Pick<ProgrammeModule, 'id' | 'type' | 'code' | 'name'> & {
|
||||
type ModuleWithChildren = Pick<ProgrammeModule, 'id' | 'group_id' | 'type' | 'code' | 'name'> & {
|
||||
// TODO: This is supposed to be in the ProgrammeModule
|
||||
degree_programme_type: DegreeProgrammeType | null
|
||||
|
||||
@@ -90,6 +90,7 @@ const recursivelyGetModuleAndChildren = async (code: string, curriculum_period_i
|
||||
GROUP BY pm.id, c.name, c.code, c.order, c.id
|
||||
) SELECT
|
||||
id,
|
||||
group_id,
|
||||
type,
|
||||
module_order,
|
||||
parent_code,
|
||||
@@ -141,7 +142,9 @@ const labelProgrammes = (modules: ModuleWithChildren[], excludedCourses: Exclude
|
||||
label: `${module.parent_code}\n${module.parent_name?.fi}`,
|
||||
orderNumber: module.module_order,
|
||||
}
|
||||
const foundCourse = excludedCourses.find(course => course.course_code === module.code)
|
||||
const foundCourse = excludedCourses.find(course =>
|
||||
course.course_group_id ? course.course_group_id === module.group_id : course.course_code === module.code
|
||||
)
|
||||
const visible = { visibility: !foundCourse, id: foundCourse?.id ?? null }
|
||||
|
||||
return { ...module, label, visible }
|
||||
@@ -180,6 +183,7 @@ const getCoursesAndModulesForProgramme = async (code: string, periodIds: string)
|
||||
|
||||
return {
|
||||
courses: labelProgrammes(modifiedCourses, excludedCourses).map(mod => ({
|
||||
group_id: mod.group_id,
|
||||
code: mod.code,
|
||||
name: mod.name,
|
||||
degree_programme_type: mod.degree_programme_type,
|
||||
|
||||
+4
-4
@@ -93,7 +93,7 @@ export const DegreeCourseTable = ({
|
||||
|
||||
const excludeOne = (course: ProgrammeCourse) => {
|
||||
void setExclusion({
|
||||
courseCodes: [course.code],
|
||||
courseGroupIds: [course.group_id],
|
||||
curriculumVersion: version,
|
||||
programmeCode: combinedProgramme === '' ? studyProgramme : combinedProgramme,
|
||||
})
|
||||
@@ -104,7 +104,7 @@ export const DegreeCourseTable = ({
|
||||
const module = getModule(moduleCode)
|
||||
const excludeFromProgramme = combinedProgramme === '' ? studyProgramme : combinedProgramme
|
||||
void setExclusion({
|
||||
courseCodes: module.courses.filter(course => course.visible.visibility).map(course => course.code),
|
||||
courseGroupIds: module.courses.filter(course => course.visible.visibility).map(course => course.group_id),
|
||||
curriculumVersion: version,
|
||||
programmeCode: excludeFromProgramme,
|
||||
})
|
||||
@@ -113,7 +113,7 @@ export const DegreeCourseTable = ({
|
||||
|
||||
const removeOne = (course: ProgrammeCourse) => {
|
||||
void removeExclusion({
|
||||
courseCodes: [course.code],
|
||||
courseGroupIds: [course.group_id],
|
||||
curriculumVersion: version,
|
||||
programmeCode: studyProgramme,
|
||||
})
|
||||
@@ -123,7 +123,7 @@ export const DegreeCourseTable = ({
|
||||
const deleteAll = (moduleCode: string) => {
|
||||
const module = getModule(moduleCode)
|
||||
void removeExclusion({
|
||||
courseCodes: module.courses.map(course => course.code),
|
||||
courseGroupIds: module.courses.map(course => course.group_id),
|
||||
curriculumVersion: version,
|
||||
programmeCode: studyProgramme,
|
||||
})
|
||||
|
||||
@@ -5,17 +5,17 @@ import type { ExcludedCoursesResBody, ExcludedCoursesReqBody } from '@oodikone/s
|
||||
const courseExclusionsApi = RTKApi.injectEndpoints({
|
||||
endpoints: builder => ({
|
||||
setCourseExclusion: builder.mutation<ExcludedCoursesResBody, ExcludedCoursesReqBody>({
|
||||
query: ({ courseCodes, curriculumVersion, programmeCode }) => ({
|
||||
query: ({ courseGroupIds, curriculumVersion, programmeCode }) => ({
|
||||
url: `/course-exclusions/`,
|
||||
method: 'POST',
|
||||
body: { courseCodes, curriculumVersion, programmeCode },
|
||||
body: { courseGroupIds, curriculumVersion, programmeCode },
|
||||
}),
|
||||
}),
|
||||
removeCourseExclusion: builder.mutation<ExcludedCoursesResBody, ExcludedCoursesReqBody>({
|
||||
query: ({ courseCodes, curriculumVersion, programmeCode }) => ({
|
||||
query: ({ courseGroupIds, curriculumVersion, programmeCode }) => ({
|
||||
url: `/course-exclusions/`,
|
||||
method: 'DELETE',
|
||||
body: { courseCodes, curriculumVersion, programmeCode },
|
||||
body: { courseGroupIds, curriculumVersion, programmeCode },
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
|
||||
@@ -5,7 +5,9 @@ export type ExcludedCourseCreation = Optional<ExcludedCourse, 'id' | 'createdAt'
|
||||
export type ExcludedCourse = {
|
||||
id: number
|
||||
programme_code: string
|
||||
course_code: string
|
||||
// TODO: Remove course_code once all rows have been migrated to course_group_id
|
||||
course_code: string | null
|
||||
course_group_id: string | null
|
||||
curriculum_version: string
|
||||
createdAt: Date
|
||||
updatedAt: Date
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export type ExcludedCoursesResBody = void
|
||||
export type ExcludedCoursesReqBody = {
|
||||
programmeCode: string
|
||||
courseCodes: string[]
|
||||
courseGroupIds: string[]
|
||||
curriculumVersion: string
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user