Skip to content

Commit d4d60f9

Browse files
committed
[Meta] Rework population stats
This will have failing lint. The commit is done on purpose.
1 parent 5018bc8 commit d4d60f9

File tree

18 files changed

+563
-195
lines changed

18 files changed

+563
-195
lines changed

services/backend/src/services/populations/formatStatisticsForApi.ts

Lines changed: 2 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { CreditModel } from '../../models'
66
import { hasTransferredFromOrToProgramme } from '../studyProgramme/studyProgrammeHelpers'
77
import type { StudentStudyPlan, StudentStudyRight } from './getStudentData'
88
import { getCurriculumVersion } from './shared'
9-
import type { AnonymousCredit, AnonymousEnrollment } from './statisticsOf'
9+
import type { AnonymousCredit } from './statisticsOf'
1010

1111
const yearMap: [string, keyof ProgressCriteria['courses']][] = [
1212
['year1', 'yearOne'],
@@ -151,35 +151,13 @@ export const formatStudentForAPI = (
151151
startDate: string,
152152
student: StudentData,
153153
tags: StudentTags[],
154-
credits: AnonymousCredit[],
155-
enrollments: AnonymousEnrollment[],
156154
optionData: Name | undefined,
157-
criteria: ProgressCriteria
158-
): FormattedStudent => {
155+
): Omit<FormattedStudent, 'course' | 'enrollment' | 'criteriaProgress'> => {
159156
const { studentnumber, studyRights, studyplans } = student
160157

161158
const hops = studyplans.find(plan => plan.programme_code === code)
162159
const [transferredStudyright, transferSource] = getTransferSource(code, studyRights)
163160

164-
const courses = credits.map(credit => {
165-
const attainmentDateNormalized = credit.attainment_date.toISOString()
166-
const passed =
167-
CreditModel.passed({ credittypecode: credit.credittypecode }) ||
168-
CreditModel.improved({ credittypecode: credit.credittypecode })
169-
170-
return {
171-
course_code: credit.course_code,
172-
date: attainmentDateNormalized,
173-
passed,
174-
grade: passed ? credit.grade : 'Hyl.',
175-
credits: credit.credits,
176-
isStudyModuleCredit: credit.isStudyModule,
177-
credittypecode: credit.credittypecode,
178-
language: credit.language,
179-
studyright_id: credit.studyright_id,
180-
}
181-
})
182-
183161
return {
184162
firstnames: student.firstnames,
185163
lastname: student.lastname,
@@ -199,15 +177,12 @@ export const formatStudentForAPI = (
199177
birthdate: student.birthdate,
200178
sis_person_id: student.sis_person_id,
201179
citizenships: student.citizenships,
202-
criteriaProgress: getProgressCriteria(startDate, criteria, credits, hops),
203180
curriculumVersion: getCurriculumVersion(hops?.curriculum_period_id),
204181

205182
tags,
206183
transferredStudyright,
207184
transferSource,
208185
studyRights,
209186
studyplans,
210-
courses,
211-
enrollments,
212187
}
213188
}

services/backend/src/services/populations/shared.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -244,8 +244,8 @@ export const parseCourseData = async (
244244
}
245245
}
246246

247-
const courses = await getCourses(Array.from(coursestats.keys()))
248-
const courseSubMap = new Map(courses.map(({ code, name, substitutions }) => [code, { name, substitutions }]))
247+
const courses = await getCourses()
248+
const courseSubMap = new Map(courses.map(({ code, ...rest }) => [code, rest]))
249249

250250
return Array.from(coursestats.entries()).map(([code, { attempts, enrollments, grades, students, stats }]) => {
251251
const courseMapObj = courseSubMap.get(code)

services/backend/src/services/populations/statisticsOf.ts

Lines changed: 8 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import {
1010
getCredits,
1111
getStudyRightElementsForStudyRight,
1212
} from './getStudentData'
13-
import { getOptionsForStudents, parseCourseData } from './shared'
13+
import { getCourses, getOptionsForStudents } from './shared'
1414

1515
export type OptimizedStatisticsQuery = {
1616
userId: string
@@ -45,43 +45,22 @@ export const statisticsOf = async (
4545
getStudyRightElementsForStudyRight(studentNumbers, code),
4646
])
4747

48-
const studentStartingYears = new Map(
49-
students.map(({ studentnumber, studyRights }) => [
50-
studentnumber,
51-
studyRights
52-
?.flatMap(({ studyRightElements }) => studyRightElements)
53-
.find(element => element.code === code)
54-
?.startDate.getFullYear() ?? +mockedStartDate,
55-
])
56-
)
57-
58-
const creditsAndEnrollmentsByStudent = new Map<string, [AnonymousCredit[], AnonymousEnrollment[]]>(
59-
studentNumbers.map(n => [n, [[], []]])
60-
)
61-
62-
for (const credit of credits) {
63-
const { student_studentnumber, ...rest } = credit
64-
creditsAndEnrollmentsByStudent.get(student_studentnumber)![0].push(rest)
65-
}
66-
67-
for (const enrollment of enrollments) {
68-
const { studentnumber, ...rest } = enrollment
69-
creditsAndEnrollmentsByStudent.get(studentnumber)![1].push(rest)
70-
}
71-
72-
const formattedCoursestats = await parseCourseData(studentStartingYears, enrollments, credits)
48+
const courseCodes = new Set<string>()
49+
for (const { course_code } of credits) courseCodes.add(course_code)
50+
for (const { course_code } of enrollments) courseCodes.add(course_code)
51+
const courses = await getCourses(Array.from(courseCodes))
7352

7453
const optionData = getOptionsForStudents(studyRightElementsForStudyRight, degreeProgrammeType)
7554
const formattedStudents = students.map(student => {
7655
const tags = tagMap.get(student.studentnumber) ?? []
7756
const options = optionData.get(student.studentnumber)
78-
const [credits, enrollments] = creditsAndEnrollmentsByStudent.get(student.studentnumber)!
7957

80-
return formatStudentForAPI(code, mockedStartDate, student, tags, credits, enrollments, options, criteria)
58+
return formatStudentForAPI(code, mockedStartDate, student, tags, options)
8159
})
8260

8361
return {
84-
coursestatistics: formattedCoursestats,
8562
students: formattedStudents,
63+
criteria,
64+
coursestatistics: { courses, enrollments, credits },
8665
}
8766
}

services/frontend/src/components/CoursePopulation/index.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -174,13 +174,13 @@ export const CoursePopulation = () => {
174174

175175
return (
176176
<FilterView
177-
courses={population?.coursestatistics ?? []}
177+
coursestatistics={population?.coursestatistics}
178178
displayTray={!isFetching}
179179
filters={[
180180
genderFilter(),
181181
studentNumberFilter(),
182182
ageFilter(),
183-
courseFilter({ courses: population?.coursestatistics }),
183+
courseFilter({ courses: population?.coursestatistics.courses }),
184184
creditsEarnedFilter(),
185185
startYearAtUniFilter(),
186186
programmeFilter({

services/frontend/src/components/CustomPopulation/CustomPopulationWrapper.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ export const CustomPopulationWrapper = ({
4646
const filtersList = [
4747
genderFilter(),
4848
ageFilter(),
49-
courseFilter({ courses: population?.coursestatistics ?? [] }),
49+
courseFilter({ courses: population?.coursestatistics.courses ?? [] }),
5050
creditsEarnedFilter(),
5151
transferredToProgrammeFilter(),
5252
startYearAtUniFilter(),
@@ -66,7 +66,7 @@ export const CustomPopulationWrapper = ({
6666

6767
return (
6868
<FilterView
69-
courses={population?.coursestatistics ?? []}
69+
coursestatistics={population?.coursestatistics}
7070
displayTray={populationStudents.length > 0}
7171
filters={filters}
7272
initialOptions={[]}

services/frontend/src/components/FilterView/filters/courses/CourseCard.tsx

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ export const CourseCard = ({
4747
key,
4848
text: label,
4949
value: type,
50-
disabled: !Object.keys(course?.students[key] ?? {}).length,
50+
disabled: !course?.students[key]?.size,
5151
}))
5252

5353
return (
@@ -63,19 +63,19 @@ export const CourseCard = ({
6363
>
6464
<Stack direction="row" sx={{ justifyContent: 'space-between' }}>
6565
<Box sx={{ mb: 2 }}>
66-
<Typography>{getTextIn(course.course?.name)}</Typography>
67-
{course.course?.substitutions?.length ? (
68-
<Tooltip title={getSubstitutionTooltip(course.course.substitutions)}>
66+
<Typography>{getTextIn(course?.name)}</Typography>
67+
{course?.substitutions?.length ? (
68+
<Tooltip title={getSubstitutionTooltip(course.substitutions)}>
6969
<Typography sx={{ color: 'text.secondary' }}>
70-
{course.course?.code}... +{course.course?.substitutions?.length}
70+
{course?.code}... +{course?.substitutions?.length}
7171
</Typography>
7272
</Tooltip>
7373
) : (
74-
<Typography sx={{ color: 'text.secondary' }}>{course.course?.code}</Typography>
74+
<Typography sx={{ color: 'text.secondary' }}>{course?.code}</Typography>
7575
)}
7676
</Box>
7777
<ClearIcon
78-
data-cy={`courseFilter-${course.course?.code}-clear`}
78+
data-cy={`courseFilter-${course?.code}-clear`}
7979
onClick={() => onChange(null)}
8080
sx={{
8181
color: theme => theme.palette.error.dark,
@@ -86,7 +86,7 @@ export const CourseCard = ({
8686
/>
8787
</Stack>
8888
<FilterSelect
89-
filterKey={`courseFilter-${course.course?.code}`}
89+
filterKey={`courseFilter-${course?.code}`}
9090
label="Select course"
9191
onChange={({ target }) => onChange(target.value)}
9292
options={dropdownOptions}
Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
export const FilterType = {
2-
ALL: 'ALL',
3-
PASSED: 'PASSED',
4-
FAILED: 'FAILED',
5-
ENROLLED_NO_GRADE: 'ENROLLED_NO_GRADE',
6-
} as const
1+
export enum FilterType {
2+
ALL,
3+
PASSED,
4+
FAILED,
5+
ENROLLED_NO_GRADE,
6+
}

services/frontend/src/components/FilterView/filters/courses/index.tsx

Lines changed: 25 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
import { keyBy } from 'lodash'
2-
31
import { useLanguage } from '@/components/LanguagePicker/useLanguage'
42
import type { FilterTrayProps } from '../../FilterTray'
53
import { FilterSearchableSelect } from '../common/FilterSearchableSelect'
@@ -16,12 +14,12 @@ const CourseFilterCard = ({ precomputed, options, onOptionsChange }: FilterTrayP
1614
const { getTextIn } = useLanguage()
1715

1816
const dropdownOptions = Object.values(courseStats)
19-
.filter(cs => !courseFilters[cs.course.code])
20-
.sort((a, b) => a.course.code.localeCompare(b.course.code))
17+
.filter(cs => !courseFilters[cs.code])
18+
.sort((a, b) => a.code.localeCompare(b.code))
2119
.map(cs => ({
22-
key: `courseFilter-option-${cs.course.code}`,
23-
text: `${cs.course.code} - ${getTextIn(cs.course.name)}`,
24-
value: cs.course.code,
20+
key: `courseFilter-option-${cs.code}`,
21+
text: `${cs.code} - ${getTextIn(cs.name)}`,
22+
value: cs.code,
2523
}))
2624

2725
const setCourseFilter = (code, type) => {
@@ -64,7 +62,7 @@ export const courseFilter = createFilter({
6462

6563
precompute: ({ args }) => {
6664
const substitutedBy = args.courses.reduce(
67-
(acc, { course }) => {
65+
(acc, course) => {
6866
const { code, substitutions } = course
6967
for (const original of substitutions) {
7068
acc[original] ??= []
@@ -77,27 +75,34 @@ export const courseFilter = createFilter({
7775
)
7876

7977
return {
80-
courses: keyBy(args.courses, 'course.code'),
78+
courses: Object.fromEntries(args.courses.map(course => [course.code, course])),
8179
substitutedBy,
8280
}
8381
},
8482

8583
isActive: ({ courseFilters }) => Object.keys(courseFilters).length > 0,
8684

87-
filter({ studentNumber }, { precomputed, options }) {
88-
const filterKeys = {
89-
[FilterType.ALL]: 'all',
90-
[FilterType.PASSED]: 'passed',
91-
[FilterType.FAILED]: 'failed',
92-
[FilterType.ENROLLED_NO_GRADE]: 'enrolledNoGrade',
93-
}
85+
filter(student, { precomputed, options }) {
86+
const { courses, enrollments } = student
9487

9588
for (const [code, filterType] of Object.entries(options.courseFilters)) {
9689
const found = [code, ...(precomputed.substitutedBy[code] ?? [])].some(course => {
97-
const students = precomputed.courses?.[course]?.students ?? {}
98-
const key = filterKeys[filterType as string]
99-
100-
return students?.[key]?.includes(studentNumber)
90+
const enrolled = enrollments.some(({ course_code }) => course_code === course)
91+
const attainment = courses.some(({ course_code }) => course_code === code)
92+
const passed = courses.some(({ course_code, passed }) => course_code === code && passed)
93+
94+
switch (filterType) {
95+
case FilterType.ALL:
96+
return enrolled || attainment
97+
case FilterType.PASSED:
98+
return passed
99+
case FilterType.FAILED:
100+
return attainment && !passed
101+
case FilterType.ENROLLED_NO_GRADE:
102+
return enrolled && !attainment
103+
default:
104+
return false
105+
}
101106
})
102107

103108
if (!found) return false

services/frontend/src/components/FilterView/index.tsx

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,9 @@ import Stack from '@mui/material/Stack'
22
import { FC, useMemo } from 'react'
33

44
import { selectViewFilters } from '@/redux/filters'
5+
import type { ExpandedCourseStats } from '@/redux/populations/util'
56
import { useAppSelector } from '@/redux/hooks'
6-
import { filterCourses } from '@/util/coursesOfPopulation'
7-
import type { CourseStats } from '@oodikone/shared/routes/populations'
8-
import type { FormattedCourse as Course } from '@oodikone/shared/types/courseData'
7+
import { filterCourses, type FilteredCourse } from '@/util/coursesOfPopulation'
98
import type { FormattedStudent as Student } from '@oodikone/shared/types/studentData'
109

1110
import { PageLayout } from '../common/PageLayout'
@@ -16,14 +15,14 @@ import type { Filter } from './filters/createFilter'
1615
import { FilterTray } from './FilterTray'
1716

1817
export const FilterView: FC<{
19-
children: (filteredStudents: Student[], filteredCourses: Course[]) => React.ReactNode
18+
children: (filteredStudents: Student[], filteredCourses: FilteredCourse[]) => React.ReactNode
2019
name: string
2120
filters: Filter[]
2221
students: Student[]
23-
courses: CourseStats[]
22+
coursestatistics: ExpandedCourseStats | undefined
2423
displayTray: boolean
2524
initialOptions: Record<Filter['key'], any>
26-
}> = ({ children, name, filters, students, courses, displayTray, initialOptions }) => {
25+
}> = ({ children, name, filters, students, coursestatistics, displayTray, initialOptions }) => {
2726
const storedOptions = useAppSelector(state => selectViewFilters(state, name))
2827

2928
const filterArgs = Object.fromEntries(filters.map(({ key, args }) => [key, args]))
@@ -59,15 +58,16 @@ export const FilterView: FC<{
5958
})
6059

6160
const filteredStudents = useMemo(
62-
() =>
63-
filters
64-
.filter(({ key, isActive }) => isActive(filterOptions[key]))
65-
.reduce((students, { key, filter }) => {
66-
return students.filter(student => filter(structuredClone(student), getFilterContext(key)))
67-
}, students),
61+
() => filters
62+
.filter(({ key, isActive }) => isActive(filterOptions[key]))
63+
.reduce((students, { key, filter }) => {
64+
return students.filter(student => filter(structuredClone(student), getFilterContext(key)))
65+
}, students),
6866
[filters, filterOptions]
6967
)
70-
const filteredCourses = filterCourses(courses, filteredStudents.length)
68+
69+
const filteredCourses = useMemo(() => filterCourses(coursestatistics, filteredStudents), [filters, filterOptions])
70+
7171

7272
const ctxState: FilterViewContextState = { viewName: name, getContextByKey: getFilterContext }
7373

0 commit comments

Comments
 (0)