mirror of
https://github.com/UniversityOfHelsinkiCS/oodikone.git
synced 2026-10-12 19:03:39 +03:00
[Studyplans] embrace course unit id
This commit is contained in:
committed by
Aaro Koivukangas
parent
756338d15f
commit
7af9d19f04
@@ -173,9 +173,26 @@ const getStudents = async (studentNumbers: string[]): Promise<StudentWithCourses
|
||||
],
|
||||
})
|
||||
|
||||
return students.map(student => {
|
||||
const { studyplans, ...rest }: StudentWithStudyplanNested = student.toJSON()
|
||||
const coursesInStudyPlan = studyplans.flatMap(studyplan => studyplan.included_courses)
|
||||
const plainStudents = students.map((student): StudentWithStudyplanNested => student.toJSON())
|
||||
|
||||
// included_courses holds course ids, with a rare fallback to a raw code for "custom" (non-catalogued)
|
||||
// entries. Resolve ids to their current code for display - anything missing from the map is already a code.
|
||||
const includedCourseIds = [
|
||||
...new Set(
|
||||
plainStudents.flatMap(student => student.studyplans.flatMap(({ included_courses }) => included_courses))
|
||||
),
|
||||
]
|
||||
const courses: Pick<CourseModel, 'id' | 'code'>[] = await CourseModel.findAll({
|
||||
attributes: ['id', 'code'],
|
||||
where: { id: { [Op.in]: includedCourseIds } },
|
||||
raw: true,
|
||||
})
|
||||
const idToCode = Object.fromEntries(courses.map(({ id, code }) => [id, code]))
|
||||
|
||||
return plainStudents.map(({ studyplans, ...rest }) => {
|
||||
const coursesInStudyPlan = studyplans.flatMap(studyplan =>
|
||||
studyplan.included_courses.map(idOrCode => idToCode[idOrCode] ?? idOrCode)
|
||||
)
|
||||
return { ...rest, coursesInStudyPlan }
|
||||
})
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@ const findThesisAndLatestAndEarliestAttainments = (
|
||||
thesisData = attainment
|
||||
}
|
||||
attainmentDates.latestTotal ??= attainment.attainment_date
|
||||
if (studyPlan.included_courses.includes(attainment.course?.code)) {
|
||||
if (studyPlan.included_courses.includes(attainment.course_id)) {
|
||||
attainmentDates.latestHops ??= attainment.attainment_date
|
||||
if (
|
||||
!attainmentDates.earliestHops ||
|
||||
@@ -230,7 +230,7 @@ export const findStudentsCloseToGraduation = async (studentNumbers?: string[]) =
|
||||
},
|
||||
{
|
||||
model: CreditModel,
|
||||
attributes: ['attainment_date', 'grade', 'studyright_id'],
|
||||
attributes: ['attainment_date', 'grade', 'studyright_id', 'course_id'],
|
||||
where: { credittypecode: CreditTypeCode.PASSED },
|
||||
include: [
|
||||
{
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Op } from 'sequelize'
|
||||
|
||||
import type { Credit, Student } from '@oodikone/shared/models'
|
||||
import type { Credit, Student, Studyplan } from '@oodikone/shared/models'
|
||||
import { CreditTypeCode, EnrollmentState, Unification } from '@oodikone/shared/types'
|
||||
import { FormattedStudentForSearch, StudentPageStudent } from '@oodikone/shared/types/studentData'
|
||||
import { enrollmentTimeDateThresholdAcademicYear, splitByEmptySpace } from '@oodikone/shared/util'
|
||||
@@ -213,7 +213,7 @@ const formatSharedStudentData = ({
|
||||
updatedAt,
|
||||
createdAt,
|
||||
sis_person_id,
|
||||
}: Student): Omit<StudentPageStudent, 'tags'> => {
|
||||
}: Student): Omit<StudentPageStudent, 'tags' | 'idToGroupIdMap'> => {
|
||||
const toCourse = ({ grade, credits, credittypecode, is_open, attainment_date, course, isStudyModule }: Credit) => {
|
||||
return {
|
||||
course: {
|
||||
@@ -251,13 +251,25 @@ const formatSharedStudentData = ({
|
||||
}
|
||||
}
|
||||
|
||||
const getIdToGroupIdMap = async (studyplans: Pick<Studyplan, 'included_courses' | 'includedModules'>[]) => {
|
||||
const ids = [...new Set(studyplans.flatMap(plan => [...plan.included_courses, ...plan.includedModules]))]
|
||||
const courses = await CourseModel.findAll({
|
||||
attributes: ['id', 'groupId'],
|
||||
where: { id: { [Op.in]: ids } },
|
||||
raw: true,
|
||||
})
|
||||
return Object.fromEntries(courses.map(({ id, groupId }) => [id, groupId]))
|
||||
}
|
||||
|
||||
export const withStudentNumber = async (studentNumber: string, userId: string): Promise<StudentPageStudent | null> => {
|
||||
const student = await byStudentNumber(studentNumber)
|
||||
if (student == null) {
|
||||
return null
|
||||
}
|
||||
const idToGroupIdMap = await getIdToGroupIdMap(student.studyplans)
|
||||
return {
|
||||
...formatSharedStudentData(student),
|
||||
idToGroupIdMap,
|
||||
tags: student.tags
|
||||
.filter(({ tag }) => !tag.personal_user_id || tag.personal_user_id === userId)
|
||||
.map(tag => ({
|
||||
|
||||
@@ -43,7 +43,7 @@ export const getStudyRightsInProgramme = async (
|
||||
include: [
|
||||
{
|
||||
model: CreditModel,
|
||||
attributes: ['attainment_date', 'credits', 'course_code'],
|
||||
attributes: ['attainment_date', 'credits', 'course_code', 'course_id'],
|
||||
required: false,
|
||||
where: {
|
||||
isStudyModule: false,
|
||||
|
||||
@@ -247,7 +247,7 @@ const getMainStatsByTrackAndYear = async (
|
||||
|
||||
const latestAttainmentWithinYearAndInHops = dayjs(
|
||||
studyRight.student.credits
|
||||
.filter(credit => studyRight.studyPlans.some(sp => sp.included_courses.includes(credit.course_code)))
|
||||
.filter(credit => studyRight.studyPlans.some(sp => sp.included_courses.includes(credit.course_id)))
|
||||
.reduce((latest, { attainment_date }) => {
|
||||
return attainment_date > latest ? attainment_date : latest
|
||||
}, new Date('1900-01-01'))
|
||||
|
||||
+2
-12
@@ -57,7 +57,6 @@ const studentMapper = (
|
||||
curriculumGroupIds: string[],
|
||||
substitutionsForGroupId: Record<string, string[][]>,
|
||||
idToGroupIdMap: Record<string, string>,
|
||||
codeToGroupId: Record<string, string>,
|
||||
groupIdToCode: Record<string, string>
|
||||
) => {
|
||||
const courseMap = {}
|
||||
@@ -72,7 +71,7 @@ const studentMapper = (
|
||||
)
|
||||
const hopsGroupIds = student.studyplans.flatMap(studyPlan =>
|
||||
studyPlan.included_courses
|
||||
.map(code => codeToGroupId[code] ?? code)
|
||||
.map(id => idToGroupIdMap[id] ?? id)
|
||||
.filter(groupId => curriculumGroupIds.includes(groupId))
|
||||
)
|
||||
|
||||
@@ -346,19 +345,10 @@ export const CoursesTabContainer = ({ curriculum, students, courses, idToGroupId
|
||||
curriculumGroupIds,
|
||||
substitutionsForGroupId,
|
||||
idToGroupIdMap,
|
||||
codeToGroupId,
|
||||
groupIdToCode
|
||||
)
|
||||
),
|
||||
[
|
||||
students,
|
||||
includeSubstitutions,
|
||||
curriculumGroupIds,
|
||||
substitutionsForGroupId,
|
||||
idToGroupIdMap,
|
||||
codeToGroupId,
|
||||
groupIdToCode,
|
||||
]
|
||||
[students, includeSubstitutions, curriculumGroupIds, substitutionsForGroupId, idToGroupIdMap, groupIdToCode]
|
||||
)
|
||||
|
||||
const columns = useGetColumnDefinitions(coursesByParentModule)
|
||||
|
||||
+28
-7
@@ -2,6 +2,7 @@ import { useMemo } from 'react'
|
||||
|
||||
import { ModulesTab } from '@/components/PopulationComponents/Students/Table/ModulesTab/ModulesTable'
|
||||
import { ExtendedCurriculumDetails } from '@/hooks/useCurriculums'
|
||||
import { FilteredCourse } from '@/util/coursesOfPopulation'
|
||||
import { createLocaleComparator } from '@/util/comparator'
|
||||
import { formatISODate } from '@/util/timeAndDate'
|
||||
import { Name, ProgrammeCourse, FormattedStudent as Student } from '@oodikone/shared/types'
|
||||
@@ -18,7 +19,12 @@ export type FormattedModules = Record<string, Name>
|
||||
|
||||
type StudyModuleData = { code: string; completed: boolean; completionDate: string | null }
|
||||
|
||||
const getModulesFromRelevantStudyPlan = (student: Student, degreeProgrammeCodes: string[]): StudyModuleData[] => {
|
||||
const getModulesFromRelevantStudyPlan = (
|
||||
student: Student,
|
||||
degreeProgrammeCodes: string[],
|
||||
idToGroupIdMap: Record<string, string>,
|
||||
groupIdToCode: Record<string, string>
|
||||
): StudyModuleData[] => {
|
||||
if (!student.studyRights || !Array.isArray(student.studyRights)) {
|
||||
return []
|
||||
}
|
||||
@@ -35,9 +41,10 @@ const getModulesFromRelevantStudyPlan = (student: Student, degreeProgrammeCodes:
|
||||
)?.includedModules ?? []
|
||||
|
||||
return studyModules.map(studyModule => {
|
||||
const completion = student.courses.find(course => course.course_code === studyModule && course.passed)
|
||||
const moduleGroupId = idToGroupIdMap[studyModule] ?? studyModule
|
||||
const completion = student.courses.find(course => course.course_id === moduleGroupId && course.passed)
|
||||
return {
|
||||
code: studyModule,
|
||||
code: groupIdToCode[moduleGroupId] ?? studyModule,
|
||||
completed: !!completion,
|
||||
completionDate: completion ? formatISODate(completion.date) : null,
|
||||
}
|
||||
@@ -47,12 +54,17 @@ const getModulesFromRelevantStudyPlan = (student: Student, degreeProgrammeCodes:
|
||||
return []
|
||||
}
|
||||
|
||||
const formatStudent = (student: Student, degreeProgrammeCodes: string[]): ModuleTabStudent => {
|
||||
const formatStudent = (
|
||||
student: Student,
|
||||
degreeProgrammeCodes: string[],
|
||||
idToGroupIdMap: Record<string, string>,
|
||||
groupIdToCode: Record<string, string>
|
||||
): ModuleTabStudent => {
|
||||
return {
|
||||
firstNames: student.firstnames,
|
||||
lastName: student.lastname,
|
||||
studentNumber: student.studentNumber,
|
||||
studyModulesInHOPS: getModulesFromRelevantStudyPlan(student, degreeProgrammeCodes),
|
||||
studyModulesInHOPS: getModulesFromRelevantStudyPlan(student, degreeProgrammeCodes, idToGroupIdMap, groupIdToCode),
|
||||
sisPersonID: student.sis_person_id,
|
||||
}
|
||||
}
|
||||
@@ -76,9 +88,13 @@ const getDegreeProgrammeCodes = (curriculumModules): string[] => {
|
||||
export const ModulesTabContainer = ({
|
||||
curriculum,
|
||||
students,
|
||||
courses,
|
||||
idToGroupIdMap,
|
||||
}: {
|
||||
curriculum: ExtendedCurriculumDetails | null | undefined
|
||||
students: Student[]
|
||||
courses: FilteredCourse[]
|
||||
idToGroupIdMap: Record<string, string>
|
||||
}) => {
|
||||
const curriculumModules = useMemo(
|
||||
() => (curriculum ? [...curriculum.defaultProgrammeModules, ...curriculum.secondProgrammeModules] : []),
|
||||
@@ -93,9 +109,14 @@ export const ModulesTabContainer = ({
|
||||
|
||||
const formattedModules = useMemo(() => getAllModules(curriculumCourses), [curriculumCourses])
|
||||
|
||||
const groupIdToCode: Record<string, string> = useMemo(
|
||||
() => Object.fromEntries(courses.map(({ course }) => [course.groupId, course.code])),
|
||||
[courses]
|
||||
)
|
||||
|
||||
const formattedStudents = useMemo(
|
||||
() => students.map(student => formatStudent(student, degreeProgrammeCodes)),
|
||||
[students, degreeProgrammeCodes]
|
||||
() => students.map(student => formatStudent(student, degreeProgrammeCodes, idToGroupIdMap, groupIdToCode)),
|
||||
[students, degreeProgrammeCodes, idToGroupIdMap, groupIdToCode]
|
||||
)
|
||||
|
||||
return <ModulesTab formattedModules={formattedModules} formattedStudents={formattedStudents} />
|
||||
|
||||
@@ -106,7 +106,14 @@ export const PopulationStudents = ({
|
||||
students={filteredStudents}
|
||||
/>
|
||||
),
|
||||
Modules: <ModulesTab curriculum={curriculum} students={filteredStudents} />,
|
||||
Modules: (
|
||||
<ModulesTab
|
||||
courses={filteredCourses ?? []}
|
||||
curriculum={curriculum}
|
||||
idToGroupIdMap={idToGroupIdMap ?? {}}
|
||||
students={filteredStudents}
|
||||
/>
|
||||
),
|
||||
Tags: <TagsTab combinedProgramme={combinedProgramme} programme={programme} students={filteredStudents} />,
|
||||
Progress: (
|
||||
<ProgressTab
|
||||
|
||||
@@ -19,13 +19,14 @@ import { StudentStudyPlan } from '@oodikone/shared/types/studentData'
|
||||
|
||||
const creditsColumnIndex = 3
|
||||
|
||||
// Some courses are without AY in the beginning in the studyplan even though the credits are registered with AY.
|
||||
const isInStudyPlan = (plan: StudentStudyPlan, code: string) =>
|
||||
const isInStudyPlan = (
|
||||
plan: StudentStudyPlan,
|
||||
idToGroupIdMap: Record<string, string>,
|
||||
course: { code: string; groupId: string }
|
||||
) =>
|
||||
plan &&
|
||||
(plan.included_courses.includes(code) ||
|
||||
plan.included_courses.includes(code.replace('AY', '')) ||
|
||||
plan.includedModules.includes(code) ||
|
||||
code === plan.programme_code)
|
||||
([...plan.included_courses, ...plan.includedModules].some(id => (idToGroupIdMap[id] ?? id) === course.groupId) ||
|
||||
course.code === plan.programme_code)
|
||||
|
||||
const getAcademicYear = (date: string) => {
|
||||
const year = new Date(date).getFullYear()
|
||||
@@ -52,7 +53,7 @@ export const CourseTables = ({ student, selectedStudyPlanId }) => {
|
||||
const attainment = student.courses[i]
|
||||
|
||||
const { course, credits, credittypecode, date, grade, isOpenCourse, isStudyModuleCredit, passed } = attainment
|
||||
const isIncluded = isInStudyPlan(studyPlan, course.code)
|
||||
const isIncluded = isInStudyPlan(studyPlan, student.idToGroupIdMap, course)
|
||||
const academicYear = getAcademicYear(date)
|
||||
|
||||
groupedCourses[academicYear] ??= []
|
||||
|
||||
@@ -100,6 +100,8 @@ export type StudentPageStudent = {
|
||||
updatedAt: Date
|
||||
studyplans: Studyplan[]
|
||||
sisPersonId: string
|
||||
// Resolves the ids in each studyplan's included_courses/includedModules to their groupId.
|
||||
idToGroupIdMap: Record<string, string>
|
||||
}
|
||||
|
||||
export type StudentCourse = {
|
||||
|
||||
Reference in New Issue
Block a user