Compare commits

...
52 Commits
Author SHA1 Message Date
outisa 4b86ed252f Add bachelor column to excel 2023-03-23 15:03:04 +02:00
outisa f535a5dbdb Fix admission filter to work with 'Ei valintatapaa' 2023-03-23 14:34:22 +02:00
outisa ffe6425999 Fix excel to show credits correctly in since studystart column 2023-03-23 14:17:11 +02:00
outisa 256997633a Fix test 2023-03-22 18:06:28 +02:00
outisa 1d3fef6c8d Reduce red from console 2023-03-22 17:45:30 +02:00
outisa 428e46084c [Study programme-Programme courses] Add new stats columns 2023-03-22 17:27:33 +02:00
outisa 09f4814f80 Remove red from the dev console 2023-03-22 14:22:56 +02:00
outisa a220050a53 Possible fix to Sentry error 2023-03-22 13:58:47 +02:00
outisa 115674636f [class stats-progress table] Add more columns csv 2023-03-20 18:52:20 +02:00
outisa f901fec6a3 Infotext betterment 2023-03-20 10:18:47 +02:00
Laura Sinikallio a77ba0e1e1 Edit info text 2023-03-19 20:40:31 +02:00
Laura Sinikallio 434183bac0 Merge branch 'master' of github.com:UniversityOfHelsinkiCS/oodikone 2023-03-19 20:33:38 +02:00
Laura Sinikallio 7993c2b45a Edit info text 2023-03-19 20:33:03 +02:00
Laura Sinikallio 8551a76fc9 Add info to evaluation programme view 2023-03-19 20:29:58 +02:00
Laura Sinikallio b25f9ba55f Add info to evaluation faculty view 2023-03-19 20:12:08 +02:00
outisa f28276e086 Clean warnings from the console 2023-03-19 20:11:45 +02:00
outisa 6327ca7e2c [Class stats - Progress table] Add infotexts 2023-03-19 20:01:49 +02:00
Laura Sinikallio 625f2962eb Clean warnings 2023-03-19 19:44:37 +02:00
Laura Sinikallio f85e9f0a5d Fetch and display programme name 2023-03-19 19:29:35 +02:00
Laura Sinikallio d4d67b239e Move toggle 2023-03-19 19:03:35 +02:00
Laura Sinikallio 83d17a3026 Make custom endpoint for tilannekuvalomake programme view 2023-03-19 19:02:28 +02:00
Laura Sinikallio e3f8fdb4ae Add redirection rules 2023-03-17 18:59:19 +02:00
Laura Sinikallio b7051ccd41 Add header placeholder 2023-03-17 18:56:28 +02:00
Laura Sinikallio b0a0be7969 Add faculty view for tilannekuvalomake 2023-03-17 18:51:37 +02:00
Laura Sinikallio 896cb46ad1 Add redirection for malformed url 2023-03-17 17:11:56 +02:00
Laura Sinikallio 17f7289b1c Create new view for Tilannekuvalomake 2023-03-17 16:53:41 +02:00
outisa a019dd9672 Fix minor bug 2023-03-16 21:13:10 +02:00
outisa 881aaf93b4 [Class stats] Progress table betterments 2023-03-16 17:06:28 +02:00
outisa 5c973d4f8c Change icon 2023-03-16 16:57:42 +02:00
outisa f9e8bf6d31 Add consistency to icons in different views 2023-03-16 10:49:42 +02:00
outisa a2726d4894 [Class stats - progress table] Show checked credits criteria correctly 2023-03-15 18:43:05 +02:00
outisa 11377da7bd [Class stats - progress table] Make csv better 2023-03-15 18:38:18 +02:00
outisa 8c4515b158 [Class stats-Progress table] Add hidden fields csv 2023-03-15 18:13:46 +02:00
outisa db3cd52d69 Refactor fetching courses 2023-03-12 20:08:13 +02:00
outisa b4c8bf0f4a Fix checking the passed courses for criteria 2023-03-09 20:51:29 +02:00
outisa 384afb1e5f Fix keeping track from enrollmentNoGrade 2023-03-09 20:35:13 +02:00
outisa 090d327405 Cypress, please make my day 2023-03-09 19:10:04 +02:00
outisa 30117e60b3 Fix message text 2023-03-09 16:44:23 +02:00
outisa 9a4fec0974 [Faculty basic] Remove any effects from special excluded toggle for credits 2023-03-09 16:41:43 +02:00
outisa 37114c5570 Add betterments for progress table 2023-02-24 17:06:29 +02:00
outisa 63b7c5876d Fix full screen view, if nav bar visible 2023-02-24 14:37:03 +02:00
outisa 9790c829c6 [Tags] Change button color from positive to green 2023-02-23 17:37:52 +02:00
outisa 082aaa4022 [Courses filter] Remove Failed manyt times and passed after retry options 2023-02-23 17:14:59 +02:00
outisa 3906efbcca Add colors for buttons 2023-02-23 16:51:00 +02:00
outisa 9d575d6c88 [Degree courses] Make button betterments 2023-02-23 16:39:52 +02:00
outisa 4d4a288719 Reduce red from the console 2023-02-23 16:21:08 +02:00
outisa a39f522501 [Class statistics] Add first draft for student progress table 2023-02-23 16:19:46 +02:00
outisa 32293eb050 [Courses filter] Add enrolled, no grade option 2023-02-17 15:29:30 +02:00
outisa ed56d32cce Remove unused code 2023-02-17 15:22:38 +02:00
Miko Keskimäki f4b04d3283 Don't reset studyGuidanceGroups roles each time users table is rendered 2023-02-17 14:54:30 +02:00
outisa 3a6e5132f3 Fix filter backend code not to crash studyguidance groups 2023-02-14 19:00:16 +02:00
outisa 694e537baa Fix parts of course filter 2023-02-14 18:49:37 +02:00
52 changed files with 1900 additions and 400 deletions
+3 -2
View File
@@ -1,3 +1,4 @@
/* eslint-disable cypress/no-unnecessary-waiting */
/// <reference types="Cypress" />
// const baseUrl = Cypress.config().baseUrl
@@ -49,8 +50,6 @@ const createRunTestStepWithPreAndPostPartsFunction = amountWithoutFiltering => {
const card = cy.cs(`${filterName}-filter-card`)
card.invoke('attr', 'data-open').then(open => {
console.log('open', open)
const getHeader = () => cy.cs(`${filterName}-header`)
if (open === 'false') {
@@ -325,6 +324,7 @@ describe('Custom Population Statistics', () => {
runTestStepWithPreAndPostParts('Age', () => {
testRangeFilter('Age-filter-card', 20, 30, 1)
})
cy.cs('reset-all-filters').click()
})
it('Programme defaults to "Active Study Right" mode', () => {
@@ -340,6 +340,7 @@ describe('Custom Population Statistics', () => {
checkFilteringResult(5)
programmeDropdown.get('i.delete').click()
})
cy.cs('reset-all-filters').click()
})
it('Gender filter works', () => {
+1 -1
View File
@@ -369,7 +369,7 @@ describe('Studyprogramme overview', () => {
cy.get('[data-cy=creditsStudentsToggle]').first().click()
cy.get('[data-cy=CoursesSortableTable]').within(() => {
cy.get('tr').eq(1).contains('267')
cy.get('tr').eq(2).contains('267')
})
})
})
+2 -2
View File
@@ -144,7 +144,7 @@ const OpenUniPopulationSearch = sequelizeKone.define(
{ underscored: true, timestamps: true }
)
const StudyProgrammeCriteria = sequelizeKone.define(
const ProgressCriteria = sequelizeKone.define(
'progress_criteria',
{
code: {
@@ -218,5 +218,5 @@ module.exports = {
sequelizeKone,
ExcludedCourse,
StudyGuidanceGroupTag,
StudyProgrammeCriteria,
ProgressCriteria,
}
+3 -4
View File
@@ -84,11 +84,10 @@ router.get('/:id/basicstats', async (req, res) => {
router.get('/:id/creditstats', async (req, res) => {
const code = req.params.id
const yearType = req.query?.year_type
const specialGroups = req.query?.special_groups
const programmeFilter = req.query?.programme_filter
if (!code) return res.status(422).end()
const data = await getCreditStats(code, yearType, programmeFilter, specialGroups)
const data = await getCreditStats(code, yearType, programmeFilter)
if (data) return res.json(data)
const programmes = await getProgrammes(code, programmeFilter)
@@ -97,9 +96,9 @@ router.get('/:id/creditstats', async (req, res) => {
// list of all programmes is also needed in classification of credits
const allProgrammes = programmeFilter === 'ALL_PROGRAMMES' ? programmes : await getProgrammes(code, 'ALL_PROGRAMMES')
let updatedStats = await combineFacultyCredits(code, programmes.data, allProgrammes.data, yearType, specialGroups)
let updatedStats = await combineFacultyCredits(code, programmes.data, allProgrammes.data, yearType)
if (updatedStats) {
updatedStats = await setCreditStats(updatedStats, yearType, programmeFilter, specialGroups)
updatedStats = await setCreditStats(updatedStats, yearType, programmeFilter)
}
return res.json(updatedStats)
-14
View File
@@ -82,20 +82,6 @@ router.post('/v2/populationstatistics/courses', async (req, res) => {
return
}
//These cause problems in front (where they were originally calculated)
// Do them here for the moment
for (const course of result.coursestatistics) {
// course.students.notParticipated = {}
course.students.didNotPass = {}
for (const student of Object.keys(course.students.all)) {
if (
!Object.keys(course.students.passed).includes(student) &&
!Object.keys(course.students.retryPassed).includes(student)
)
course.students.didNotPass[student] = true
}
}
res.json(result)
}
})
@@ -15,6 +15,7 @@ const {
setStudytrackStats,
} = require('../services/analyticsService')
const { updateBasicView, updateStudytrackView } = require('../services/studyprogrammeUpdates')
const { getProgrammeName } = require('../services/studyprogramme')
const logger = require('../util/logger')
router.get('/v2/studyprogrammes/:id/basicstats', async (req, res) => {
@@ -143,4 +144,73 @@ router.get('/v2/studyprogrammes/:id/update_studytrackview', async (req, res) =>
}
})
router.get('/v2/studyprogrammes/:id/evaluationstats', async (req, res) => {
const code = req.params.id
const yearType = req.query?.year_type
const specialGroups = req.query?.special_groups
const graduated = req.query?.graduated
if (!code) return res.status(422).end()
// Statistics for Tilannekuvalomake view
let gradData = await getGraduationStats(code, yearType, specialGroups)
if (!gradData) {
const updatedStats = await getGraduationStatsForStudytrack({
studyprogramme: req.params.id,
settings: {
isAcademicYear: yearType === 'ACADEMIC_YEAR',
includeAllSpecials: specialGroups === 'SPECIAL_INCLUDED',
},
})
if (updatedStats) {
await setGraduationStats(updatedStats, yearType, specialGroups)
gradData = updatedStats
}
}
let progressData = await getStudytrackStats(code, graduated, specialGroups)
if (!progressData) {
const updated = await getStudytrackStatsForStudyprogramme({
studyprogramme: code,
settings: {
graduated: graduated === 'GRADUATED_INCLUDED',
specialGroups: specialGroups === 'SPECIAL_INCLUDED',
},
})
if (updated) {
await setStudytrackStats(updated, graduated, specialGroups)
progressData = updated
}
}
const programmeName = await getProgrammeName(code)
delete gradData.tableStats
delete gradData.graphStats
delete gradData.titles
const data = {
id: code,
programmeName: programmeName?.name,
status: gradData?.status,
lastUpdated: gradData.lastUpdated,
graduations: gradData,
progress: {
creditTableStats: {},
creditTableTitles: progressData?.creditTableTitles,
graphData: {
creditGraphStats: {},
id: progressData?.id,
years: progressData?.years,
},
},
}
data.progress.creditTableStats[code] = progressData?.creditTableStats[code]
data.progress.graphData.creditGraphStats[code] = progressData?.creditGraphStats[code]
return res.json(data)
})
module.exports = router
@@ -26,7 +26,9 @@ class CourseStatsCounter {
failedMany: {},
improvedPassedGrade: {},
markedToSemester: {},
enrolledNoGrade: {},
}
this.stats = {
students: 0,
passed: 0,
@@ -38,10 +40,10 @@ class CourseStatsCounter {
percentage: undefined,
passedOfPopulation: undefined,
triedOfPopulation: undefined,
perStudent: undefined,
passingSemesters: this.initializePassingSemesters(),
}
this.enrollments = {
enrolledNoGrade: new Set(),
semesters: this.initializePassingSemesters({}),
}
this.grades = {}
@@ -184,11 +186,20 @@ class CourseStatsCounter {
getFinalStats() {
const stats = this.stats
const students = this.students
const allStudents = Object.keys(students.all)
const totalEnrolledNoGrade = [...(this.enrollments.ENROLLED || [])].filter(
student => !allStudents.includes(student)
).length
const studs = this.enrollments
? Object.keys(this.enrollments)
.filter(key => key !== 'ENROLLED' && key !== 'semesters')
.reduce((acc, key) => [...acc, ...[...this.enrollments[key]].map(studentnumber => studentnumber)], [])
: []
const allStudents = Object.keys(students.all).map(student => student)
const fileredEnrolledNoGrade = this.enrollments['ENROLLED']
? [...this.enrollments['ENROLLED']]
.filter(student => !studs.includes(student) && !allStudents.includes(student))
.reduce((acc, student) => ({ ...acc, [student]: true }), {})
: {}
students.all = { ...students.all, ...fileredEnrolledNoGrade }
students.enrolledNoGrade = fileredEnrolledNoGrade
stats.students = lengthOf(students.all)
stats.passed = lengthOf(students.passed)
stats.failed = lengthOf(students.failed)
@@ -200,8 +211,8 @@ class CourseStatsCounter {
stats.triedOfPopulation = percentageOf(stats.students, this.studentsInTotal)
stats.perStudent = stats.attempts / (stats.passed + stats.failed)
stats.passingSemestersCumulative = this.getPassingSemestersCumulative()
stats.totalStudents = stats.students + totalEnrolledNoGrade
stats.totalEnrolledNoGrade = totalEnrolledNoGrade
stats.totalStudents = stats.students
stats.totalEnrolledNoGrade = lengthOf(fileredEnrolledNoGrade)
stats.percentageWithEnrollments = percentageOf(stats.passed, stats.totalStudents)
return {
@@ -7,14 +7,8 @@ const {
isNonMajorCredit,
getYearsArray,
getStartDate,
isSpecialGroupCredit,
} = require('../studyprogrammeHelpers')
const {
getStudyRights,
getCreditsForStudyProgramme,
allTransfers,
getCourseCodesForStudyProgramme,
} = require('../studyprogramme')
const { getStudyRights, getCreditsForStudyProgramme, getCourseCodesForStudyProgramme } = require('../studyprogramme')
const isFacultyNonMajorCredit = (studyrights, attainment_date, facultyProgrammes) => {
let right = ''
@@ -42,25 +36,14 @@ const isFacultyNonMajorCredit = (studyrights, attainment_date, facultyProgrammes
// Fetches all the credits for the studyprogramme and divides them into major-students, non-major faculty students,
// non-major other faculty students adn non-degree student credits
// Division is done on the basis that whether the student had a primary studyright to the programme on the attainment_date
const getFacultyRegularCreditStats = async ({
studyprogramme,
since,
years,
isAcademicYear,
includeAllSpecials,
facultyProgrammes,
}) => {
const getFacultyRegularCreditStats = async ({ studyprogramme, since, years, isAcademicYear, facultyProgrammes }) => {
const providercode = mapToProviders([studyprogramme])[0]
const courses = await getCourseCodesForStudyProgramme(providercode)
// console.log(JSON.stringify(courses))
const credits = await getCreditsForStudyProgramme(courses, since)
const students = [...new Set(credits.map(({ student_studentnumber }) => student_studentnumber))]
let studyrights = await getStudyRights(students)
const transfers = (await allTransfers(studyprogramme, since)).map(t => t.studyrightid).filter(s => s !== 'undefined')
if (!includeAllSpecials) {
studyrights = studyrights.filter(s => !transfers.includes(s.studyrightid))
}
let majors = getStatsBasis(years)
let allNonMajors = getStatsBasis(years)
let facultyNonMajors = getStatsBasis(years)
@@ -71,10 +54,6 @@ const getFacultyRegularCreditStats = async ({
const studentStudyrights = studyrights.filter(studyright => studyright.studentnumber === student_studentnumber)
const attainmentYear = defineYear(attainment_date, isAcademicYear)
if (!includeAllSpecials && isSpecialGroupCredit(studentStudyrights, attainment_date, transfers)) {
return
}
if (isMajorStudentCredit(studentStudyrights, attainment_date, studyprogramme)) {
majors.graphStats[indexOf(years, attainmentYear)] += credits || 0
majors.tableStats[attainmentYear] += credits || 0
@@ -116,12 +95,11 @@ const getFacultyRegularCreditStats = async ({
// Fetches all credits for the studytrack and combines them into statistics
// These are for the faculty view which differs from study programme overview
// (no transfers, non majors split to in- and outside-faculty)
const getFacultyCreditStatsForStudytrack = async ({ studyprogramme, facultyProgrammes, settings }) => {
const { isAcademicYear, includeAllSpecials } = settings
const getFacultyCreditStatsForStudytrack = async ({ studyprogramme, facultyProgrammes, isAcademicYear }) => {
const since = getStartDate(studyprogramme, isAcademicYear)
const years = getYearsArray(since.getFullYear(), isAcademicYear)
const queryParameters = { studyprogramme, since, years, isAcademicYear, includeAllSpecials, facultyProgrammes }
const queryParameters = { studyprogramme, since, years, isAcademicYear, facultyProgrammes }
const { majors, facultyNonMajors, otherNonMajors, nonDegree } = await getFacultyRegularCreditStats(queryParameters)
const reversedYears = getYearsArray(since.getFullYear(), isAcademicYear).reverse()
@@ -129,47 +107,36 @@ const getFacultyCreditStatsForStudytrack = async ({ studyprogramme, facultyProgr
if (!dataFound) return null
const tableStats = reversedYears.map(year =>
includeAllSpecials
? [
year,
majors.tableStats[year] +
facultyNonMajors.tableStats[year] +
otherNonMajors.tableStats[year] +
nonDegree.tableStats[year],
majors.tableStats[year],
facultyNonMajors.tableStats[year],
otherNonMajors.tableStats[year],
nonDegree.tableStats[year],
]
: [year, majors.tableStats[year], majors.tableStats[year]]
)
const tableStats = reversedYears.map(year => [
year,
majors.tableStats[year] +
facultyNonMajors.tableStats[year] +
otherNonMajors.tableStats[year] +
nonDegree.tableStats[year],
majors.tableStats[year],
facultyNonMajors.tableStats[year],
otherNonMajors.tableStats[year],
nonDegree.tableStats[year],
])
const graphStats = includeAllSpecials
? [
{
name: 'Major students credits',
data: majors.graphStats,
},
{
name: 'Non-major faculty students credits',
data: facultyNonMajors.graphStats,
},
{
name: 'Non-major other faculty students credits 2',
data: otherNonMajors.graphStats,
},
{
name: 'Non-degree credits',
data: nonDegree.graphStats,
},
]
: [
{
name: 'Major students credits',
data: majors.graphStats,
},
]
const graphStats = [
{
name: 'Major students credits',
data: majors.graphStats,
},
{
name: 'Non-major faculty students credits',
data: facultyNonMajors.graphStats,
},
{
name: 'Non-major other faculty students credits 2',
data: otherNonMajors.graphStats,
},
{
name: 'Non-degree credits',
data: nonDegree.graphStats,
},
]
return {
id: studyprogramme,
@@ -179,33 +146,27 @@ const getFacultyCreditStatsForStudytrack = async ({ studyprogramme, facultyProgr
}
}
const getProgrammeCredits = async (code, yearType, specialGroups, facultyProgrammes) => {
const getProgrammeCredits = async (code, yearType, facultyProgrammes) => {
const updatedStats = await getFacultyCreditStatsForStudytrack({
studyprogramme: code,
facultyProgrammes,
settings: {
isAcademicYear: yearType === 'ACADEMIC_YEAR',
includeAllSpecials: specialGroups === 'SPECIAL_INCLUDED',
},
isAcademicYear: yearType === 'ACADEMIC_YEAR',
})
return updatedStats
}
const combineFacultyCredits = async (faculty, programmes, allProgrammes, yearType, specialGroups) => {
const combineFacultyCredits = async (faculty, programmes, allProgrammes, yearType) => {
let counts = {}
let years = []
const titles =
specialGroups === 'SPECIAL_INCLUDED'
? [
'',
'Total',
'Major students credits',
'Non-major faculty students credits',
'Non-major other faculty students credits',
'Non-degree student credits',
]
: ['', 'Total', 'Major students credits']
const titles = [
'',
'Total',
'Major students credits',
'Non-major faculty students credits',
'Non-major other faculty students credits',
'Non-degree student credits',
]
let allCredits = {
id: faculty,
@@ -220,7 +181,7 @@ const combineFacultyCredits = async (faculty, programmes, allProgrammes, yearTyp
const progCodes = allProgrammes.map(p => p.code)
for (const prog of programmes) {
if (prog.code === 'MH70_008_2') continue
const data = await getProgrammeCredits(prog.code, yearType, specialGroups, progCodes)
const data = await getProgrammeCredits(prog.code, yearType, progCodes)
if (data) {
allCredits.programmeNames[prog.progId] = { code: prog.code, ...prog.name }
allCredits.programmeTableStats[prog.progId] = data.tableStats
@@ -260,16 +221,12 @@ const combineFacultyCredits = async (faculty, programmes, allProgrammes, yearTyp
nonDegree.push(counts[year][4])
})
allCredits.graphStats =
specialGroups === 'SPECIAL_INCLUDED'
? [
{ name: 'Major students credits', data: majors },
{ name: 'Non-major faculty students credits', data: facultyNonMajor },
{ name: 'Non-major other faculty students credits', data: otherNonMajor },
{ name: 'Non-degree credits', data: nonDegree },
]
: [{ name: 'Major students credits', data: majors }]
allCredits.graphStats = [
{ name: 'Major students credits', data: majors },
{ name: 'Non-major faculty students credits', data: facultyNonMajor },
{ name: 'Non-major other faculty students credits', data: otherNonMajor },
{ name: 'Non-degree credits', data: nonDegree },
]
return allCredits
}
@@ -54,9 +54,9 @@ const getBasicStats = async (id, yearType, programmeFilter, specialGroups) => {
return JSON.parse(dataFromRedis)
}
const setCreditStats = async (data, yearType, programmeFilter, specialGroups) => {
const setCreditStats = async (data, yearType, programmeFilter) => {
const { id } = data
const redisKey = createRedisKeyForCreditStats(id, yearType, programmeFilter, specialGroups)
const redisKey = createRedisKeyForCreditStats(id, yearType, programmeFilter)
const dataToRedis = {
...data,
status: 'DONE',
@@ -100,13 +100,12 @@ const updateFacultyOverview = async (faculty, statsType) => {
)
await setBasicStats(updatedStudentInfo, option.yearType, option.programmeFilter, option.specialGroups)
}
if (statsType === 'ALL' || statsType === 'CREDITS') {
if ((statsType === 'ALL' || statsType === 'CREDITS') && specialGroups !== 'SPECIAL_EXCLUDED') {
const updatedCredits = await combineFacultyCredits(
faculty,
programmeFilter === 'ALL_PROGRAMMES' ? allProgrammes.data : newProgrammes.data,
allProgrammes.data,
yearType,
specialGroups
yearType
)
await setCreditStats(updatedCredits, yearType, programmeFilter, specialGroups)
}
+202 -29
View File
@@ -25,6 +25,7 @@ const { CourseStatsCounter } = require('./course_stats_counter')
const { getPassingSemester, semesterEnd, semesterStart } = require('../util/semester')
const { getAllProgrammes } = require('./studyrights')
const { encrypt } = require('../services/encrypt')
const { getCriteria } = require('./studyProgrammeCriteria')
const enrolmentDates = () => {
const query = 'SELECT DISTINCT s.dateOfUniversityEnrollment as date FROM Student s'
@@ -48,6 +49,7 @@ const formatStudentForPopulationStatistics = (
creditcount,
abbreviatedname,
email,
secondary_email,
phone_number,
studyrights,
studyplans,
@@ -68,7 +70,8 @@ const formatStudentForPopulationStatistics = (
credits,
startDate,
startDateMoment,
endDateMoment
endDateMoment,
criteria
) => {
const toCourse = ({ grade, attainment_date, credits, course_code, credittypecode, isStudyModule, language }) => {
const attainment_date_normailized =
@@ -87,8 +90,74 @@ const formatStudentForPopulationStatistics = (
}
}
studyrights = studyrights || []
const toProgressCriteria = () => {
const criteriaFullfilled = {
year1: {
credits: false,
totalSatisfied: 0,
},
year2: {
credits: false,
totalSatisfied: 0,
},
year3: {
credits: false,
totalSatisfied: 0,
},
}
let firstAcademic = 0
let secondAcademic = 0
let thirdAcademic = 0
if (criteria !== {}) {
const courses = credits[studentnumber] ? credits[studentnumber].map(toCourse) : []
courses.forEach(course => {
if (course.passed) {
if (
criteria?.courses?.yearOne.includes(course.course_code) ||
criteria?.courses?.yearOne.some(c => criteria?.allCourses?.yearOne[c].includes(course.course_code))
) {
criteriaFullfilled.year1.totalSatisfied += 1
}
if (
criteria?.courses?.yearTwo.includes(course.course_code) ||
criteria?.courses?.yearTwo.some(c => criteria?.allCourses?.yearTwo[c].includes(course.course_code))
) {
criteriaFullfilled.year2.totalSatisfied += 1
}
if (
criteria?.courses?.yearThree.includes(course.course_code) ||
criteria?.courses?.yearThree.some(c => criteria?.allCourses?.yearThree[c].includes(course.course_code))
) {
criteriaFullfilled.year3.totalSatisfied += 1
}
firstAcademic += moment(course.date).isBetween(moment(startDate), moment(startDate).add(1, 'year'))
? course.credits
: 0
secondAcademic += moment(course.date).isBetween(moment(startDate), moment(startDate).add(2, 'year'))
? course.credits
: 0
thirdAcademic += moment(course.date).isBetween(moment(startDate), moment(startDate).add(3, 'year'))
? course.credits
: 0
}
})
}
if (firstAcademic >= criteria?.credits?.yearOne) {
criteriaFullfilled.year1.credits = true
criteriaFullfilled.year1.totalSatisfied += 1
}
if (secondAcademic >= criteria?.credits?.yearTwo) {
criteriaFullfilled.year2.credits = true
criteriaFullfilled.year2.totalSatisfied += 1
}
if (thirdAcademic >= criteria?.credits?.yearThree) {
criteriaFullfilled.year3.credits = true
criteriaFullfilled.year3.totalSatisfied += 1
}
return criteriaFullfilled
}
studyrights = studyrights || []
const started = dateofuniversityenrollment
return {
firstnames,
@@ -104,6 +173,7 @@ const formatStudentForPopulationStatistics = (
gender_code,
gender: { gender_en, gender_fi, gender_sv },
email,
secondaryEmail: secondary_email,
phoneNumber: phone_number,
semesterenrollments: semester_enrollments || [],
updatedAt: updatedAt || createdAt,
@@ -114,6 +184,7 @@ const formatStudentForPopulationStatistics = (
birthdate,
studyplans,
sis_person_id,
criteriaProgress: toProgressCriteria(),
}
}
@@ -233,6 +304,7 @@ const getStudentsIncludeCoursesBetween = async (studentnumbers, startDate, endDa
'creditcount',
'abbreviatedname',
'email',
'secondary_email',
'phone_number',
'updatedAt',
'gender_code',
@@ -749,7 +821,8 @@ const formatStudentsForApi = async (
startDate,
endDate,
{ studyRights },
optionData
optionData,
criteria
) => {
const startDateMoment = moment(startDate)
const endDateMoment = moment(endDate)
@@ -783,8 +856,17 @@ const formatStudentsForApi = async (
})
if (student.studentnumber in optionData) student.option = optionData[student.studentnumber]
else student.option = null
stats.students.push(
formatStudentForPopulationStatistics(student, enrollments, credits, startDate, startDateMoment, endDateMoment)
formatStudentForPopulationStatistics(
student,
enrollments,
credits,
startDate,
startDateMoment,
endDateMoment,
criteria
)
)
return stats
},
@@ -868,16 +950,15 @@ const optimizedStatisticsOf = async (query, studentnumberlist) => {
nondegreeStudents,
transferredStudents
)
// wtf
// plz
const code = studyRights[0] || ''
let optionData = {}
let criteria = {}
if (code.includes('MH')) {
optionData = await getOptionsForStudents(studentnumbers, code, 'MSC')
} else if (code.includes('KH')) {
optionData = await getOptionsForStudents(studentnumbers, code, 'BSC')
criteria = await getCriteria(code)
}
const { students, enrollments, credits, extents, semesters, elementdetails, courses } =
await getStudentsIncludeCoursesBetween(
studentnumbers,
@@ -892,7 +973,8 @@ const optimizedStatisticsOf = async (query, studentnumberlist) => {
startDate,
endDate,
formattedQueryParams,
optionData
optionData,
criteria
)
return formattedStudents
@@ -902,10 +984,52 @@ const getSubstitutions = async codes => {
const courses = await Course.findAll({ where: { code: codes }, attributes: ['code', 'substitutions'], raw: true })
return [...new Set(courses.map(({ code, substitutions }) => [code, ...substitutions]).flat())]
}
// This duplicate code is added here to ensure that we get the enrollments in cases no credits found for the selected students.
const findCourseEnrollments = async (studentnumbers, beforeDate, courses = [], studentCountLimit = 0) => {
const courseCodes = courses.length > 0 ? await getSubstitutions(courses) : ['DUMMY']
const res = await sequelize.query(
`
SELECT DISTINCT ON (course.id)
course.code,
course.name,
course.coursetypecode,
course.substitutions,
course.main_course_code,
course_types.name AS course_type,
enrollment.data AS enrollments
FROM course
INNER JOIN course_types ON course_types.coursetypecode = course.coursetypecode
LEFT JOIN (
SELECT
course_id,
ARRAY_AGG(JSON_BUILD_OBJECT(
'studentnumber', studentnumber,
'state', state,
'enrollment_date_time', enrollment_date_time
)) AS data
FROM enrollment
WHERE enrollment.studentnumber IN (:studentnumbers) AND enrollment.enrollment_date_time < :beforeDate
GROUP BY enrollment.course_id
HAVING COUNT(DISTINCT enrollment.studentnumber) >= :studentCountLimit
) enrollment ON enrollment.course_id = course.id
WHERE :skipCourseCodeFilter OR course.code IN (:courseCodes)
-- GROUP BY 1, 2, 3, 4, 5, 6
`,
{
replacements: {
studentnumbers: studentnumbers.length > 0 ? studentnumbers : ['DUMMY'],
beforeDate,
studentCountLimit,
courseCodes,
skipCourseCodeFilter: courses.length === 0,
},
type: sequelize.QueryTypes.SELECT,
}
)
return res
}
const findCourses = async (studentnumbers, beforeDate, courses = [], studentCountLimit = 0) => {
const courseCodes = courses.length > 0 ? await getSubstitutions(courses) : ['DUMMY']
const res = await sequelize.query(
`
SELECT DISTINCT ON (course.id)
@@ -1030,14 +1154,22 @@ const bottlenecksOf = async (query, studentnumberlist, encryptdata = false) => {
numbers[num] = true
return numbers
}, {})
const courses = await findCourses(
studentnumbers,
dateMonthsFromNow(startDate, months),
courseCodes,
query.studentCountLimit
)
return [allstudents, courses]
const foundCourseCodes = Object.keys(keyBy(courses, 'code'))
const filteredCourseCodes = courseCodes?.filter(code => !foundCourseCodes.includes(code))
const courseEnrollements = await findCourseEnrollments(
studentnumbers,
dateMonthsFromNow(startDate, months),
filteredCourseCodes,
query.studentCountLimit
)
return [allstudents, courses, courseEnrollements]
} else {
const { months, startDate } = params
const beforeDate = months && startDate ? dateMonthsFromNow(startDate, months) : new Date()
@@ -1047,7 +1179,16 @@ const bottlenecksOf = async (query, studentnumberlist, encryptdata = false) => {
return numbers
}, {})
const courses = await findCourses(studentnumberlist, beforeDate, courseCodes, query.studentCountLimit)
return [allstudents, courses]
const foundCourseCodes = Object.keys(keyBy(courses, 'code'))
const filteredCourseCodes = courseCodes?.filter(code => !foundCourseCodes.includes(code))
const courseEnrollements = await findCourseEnrollments(
studentnumberlist,
dateMonthsFromNow(startDate, months),
filteredCourseCodes,
query.studentCountLimit
)
return [allstudents, courses, courseEnrollements]
}
}
@@ -1067,24 +1208,52 @@ const bottlenecksOf = async (query, studentnumberlist, encryptdata = false) => {
}
const params = parseQueryParams(query)
const [[allstudents, courses], error] = await Promise.all([
const allStudentsByYears = query?.selectedStudentsByYear
? Object.keys(query.selectedStudentsByYear).reduce(
(res, year) => [...res, ...query.selectedStudentsByYear[year]],
[]
)
: []
// To fix failed and enrolled, no grade filter options some not so clean and nice solutions were added
// Get the data with actual 1. courses and filtered students. 2. all students by year, if provided.
const [[allstudents, courses, courseEnrollements], [, allCourses], error] = await Promise.all([
getStudentsAndCourses(query.selectedStudents, studentnumberlist, query.courses),
getStudentsAndCourses(allStudentsByYears, null, query.courses),
isValidRequest(query, params),
])
if (error) return error
if (error) return error
// Get the substitution codes for the fetch data by selscted students
const substitutionCodes = Object.entries(courses).reduce(
(res, [, obj]) => [...res, ...(obj?.substitutions || [])],
[]
)
const codes = Object.keys(keyBy(courses, 'code')).map(code => code)
// Filter substitution courses for fetched courses -> this avoid the situation in which there is only
// courses with old course codes. Frontend NEEDS in most cases the current course.
const substitutionCourses = allCourses.filter(
obj => substitutionCodes.includes(obj.code) && !codes.includes(obj.code)
)
const bottlenecks = {
disciplines: {},
coursetypes: {},
}
const stats = {}
const startYear = parseInt(query.year, 10)
const allstudentslength = Object.keys(allstudents).length
const coursesByCode = keyBy(courses, 'code')
let coursesToLoop = courses.concat(substitutionCourses)
const codesList = coursesToLoop.map(course => course.code)
// This fix problem when Enrolled no grade is chosen. The sql query for fetching
// credits do not fetch enrollments if no credits found for selected students.
// This and other sql query ensures that enrollments are added.
const coursesOnlyWithEnrollments = courseEnrollements.filter(
course => !codesList.includes(course.code) && course.enrollments
)
coursesToLoop = coursesToLoop.concat(coursesOnlyWithEnrollments)
for (const course of courses) {
const coursesByCode = keyBy(coursesToLoop, 'code')
for (const course of coursesToLoop) {
let { course_type } = course
let maincourse = course
@@ -1101,28 +1270,32 @@ const bottlenecksOf = async (query, studentnumberlist, encryptdata = false) => {
}
const coursestats = stats[maincourse.code]
coursestats.addCourseType(course_type.coursetypecode, course_type.name)
coursestats.addCourseSubstitutions(course.substitutions)
bottlenecks.coursetypes[course_type.coursetypecode] = course_type.name
course.credits.forEach(credit => {
const { studentnumber, passingGrade, improvedGrade, failingGrade, grade, date } = parseCreditInfo(credit)
const semester = getPassingSemester(startYear, date)
coursestats.markCredit(studentnumber, grade, passingGrade, failingGrade, improvedGrade, semester)
})
if (course.enrollments) {
course.enrollments.forEach(({ studentnumber, state, enrollment_date_time }) => {
const semester = getPassingSemester(startYear, enrollment_date_time)
coursestats.markEnrollment(studentnumber, state, semester)
if ((query?.selectedStudents && query?.selectedStudents.includes(studentnumber)) || !query?.selectedStudents) {
const semester = getPassingSemester(startYear, enrollment_date_time)
coursestats.markEnrollment(studentnumber, state, semester, enrollment_date_time)
}
})
}
if (course.credits) {
course.credits.forEach(credit => {
const { studentnumber, passingGrade, improvedGrade, failingGrade, grade, date } = parseCreditInfo(credit)
if ((query?.selectedStudents && query?.selectedStudents.includes(studentnumber)) || !query?.selectedStudents) {
const semester = getPassingSemester(startYear, date)
coursestats.markCredit(studentnumber, grade, passingGrade, failingGrade, improvedGrade, semester)
}
})
}
stats[maincourse.code] = coursestats
}
bottlenecks.coursestatistics = Object.values(stats).map(coursestatistics => coursestatistics.getFinalStats())
const allStats = Object.values(stats).map(coursestatistics => coursestatistics.getFinalStats())
bottlenecks.coursestatistics = allStats.filter(course => course.stats.students > 0)
bottlenecks.allStudents = allstudentslength
if (encryptdata) encryptStudentnumbers(bottlenecks)
@@ -1,20 +1,35 @@
const { StudyProgrammeCriteria } = require('../models/models_kone')
const { ProgressCriteria } = require('../models/models_kone')
const { Course } = require('../models')
const logger = require('../util/logger')
const getCriteriaByStudyProgramme = code =>
StudyProgrammeCriteria.findOne({
ProgressCriteria.findOne({
where: {
code: code,
},
})
const formattedData = updatedData => {
const getSubstitutions = async codes => {
if (codes === []) return []
const courses = await Course.findAll({ where: { code: codes }, attributes: ['code', 'substitutions'], raw: true })
return courses.reduce((acc, { code, substitutions }) => ({ ...acc, [code]: substitutions }), {})
}
const formattedData = async updatedData => {
const allCoursesOne = await getSubstitutions(updatedData.coursesYearOne)
const allCoursesTwo = await getSubstitutions(updatedData.coursesYearTwo)
const allCoursesThree = await getSubstitutions(updatedData.coursesYearThree)
const criteria = {
courses: {
yearOne: updatedData.coursesYearOne,
yearTwo: updatedData.coursesYearTwo,
yearThree: updatedData.coursesYearThree,
},
allCourses: {
yearOne: allCoursesOne,
yearTwo: allCoursesTwo,
yearThree: allCoursesThree,
},
credits: {
yearOne: updatedData.creditsYearOne,
yearTwo: updatedData.creditsYearTwo,
@@ -35,9 +50,8 @@ const createCriteria = async (studyProgramme, coYear1, coYear2, coYear3, crYear1
creditsYearThree: crYear3,
}
try {
const createdData = await StudyProgrammeCriteria.create(newProgrammeCriteria)
const criteria = formattedData(createdData)
return criteria
const createdData = await ProgressCriteria.create(newProgrammeCriteria)
return await formattedData(createdData)
} catch (error) {
logger.error(`Create criteria failed: ${error}`)
}
@@ -64,8 +78,7 @@ const saveYearlyCreditCriteria = async (studyProgramme, credits) => {
try {
const updatedData = await studyProgrammeToUpdate.update({ ...yearlyCredits })
const criteria = formattedData(updatedData)
return criteria
return await formattedData(updatedData)
} catch (error) {
logger.error(`Update yearly credit criteria failed: ${error}`)
}
@@ -100,11 +113,28 @@ const saveYearlyCourseCriteria = async (studyProgramme, courses, year) => {
const getCriteria = async studyProgramme => {
const studyProgrammeCriteria = await getCriteriaByStudyProgramme(studyProgramme)
let allCoursesOne = []
let allCoursesTwo = []
let allCoursesThree = []
const coursesOne = studyProgrammeCriteria ? studyProgrammeCriteria.coursesYearOne : []
const coursesTwo = studyProgrammeCriteria ? studyProgrammeCriteria.coursesYearTwo : []
const coursesThree = studyProgrammeCriteria ? studyProgrammeCriteria.coursesYearThree : []
if (studyProgrammeCriteria) {
allCoursesOne = await getSubstitutions(coursesOne)
allCoursesTwo = await getSubstitutions(coursesTwo)
allCoursesThree = await getSubstitutions(coursesThree)
}
const criteria = {
courses: {
yearOne: studyProgrammeCriteria ? studyProgrammeCriteria.coursesYearOne : [],
yearTwo: studyProgrammeCriteria ? studyProgrammeCriteria.coursesYearTwo : [],
yearThree: studyProgrammeCriteria ? studyProgrammeCriteria.coursesYearThree : [],
yearOne: coursesOne,
yearTwo: coursesTwo,
yearThree: coursesThree,
},
allCourses: {
yearOne: allCoursesOne,
yearTwo: allCoursesTwo,
yearThree: allCoursesThree,
},
credits: {
yearOne: studyProgrammeCriteria ? studyProgrammeCriteria.creditsYearOne : 0,
+113 -8
View File
@@ -12,13 +12,16 @@ const {
Studyright,
StudyrightElement,
ElementDetail,
ProgrammeModule,
Transfer,
Student,
SemesterEnrollment,
Semester,
Enrollment,
} = require('../models')
const { formatStudyright, formatStudent, formatTransfer } = require('./studyprogrammeHelpers')
const { getCurrentSemester } = require('./semesters')
const logger = require('../util/logger')
const whereStudents = studentnumbers => {
return studentnumbers ? studentnumbers : { [Op.not]: null }
@@ -379,7 +382,7 @@ const getProgrammesStudyrights = async studyprogramme =>
const getStudyRights = async students =>
(
await Studyright.findAll({
attributes: ['studyrightid', 'studystartdate', 'enddate', 'graduated', 'prioritycode', 'extentcode'],
attributes: ['studyrightid', 'startdate', 'studystartdate', 'enddate', 'graduated', 'prioritycode', 'extentcode'],
where: {
studentStudentnumber: students,
},
@@ -415,9 +418,7 @@ const getCreditsForStudyProgramme = async (codes, since) => {
},
},
where: {
credittypecode: {
[Op.notIn]: [10, 9, 7],
},
credittypecode: 4,
isStudyModule: {
[Op.not]: true,
},
@@ -498,16 +499,110 @@ const getStudentsForProgrammeCourses = async (from, to, programmeCourses) => {
code: course.code,
name: course.course_name,
year: course.year,
totalAllStudents: parseInt(course.total_students),
totalPassed: parseInt(course.total_students),
totalAllcredits: parseInt(course.total_credits),
type: 'total',
type: 'passed',
}))
} catch (e) {
// eslint-disable-next-line no-console
console.log(e)
logger.log(e)
}
}
const getNotCompletedForProgrammeCourses = async (from, to, programmeCourses) => {
try {
const enrollmentsCourses = await Enrollment.findAll({
attributes: ['studentnumber', 'course_code'],
where: {
course_code: {
[Op.in]: programmeCourses,
},
studentnumber: {
[Op.ne]: null,
},
enrollment_date_time: {
[Op.between]: [from, to],
},
state: 'ENROLLED',
},
})
const allEnrollments = {}
for (const { studentnumber, course_code } of enrollmentsCourses) {
if (!(course_code in allEnrollments)) {
allEnrollments[course_code] = []
}
allEnrollments[course_code].push(studentnumber)
}
const credits = await Credit.findAll({
attributes: ['id', 'course_code', 'student_studentnumber', 'credittypecode', 'attainment_date'],
include: {
model: Course,
attributes: ['code', 'name'],
required: true,
where: {
is_study_module: false,
code: programmeCourses,
},
},
where: {
credittypecode: [4, 7, 10],
isStudyModule: {
[Op.not]: true,
},
attainment_date: {
[Op.between]: [from, to],
},
},
})
const creditsObj = credits.map(credit => {
return {
code: credit.course_code,
studentnumber: credit.student_studentnumber,
credittype: credit.credittypecode,
courseName: credit.course.name,
year: moment(credit.attainment_date).year(),
}
})
const passedByCourseCodes = {}
const notCompletedByCourseCodes = {}
const courses = {}
for (const course of creditsObj) {
if (!(course.code in courses)) {
courses[course.code] = {
code: course.code,
name: course.courseName,
year: course.year,
}
passedByCourseCodes[course.code] = []
notCompletedByCourseCodes[course.code] = []
}
if ([4, 7].includes(course.credittype)) {
passedByCourseCodes[course.code].push(course.studentnumber)
}
if (
(allEnrollments[course.code]?.includes(course.studentnumber) || course.credittype === 10) &&
!passedByCourseCodes[course.code].includes(course.studentnumber)
) {
notCompletedByCourseCodes[course.code].push(course.studentnumber)
}
}
return Object.keys(courses)
.reduce((acc, val) => [...acc, { ...courses[val] }], [])
.map(course => ({
code: course.code,
name: course.name,
year: course.year,
totalNotCompleted: [...new Set(notCompletedByCourseCodes[course.code])].length,
type: 'notCompleted',
}))
} catch (e) {
// eslint-disable-next-line no-console
logger.log(e)
}
}
const getOwnStudentsForProgrammeCourses = async (from, to, programmeCourses, studyprogramme) => {
const res = await sequelize.query(
`
@@ -534,7 +629,6 @@ const getOwnStudentsForProgrammeCourses = async (from, to, programmeCourses, stu
}
)
return res.map(course => {
// console.log('course: ', course)
const res = {
code: course.code,
name: course.course_name,
@@ -732,6 +826,15 @@ const getCurrentStudyYearStartDate = _.memoize(
)
)
const getProgrammeName = async code => {
return ProgrammeModule.findOne({
attributes: ['name'],
where: {
code,
},
})
}
module.exports = {
studytrackStudents,
enrolledStudents,
@@ -758,4 +861,6 @@ module.exports = {
getStudyRights,
getCourseCodesForStudyProgramme,
getTransferStudentsForProgrammeCourses,
getProgrammeName,
getNotCompletedForProgrammeCourses,
}
@@ -4,6 +4,7 @@ const {
getStudentsForProgrammeCourses,
getCurrentStudyYearStartDate,
getOwnStudentsForProgrammeCourses,
getNotCompletedForProgrammeCourses,
getStudentsWithoutStudyrightForProgrammeCourses,
getTransferStudentsForProgrammeCourses,
getOtherStudentsForProgrammeCourses,
@@ -39,9 +40,12 @@ const makeYearlyPromises = (years, academicYear, type, programmeCourses, studypr
let result = null
switch (type) {
case 'allStudents':
case 'passed':
result = await getStudentsForProgrammeCourses(from, to, programmeCourses)
break
case 'notCompleted':
result = await getNotCompletedForProgrammeCourses(from, to, programmeCourses)
break
case 'ownStudents':
result = await getOwnStudentsForProgrammeCourses(from, to, programmeCourses, studyprogramme)
break
@@ -74,7 +78,13 @@ const getStudyprogrammeCoursesForStudytrack = async (unixMillis, studyprogramme,
const yearRange = _.range(2017, startYear + 1)
const programmeCourses = await getAllStudyprogrammeCourses(studyprogramme)
const yearlyStudentByCoursePromises = makeYearlyPromises(yearRange, academicYear, 'allStudents', programmeCourses)
const yearlyPassedStudentByCoursePromises = makeYearlyPromises(yearRange, academicYear, 'passed', programmeCourses)
const yearlyNotCompletedStudentByCoursePromises = makeYearlyPromises(
yearRange,
academicYear,
'notCompleted',
programmeCourses
)
const yearlyProgrammeStudentsPromises = makeYearlyPromises(
yearRange,
academicYear,
@@ -105,22 +115,24 @@ const getStudyprogrammeCoursesForStudytrack = async (unixMillis, studyprogramme,
)
const [
yearlyStudentByCourse,
yearlyPassedStudentByCourse,
yearlyNotCompletedStudentByCourse,
yearlyProgrammeStudents,
yearlyStudentsWithoutStudyright,
yearlyOtherProgrammeStudents,
yearlyTransferStudents,
] = await Promise.all([
Promise.all(yearlyStudentByCoursePromises),
Promise.all(yearlyPassedStudentByCoursePromises),
Promise.all(yearlyNotCompletedStudentByCoursePromises),
Promise.all(yearlyProgrammeStudentsPromises),
Promise.all(yearlyStudentsWithoutStudyrightPromises),
Promise.all(yearlyOtherProgrammeStudentsPromises),
Promise.all(yearlyTransferStudentsPromises),
])
let maxYear = 0
const allCourses = [
...yearlyStudentByCourse.flat(),
...yearlyPassedStudentByCourse.flat(),
...yearlyNotCompletedStudentByCourse.flat(),
...yearlyProgrammeStudents.flat(),
...yearlyStudentsWithoutStudyright.flat(),
...yearlyOtherProgrammeStudents.flat(),
@@ -134,9 +146,12 @@ const getStudyprogrammeCoursesForStudytrack = async (unixMillis, studyprogramme,
years: {},
}
}
if (!acc[curr.code]['years'][curr.year]) {
acc[curr.code]['years'][curr.year] = {
totalAllStudents: 0,
totalPassed: 0,
totalNotCompleted: 0,
totalAllCredits: 0,
totalProgrammeStudents: 0,
totalProgrammeCredits: 0,
@@ -149,10 +164,16 @@ const getStudyprogrammeCoursesForStudytrack = async (unixMillis, studyprogramme,
}
}
switch (curr.type) {
case 'total':
acc[curr.code]['years'][curr.year]['totalAllStudents'] += curr.totalAllStudents
case 'passed':
acc[curr.code]['years'][curr.year]['totalPassed'] += curr.totalPassed
acc[curr.code]['years'][curr.year]['totalAllStudents'] += acc[curr.code]['years'][curr.year]['totalPassed']
acc[curr.code]['years'][curr.year]['totalAllCredits'] += curr.totalAllcredits
break
case 'notCompleted':
acc[curr.code]['years'][curr.year]['totalNotCompleted'] += curr.totalNotCompleted
acc[curr.code]['years'][curr.year]['totalAllStudents'] +=
acc[curr.code]['years'][curr.year]['totalNotCompleted']
break
case 'ownProgramme':
acc[curr.code]['years'][curr.year]['totalProgrammeStudents'] += curr.totalProgrammeStudents
acc[curr.code]['years'][curr.year]['totalProgrammeCredits'] += curr.totalProgrammeCredits
@@ -175,6 +196,8 @@ const getStudyprogrammeCoursesForStudytrack = async (unixMillis, studyprogramme,
const ayCourses = Object.keys(allCourses).filter(c => c.startsWith('AY'))
const properties = [
'totalAllStudents',
'totalPassed',
'totalNotCompleted',
'totalAllCredits',
'totalProgrammeStudents',
'totalProgrammeCredits',
@@ -200,6 +223,8 @@ const getStudyprogrammeCoursesForStudytrack = async (unixMillis, studyprogramme,
if (!allCourses[normCode]['years'][year]) {
mergedCourse['years'][year] = {
totalAllStudents: 0,
totalPassed: 0,
totalNotCompleted: 0,
totalAllCredits: 0,
totalProgrammeStudents: 0,
totalProgrammeCredits: 0,
@@ -152,16 +152,15 @@ const isNonMajorCredit = (studyrights, attainment_date) =>
)
})
const isSpecialGroupCredit = (studyrights, attainment_date, transfers) => {
const isSpecialGroupCredit = (studyrights, attainment_date, transfers) =>
studyrights.some(studyright => {
if (!studyright) return true // If there is no studyright matching the credit, is not a major student credit
if (studyright.studystartdate > attainment_date) return true // Credits before the studyright started are not major student credits
if (studyright.enddate && attainment_date > studyright.enddate) return true // Credits after studyright are not major student credits
if (studyright.studystartdate && new Date(studyright.studystartdate) > new Date(attainment_date)) return true // Credits before the studyright started are not major student credits
if (studyright.enddate && new Date(attainment_date) > new Date(studyright.enddate)) return true // Credits after studyright are not major student credits
if ([7, 9, 16, 34, 22, 99, 14, 13].includes(studyright.extentcode)) return true // Excludes non-degree studyrights and exchange students
if (transfers.includes(studyright.studyrightid)) return true // Excludes both transfers in and out of the programme
return false
})
}
const getMedian = values => {
if (values.length === 0) return 0
+2 -1
View File
@@ -174,7 +174,8 @@ const updateAccessGroups = async (username, iamGroups = [], specialGroup = {}, s
// Modify accessgroups based on current groups and iamGroups
let newAccessGroups = []
if (sisId && (await checkStudyGuidanceGroupsAccess(sisId))) newAccessGroups.push('studyGuidanceGroups')
if (currentAccessGroups.includes('studyGuidanceGroups') || (sisId && (await checkStudyGuidanceGroupsAccess(sisId))))
newAccessGroups.push('studyGuidanceGroups')
if (iamGroups.includes(courseStatisticsGroup)) newAccessGroups.push('courseStatistics')
if (jory || iamGroups.includes(facultyStatisticsGroup)) newAccessGroups.push('facultyStatistics')
if (hyOne || currentAccessGroups.includes('teachers')) newAccessGroups.push('teachers')
@@ -129,6 +129,16 @@ export default {
**Tags**
Opiskelijoille voi luoda tag:eja ryhmittelyä varten kohdassa "Study Program" > "Overview" > haluttu koulutusohjelma.
**Progress**
Tämä näkymä on tällä hetkellä käytössä vain kandivaiheen opinto-ohjelmissa.
Tällä välilehdellä näkyy opintojen kehitys annettujen kriteerien mukaan akateemista vuotta kohti. Kurssi merkataan tehdyksi, jos sillä on suoritusmerkintä tai hyväksiluku
minä tahansa lukuvuonna. Opintopistekriteerin täyttämiseen vaaditaan, että opintopisteet on suoritettu kyseisen akateemisen vuoden aikana.
**Huomaa**, että klikkaamalla taulukon yläreunassa olevaa kolme pistettä sisältävää kuvaketta, voit ladata itsellesi Excel tiedoston. Tiedosto sisältää lisäksi muita sarakkeita, kuten läsnäolon lukukausittain sekä yhteystiedot.
Yhteystietojen käyttö on kuitenkin sallittua vain opintoneuvontaan liittyvissä asiossa.
`,
StudentsCustom: `**Students**
@@ -105,4 +105,7 @@ export default {
TransferredCredits: `
Sisältää opintopisteet, jotka ovat suoritettu kyseisenä vuonna, ja hyväksiluettu tähän koulutusohjelmaan. Mukana ovat kaikenlaisilla opinto-oikeuksilla hyväksiluetut opintopisteet
`,
NotCompleted: `
Sisältää opiskelijat, jotka ovat ilmoittautuneet kurssille, mutta eivät ole suorittaneet sitä ja opiskelijat, jotka ovat saaneet hylätyn arvosanan.
`,
}
@@ -628,7 +628,7 @@ const ProtoC = ({
</Menu.Item>
{sorterNames.map(sorterName => (
<Menu.Item
basic={sorter !== sorterName}
basic={(sorter !== sorterName).toString()}
color={sorter === sorterName ? 'blue' : 'black'}
key={sorterName}
active={sorter === sorterName}
@@ -727,7 +727,7 @@ const ProtoC = ({
</Segment>
<RenderBelowGraph />
<Message>
<ReactMarkdown children={CoolDataScience.protoC} escapeHtml={false} />
<ReactMarkdown children={CoolDataScience.protoC} />
</Message>
</Segment>
)
@@ -1,3 +1,4 @@
/* eslint-disable babel/no-invalid-this */
import React, { useState, useCallback, useMemo } from 'react'
import Highcharts from 'highcharts'
import ReactHighcharts from 'react-highcharts'
@@ -449,7 +450,7 @@ const ProtoC = ({ programme = 'KH50_005' }) => {
</Menu.Item>
{sorterNames.map(sorterName => (
<Menu.Item
basic={sorter !== sorterName}
basic={(sorter !== sorterName).toString()}
color={sorter === sorterName ? 'blue' : 'black'}
key={sorterName}
active={sorter === sorterName}
@@ -485,7 +486,7 @@ const ProtoC = ({ programme = 'KH50_005' }) => {
<Message>
{
// eslint-disable-next-line react/no-children-prop
<ReactMarkdown children={CoolDataScience.protoC2} escapeHtml={false} />
<ReactMarkdown children={CoolDataScience.protoC2} />
}
</Message>
</Segment>
@@ -541,7 +542,7 @@ const ProtoC = ({ programme = 'KH50_005' }) => {
</Menu.Item>
{sorterNames.map(sorterName => (
<Menu.Item
basic={sorter !== sorterName}
basic={(sorter !== sorterName).toString()}
color={sorter === sorterName ? 'blue' : 'black'}
key={sorterName}
active={sorter === sorterName}
@@ -594,7 +595,7 @@ const ProtoC = ({ programme = 'KH50_005' }) => {
<Message>
{
// eslint-disable-next-line react/no-children-prop
<ReactMarkdown children={CoolDataScience.protoC2} escapeHtml={false} />
<ReactMarkdown children={CoolDataScience.protoC2} />
}
</Message>
</Segment>
@@ -100,7 +100,7 @@ const Chart = React.memo(({ tickDates, snapshots }) => (
return `${d.getDate()}.${d.getMonth()}.${d
.getFullYear()
.toString()
.substr(-2)}`
.substring(-2)}`
}
},
tickPositions: tickDates.map(date => date.getTime()),
@@ -292,7 +292,7 @@ const ProtoG = ({ uberdata, years, isLoading, loadingYears, getUberDispatch, get
)}
<Message>
{//eslint-disable-next-line react/no-children-prop
<ReactMarkdown children={CoolDataScience.protoC} escapeHtml={false} />
<ReactMarkdown children={CoolDataScience.protoC} />
}
</Message>
</Segment>
@@ -185,7 +185,7 @@ export default ({ value, changeSetting, settings, generalHelp }) => {
<div style={{ padding: '1em' }}>
{
// eslint-disable-next-line react/no-children-prop
<ReactMarkdown children={unindent(generalHelp)} escapeHtml={false} />
<ReactMarkdown children={unindent(generalHelp)} />
}
</div>
{settings.map(({ key, label, long }) => (
@@ -196,7 +196,7 @@ export default ({ value, changeSetting, settings, generalHelp }) => {
Valinta "<i>{label}</i>"
</b>
<div style={{ margin: '0.5em', fontSize: '0.9em' }}>
<ReactMarkdown escapeHtml={false}>{unindent(long ?? '')}</ReactMarkdown>
<ReactMarkdown>{unindent(long ?? '')}</ReactMarkdown>
</div>
</div>
</div>
@@ -39,8 +39,8 @@ const CustomOpenUniPopulation = () => {
<p>
Here you can create custom population using a list of courses. Clicking the blue custom population button will
open a modal where you can enter a list of courses. <br />
<Icon fitted name="check" color="green" />: Student passed the course. <br />
<Icon fitted name="times" color="red" />: Student failed the course. <br />
<Icon fitted name="check" color="green" />: Student has passed the course. <br />
<Icon fitted name="times" color="red" />: Student has failed the course. <br />
<Icon fitted name="minus" color="grey" />: Student has enrolled, but has not received any grade from the
course. <br />
<b>Empty cell</b>: Student has no enrollments for the course. <br />
@@ -0,0 +1,64 @@
import React from 'react'
import GraduationTimes from 'components/FacultyStatistics/TimesAndPaths/GraduationTimes'
const FacultyGraduations = ({ faculty, graduationStats, groupByStartYear, showMedian }) => {
const groupBy = groupByStartYear ? 'byStartYear' : 'byGradYear'
const label = groupByStartYear ? 'Start year' : 'Graduation year'
const data = graduationStats?.data?.[groupBy].medians
const goals = graduationStats?.data?.goals
const goalExceptions = { ...goals?.exceptions, needed: faculty === 'H30' }
const programmeData = graduationStats?.data?.[groupBy].programmes.medians
const programmeNames = graduationStats?.data?.programmeNames
const classSizes = graduationStats?.data?.classSizes
const commonProps = { label, programmeNames, showMedian, classSizes, goalExceptions }
return (
<>
<div>
<GraduationTimes
level="bachelor"
title="Bachelor"
data={data?.bachelor}
goal={goals?.bachelor}
levelProgrammeData={programmeData?.bachelor}
{...commonProps}
/>
<GraduationTimes
level="bcMsCombo"
title="Bachelor + Master"
data={data?.bcMsCombo}
goal={goals?.bcMsCombo}
levelProgrammeData={programmeData?.bcMsCombo}
groupBy={groupBy}
{...commonProps}
/>
<GraduationTimes
level="master"
title="Master"
data={data?.master}
goal={goals?.master}
levelProgrammeData={programmeData?.master}
{...commonProps}
/>
<GraduationTimes
level="doctor"
title="Doctor"
data={data?.doctor}
goal={goals?.doctor}
levelProgrammeData={programmeData?.doctor}
{...commonProps}
/>
<GraduationTimes
level="licentiate"
title="Licentiate"
data={data?.licentiate}
goal={goals?.licentiate}
levelProgrammeData={programmeData?.licentiate}
{...commonProps}
/>
</div>
</>
)
}
export default FacultyGraduations
@@ -0,0 +1,153 @@
import React from 'react'
import { Message } from 'semantic-ui-react'
import FacultyProgressTable from '../FacultyStatistics/FacultyProgrammeOverview/FacultyProgressTable'
import FacultyBarChart from '../FacultyStatistics/FacultyProgrammeOverview/FacultyBarChart'
import sortProgrammeKeys from '../FacultyStatistics/facultyHelpers'
const FacultyProgress = ({ faculty, progressStats, language, getDivider }) => {
return (
<>
{getDivider('Bachelor', 'BachelorStudentsOfTheFacultyByStartingYear', 'no-infobox')}
<div className="section-container">
<div className="graph-container">
<FacultyBarChart
cypress="FacultyBachelorsProgress"
data={{
id: faculty,
stats: progressStats?.data.bachelorsGraphStats,
years: progressStats?.data.years,
}}
/>
</div>
<div className="table-container">
<FacultyProgressTable
data={progressStats?.data.bachelorsTableStats}
programmeStats={progressStats?.data.bachelorsProgStats}
titles={progressStats?.data.bachelorTitles}
sortedKeys={sortProgrammeKeys(
Object.keys(progressStats?.data.bachelorsProgStats).map(obj => [
obj,
progressStats?.data?.programmeNames[obj].code,
]),
faculty
).map(listObj => listObj[0])}
progressYearsVisible={Array(progressStats?.data.years.slice(1).length).fill(false)}
programmeNames={progressStats?.data.programmeNames}
language={language}
cypress="FacultyBachelorsProgressTable"
progressTitles={progressStats?.data.yearlyBachelorTitles}
/>
</div>
</div>
{getDivider('Bachelor + Master', 'ProgressOfBachelorMaster', 'no-infobox')}
<Message data-cy="FacultyProgrammesShownInfo">
The starting year is the studyright start in the bachelor programme. The credits are comouted by the start date
of the bachelor programme and at the moment, they do not include any transferred credits. Thus, in these
statistics some students have less credits than in reality.
</Message>
<div className="section-container">
<div className="graph-container">
<FacultyBarChart
cypress="FacultyBachelorMastersProgress"
data={{
id: faculty,
stats: progressStats?.data.bcMsGraphStats,
years: progressStats?.data.years,
}}
/>
</div>
<div className="table-container">
<FacultyProgressTable
data={progressStats?.data.bcMsTableStats}
programmeStats={progressStats?.data.bcMsProgStats}
titles={progressStats?.data.bcMsTitles}
sortedKeys={sortProgrammeKeys(
Object.keys(progressStats?.data.bcMsProgStats).map(obj => [
obj,
progressStats?.data?.programmeNames[obj].code,
]),
faculty
).map(listObj => listObj[0])}
progressYearsVisible={Array(progressStats?.data.years.slice(1).length).fill(false)}
programmeNames={progressStats?.data.programmeNames}
language={language}
cypress="FacultyBachelorMasterProgressTable"
progressTitles={progressStats?.data.yearlyBcMsTitles}
needsExtra={faculty === 'H60' ? 'NO EXTRA' : 'EXTRA HEIGHT'}
/>
</div>
</div>
{!(faculty === 'H90') && (
<>
{getDivider('Master', 'MasterStudentsOfTheFacultyByStartingYear', 'no-infobox')}
<div className="section-container">
<div className="graph-container">
<FacultyBarChart
cypress="FacultyMastersProgress"
data={{
id: faculty,
stats: progressStats?.data.mastersGraphStats,
years: progressStats?.data.years,
}}
/>
</div>
<div className="table-container">
<FacultyProgressTable
data={progressStats?.data.mastersTableStats}
programmeStats={progressStats?.data.mastersProgStats}
titles={progressStats?.data.mastersTitles}
sortedKeys={sortProgrammeKeys(
Object.keys(progressStats?.data.mastersProgStats).map(obj => [
obj,
progressStats?.data?.programmeNames[obj].code,
]),
faculty
).map(listObj => listObj[0])}
progressYearsVisible={Array(progressStats?.data.years.slice(1).length).fill(false)}
programmeNames={progressStats?.data.programmeNames}
language={language}
cypress="FacultyMastersProgressTable"
progressTitles={progressStats?.data.yearlyMasterTitles}
needsExtra={faculty === 'H60' ? 'NO EXTRA' : 'EXTRA HEIGHT'}
/>
</div>
</div>
</>
)}
{getDivider('Doctor', 'DoctoralStudentsOfTheFacultyByStartingYear', 'no-infobox')}
<div className="section-container">
<div className="graph-container">
<FacultyBarChart
cypress="FacultyDoctoralProgress"
data={{
id: faculty,
stats: progressStats?.data.doctoralGraphStats,
years: progressStats?.data.years,
}}
/>
</div>
<div className="table-container">
<FacultyProgressTable
data={progressStats?.data.doctoralTableStats}
programmeStats={progressStats?.data.doctoralProgStats}
titles={progressStats?.data.doctoralTitles}
sortedKeys={sortProgrammeKeys(
Object.keys(progressStats?.data.doctoralProgStats).map(obj => [
obj,
progressStats?.data?.programmeNames[obj].code,
]),
faculty
).map(listObj => listObj[0])}
progressYearsVisible={Array(progressStats?.data.years.slice(1).length).fill(false)}
programmeNames={progressStats?.data.programmeNames}
language={language}
cypress="FacultyDoctoralProgressTable"
/>
</div>
</div>
</>
)
}
export default FacultyProgress
@@ -0,0 +1,145 @@
import React, { useState } from 'react'
import { Divider, Header, Loader, Message } from 'semantic-ui-react'
import {
useGetFacultiesQuery,
useGetFacultyProgressStatsQuery,
useGetFacultyGraduationTimesQuery,
} from 'redux/facultyStats'
import { getTextIn } from 'common'
import FacultyGraduations from './FacultyGraduations'
import FacultyProgress from './FacultyProgress'
import Toggle from '../StudyProgramme/Toggle'
import InfoBox from '../Info/InfoBox'
import InfotoolTips from '../../common/InfoToolTips'
import useLanguage from '../LanguagePicker/useLanguage'
import '../FacultyStatistics/faculty.css'
const FacultyView = ({ faculty }) => {
const [graduatedGroup, setGraduatedGroup] = useState(false)
const [showMedian, setShowMedian] = useState(false)
const toolTips = InfotoolTips.Faculty
const studyProgrammeFilter = 'NEW_STUDY_PROGRAMMES'
const specials = 'SPECIAL_EXCLUDED'
const graduated = graduatedGroup ? 'GRADUATED_EXCLUDED' : 'GRADUATED_INCLUDED'
const { language } = useLanguage()
const allFaculties = useGetFacultiesQuery()
const faculties = allFaculties?.data
const facultyDetails = faculties && faculty && faculties.find(f => f.code === faculty)
const facultyName = facultyDetails && getTextIn(facultyDetails.name, language)
const progressStats = useGetFacultyProgressStatsQuery({
id: faculty,
studyProgrammeFilter,
specialGroups: specials,
graduated,
})
const graduationStats = useGetFacultyGraduationTimesQuery({ id: faculty, studyProgrammeFilter })
const getDivider = (title, toolTipText, content, cypress = undefined) => (
<>
<div className="divider">
<Divider data-cy={`Section-${toolTipText}`} horizontal>
{title}
</Divider>
</div>
{content === 'no-infobox' ? null : <InfoBox content={content} cypress={cypress} />}
</>
)
if (allFaculties.isLoading || allFaculties.isFetching) {
return <Loader active style={{ marginTop: '10em' }} />
}
const isFetchingOrLoading =
progressStats.isLoading || progressStats.isFetching || graduationStats.isLoading || graduationStats.isFetching
const isError =
(progressStats.isError && graduationStats.isError) ||
(progressStats.isSuccess && !progressStats.data && graduationStats.isSuccess && !graduationStats.data)
if (isError) return <h3>Something went wrong, please try refreshing the page.</h3>
return (
<>
<div align="center" style={{ padding: '30px' }}>
<Header textAlign="center">{facultyName}</Header>
<span>{faculty}</span>
</div>
<Message info>
<Message.Header>This view is an abridged version of Oodikone's Faculty Overview</Message.Header>
<p>
In these statistics, all special studyrights have been excluded, eg. exchange students and non-degree
students.
</p>
<p>
<b>You can find more statistics on this and all other faculties of the university in main Faculty view. </b>
The full view includes details such as: how many have graduated or started in each faculty and in its
programmes; credits produced by the faculty; populations backgrounds and enrollment statuses. This view
provides statistics both on the whole faculty level and a breakdown of how they are spread among the faculty's
programmes.
</p>
<p>Access the full Faculty view by clicking 'Faculty' in the top navigation bar.</p>
</Message>
<div className="faculty-overview">
{isFetchingOrLoading ? (
<Loader active style={{ marginTop: '15em' }} />
) : (
<div className="programmes-overview">
{progressStats.isSuccess && progressStats.data && (
<>
{getDivider(
'Progress of students of the faculty ',
'BachelorStudentsOfTheFacultyByStartingYear',
toolTips.StudentProgress,
'InfoFacultyProgress'
)}
<div className="toggle-container">
<Toggle
cypress="GraduatedToggle"
toolTips={toolTips.GraduatedToggle}
firstLabel="Graduated included"
secondLabel="Graduated excluded"
value={graduatedGroup}
setValue={setGraduatedGroup}
/>
</div>
<FacultyProgress
faculty={faculty}
progressStats={progressStats}
language={language}
getDivider={getDivider}
/>
</>
)}
{graduationStats.isSuccess && graduationStats.data && (
<>
{getDivider('Average graduation times', 'AverageGraduationTimes', toolTips.AverageGraduationTimes)}
<div className="toggle-container">
<Toggle
cypress="GraduationTimeToggle"
firstLabel="Breakdown"
secondLabel="Median times"
value={showMedian}
setValue={setShowMedian}
/>
</div>
<FacultyGraduations
faculty={faculty}
graduationStats={graduationStats}
groupByStartYear={false}
showMedian={showMedian}
/>
</>
)}
</div>
)}
</div>
</>
)
}
export default FacultyView
@@ -0,0 +1,194 @@
import React, { useState } from 'react'
import { Divider, Header, Loader, Message, Segment } from 'semantic-ui-react'
import { useGetEvaluationStatsQuery } from 'redux/studyProgramme'
import { getTextIn } from '../../common'
import useLanguage from '../LanguagePicker/useLanguage'
import Toggle from '../StudyProgramme/Toggle'
import InfoBox from '../Info/InfoBox'
import MedianTimeBarChart from '../StudyProgramme/MedianTimeBarChart'
import BreakdownBarChart from '../StudyProgramme/BreakdownBarChart'
import StackedBarChart from '../StudyProgramme/BasicOverview/StackedBarChart'
import BarChart from '../StudyProgramme/StudytrackOverview/BarChart'
import BasicDataTable from '../StudyProgramme/StudytrackOverview/BasicDataTable'
import DataTable from '../StudyProgramme/BasicOverview/DataTable'
import InfotoolTips from '../../common/InfoToolTips'
import '../StudyProgramme/studyprogramme.css'
const ProgrammeView = ({ studyprogramme }) => {
const { language } = useLanguage()
const [showMedian, setShowMedian] = useState(false)
const [academicYear, setAcademicYear] = useState(false)
const [graduated, setGraduated] = useState(false)
const toolTips = InfotoolTips.Studyprogramme
const yearType = academicYear ? 'ACADEMIC_YEAR' : 'CALENDAR_YEAR'
const grad = graduated ? 'GRADUATED_EXCLUDED' : 'GRADUATED_INCLUDED'
const statistics = useGetEvaluationStatsQuery({
id: studyprogramme,
yearType,
specialGroups: 'SPECIAL_EXCLUDED',
graduated: grad,
})
const programmeName = statistics?.data?.programmeName && getTextIn(statistics?.data?.programmeName, language)
const graduationData = statistics?.data?.graduations
const progressData = statistics?.data?.progress
const doCombo = graduationData?.doCombo
const timesData = graduationData?.graduationTimes
const getDivider = (title, toolTipText) => (
<>
<div className="divider">
<Divider data-cy={`Section-${toolTipText}`} horizontal>
{title}
</Divider>
</div>
<InfoBox content={toolTips[toolTipText]} />
</>
)
const displayMedian = () => (
<>
{doCombo && (
<MedianTimeBarChart
data={graduationData?.comboTimes?.medians}
goal={graduationData?.comboTimes?.goal}
title="Bachelor + Master studyright"
byStartYear={false}
/>
)}
<MedianTimeBarChart
data={graduationData?.graduationTimes?.medians}
goal={graduationData?.graduationTimes?.goal}
title={doCombo ? 'Master studyright' : ' '}
byStartYear={false}
/>
</>
)
const displayBreakdown = () => (
<>
{doCombo && <BreakdownBarChart data={graduationData?.comboTimes?.medians} title="Bachelor + Master studyright" />}
<BreakdownBarChart data={timesData?.medians} title={doCombo ? 'Master studyright' : ' '} />
</>
)
const isFetchingOrLoading = statistics.isLoading || statistics.isFetching
const isError = statistics.isError || (statistics.isSuccess && !statistics.data)
if (isError) return <h3>Something went wrong, please try refreshing the page.</h3>
return (
<>
<div align="center" style={{ padding: '30px' }}>
<Header textAlign="center">{programmeName}</Header>
<span>{studyprogramme}</span>
</div>
<div className="studyprogramme-overview">
{isFetchingOrLoading ? (
<Loader active style={{ marginTop: '10em' }} />
) : (
<Segment className="contentSegment">
<Message info>
<Message.Header>This view is an abridged version of Oodikone's Studyprogramme Overview</Message.Header>
<p>
In these statistics, all special studyrights have been excluded, eg. exchange students and non-degree
students.
</p>
<p>
<b>
You can find more statistics on this and other studyprogrammes of the university in main
Studyprogramme Overview.{' '}
</b>
The full view includes details such as: how many have graduated or started in each programme and in its
studytracks; credits produced by the programme; population backgrounds and enrollment statuses.
</p>
<p>
Access the full Studyprogramme Overview by clicking 'Studyprogramme' in the top navigation bar and then
'Overview'. Alternatively, you can select 'Search by class' to explore studyprogrammes by starting class
with interactive statistics and visualizations.
</p>
<p>
{' '}
Note that both views have access restrictions. If you can't access either view and feel you should have
access, please contact oodikone@helsinki.fi.
</p>
</Message>
<div>
{getDivider(`Progress of students of the studyprogramme by starting year`, 'StudytrackProgress')}
<Toggle
cypress="GraduatedToggle"
toolTips={toolTips.GraduatedToggle}
firstLabel="Graduated included"
secondLabel="Graduated excluded"
value={graduated}
setValue={setGraduated}
/>
<div className="section-container">
<BarChart cypress="StudytrackProgress" data={progressData?.graphData} track={studyprogramme} />
<BasicDataTable
cypress="StudytrackProgress"
data={progressData?.creditTableStats}
track={studyprogramme}
titles={progressData?.creditTableTitles}
/>
</div>
{graduationData?.programmesBeforeOrAfterGraphStats && (
<>
{getDivider('Graduation times', 'AverageGraduationTimes')}
<div className="toggle-container">
<Toggle
cypress="GraduationTimeToggle"
firstLabel="Breakdown"
secondLabel="Median time"
value={showMedian}
setValue={setShowMedian}
/>
<div className="toggle-container">
<Toggle
cypress="YearToggle"
toolTips={toolTips.YearToggle}
firstLabel="Calendar year"
secondLabel="Academic year"
value={academicYear}
setValue={setAcademicYear}
/>
</div>
</div>
<div className={`section-container${doCombo ? '' : '-centered'}`}>
{showMedian ? displayMedian() : displayBreakdown()}
</div>
{getDivider(
studyprogramme.includes('KH')
? 'Primary master programme studies after this programme'
: 'Primary bachelor programme studies before this programme',
'ProgrammesBeforeOrAfter'
)}
<div className="section-container">
<StackedBarChart
cypress="ProgrammesBeforeOrAfter"
wideTable
data={graduationData?.programmesBeforeOrAfterGraphStats}
labels={graduationData?.years}
/>
<DataTable
wideTable
data={graduationData?.programmesBeforeOrAfterTableStats}
titles={graduationData?.programmesBeforeOrAfterTitles}
/>
</div>
</>
)}
</div>
</Segment>
)}
</div>
</>
)
}
export default ProgrammeView
@@ -0,0 +1,24 @@
import React from 'react'
import { Redirect, withRouter } from 'react-router-dom'
import { Segment } from 'semantic-ui-react'
import FacultyView from './FacultyView'
import ProgrammeView from './ProgrammeView'
const EvaluationOverview = props => {
const { id, level } = props.match.params
if (!(level === 'programme' || level === 'faculty') || !id) {
return <Redirect to="/" />
}
return (
<div className="segmentContainer">
<Segment className="contentSegment">
{level === 'programme' ? <ProgrammeView studyprogramme={id} /> : <FacultyView faculty={id} />}
</Segment>
</div>
)
}
export default withRouter(EvaluationOverview)
@@ -118,13 +118,11 @@ const Overview = ({
if (isError) return <h3>Something went wrong, please try refreshing the page.</h3>
let creditShortTitles = []
const creditShortTitles = ['Code', 'Total', 'Major', 'Non-major', 'Non-major other', 'Non-degree']
let transferShortTitles = []
if (special === 'SPECIAL_INCLUDED') {
creditShortTitles = ['Code', 'Total', 'Major', 'Non-major', 'Non-major other', 'Non-degree']
transferShortTitles = ['Code', 'Started', 'Graduated', 'Transferred in', 'Transferred away', 'Transferred to']
} else {
creditShortTitles = ['Code', 'Major']
transferShortTitles = ['Code', 'Started', 'Graduated']
}
@@ -222,7 +220,8 @@ const Overview = ({
)}
{special === 'SPECIAL_EXCLUDED' && (
<Message data-cy="FacultyExcludeSpecialsInfo">
Please note that the data might be inaccurate in some fields. Checking the values is in progress.
Please note: exluding the special studyrights does not have any effects to credits produced by faculty
-view.
</Message>
)}
{basics.isSuccess && basics.data && (
@@ -274,58 +273,6 @@ const Overview = ({
</div>
</>
)}
{credits.isSuccess && credits.data && (
<>
{getDivider(
'Credits produced by the faculty',
'CreditsProducedByTheFaculty',
credits?.data?.titles,
credits?.data?.tableStats,
credits?.data?.programmeTableStats,
credits?.data?.programmeNames
)}
<div className="section-container">
<div className="graph-container-narrow">
<StackedBarChart
cypress="CreditsProducedByTheFaculty"
data={credits?.data?.graphStats}
labels={credits?.data?.years}
wideTable="narrow"
/>
</div>
<div className="table-container-wide">
<InteractiveDataTable
cypress="CreditsProducedByTheFaculty"
dataStats={credits?.data?.tableStats}
dataProgrammeStats={credits?.data?.programmeTableStats}
programmeNames={credits?.data?.programmeNames}
sortedKeys={sortProgrammeKeys(
Object.keys(credits?.data.programmeTableStats).map(obj => [
obj,
credits?.data?.programmeNames[obj].code,
]),
faculty.code
).map(listObj => listObj[0])}
plotLinePlaces={getChartPlotLinePlaces(
sortProgrammeKeys(
Object.keys(credits?.data?.programmeTableStats).map(obj => [
obj,
credits?.data?.programmeNames[obj].code,
]),
faculty.code
)
)}
titles={credits?.data?.titles}
extraHeight="EXTRA HEIGHT"
sliceStart={2}
language={language}
yearsVisible={Array(credits?.data?.tableStats.length).fill(false)}
shortNames={creditShortTitles}
/>
</div>
</div>
</>
)}
{basics.isSuccess && basics.data && (
<>
{getDivider(
@@ -423,6 +370,58 @@ const Overview = ({
</div>
</>
)}
{credits.isSuccess && credits.data && (
<>
{getDivider(
'Credits produced by the faculty',
'CreditsProducedByTheFaculty',
credits?.data?.titles,
credits?.data?.tableStats,
credits?.data?.programmeTableStats,
credits?.data?.programmeNames
)}
<div className="section-container">
<div className="graph-container-narrow">
<StackedBarChart
cypress="CreditsProducedByTheFaculty"
data={credits?.data?.graphStats}
labels={credits?.data?.years}
wideTable="narrow"
/>
</div>
<div className="table-container-wide">
<InteractiveDataTable
cypress="CreditsProducedByTheFaculty"
dataStats={credits?.data?.tableStats}
dataProgrammeStats={credits?.data?.programmeTableStats}
programmeNames={credits?.data?.programmeNames}
sortedKeys={sortProgrammeKeys(
Object.keys(credits?.data.programmeTableStats).map(obj => [
obj,
credits?.data?.programmeNames[obj].code,
]),
faculty.code
).map(listObj => listObj[0])}
plotLinePlaces={getChartPlotLinePlaces(
sortProgrammeKeys(
Object.keys(credits?.data?.programmeTableStats).map(obj => [
obj,
credits?.data?.programmeNames[obj].code,
]),
faculty.code
)
)}
titles={credits?.data?.titles}
extraHeight="EXTRA HEIGHT"
sliceStart={2}
language={language}
yearsVisible={Array(credits?.data?.tableStats.length).fill(false)}
shortNames={creditShortTitles}
/>
</div>
</div>
</>
)}
</>
)}
</div>
@@ -110,7 +110,7 @@ const FigureContainer = ({ children, style }) => {
height: '100% !important',
}}
>
<div style={{ inset: 0, position: 'absolute', zIndex: 900, height: '100000vh' }}>
<div style={{ inset: '70px 0px 0px 0px', position: 'absolute', zIndex: 900, height: '100000vh' }}>
{isFullscreen && <OutPortal node={portalNode} />}
</div>
</Portal>
@@ -74,7 +74,7 @@ const FilterTray = () => {
</div>
{haveOptionsBeenChanged && (
<div style={{ marginTop: '0.5rem', textAlign: 'center' }}>
<Button compact size="mini" onClick={resetFilters}>
<Button compact size="mini" onClick={resetFilters} data-cy="reset-all-filters">
Reset All Filters
</Button>
</div>
@@ -17,7 +17,7 @@ const admissionTypeFilter = code => value => student =>
student.studyrights.some(
sr =>
sr.studyright_elements.some(sre => sre.code === code) &&
(value === null ? !sr.admission_type : sr.admission_type === value)
(value === null || value === 'Ei valintatapaa' ? !sr.admission_type : sr.admission_type === value)
)
const AdmissionTypeFilterCard = ({ options, onOptionsChange, withoutSelf, code }) => {
@@ -7,26 +7,20 @@ import { FilterType } from './filterType'
const filterTexts = {
[FilterType.ALL]: { label: 'All' },
[FilterType.PASSED]: { label: 'Passed' },
[FilterType.PASSED_AFTER_FAILURE]: { label: 'Passed After Failure' },
// [FilterType.FAILED]: { label: 'Failed' },
// [FilterType.FAILED_MANY_TIMES]: { label: 'Failed Multiple Times' },
// [FilterType.NOT_PARTICIPATED]: { label: 'Not Participated' },
// [FilterType.DID_NOT_PASS]: { label: "Didn't Pass" },
[FilterType.FAILED]: { label: 'Failed' },
[FilterType.ENROLLED_NO_GRADE]: { label: 'Enrolled, No Grade' },
// [FilterType.NOT_PARTICIPATED]: { label: 'Not Participated'},
}
// a bandaid solution to prevent oodikone crashing
const translate = {
ALL: 'all',
PASSED: 'passed',
PASSED_AFTER_FAILURE: 'retryPassed',
FAILED: 'failed',
FAILED_MANY_TIMES: 'failedMany',
NOT_PARTICIPATED: 'notParticipated',
DID_NOT_PASS: 'didNotPass',
ENROLLED_NO_GRADE: 'enrolledNoGrade',
// NOT_PARTICIPATED: 'notParticipated',
}
const CourseCard = ({ course, filterType, onChange }) => {
// Object.entries(filterTexts).map(([type, { label, info }]) => console.log(type, label, translate[type]))
// console.log('course', course)
const { language } = useLanguage()
const name = 'courseFilter'
@@ -47,7 +41,7 @@ const CourseCard = ({ course, filterType, onChange }) => {
fluid
className="mini"
button
data-cy={`${name}-${course.course.code}-dropdown`}
data-cy={`${name}-${course?.course?.code}-dropdown`}
style={{ marginTop: '0.5rem' }}
>
<Dropdown.Menu>
@@ -74,7 +68,7 @@ const CourseCard = ({ course, filterType, onChange }) => {
size="tiny"
onClick={clear}
icon
data-cy={`${name}-${course.course.code}-clear`}
data-cy={`${name}-${course?.course?.code}-clear`}
style={{ marginTop: '0.5rem' }}
>
<Icon name="close" />
@@ -2,9 +2,7 @@
export const FilterType = {
ALL: 'ALL',
PASSED: 'PASSED',
PASSED_AFTER_FAILURE: 'PASSED_AFTER_FAILURE',
FAILED: 'FAILED',
FAILED_MANY_TIMES: 'FAILED_MANY_TIMES',
NOT_PARTICIPATED: 'NOT_PARTICIPATED',
DID_NOT_PASS: 'DID_NOT_PASS',
ENROLLED_NO_GRADE: 'ENROLLED_NO_GRADE',
// NOT_PARTICIPATED: 'NOT_PARTICIPATED',
}
@@ -72,20 +72,17 @@ const CourseFilterCard = ({ courseStats, options, onOptionsChange }) => {
}
const createFilterFunc =
(key, invert = false) =>
({ studentNumber }, { students }) =>
Object.keys(students[key]).includes(studentNumber) !== invert
key =>
({ studentNumber }, { students }) => {
return Object.keys(students[key]).includes(studentNumber)
}
const filterFunctions = {
[FilterType.ALL]: createFilterFunc('all'),
[FilterType.PASSED]: createFilterFunc('passed'),
[FilterType.PASSED_AFTER_FAILURE]: createFilterFunc('retryPassed'),
// [FilterType.FAILED]: createFilterFunc('failed'),
// [FilterType.FAILED_MANY_TIMES]: createFilterFunc('failedMany'),
// [FilterType.DID_NOT_PASS]: createFilterFunc('didNotPass'),
// [FilterType.NOT_PARTICIPATED]: createFilterFunc('all', true),
// [FilterType.DID_NOT_PASS]: ({ studentNumber }, { students }) =>
// !Object.keys(students.all).includes(studentNumber) || Object.keys(students.failed).includes(studentNumber),
[FilterType.FAILED]: createFilterFunc('failed'),
[FilterType.ENROLLED_NO_GRADE]: createFilterFunc('enrolledNoGrade'),
// [FilterType.NOT_PARTICIPATED]: createFilterFunc('not_participated'),
}
const filter = createFilter({
@@ -7,32 +7,6 @@ import { selectViewFilters, setFilterOptions, resetViewFilters, resetFilter } fr
import FilterViewContext from './FilterViewContext'
import FilterTray from './FilterTray'
/* const getFilterContext = (filter, students, options, precomputed) => ({
options: options[filter.key] ?? filter.defaultOptions,
students,
precomputed: precomputed[filter.key],
})
const precompute = (students, filters, options) =>
_.chain(filters)
.filter(({ precompute }) => precompute)
.map(({ key, precompute, defaultOptions }) => precompute({
students,
options: options[key] ?? defaultOptions,
precomputed: null,
}))
.fromPairs()
.value()
const applyFilters = (students, filters, options, precomputed) => {
return filters
.map((filter) => [filter, { students, options: options[filter.key], precomputed: precomputed[filter.key] }])
.filter(([{ key, isActive }, ctx]) => isActive(options[key], ctx))
.reduce((students, [{ key, filter }, ctx]) => {
return students.filter(student => filter(student, options[key], precomputed[key]))
}, students)
} */
const resolveFilterOptions = (store, filters, initialOptions) => {
return fp.flow(
fp.map(({ key, defaultOptions }) => [
@@ -4,9 +4,9 @@ import { Accordion } from 'semantic-ui-react'
import useFilters from 'components/FilterView/useFilters'
import studyPlanFilter from 'components/FilterView/filters/hops'
import { creditDateFilter } from 'components/FilterView/filters'
import { useGetProgressCriteriaQuery } from 'redux/programmeProgressCriteria'
import { useLocalStorage } from '../../common/hooks'
import { useGetAuthorizedUserQuery } from '../../redux/auth'
import CreditAccumulationGraphHighCharts from '../CreditAccumulationGraphHighCharts'
import PopulationStudents from '../PopulationStudents'
import PopulationCourses from '../PopulationCourses'
@@ -37,6 +37,7 @@ const PopulationDetails = ({
const studentTableRef = useRef()
const { useFilterSelector } = useFilters()
const creditDateFilterOptions = useFilterSelector(creditDateFilter.selectors.selectOptions)
const criteria = useGetProgressCriteriaQuery({ programmeCode: query?.studyRights?.programme })
const handleClick = index => {
const indexes = [...activeIndex].sort()
@@ -190,6 +191,9 @@ const PopulationDetails = ({
language={language}
filteredStudents={filteredStudents}
dataExport={dataExport}
criteria={criteria?.data}
programmeCode={query?.studyRights?.programme}
year={query?.year}
/>
</div>
),
@@ -435,7 +435,7 @@ const PopulationSearchForm = props => {
</Grid.Row>
</Grid>
<Message error color="blue" header={errorText} />
<Form.Button onClick={handleSubmit} disabled={isQueryInvalid || query.months < 0}>
<Form.Button color="blue" onClick={handleSubmit} disabled={isQueryInvalid || query.months < 0}>
See class
</Form.Button>
<SearchHistory items={searchHistory} updateItem={updateItemInSearchHistory} handleSearch={pushQueryToUrl} />
@@ -3,14 +3,14 @@ import React from 'react'
import { useSelector } from 'react-redux'
import { Dropdown } from 'semantic-ui-react'
import xlsx from 'xlsx'
import { getTextIn, reformatDate } from '../../common'
import { getTextIn, reformatDate, getStudentTotalCredits } from '../../common'
import sendEvent from '../../common/sendEvent'
import { PRIORITYCODE_TEXTS } from '../../constants'
import useLanguage from '../LanguagePicker/useLanguage'
const sendAnalytics = sendEvent.populationStudents
export default ({ students }) => {
export default ({ students, programmeCode }) => {
const { language } = useLanguage()
const mandatoryCourses = useSelector(({ populationMandatoryCourses }) => populationMandatoryCourses.data)
const populationStatistics = useSelector(({ populations }) => populations.data)
@@ -133,14 +133,46 @@ export default ({ students }) => {
return res ? Number(res[0]) : Number.MAX_VALUE
},
])
// Exact same code in GeneralTab REFACTOR (function in common/index.js file?)
const studentToStudyrightStartMap = students.reduce((res, sn) => {
const currentStudyright = sn.studyrights?.find(studyright =>
studyright.studyright_elements.some(e => e.code === programmeCode)
)
if (currentStudyright?.studyrightid && currentStudyright.studyrightid.slice(-2) === '-2') {
const bachelorId = currentStudyright.studyrightid.replace(/-2$/, '-1')
const bacherlorStudyright = sn.studyrights.find(studyright => studyright.studyrightid === bachelorId)
res[sn.studentNumber] = bacherlorStudyright?.startdate || null
} else {
res[sn.studentNumber] = currentStudyright?.startdate || null
}
return res
}, {})
const studentToProgrammeStartMap = students.reduce((res, sn) => {
const targetStudyright = flatten(
sn.studyrights.reduce((acc, curr) => {
acc.push(curr.studyright_elements)
return acc
}, [])
).filter(e => e.code === programmeCode)
// clean up odd bachelor start dates, (givendate)
res[sn.studentNumber] = new Date(
Math.max(new Date(targetStudyright[0]?.startdate), new Date(studentToStudyrightStartMap[sn.studentNumber]))
)
return res
}, {})
const worksheet = xlsx.utils.json_to_sheet(
students.map(s => ({
'last name': s.lastname,
'given names': s.firstnames,
'student number': s.studentNumber,
'all credits': s.allCredits,
'all credits': s.allCredits ? s.allCredits : s.credits,
'hops credits': s.hopsCredits,
'credits since start': s.credits,
'credits since start': getStudentTotalCredits({
...s,
courses: s.courses.filter(c => new Date(c.date) >= studentToProgrammeStartMap[s.studentNumber]),
}),
'phone number': s.phoneNumber,
email: s.email,
'transferred from': s.transferredStudyright ? transferFrom(s) : '',
@@ -152,6 +184,7 @@ export default ({ students }) => {
'start of studyright': reformatDate(getStartOfStudyright(s.studyrights), 'YYYY-MM-DD'),
'started in programme': reformatDate(getStartedInProgramme(s.studyrights), 'YYYY-MM-DD'),
'admission type': parseInt(queryYear, 10 >= 2020) ? getAdmissionType(s.studyrights) : undefined,
bachelor: s.option ? getTextIn(s.option.name, language) : '',
'updated at': reformatDate(s.updatedAt, 'YYYY-MM-DD hh:mm:ss'),
'mandatory total passed': totalMandatoryPassed(s.studentNumber, codes),
...sortedMandatory.reduce((acc, m) => {
@@ -101,7 +101,6 @@ const PopulationStatistics = () => {
const title =
location.search === '' ? 'Class statistics' : `${getTextIn(programmeName, language)} ${getYearText(query?.year)}`
return (
<FilterView
name="PopulationStatistics"
@@ -138,7 +137,7 @@ const PopulationStatistics = () => {
queryIsSet={queryIsSet}
query={query}
isLoading={isLoading}
dataExport={<DataExport students={filteredStudents} />}
dataExport={<DataExport students={filteredStudents} programmeCode={query?.studyRights?.programme} />}
allStudents={samples}
selectedStudentsByYear={selectedStudentsByYear}
filteredStudents={filteredStudents}
@@ -0,0 +1,449 @@
import React, { useMemo } from 'react'
import { useSelector } from 'react-redux'
import { Link } from 'react-router-dom'
import { Tab, Item, Icon, Message } from 'semantic-ui-react'
import SortableTable from 'components/SortableTable'
import sendEvent from 'common/sendEvent'
import { getTextIn } from 'common'
import { keyBy } from 'lodash'
const sendAnalytics = sendEvent.populationStudents
const ProgressTable = ({ criteria, students, months, programme }) => {
const mandatoryCourses = useSelector(state => state?.populationMandatoryCourses?.data)
const namesVisible = useSelector(state => state?.settings?.namesVisible)
const findRowContent = (s, courseCode, year, label) => {
if (courseCode.includes('Credits'))
return s.criteriaProgress[year] && s.criteriaProgress[year].credits ? (
<Icon fitted name="check" title="Checked" color="green" />
) : null
const courses = s.courses.filter(
course => course.course_code === courseCode || criteria.allCourses[label][courseCode].includes(course.course_code)
)
if (courses && courses.some(course => course.credittypecode === 9))
return <Icon name="clipboard check" title="Credit transfer" color="green" />
if (courses && courses.some(course => course.passed))
return <Icon fitted name="check" title="Passed" color="green" />
if (courses && courses.some(course => course.passed === false))
return <Icon fitted name="times" title="Failed" color="red" />
// if (failedManyTimes) return <Icon fitted name="times" color="red" />
if (s.enrollments && s.enrollments.map(course => course.course_code).includes(courseCode))
return <Icon fitted name="minus" title="Unfinished" color="grey" />
return null
}
const findCsvText = (s, courseCode, year, label) => {
if (courseCode.includes('Credits'))
return s.criteriaProgress[year] && s.criteriaProgress[year].credits ? 'Passed' : ''
const courses = s.courses.filter(
course =>
course.course_code === courseCode || criteria?.allCourses[label][courseCode].includes(course.course_code)
)
if (courses && courses.some(course => course.credittypecode === 9)) return 'Credit transfer'
if (courses && courses.some(course => course.passed)) return 'Passed'
if (courses && courses.some(course => course.passed === false)) return 'Failed'
// if (failedManyTimes) return <Icon fitted name="times" color="red" />
if (s.enrollments && s.enrollments.map(course => course.course_code).includes(courseCode)) return 'Unfinished'
return ''
}
const columns = useMemo(() => {
const studentColumns = []
if (namesVisible) {
studentColumns.push(
{
key: 'lastname_visible',
title: 'Last name',
getRowVal: s => s.lastname,
cellProps: { title: 'last name' },
export: false,
child: true,
},
{
key: 'firstname_visible',
title: 'Given names',
getRowVal: s => s.firstnames,
cellProps: { title: 'first names' },
export: false,
child: true,
}
)
}
studentColumns.push({
key: 'studentnumber-parent',
title: 'Student Number',
cellProps: { title: 'student number', className: 'studentNumber' },
getRowVal: s => (s.total ? '*' : s.studentNumber),
getRowContent: s => (
<div>
<span>{s.studentNumber}</span>
<Item
as={Link}
to={`/students/${s.studentNumber}`}
onClick={() => {
sendAnalytics('Student details button clicked', 'Student progress table')
}}
>
<Icon style={{ borderLeft: '1em' }} name="user outline" />
</Item>
</div>
),
child: true,
})
const courses = keyBy(mandatoryCourses, 'code')
const labelCriteria = Object.keys(criteria.courses).reduce((acc, year) => {
acc[year] = [
{ code: `Credits: ${criteria.credits[year]}`, name: '' },
...[...criteria.courses[year]]
.sort((a, b) => a.localeCompare(b))
.map(courseCode => ({
code: courseCode,
name: courses[courseCode] ? courses[courseCode].name : '',
})),
{ code: `Total`, name: '' },
]
return acc
}, {})
const criteriaHeaders = [
{ title: months < 12 ? 'Academic Year 1 (in progress)' : 'Academic Year 1', year: 'year1', label: 'yearOne' },
{ title: months < 24 ? 'Academic Year 2 (in progress)' : 'Academic Year 2', year: 'year2', label: 'yearTwo' },
{ title: months < 36 ? 'Academic Year 3 (in progress)' : 'Academic Year 3', year: 'year3', label: 'yearThree' },
]
const getEnrollmentValue = enrollmentObj => {
if (enrollmentObj?.enrollmenttype === 1) return 'Present'
if (enrollmentObj?.enrollmenttype === 2) return 'Absent'
return 'Inactive'
}
const columns = []
const createContent = (labels, year, label, enrollStatusIdx) => {
return labels.map(m => ({
key: `${year}-${m.code}`,
title: (
<div
key={`${year}-${m.code}-${getTextIn(m.name)}`}
style={{ maxWidth: '8em', whiteSpace: 'normal', overflow: 'hidden', width: 'max-content' }}
>
<div key={`${m.code}-${year}`}>{m.code}</div>
<div key={`${getTextIn(m.name)}`} style={{ color: 'gray', fontWeight: 'normal' }}>
{getTextIn(m.name)}
</div>
</div>
),
textTitle:
m.name === ''
? `${m.code} ${enrollStatusIdx === 0 ? enrollStatusIdx + 1 : enrollStatusIdx}`
: `${m.code}-${getTextIn(m.name)}`,
headerProps: { title: `${m.code}, ${year}` },
cellProps: {
style: { verticalAlign: 'middle', textAlign: 'center' },
},
getRowVal: s => {
if (m.code.includes('Total')) return s.criteriaProgress[year] ? s.criteriaProgress[year].totalSatisfied : 0
return findCsvText(s, m.code, year, label)
},
getRowExportVal: s => {
if (m.code.includes('Total')) return s.criteriaProgress[year] ? s.criteriaProgress[year].totalSatisfied : 0
return findCsvText(s, m.code, year, label)
},
getRowContent: s => {
if (m.code.includes('Total')) return s.criteriaProgress[year] ? s.criteriaProgress[year].totalSatisfied : 0
return findRowContent(s, m.code, year, label)
},
child: true,
}))
}
columns.push(
{
key: 'general',
title: <b>Labels:</b>,
textTitle: null,
parent: true,
children: studentColumns,
},
{
key: criteriaHeaders[0].title,
title: (
<div style={{ whiteSpace: 'nowrap', overflow: 'hidden' }}>
<div>{criteriaHeaders[0].title}</div>
</div>
),
textTitle: null,
parent: true,
children: createContent(
labelCriteria[criteriaHeaders[0].label],
criteriaHeaders[0].year,
criteriaHeaders[0].label,
0
),
},
{
key: 'hidden-1',
textTitle: null,
mergeHeader: true,
parent: true,
children: [
{
key: `absence-first-Fall`,
export: true,
forceToolsMode: 'none',
textTitle: `Enrollment Status FALL 1`,
cellProps: { style: { display: 'none' } },
getRowVal: s => getEnrollmentValue(s.semesterenrollments[0]),
child: true,
},
{
key: `absence-first-Spring`,
export: true,
forceToolsMode: 'none',
textTitle: `Enrollment Status SPRING 1`,
cellProps: { style: { display: 'none' } },
getRowVal: s => getEnrollmentValue(s.semesterenrollments[1]),
child: true,
},
{
key: 'second_academic',
export: true,
forceToolsMode: 'none',
textTitle: ' ',
cellProps: { style: { display: 'none' } },
getRowVal: () => ' ',
child: true,
},
],
}
)
if (months > 12) {
columns.push(
{
key: criteriaHeaders[1].title,
title: (
<div style={{ whiteSpace: 'nowrap', overflow: 'hidden' }}>
<div>{criteriaHeaders[1].title}</div>
</div>
),
textTitle: null,
parent: true,
children: createContent(
labelCriteria[criteriaHeaders[1].label],
criteriaHeaders[1].year,
criteriaHeaders[1].label,
2
),
},
{
key: 'hidden-2',
textTitle: null,
mergeHeader: true,
parent: true,
children: [
{
key: `absence-second-Fall`,
export: true,
forceToolsMode: 'none',
textTitle: `Enrollment Status FALL 2`,
cellProps: { style: { display: 'none' } },
getRowVal: s => getEnrollmentValue(s.semesterenrollments[2]),
child: true,
},
{
key: `absence-second-Spring`,
export: true,
forceToolsMode: 'none',
textTitle: `Enrollment Status SPRING 2`,
cellProps: { style: { display: 'none' } },
getRowVal: s => getEnrollmentValue(s.semesterenrollments[3]),
child: true,
},
{
key: 'third_academic',
export: true,
forceToolsMode: 'none',
textTitle: ' ',
cellProps: { style: { display: 'none' } },
getRowVal: () => ' ',
child: true,
},
],
}
)
}
if (months > 24) {
columns.push(
{
key: criteriaHeaders[2].title,
title: (
<div style={{ whiteSpace: 'nowrap', overflow: 'hidden' }}>
<div>{criteriaHeaders[2].title}</div>
</div>
),
textTitle: null,
parent: true,
children: createContent(
labelCriteria[criteriaHeaders[2].label],
criteriaHeaders[2].year,
criteriaHeaders[2].label,
4
),
},
{
key: 'hidden-3',
textTitle: null,
mergeHeader: true,
parent: true,
children: [
{
key: `absence-third-Fall`,
export: true,
forceToolsMode: 'none',
textTitle: `Enrollment Status FALL 3`,
cellProps: { style: { display: 'none' } },
getRowVal: s => getEnrollmentValue(s.semesterenrollments[4]),
child: true,
},
{
key: `absence-third-Spring`,
export: true,
forceToolsMode: 'none',
textTitle: `Enrollment Status SPRING 3`,
cellProps: { style: { display: 'none' } },
getRowVal: s => getEnrollmentValue(s.semesterenrollments[5]),
child: true,
},
{
key: 'empty',
export: true,
forceToolsMode: 'none',
textTitle: ' ',
cellProps: { style: { display: 'none' } },
getRowVal: () => ' ',
child: true,
},
],
}
)
}
const columnsToHide = [
{
key: 'hidden-phoneNumber',
export: true,
forceToolsMode: 'none',
textTitle: `Phone number`,
headerProps: { style: { display: 'none' } },
cellProps: { style: { display: 'none' } },
getRowVal: s => s.phoneNumber,
child: true,
},
{
key: 'hidden-email',
export: true,
forceToolsMode: 'none',
textTitle: 'Email',
headerProps: { style: { display: 'none' } },
cellProps: { style: { display: 'none' } },
getRowVal: s => s.email,
child: true,
},
{
key: 'hidden-secondary-email',
export: true,
forceToolsMode: 'none',
textTitle: 'Secondary Email',
headerProps: { style: { display: 'none' } },
cellProps: { style: { display: 'none' } },
getRowVal: s => s.secondaryEmail,
child: true,
},
{
key: 'lastname-hidden',
textTitle: 'Last Name',
export: true,
forceToolsMode: 'none',
headerProps: { style: { display: 'none' } },
getRowVal: s => s.lastname,
cellProps: { style: { display: 'none' } },
child: true,
},
{
key: 'firstname-hidden',
textTitle: 'First Names',
export: true,
forceToolsMode: 'none',
headerProps: { style: { display: 'none' } },
getRowVal: s => s.firstnames,
cellProps: { style: { display: 'none' } },
child: true,
},
]
columns.push({
key: 'hiddenFiles',
title: '',
mergeHeader: true,
textTitle: null,
parent: true,
children: columnsToHide,
})
return columns
}, [criteria, students, mandatoryCourses, getTextIn, namesVisible])
const isCriteriaSet =
criteria && Object.keys(criteria.courses).some(yearCourses => criteria.courses[yearCourses].length > 0)
const data = useMemo(() => {
return students
}, [students])
return (
<Tab.Pane>
<div style={{ display: 'flex' }}>
<div style={{ maxHeight: '80vh', width: '100%' }}>
<h5>
Criteria can be changed{' '}
<Link
to={`/study-programme/${programme}?p_m_tab=0&p_tab=3`}
onClick={() => {
sendAnalytics('No criteria defined button clicked', 'Degree courses tab')
}}
>
here.
</Link>{' '}
Please refresh page after changes.
</h5>
<Message style={{ maxWidth: '800px', fontSize: '16px' }}>
<p>
<Icon fitted name="check" color="green" />: Student has passed the course.{' '}
<Icon fitted name="clipboard check" color="green" />: Student has credit transfer for the course. <br />
<Icon fitted name="times" color="red" />: Student has failed the course.{' '}
<Icon fitted name="minus" color="grey" />: Student has enrolled, but has not received any grade from the
course.
</p>
</Message>
{isCriteriaSet ? (
<SortableTable
style={{ height: '80vh' }}
tableId="progress-of-population-students"
title={`Progress of population's students after predefined criteria`}
getRowKey={s => s.studentNumber}
tableProps={{
collapsing: true,
basic: true,
compact: 'very',
padded: false,
celled: true,
}}
columns={columns}
data={data}
/>
) : (
<div>
<h3>There is no criteria set for this programme.</h3>
</div>
)}
</div>
</div>
</Tab.Pane>
)
}
export default ProgressTable
@@ -19,6 +19,7 @@ import InfoBox from '../Info/InfoBox'
import CheckStudentList from './CheckStudentList'
import TagPopulation from '../TagPopulation'
import TagList from '../TagList'
import ProgressTable from './ProgressTable'
import './populationStudents.css'
import GeneralTab from './StudentTable/GeneralTab'
import sendEvent, { ANALYTICS_CATEGORIES } from '../../common/sendEvent'
@@ -337,6 +338,8 @@ const Panes = ({
queryStudyrights,
from,
to,
criteria,
months,
}) => {
const { handleTabChange } = useTabChangeAnalytics(
ANALYTICS_CATEGORIES.populationStudents,
@@ -383,7 +386,7 @@ const Panes = ({
<h3>
No tags defined. You can define them{' '}
<Link
to={`/study-programme/${queryStudyrights[0]}?p_m_tab=0&p_tab=6`}
to={`/study-programme/${queryStudyrights[0]}?p_m_tab=0&p_tab=4`}
onClick={() => {
sendAnalytics('No tags defined button clicked', 'Tags tab')
}}
@@ -408,6 +411,17 @@ const Panes = ({
</Tab.Pane>
),
},
{
menuItem: 'Progress',
render: () => (
<ProgressTable
students={filteredStudents}
criteria={criteria}
months={months}
programme={queryStudyrights[0]}
/>
),
},
]
const panes = panesAvailable.filter(pane => visiblePanes.includes(pane.menuItem))
@@ -441,6 +455,7 @@ const PopulationStudents = ({
studyGuidanceGroup,
from,
to,
criteria,
}) => {
const [state, setState] = useState({})
const studentRef = useRef()
@@ -449,6 +464,7 @@ const PopulationStudents = ({
const { data: tags } = useSelector(({ tags }) => tags)
const { query } = useSelector(({ populations }) => populations)
const queryStudyrights = query ? Object.values(query.studyRights) : []
const months = query ? query.months : 0
const prevShowList = usePrevious(showList)
const { isAdmin } = useGetAuthorizedUserQuery()
const admin = isAdmin
@@ -472,7 +488,6 @@ const PopulationStudents = ({
}, [prevShowList])
if (filteredStudents.length === 0) return null
return (
<>
<span style={{ marginRight: '0.5rem' }} ref={studentRef}>
@@ -487,10 +502,12 @@ const PopulationStudents = ({
variant={variant}
studentToTargetCourseDateMap={studentToTargetCourseDateMap}
tags={tags}
criteria={criteria}
studyGuidanceGroup={studyGuidanceGroup}
coursecode={coursecode}
from={from}
to={to}
months={months}
/>
</>
)
@@ -498,14 +515,16 @@ const PopulationStudents = ({
const PopulationStudentsContainer = ({ ...props }) => {
const { variant } = props
if (!['population', 'customPopulation', 'coursePopulation', 'studyGuidanceGroupPopulation'].includes(variant)) {
throw new Error(`${variant} is not a proper variant!`)
}
const contentByVariant = {
population: {
panesToInclude: ['General', 'Courses', 'Tags'],
panesToInclude:
props.year === 'All' || (props.programmeCode && !props.programmeCode.includes('KH'))
? ['General', 'Courses', 'Tags']
: ['General', 'Courses', 'Tags', 'Progress'],
infotoolTipContent: infotoolTips.PopulationStatistics.StudentsClass,
},
coursePopulation: {
@@ -41,6 +41,7 @@ const Trends = React.lazy(() => retry(() => import('../Trends')))
const StudyGuidanceGroups = React.lazy(() => retry(() => import('../StudyGuidanceGroups')))
const FacultyStatistics = React.lazy(() => retry(() => import('../FacultyStatistics')))
const CustomOpenUniPopulations = React.lazy(() => retry(() => import('../CustomOpenUniPopulation')))
const EvaluationOverview = React.lazy(() => retry(() => import('../EvaluationOverview')))
const routes = {
students: '/students/:studentNumber?',
@@ -55,6 +56,7 @@ const routes = {
studyGuidanceGroups: '/studyguidancegroups/:groupid?',
faculties: '/faculties/:facultyCode?',
customOpenUniPopulation: '/openunipopulation',
evaluationOverview: '/evaluationoverview/:level?/:id?',
}
const Routes = () => (
@@ -113,6 +115,13 @@ const Routes = () => (
path={routes.studyGuidanceGroups}
component={StudyGuidanceGroups}
/>
<ProtectedRoute
requiredRoles={['admin']}
requireUserHasRights
exact
path={routes.evaluationOverview}
component={EvaluationOverview}
/>
<Redirect from="/cool-data-science" to="/trends" />
<Redirect to="/" />
</Switch>
@@ -446,7 +446,7 @@ const ColumnHeaderContent = React.memo(({ column, colSpan, state, dispatch, rowS
}}
>
<SizeMeasurer onSizeChange={onTitleSizeChange} style={{ display: 'inline-flex', alignItems: 'center' }}>
<p style={{ whiteSpace: 'pre' }}>{column.title}</p>
{typeof column.title === 'string' ? <p style={{ whiteSpace: 'pre' }}>{column.title}</p> : column.title}
{column.helpText && !forcedTitleWidth && (
<Icon
name="question circle outline"
@@ -27,11 +27,11 @@ const CourseParticipationTable = ({ student, language, clearCourseStats, studyri
if (isStudyModuleCredit) {
icon = <Icon name="certificate" color="purple" />
} else if (c.credittypecode === 9) {
icon = <Icon name="file alternative outline" title="Credit transfer" color="green" />
icon = <Icon name="clipboard check" title="Credit transfer" color="green" />
} else if (passed) {
icon = <Icon name="check circle outline" color="green" />
icon = <Icon name="check" color="green" />
} else {
icon = <Icon name="circle outline" color="red" />
icon = <Icon name="times" color="red" />
}
if (!courseRowsByAcademicYear[`${new Date(date).getFullYear()}-${new Date(date).getFullYear() + 1}`]) {
@@ -83,6 +83,7 @@ const DegreeCourses = ({ studyProgramme, criteria, setCriteria, setExclusion, re
const setExclusionButton = course => (
<Button
color="blue"
onClick={() => {
setExclusion(studyProgramme, [course.code])
sendAnalytics('Set hidden button pressed', studyProgramme, course.name.fi)
@@ -93,6 +94,7 @@ const DegreeCourses = ({ studyProgramme, criteria, setCriteria, setExclusion, re
)
const deleteButton = course => (
<Button
color="blue"
onClick={() => {
removeExclusion(studyProgramme, [course.visible.id])
sendAnalytics('Set visible button pressed', studyProgramme, course.name.fi)
@@ -104,6 +106,7 @@ const DegreeCourses = ({ studyProgramme, criteria, setCriteria, setExclusion, re
const setCriteriaButton = (course, criterionYear) => (
<Button
color="green"
onClick={() => {
const year = getYear(criterionYear)
const courses = criteria.courses ? [...criteria.courses[year], course.code] : [course.code]
@@ -115,6 +118,7 @@ const DegreeCourses = ({ studyProgramme, criteria, setCriteria, setExclusion, re
)
const deleteCriteriaButton = (course, criterionYear) => (
<Button
color="red"
onClick={() => {
const year = getYear(criterionYear)
const filteredCourses = criteria?.courses[year]?.filter(courseCode => courseCode !== course.code)
@@ -177,6 +181,7 @@ const DegreeCourses = ({ studyProgramme, criteria, setCriteria, setExclusion, re
const showAllButton = module => (
<Button
color="blue"
onClick={() => {
deleteAll(module)
sendAnalytics('ShowAllButton pressed', module)
@@ -188,6 +193,7 @@ const DegreeCourses = ({ studyProgramme, criteria, setCriteria, setExclusion, re
const hideAllButton = module => (
<Button
color="blue"
onClick={() => {
excludeAll(module)
sendAnalytics('HideAllButton pressed', module)
@@ -236,9 +242,9 @@ const DegreeCourses = ({ studyProgramme, criteria, setCriteria, setExclusion, re
<Message.Header>Change visibility of degree courses and select criteria for academic years</Message.Header>
<p>
Here you can change visibility of degree courses as and set course and credits criteria. Each academic
year have their own course and credits criteria. The progress of the students by these criteria will be
shown in class statistics view. These statistics will be available at the latest at Friday on 10.2..
year have their own course and credits criteria.
</p>
<p>The progress of the students by these criteria will be shown in class statistics view.</p>
</Message>
<Container>
<h5>Credit criteria</h5>
@@ -263,7 +269,7 @@ const DegreeCourses = ({ studyProgramme, criteria, setCriteria, setExclusion, re
onChange={e => setCreditsLimit3(e.target.value)}
/>
</Form.Group>
<Form.Button content="Save credit changes" onClick={setCreditsLimitCriteria} />
<Form.Button color="green" content="Save credit changes" onClick={setCreditsLimitCriteria} />
</Form>
</Container>
</>
@@ -43,7 +43,7 @@ const TagModal = ({ tags, studytrack, createMultipleStudentTag, pending, success
return (
<Modal
trigger={
<Button size="small" disabled={!tags.length} onClick={() => setModalOpen(true)}>
<Button size="small" disabled={!tags.length} color="blue" onClick={() => setModalOpen(true)}>
Add tags to students
</Button>
}
@@ -53,7 +53,11 @@ const Tags = ({ createTag, deleteTag, getTagsByStudytrack, tags, studyprogramme
setTagname(target.value)
}
const deleteButton = tag => <Button onClick={() => setConfirm(tag)}>Delete</Button>
const deleteButton = tag => (
<Button negative onClick={() => setConfirm(tag)}>
Delete
</Button>
)
const populationUrl = tag => {
if (!tag.year) {
@@ -160,7 +164,11 @@ const Tags = ({ createTag, deleteTag, getTagsByStudytrack, tags, studyprogramme
<label>Personal tag</label>
<Form.Checkbox toggle checked={personal} onClick={() => setPersonal(!personal)} />
</Form.Field>
<Button disabled={!tagname.trim() || tags.some(t => t.tagname === tagname.trim())} onClick={handleSubmit}>
<Button
color="green"
disabled={!tagname.trim() || tags.some(t => t.tagname === tagname.trim())}
onClick={handleSubmit}
>
{' '}
Create new tag{' '}
</Button>
@@ -15,7 +15,7 @@ const CourseTabs = ({ data, showStudents, handleShowStudentsChange }) => {
]
const panes = paneTypes.map(({ icon, label }) => ({
menuItem: { icon, content: label },
menuItem: { icon, content: label, key: label },
render: () => (
<Tab.Pane>
<Toggle
@@ -15,83 +15,135 @@ const getColumns = (language, showStudents) => {
if (showStudents) {
columns = [
{
key: 'code',
mergeHeader: true,
merge: true,
key: 'Course',
title: 'Course info',
parent: true,
children: [
{
key: 'course_code',
title: 'Code ',
export: true,
getRowVal: course => course.code,
getRowContent: course => course.code,
key: 'code',
mergeHeader: true,
merge: true,
children: [
{
key: 'course_code',
title: 'Code ',
export: true,
getRowVal: course => course.code,
getRowContent: course => course.code,
},
{
key: 'go-to-course',
export: false,
getRowContent: course => (
<Item
as={Link}
to={`/coursestatistics?courseCodes=["${encodeURIComponent(
course.code
)}"]&separate=false&unifyOpenUniCourses=false`}
>
<Icon
name="level up alternate"
onClick={() => sendAnalytics('Courses of Population course stats button clicked', course.code)}
/>
</Item>
),
},
],
},
{
key: 'go-to-course',
export: false,
getRowContent: course => (
<Item
as={Link}
to={`/coursestatistics?courseCodes=["${encodeURIComponent(
course.code
)}"]&separate=false&unifyOpenUniCourses=false`}
>
<Icon
name="level up alternate"
onClick={() => sendAnalytics('Courses of Population course stats button clicked', course.code)}
/>
</Item>
),
key: 'name',
title: 'Name',
getRowVal: course => course.name[language],
getRowContent: course => course.name[language],
},
],
},
{
key: 'name',
title: 'Name',
getRowVal: course => course.name[language],
getRowContent: course => course.name[language],
},
{
key: 'total',
title: 'All students',
cellStyle: { textAlign: 'right' },
filterType: 'range',
getRowVal: course => course.totalAllStudents,
getRowContent: course => course.totalAllStudents,
title: '',
parent: true,
children: [
{
key: 'total',
title: 'Total',
cellStyle: { textAlign: 'right' },
filterType: 'range',
getRowVal: course => course.totalAllStudents,
getRowContent: course => course.totalAllStudents,
},
],
},
{
key: 'totalOwnProgramme',
title: 'Major students',
cellStyle: { textAlign: 'right' },
filterType: 'range',
getRowVal: course => course.totalProgrammeStudents,
getRowContent: course => course.totalProgrammeStudents,
key: 'breakdown',
title: 'Breakdown of Total',
parent: true,
children: [
{
key: 'passed',
title: 'Passed',
cellStyle: { textAlign: 'right' },
filterType: 'range',
getRowVal: course => course.totalAllPassed,
getRowContent: course => course.totalAllPassed,
},
{
key: 'not-completed',
title: 'Not Completed',
helpText: toolTips.NotCompleted,
cellStyle: { textAlign: 'right' },
filterType: 'range',
getRowVal: course => course.totalAllNotCompleted,
getRowContent: course => course.totalAllNotCompleted,
},
],
},
{
key: 'totalOtherProgramme',
title: 'Non-major students',
cellStyle: { textAlign: 'right' },
filterType: 'range',
getRowVal: course => course.totalOtherProgrammeStudents,
getRowContent: course => course.totalOtherProgrammeStudents,
key: 'breakdown-passed',
title: 'Breakdown Statistics of Passed Students',
parent: true,
children: [
{
key: 'totalOwnProgramme',
title: 'Major students',
cellStyle: { textAlign: 'right' },
filterType: 'range',
getRowVal: course => course.totalProgrammeStudents,
getRowContent: course => course.totalProgrammeStudents,
},
{
key: 'totalOtherProgramme',
title: 'Non-major students',
cellStyle: { textAlign: 'right' },
filterType: 'range',
getRowVal: course => course.totalOtherProgrammeStudents,
getRowContent: course => course.totalOtherProgrammeStudents,
},
{
key: 'totalWithoutStudyright',
title: 'Non-degree students',
helpText: toolTips.NonDegree,
cellStyle: { textAlign: 'right' },
filterType: 'range',
getRowVal: course => course.totalWithoutStudyrightStudents,
getRowContent: course => course.totalWithoutStudyrightStudents,
},
],
},
{
key: 'totalWithoutStudyright',
title: 'Non-degree students',
helpText: toolTips.NonDegree,
cellStyle: { textAlign: 'right' },
filterType: 'range',
getRowVal: course => course.totalWithoutStudyrightStudents,
getRowContent: course => course.totalWithoutStudyrightStudents,
},
{
key: 'transfer',
title: 'Transferred students',
helpText: toolTips.TransferredCourses,
cellStyle: { textAlign: 'right' },
filterType: 'range',
getRowVal: course => course.totalTransferStudents,
getRowContent: course => course.totalTransferStudents,
key: 'exluded',
title: 'Not Included to Passed',
parent: true,
children: [
{
key: 'transfer',
title: 'Transferred students',
helpText: toolTips.TransferredCourses,
cellStyle: { textAlign: 'right' },
filterType: 'range',
getRowVal: course => course.totalTransferStudents,
getRowContent: course => course.totalTransferStudents,
},
],
},
]
} else {
@@ -56,6 +56,8 @@ const ProgrammeCoursesOverview = ({ studyProgramme, academicYear, setAcademicYea
(acc, curr) => {
if (yearRange.includes(Number(curr[0]))) {
acc.totalAllStudents += curr[1].totalAllStudents
acc.totalAllPassed += curr[1].totalPassed
acc.totalAllNotCompleted += curr[1].totalNotCompleted
acc.totalAllCredits += curr[1].totalAllCredits
acc.totalProgrammeStudents += curr[1].totalProgrammeStudents
acc.totalProgrammeCredits += curr[1].totalProgrammeCredits
@@ -71,6 +73,8 @@ const ProgrammeCoursesOverview = ({ studyProgramme, academicYear, setAcademicYea
},
{
totalAllStudents: 0,
totalAllPassed: 0,
totalAllNotCompleted: 0,
totalAllCredits: 0,
totalProgrammeStudents: 0,
totalProgrammeCredits: 0,
@@ -27,6 +27,10 @@ const studyprogrammeApi = RTKApi.injectEndpoints({
getProgrammeCoursesStats: builder.query({
query: ({ id, academicyear }) => `/v2/studyprogrammes/${id}/coursestats?academicyear=${academicyear}`,
}),
getEvaluationStats: builder.query({
query: ({ id, yearType, specialGroups, graduated }) =>
`/v2/studyprogrammes/${id}/evaluationstats?year_type=${yearType}&special_groups=${specialGroups}&graduated=${graduated}`,
}),
}),
overrideExisting: false,
})
@@ -39,4 +43,5 @@ export const {
useUpdateBasicViewQuery,
useUpdateStudytrackViewQuery,
useGetProgrammeCoursesStatsQuery,
useGetEvaluationStatsQuery,
} = studyprogrammeApi