Compare commits

...
108 Commits
Author SHA1 Message Date
outisa 992d1c13dc [Faculty] Fix faculty progress chart bug for missing data 2022-09-20 09:14:07 +03:00
outisa 7024345088 [Faculty] First betterments for the math 2022-09-19 23:43:58 +03:00
outisa 9560f84395 [Faculty] Add faculty progress for students 2022-09-19 22:53:31 +03:00
Laura Sinikallio 9f9ab3ce09 Faculty - Show programme level graduation times 2022-09-19 19:11:17 +03:00
Laura Sinikallio 4ac29fcfe8 Add faculty grad times info text 2022-09-19 15:17:25 +03:00
Laura Sinikallio 174a3069fe Ordered faculty and studyprogramme lists 2022-09-19 14:31:04 +03:00
Miko Keskimäki fc6b77e32b Log when using iam rights to access view for Grafana 2022-09-16 09:55:21 +03:00
Laura Sinikallio 6cfb89336f Show graduation times by start year 2022-09-15 19:53:40 +03:00
Laura Sinikallio 635409442c Remove old graduation time display code 2022-09-15 11:28:39 +03:00
outisa 2ffd2db995 Merge branch 'master' of github.com:UniversityOfHelsinkiCS/oodikone 2022-09-15 11:08:11 +03:00
outisa 431c2292cb Fix faculty access 2022-09-15 11:02:52 +03:00
Laura Sinikallio 1a4a1a1f1c Redo faculty average graduation times display 2022-09-14 15:30:43 +03:00
outisa fe1b5ac4c6 Merge branch 'master' of github.com:UniversityOfHelsinkiCS/oodikone 2022-09-13 16:08:07 +03:00
outisa 0e9253f698 Update Faculty updater 2022-09-13 16:07:47 +03:00
Miko Keskimäki 11f9b935b4 Log iam rights status via accessLogger for Grafana 2022-09-13 12:53:50 +03:00
outisa a16896e74d [Faculty] Update updater 2022-09-12 13:33:47 +03:00
outisa 814599d6ea [Faculty] Small refactoring and adjusting numbers 2022-09-10 12:18:01 +03:00
outisa 8f2dcac0f9 Fix bug for updating statistics 2022-09-09 18:24:29 +03:00
outisa 240659afc9 [Faculty] Add include/exclude studywork toggle 2022-09-09 15:12:22 +03:00
outisa b9059e2a52 Merge branch 'master' of github.com:UniversityOfHelsinkiCS/oodikone 2022-09-09 15:09:22 +03:00
outisa a1688f7cdd [Faculty] Add infotool text 2022-09-09 14:19:05 +03:00
Laura Sinikallio 2c378bfca2 Patch breaking population statistics course filters ('didn't participate' disabled for now) 2022-09-09 11:54:39 +03:00
outisa aec495b161 [Navigation] Fix issue for faculty tab showing wrongly 2022-09-06 09:58:59 +03:00
outisa 486bc1a194 Merge branch 'master' of github.com:UniversityOfHelsinkiCS/oodikone 2022-09-02 14:19:55 +03:00
outisa 18bcb91d09 Add one more test for faculty view 2022-09-02 14:19:19 +03:00
outisa 5c4d73f555 Add new tests for faculty 2022-09-02 13:21:50 +03:00
Laura Sinikallio 27e8450789 Small clean-ups 2022-09-01 14:40:35 +03:00
Laura Sinikallio 56733f2445 Subtract statutory absences from faculty graduation times 2022-09-01 14:34:41 +03:00
Laura Sinikallio 089b00fcd6 Show graduation times only for levels with data 2022-09-01 11:52:05 +03:00
Laura Sinikallio a709fe70ba Merge branch 'master' of github.com:UniversityOfHelsinkiCS/oodikone 2022-08-31 19:23:14 +03:00
Laura Sinikallio f26e4b4859 Betterments to graduations times 2022-08-31 19:23:06 +03:00
outisa 18a382167b Fix accessrights in navigation bar 2022-08-31 16:25:32 +03:00
outisa b4be0a3e6b [Faculty] Fix accessrights problems 2022-08-31 15:44:41 +03:00
outisa 596ac08ad4 Fix bug related to accessrights 2022-08-31 13:55:40 +03:00
outisa 9c6ad0027a Modify faculty accessrights in navigation bar 2022-08-31 12:05:03 +03:00
outisa 27ce374f35 [faculty] Modify access rights 2022-08-31 11:55:17 +03:00
Laura Sinikallio 7e6f311b62 Faculty - move grad times to new tab 2022-08-31 10:22:43 +03:00
Laura Sinikallio dd77fdff9e Initial pipeline for graduation times, WIP 2022-08-30 17:48:58 +03:00
outisa 0b7adf4888 [Faculty] Add messagebox for incomplete data 2022-08-30 14:12:57 +03:00
outisa 9deee8a384 [Faculty] Change color codes (perhaps) better 2022-08-30 13:58:11 +03:00
outisa 81791381a5 [Faculty] Make betterments for faculty view 2022-08-30 12:45:59 +03:00
outisa 9806639c1b [Faculty] Show total in on-hover tooltip 2022-08-30 12:13:51 +03:00
outisa 5e9a3c4b99 [Faculty] Change others to licentiates (thesis writers) 2022-08-30 12:01:38 +03:00
outisa 12f73f4dee [Faculty] Show numbers with max. 1 decimal 2022-08-30 11:54:04 +03:00
outisa 9d27dfebe5 [Faculty] Make betterments for basic view 2022-08-29 17:15:38 +03:00
outisa 7bebd4c845 Fix updating thesis writers 2022-08-29 17:14:05 +03:00
outisa 43d2e84b32 [Faculty] Add thesis writers to redis 2022-08-29 14:47:25 +03:00
Roni Lindholm ee1cb395a2 Fix custom population crash 2022-08-29 11:45:26 +03:00
Roni Lindholm 4e221d8867 Fix missing students from population of course when no attainment
Enrollments were retrieved by enrollment date instead of semester code
which corresponds to when the course is acually organized.
2022-08-29 11:11:03 +03:00
Roni Lindholm 3aab7b74a3 Fix counting of total enrollments in course stats attempts tab 2022-08-29 10:05:29 +03:00
Roni Lindholm 92651b8bc7 Make course stats search by course code case insenstive 2022-08-29 09:47:30 +03:00
Miko Keskimäki 08b53c34f2 Dependabot updates 2022-08-29 08:52:00 +03:00
Laura Sinikallio 8d6a311936 Add note to update tab 2022-08-27 12:59:05 +03:00
Laura Sinikallio 79b8b92b66 Implement faculty info texts 2022-08-27 12:03:04 +03:00
outisa fa8a07481a [Faculty] Add small adjustment for sorting 2022-08-27 10:45:23 +03:00
outisa f79d6ef0fb [Faculty] Find thesis writers only for new programmes 2022-08-26 21:43:44 +03:00
outisa bb25541202 Disable flaky faculty cypress test 2022-08-26 17:22:21 +03:00
outisa 3530723bd5 [Faculty] Change sort by selection 2022-08-26 16:41:31 +03:00
Laura Sinikallio f2ffaacc6c Info texts for faculty 2022-08-26 16:12:42 +03:00
Laura Sinikallio 14e95fe4df Add update statistics tab to faculty page, refactor programmes tab basis 2022-08-26 15:25:04 +03:00
Laura Sinikallio f69b12a1f9 Add faculty stats refresh to cronjob 2022-08-26 13:13:13 +03:00
Laura Sinikallio e4e527eaeb Imporove faculty filtering 2022-08-26 12:12:53 +03:00
Laura Sinikallio c4c8f6123b Tools for refreshing faculty stats with cronjob (unplugged) 2022-08-25 16:14:03 +03:00
Laura Sinikallio bdb71242a7 Redis edits from previous changes 2022-08-25 14:40:28 +03:00
Laura Sinikallio a3a36fcef1 Put back faculty frontside programme sorting 2022-08-25 14:17:56 +03:00
Laura Sinikallio adbcb1bb88 Faculty toggle for old programmes filters from all stats 2022-08-25 13:51:22 +03:00
outisa 37504f9e97 Fix minor bug for finding thesis writers 2022-08-24 18:29:13 +03:00
outisa 1722bcd7a3 Change order of the navigation bar 2022-08-24 17:10:37 +03:00
outisa a770873da5 [Faculty] Add language options for programme names 2022-08-24 16:45:18 +03:00
Laura Sinikallio a4c3385956 Redis for faculty credits and student info -> speed 2022-08-24 14:04:32 +03:00
Laura Sinikallio 9fe491e7cc Fix error, but better 2022-08-24 13:00:13 +03:00
Laura Sinikallio 42213948b7 Fix redis error 2022-08-24 12:28:10 +03:00
Laura Sinikallio 1014348e5b Redis for faculty programmes 2022-08-24 11:48:25 +03:00
outisa dcecd66e49 [Faculty] Fix sorting bug 2022-08-24 10:33:58 +03:00
Laura Sinikallio 4509e51f66 Refactor file structure, start of redis for faculty 2022-08-24 10:11:38 +03:00
outisa 90841369bb [Faculty] Modify calculation of thesis writers 2022-08-24 09:55:26 +03:00
outisa c3e41c8a60 [Faculty] Add small refactoring 2022-08-24 09:47:39 +03:00
Laura Sinikallio 81ffbf653e Basis for faculty graduation times 2022-08-23 10:20:28 +03:00
Laura Sinikallio b46e30c8fa Switch rest of faculty to new programme search 2022-08-22 15:52:46 +03:00
Miko Keskimäki 3619698744 Fix error with encryption when no selectable students for year exist 2022-08-22 14:21:35 +03:00
Laura Sinikallio 81ee6b8146 Improve faculty programmes retrieval 2022-08-22 14:03:49 +03:00
outisa c9b27fdc64 Merge branch 'master' of github.com:UniversityOfHelsinkiCS/oodikone 2022-08-22 12:55:02 +03:00
outisa 6b50c07ed7 [Faculty] Add toggle for showing only new programmes or all programmes 2022-08-22 12:54:47 +03:00
Roni Lindholm d2670e180c Fix empty study guidance group crash 2022-08-22 11:44:30 +03:00
Roni Lindholm 580e1b4ed0 Minor study guidance group tweaks 2022-08-22 11:08:42 +03:00
Laura Sinikallio e84e487307 Small fix to programme names for faculty 2022-08-22 10:02:08 +03:00
Roni Lindholm d0a54c95a6 Refactor UI for editing of study guidance group metadata
Open editing form to a modal and wrap study guidance table on small
screens.
2022-08-22 09:38:41 +03:00
outisa 18ac10f399 [Faculty] Add sorting by column values (not yet pretty) 2022-08-19 19:20:33 +03:00
Roni Lindholm 263e070b13 Fix open uni credits in teacher leaderboard 2022-08-19 11:17:03 +03:00
Roni Lindholm 8cf9ceac6e Try to fix tests 2022-08-19 08:48:56 +03:00
Roni Lindholm 32005ce23f Add transferred credits and students to programme courses 2022-08-19 08:21:54 +03:00
outisa 111a397811 [Faculty] Make charts to look prettier 2022-08-18 14:14:29 +03:00
outisa 454032368a [Faculty] Fix bug: tooltip not showing on thesis writers chart 2022-08-18 13:52:06 +03:00
Laura Sinikallio 5509cd4b9a Faculty backend tweaks 2022-08-17 16:16:01 +03:00
outisa 346c9f9803 [Faculty] Make betterments in programmelevel graphs 2022-08-17 15:21:33 +03:00
Laura Sinikallio 0a738df3d0 Improvements to faculty's programme search 2022-08-17 15:01:00 +03:00
Miko Keskimäki b67da1c2c4 Enable IAM tests
IAM user added to test db image so should work now
2022-08-17 10:14:30 +03:00
outisa e700586588 [Faculty] Add programmestats thesis writers (missing old ones) 2022-08-16 23:39:43 +03:00
outisa 9bb7c1c6f1 Merge branch 'master' of github.com:UniversityOfHelsinkiCS/oodikone 2022-08-16 18:28:53 +03:00
Miko Keskimäki 7f70968531 Remove 00:00:00 time from general student information csv data 2022-08-16 11:08:40 +03:00
outisa 9ae35e46db [Faculty] Add backend functionality for finding faculty thesis writers 2022-08-16 10:42:35 +03:00
Miko Keskimäki 3d667fe1c6 Update student progress test data to match credits since start of studyright 2022-08-15 14:03:33 +03:00
Miko Keskimäki 62e07ce571 Show only credits gained since start of studyright in study programme progresstable #3661 2022-08-15 13:33:42 +03:00
Miko Keskimäki 38cd587369 Dependabot updates 2022-08-15 11:49:04 +03:00
outisa 77c9801e4c FacultyStats: Add custom sorting 2022-08-12 14:30:25 +03:00
Miko Keskimäki d3f1e101ce Prevent iam users from hiding degree courses 2022-08-12 13:12:41 +03:00
Miko Keskimäki 10a2d2743e Remove links to course statistics when only iam rights 2022-08-12 13:11:07 +03:00
Laura Sinikallio 4d26f42f25 Fix graduations error 2022-08-12 09:27:02 +03:00
78 changed files with 4158 additions and 1327 deletions
+102 -33
View File
@@ -13,58 +13,127 @@ describe('Faculty overview', () => {
})
})
describe('Faculty basic information', () => {
describe('Faculty basic information: admin user', () => {
beforeEach(() => {
cy.init('/faculties', 'admin')
cy.contains('td', 'H90').click()
})
it('Credits produced by faculty are shown', () => {
cy.get(['data-cy=Section-CreditsProducedByTheFaculty'])
cy.get(['data-cy=Graph-CreditsProducedByTheFaculty'])
cy.get('[data-cy="Section-CreditsProducedByTheFaculty"]').should('be.visible')
cy.get('[data-cy="Graph-CreditsProducedByTheFaculty"]').should('be.visible')
})
it('Update statistics tab is shown', () => {
cy.get('[data-cy="FacultySegmentContainer"]').should('contain', 'Update statistics')
})
})
describe('Study programme information', () => {
describe('Faculty basic information: basic user', () => {
beforeEach(() => {
cy.init('/faculties', 'admin')
cy.init('/faculties')
cy.contains('td', 'H80').click()
})
it('Basic information tab show all graphs and tables', () => {
cy.get('[data-cy="Table-CreditsProducedByTheFaculty"]').should('be.visible')
cy.get('[data-cy="Graph-CreditsProducedByTheFaculty"]').should('be.visible')
cy.get('[data-cy="Table-ThesisWritersOfTheFaculty"]').should('be.visible')
cy.get('[data-cy="Graph-ThesisWritersOfTheFaculty"]').should('be.visible')
cy.get('[data-cy="Table-StudentsOfTheFaculty"]').should('be.visible')
cy.get('[data-cy="Graph-StudentsOfTheFaculty"]').should('be.visible')
cy.get('[data-cy="Table-GraduatedOfTheFaculty"]').should('be.visible')
cy.get('[data-cy="Graph-GraduatedOfTheFaculty"]').should('be.visible')
})
it('Correct tabs are shown', () => {
cy.get('[data-cy="FacultySegmentContainer"]').should('contain', 'Basic information')
// Fix these when access is gained for facultyStatistics users
cy.get('[data-cy="FacultySegmentContainer"]').should('not.contain', 'Programmes and student populations')
cy.get('[data-cy="FacultySegmentContainer"]').should('not.contain', 'Graduation times')
cy.get('[data-cy="FacultySegmentContainer"]').should('not.contain', 'Update statistics')
})
it('Toggle years works', () => {
cy.get('[data-cy="Table-CreditsProducedByTheFaculty"]').should('contain', '2022')
cy.get('[data-cy="Table-ThesisWritersOfTheFaculty"]').should('contain', '2022')
cy.get('[data-cy="Table-StudentsOfTheFaculty"]').should('contain', '2022')
cy.get('[data-cy="Table-GraduatedOfTheFaculty"]').should('contain', '2022')
cy.get('[data-cy="YearToggle"]').click()
cy.get('[data-cy="Table-CreditsProducedByTheFaculty"]').should('contain', '2022 - 2023')
cy.get('[data-cy="Table-ThesisWritersOfTheFaculty"]').should('contain', '2022 - 2023')
cy.get('[data-cy="Table-StudentsOfTheFaculty"]').should('contain', '2022 - 2023')
cy.get('[data-cy="Table-GraduatedOfTheFaculty"]').should('contain', '2022 - 2023')
})
it('Toggle programmes works', () => {
cy.get('[data-cy="FacultyProgrammesShownInfo"]').should('not.exist')
cy.get('[data-cy="ProgrammeToggle"]').click()
cy.get('[data-cy="FacultyProgrammesShownInfo"]').should('be.visible')
})
it('Students of the faculty infobox works', () => {
cy.get('[data-cy="StudentsOfTheFaculty-info-content"]').should('not.exist')
cy.get('[data-cy="StudentsOfTheFaculty-open-info"]').click()
cy.get('[data-cy="StudentsOfTheFaculty-info-content"]').should('be.visible')
cy.get('[data-cy="StudentsOfTheFaculty-close-info"]').click()
cy.get('[data-cy="StudentsOfTheFaculty-info-content"]').should('not.exist')
})
})
describe('Study programme information: users with access', () => {
beforeEach(() => {
cy.init('/faculties')
cy.contains('td', 'H80').click()
})
it('Study programme credit information is not visible in the beginning', () => {
cy.get('table[data-cy="Table-CreditsProducedByTheFaculty"]').should('be.visible')
cy.get('td[data-cy="Cell-CreditsProducedByTheFaculty-0"]').should('not.be.visible')
cy.get('td[data-cy="Cell-CreditsProducedByTheFaculty-1"]').should('not.be.visible')
cy.get('td[data-cy="Cell-CreditsProducedByTheFaculty-2"]').should('not.be.visible')
cy.get('td[data-cy="Cell-CreditsProducedByTheFaculty-3"]').should('not.be.visible')
cy.get('td[data-cy="Cell-CreditsProducedByTheFaculty-4"]').should('not.be.visible')
cy.get('td[data-cy="Cell-CreditsProducedByTheFaculty-5"]').should('not.be.visible')
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-0"]').should('not.be.visible')
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-1"]').should('not.be.visible')
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-2"]').should('not.be.visible')
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-3"]').should('not.be.visible')
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-4"]').should('not.be.visible')
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-5"]').should('not.be.visible')
})
it('Study programme credit information can be toggled', () => {
cy.get('table[data-cy="Table-CreditsProducedByTheFaculty"]').should('be.visible')
cy.get('[data-cy="Table-CreditsProducedByTheFaculty"]').should('be.visible')
cy.get('div[data-cy="Button-Show-CreditsProducedByTheFaculty-0"]').click()
cy.get('td[data-cy="Cell-CreditsProducedByTheFaculty-0"]').should('be.visible')
cy.get('td[data-cy="Cell-CreditsProducedByTheFaculty-1"]').should('not.be.visible')
cy.get('td[data-cy="Cell-CreditsProducedByTheFaculty-2"]').should('not.be.visible')
cy.get('td[data-cy="Cell-CreditsProducedByTheFaculty-3"]').should('not.be.visible')
cy.get('td[data-cy="Cell-CreditsProducedByTheFaculty-4"]').should('not.be.visible')
cy.get('td[data-cy="Cell-CreditsProducedByTheFaculty-5"]').should('not.be.visible')
cy.get('div[data-cy="Button-Show-CreditsProducedByTheFaculty-3"]').click()
cy.get('td[data-cy="Cell-CreditsProducedByTheFaculty-0"]').should('be.visible')
cy.get('td[data-cy="Cell-CreditsProducedByTheFaculty-1"]').should('not.be.visible')
cy.get('td[data-cy="Cell-CreditsProducedByTheFaculty-2"]').should('not.be.visible')
cy.get('td[data-cy="Cell-CreditsProducedByTheFaculty-3"]').should('be.visible')
cy.get('td[data-cy="Cell-CreditsProducedByTheFaculty-4"]').should('not.be.visible')
cy.get('td[data-cy="Cell-CreditsProducedByTheFaculty-5"]').should('not.be.visible')
cy.get('div[data-cy="Button-Hide-CreditsProducedByTheFaculty-0"]').click()
cy.get('td[data-cy="Cell-CreditsProducedByTheFaculty-0"]').should('not.be.visible')
cy.get('td[data-cy="Cell-CreditsProducedByTheFaculty-1"]').should('not.be.visible')
cy.get('td[data-cy="Cell-CreditsProducedByTheFaculty-2"]').should('not.be.visible')
cy.get('td[data-cy="Cell-CreditsProducedByTheFaculty-3"]').should('be.visible')
cy.get('td[data-cy="Cell-CreditsProducedByTheFaculty-4"]').should('not.be.visible')
cy.get('td[data-cy="Cell-CreditsProducedByTheFaculty-5"]').should('not.be.visible')
// I tried to fix this without cy.wait() method without effort. Sorting the programmes
// or something else causes rerendering of the view and yield to failing tests.
// eslint-disable-next-line cypress/no-unnecessary-waiting
cy.wait(10000)
cy.get('[data-cy="Button-Show-CreditsProducedByTheFaculty-0"]').click()
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-0"]').should('be.visible')
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-1"]').should('not.be.visible')
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-2"]').should('not.be.visible')
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-3"]').should('not.be.visible')
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-4"]').should('not.be.visible')
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-5"]').should('not.be.visible')
cy.get('[data-cy="Button-Show-CreditsProducedByTheFaculty-3"]').click()
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-0"]').should('be.visible')
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-1"]').should('not.be.visible')
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-2"]').should('not.be.visible')
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-3"]').should('be.visible')
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-4"]').should('not.be.visible')
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-5"]').should('not.be.visible')
cy.get('[data-cy="Button-Hide-CreditsProducedByTheFaculty-0"]').click()
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-0"]').should('not.be.visible')
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-1"]').should('not.be.visible')
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-2"]').should('not.be.visible')
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-3"]').should('be.visible')
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-4"]').should('not.be.visible')
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-5"]').should('not.be.visible')
})
it('Graph stays open when sorted', () => {
cy.get('[data-cy="Table-CreditsProducedByTheFaculty"]').should('be.visible')
// eslint-disable-next-line cypress/no-unnecessary-waiting
cy.wait(10000)
cy.get('[data-cy="Button-Show-CreditsProducedByTheFaculty-0"]').click()
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-0"]').should('be.visible')
cy.get('[data-cy="Menu-CreditsProducedByTheFaculty-Total"]').click()
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-0"]').should('be.visible')
})
})
})
+2 -2
View File
@@ -223,7 +223,7 @@ describe('Population Statistics tests', () => {
cy.init(pathToCSBach2017, 'onlyiamrights')
})
it.skip('Population statistics is visible', () => {
it('Population statistics is visible', () => {
cy.reload()
cy.get('.card').within(() => {
@@ -238,7 +238,7 @@ describe('Population Statistics tests', () => {
cy.contains('Credit statistics')
})
it.skip('Students tab is not available', () => {
it('Students tab is not available', () => {
cy.contains('Students (170)').should('not.exist')
// TODO: somehow check that obfuscation works
})
+12 -12
View File
@@ -332,11 +332,11 @@ describe('Studyprogramme overview', () => {
cy.contains('a', 'Tietojenkäsittelytieteen kandiohjelma').click()
cy.get('.attached').contains('Studytracks and student populations').click()
const tableContents = [
['2020 - 2021', 12, 5, 2, 2, 0, 2, 0, 1],
['2019 - 2020', 86, 13, 15, 32, 11, 2, 0, 13],
['2018 - 2019', 162, 7, 16, 35, 33, 27, 18, 26],
['2017 - 2018', 171, 10, 22, 18, 22, 24, 27, 48],
['Total', 431, 35, 55, 87, 66, 55, 45, 88],
['2020 - 2021', 12, 12, 0, 0, 0, 0, 0, 0],
['2019 - 2020', 86, 26, 34, 21, 5, 0, 0, 0],
['2018 - 2019', 162, 10, 27, 47, 36, 26, 12, 4],
['2017 - 2018', 171, 20, 21, 26, 22, 22, 33, 27],
['Total', 431, 51, 64, 97, 79, 55, 51, 34],
]
cy.checkTableStats(tableContents, 'StudytrackProgress')
@@ -350,9 +350,9 @@ describe('Studyprogramme overview', () => {
.should('contain', 'Less than 30 credits')
.should('contain', '30-59 credits')
.should('contain', 'More than 180 credits')
.should('contain', '5.8%') // The percentage for less than 30 credits in 2017-2018, to check that the graph renders
.should('contain', '4.3%') // The percentage for less than 30 credits in 2018-2019
.should('contain', '15.1%') // The percentage for less than 30 credits in 2019-2020
.should('contain', '11.7%') // The percentage for less than 30 credits in 2017-2018, to check that the graph renders
.should('contain', '6.2%') // The percentage for less than 30 credits in 2018-2019
.should('contain', '30.2%') // The percentage for less than 30 credits in 2019-2020
cy.get('[data-cy=Graph-2018-AverageGraduationTimes]')
.should('contain', '2018 - 2019')
@@ -411,7 +411,7 @@ describe('Studyprogramme overview', () => {
cy.get('[data-cy=CoursesSortableTable]').within(() => {
// Test sorting by different columns
cy.get('th').eq(0).click()
cy.get('tr').eq(1).contains('Tietojenkäsittelytieteen kisälliopetus: Java-ohjelmointi')
cy.get('tr').eq(2).contains('Tietojenkäsittelytieteen kisälliopetus: Java-ohjelmointi')
cy.get('th').eq(1).click()
cy.get('tr').eq(1).contains('Äidinkielinen viestintä')
cy.get('th').eq(2).click()
@@ -505,7 +505,7 @@ describe('Studyprogramme overview', () => {
cy.init('/study-programme', 'onlyiamrights')
})
it.skip('can access programme and correct tabs are visible', () => {
it('can access programme and correct tabs are visible', () => {
cy.reload()
cy.contains('a', 'Tietojenkäsittelytieteen kandiohjelma').click()
@@ -517,7 +517,7 @@ describe('Studyprogramme overview', () => {
cy.contains('Degree Courses').should('not.exist')
})
it.skip('can access basic information', () => {
it('can access basic information', () => {
cy.contains('a', 'Tietojenkäsittelytieteen kandiohjelma').click()
cy.contains('Basic information').click()
@@ -528,7 +528,7 @@ describe('Studyprogramme overview', () => {
cy.get('[data-cy=Section-AverageGraduationTimes]')
})
it.skip('can access studytracks', () => {
it('can access studytracks', () => {
cy.contains('a', 'Tietojenkäsittelytieteen kandiohjelma').click()
cy.get('.attached').contains('Studytracks and student populations').click()
+154 -213
View File
@@ -11,12 +11,12 @@
"moment": "^2.29.4"
},
"devDependencies": {
"@babel/core": "^7.18.10",
"@babel/core": "^7.18.13",
"@babel/eslint-parser": "^7.17.0",
"@babel/plugin-syntax-jsx": "^7.16.7",
"concurrently": "^7.3.0",
"cypress": "^10.4.0",
"eslint": "^8.21.0",
"cypress": "^10.6.0",
"eslint": "^8.23.0",
"eslint-config-airbnb": "^19.0.4",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-babel": "^5.3.1",
@@ -25,13 +25,13 @@
"eslint-plugin-jsx-a11y": "^6.5.1",
"eslint-plugin-prefer-object-spread": "^1.2.1",
"eslint-plugin-prettier": "^4.2.1",
"eslint-plugin-react": "^7.29.3",
"eslint-plugin-react": "^7.31.1",
"eslint-plugin-react-hooks": "^4.3.0",
"husky": "^8.0.1",
"lint-staged": "^13.0.3",
"node-actionlint": "^1.2.2",
"prettier": "^2.5.1",
"stylelint": "^14.9.1",
"stylelint": "^14.11.0",
"stylelint-config-prettier": "^9.0.3",
"stylelint-config-standard": "^26.0.0",
"stylelint-prettier": "^2.0.0"
@@ -72,21 +72,21 @@
}
},
"node_modules/@babel/core": {
"version": "7.18.10",
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.18.10.tgz",
"integrity": "sha512-JQM6k6ENcBFKVtWvLavlvi/mPcpYZ3+R+2EySDEMSMbp7Mn4FexlbbJVrx2R7Ijhr01T8gyqrOaABWIOgxeUyw==",
"version": "7.18.13",
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.18.13.tgz",
"integrity": "sha512-ZisbOvRRusFktksHSG6pjj1CSvkPkcZq/KHD45LAkVP/oiHJkNBZWfpvlLmX8OtHDG8IuzsFlVRWo08w7Qxn0A==",
"dev": true,
"dependencies": {
"@ampproject/remapping": "^2.1.0",
"@babel/code-frame": "^7.18.6",
"@babel/generator": "^7.18.10",
"@babel/generator": "^7.18.13",
"@babel/helper-compilation-targets": "^7.18.9",
"@babel/helper-module-transforms": "^7.18.9",
"@babel/helpers": "^7.18.9",
"@babel/parser": "^7.18.10",
"@babel/parser": "^7.18.13",
"@babel/template": "^7.18.10",
"@babel/traverse": "^7.18.10",
"@babel/types": "^7.18.10",
"@babel/traverse": "^7.18.13",
"@babel/types": "^7.18.13",
"convert-source-map": "^1.7.0",
"debug": "^4.1.0",
"gensync": "^1.0.0-beta.2",
@@ -120,12 +120,12 @@
}
},
"node_modules/@babel/generator": {
"version": "7.18.10",
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.18.10.tgz",
"integrity": "sha512-0+sW7e3HjQbiHbj1NeU/vN8ornohYlacAfZIaXhdoGweQqgcNy69COVciYYqEXJ/v+9OBA7Frxm4CVAuNqKeNA==",
"version": "7.18.13",
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.18.13.tgz",
"integrity": "sha512-CkPg8ySSPuHTYPJYo7IRALdqyjM9HCbt/3uOBEFbzyGVP6Mn8bwFPB0jX6982JVNBlYzM1nnPkfjuXSOPtQeEQ==",
"dev": true,
"dependencies": {
"@babel/types": "^7.18.10",
"@babel/types": "^7.18.13",
"@jridgewell/gen-mapping": "^0.3.2",
"jsesc": "^2.5.1"
},
@@ -319,9 +319,9 @@
}
},
"node_modules/@babel/parser": {
"version": "7.18.11",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.18.11.tgz",
"integrity": "sha512-9JKn5vN+hDt0Hdqn1PiJ2guflwP+B6Ga8qbDuoF0PzzVhrzsKIJo8yGqVk6CmMHiMei9w1C1Bp9IMJSIK+HPIQ==",
"version": "7.18.13",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.18.13.tgz",
"integrity": "sha512-dgXcIfMuQ0kgzLB2b9tRZs7TTFFaGM2AbtA4fJgUUYukzGH4jwsS7hzQHEGs67jdehpm22vkgKwvbU+aEflgwg==",
"dev": true,
"bin": {
"parser": "bin/babel-parser.js"
@@ -385,19 +385,19 @@
}
},
"node_modules/@babel/traverse": {
"version": "7.18.11",
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.18.11.tgz",
"integrity": "sha512-TG9PiM2R/cWCAy6BPJKeHzNbu4lPzOSZpeMfeNErskGpTJx6trEvFaVCbDvpcxwy49BKWmEPwiW8mrysNiDvIQ==",
"version": "7.18.13",
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.18.13.tgz",
"integrity": "sha512-N6kt9X1jRMLPxxxPYWi7tgvJRH/rtoU+dbKAPDM44RFHiMH8igdsaSBgFeskhSl/kLWLDUvIh1RXCrTmg0/zvA==",
"dev": true,
"dependencies": {
"@babel/code-frame": "^7.18.6",
"@babel/generator": "^7.18.10",
"@babel/generator": "^7.18.13",
"@babel/helper-environment-visitor": "^7.18.9",
"@babel/helper-function-name": "^7.18.9",
"@babel/helper-hoist-variables": "^7.18.6",
"@babel/helper-split-export-declaration": "^7.18.6",
"@babel/parser": "^7.18.11",
"@babel/types": "^7.18.10",
"@babel/parser": "^7.18.13",
"@babel/types": "^7.18.13",
"debug": "^4.1.0",
"globals": "^11.1.0"
},
@@ -406,9 +406,9 @@
}
},
"node_modules/@babel/types": {
"version": "7.18.10",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.18.10.tgz",
"integrity": "sha512-MJvnbEiiNkpjo+LknnmRrqbY1GPUUggjv+wQVjetM/AONoupqRALB7I6jGqNUAZsKcRIEu2J6FRFvsczljjsaQ==",
"version": "7.18.13",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.18.13.tgz",
"integrity": "sha512-ePqfTihzW0W6XAU+aMw2ykilisStJfDnsejDCXRchCcMJ4O0+8DhPXf2YUbZ6wjBlsEmZwLK/sPweWtu8hcJYQ==",
"dev": true,
"dependencies": {
"@babel/helper-string-parser": "^7.18.10",
@@ -495,14 +495,14 @@
}
},
"node_modules/@eslint/eslintrc": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.3.0.tgz",
"integrity": "sha512-UWW0TMTmk2d7hLcWD1/e2g5HDM/HQ3csaLSqXCfqwh4uNDuNqlaKWXmEsL4Cs41Z0KnILNvwbHAah3C2yt06kw==",
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.3.1.tgz",
"integrity": "sha512-OhSY22oQQdw3zgPOOwdoj01l/Dzl1Z+xyUP33tkSN+aqyEhymJCcPHyXt+ylW8FSe0TfRC2VG+ROQOapD0aZSQ==",
"dev": true,
"dependencies": {
"ajv": "^6.12.4",
"debug": "^4.3.2",
"espree": "^9.3.2",
"espree": "^9.4.0",
"globals": "^13.15.0",
"ignore": "^5.2.0",
"import-fresh": "^3.2.1",
@@ -512,6 +512,9 @@
},
"engines": {
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
},
"funding": {
"url": "https://opencollective.com/eslint"
}
},
"node_modules/@eslint/eslintrc/node_modules/globals": {
@@ -565,6 +568,19 @@
"url": "https://github.com/sponsors/nzakas"
}
},
"node_modules/@humanwhocodes/module-importer": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz",
"integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==",
"dev": true,
"engines": {
"node": ">=12.22"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/nzakas"
}
},
"node_modules/@humanwhocodes/object-schema": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz",
@@ -1297,27 +1313,6 @@
"wrap-ansi": "^7.0.0"
}
},
"node_modules/clone-regexp": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/clone-regexp/-/clone-regexp-2.2.0.tgz",
"integrity": "sha512-beMpP7BOtTipFuW8hrJvREQ2DrRu3BE7by0ZpibtfBA+qfHYvMGTc2Yb1JMYPKg/JUw0CHYvpg796aNTSW9z7Q==",
"dev": true,
"dependencies": {
"is-regexp": "^2.0.0"
},
"engines": {
"node": ">=6"
}
},
"node_modules/clone-regexp/node_modules/is-regexp": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/is-regexp/-/is-regexp-2.1.0.tgz",
"integrity": "sha512-OZ4IlER3zmRIoB9AqNhEggVxqIH4ofDns5nRrPS6yQxXE1TPCUpFznBfRQmQa8uC+pXqjMnukiJBxCisIxiLGA==",
"dev": true,
"engines": {
"node": ">=6"
}
},
"node_modules/color-convert": {
"version": "1.9.3",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
@@ -1334,9 +1329,9 @@
"dev": true
},
"node_modules/colord": {
"version": "2.9.2",
"resolved": "https://registry.npmjs.org/colord/-/colord-2.9.2.tgz",
"integrity": "sha512-Uqbg+J445nc1TKn4FoDPS6ZZqAvEDnwrH42yo8B40JSOgSLxMZ/gt3h4nmCtPLQeXhjJJkqBx7SCY35WnIixaQ==",
"version": "2.9.3",
"resolved": "https://registry.npmjs.org/colord/-/colord-2.9.3.tgz",
"integrity": "sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==",
"dev": true
},
"node_modules/colorette": {
@@ -1573,9 +1568,9 @@
}
},
"node_modules/cypress": {
"version": "10.4.0",
"resolved": "https://registry.npmjs.org/cypress/-/cypress-10.4.0.tgz",
"integrity": "sha512-OM7F8MRE01SHQRVVzunid1ZK1m90XTxYnl+7uZfIrB4CYqUDCrZEeSyCXzIbsS6qcaijVCAhqDL60SxG8N6hew==",
"version": "10.6.0",
"resolved": "https://registry.npmjs.org/cypress/-/cypress-10.6.0.tgz",
"integrity": "sha512-6sOpHjostp8gcLO34p6r/Ci342lBs8S5z9/eb3ZCQ22w2cIhMWGUoGKkosabPBfKcvRS9BE4UxybBtlIs8gTQA==",
"dev": true,
"hasInstallScript": true,
"dependencies": {
@@ -2009,14 +2004,15 @@
}
},
"node_modules/eslint": {
"version": "8.21.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-8.21.0.tgz",
"integrity": "sha512-/XJ1+Qurf1T9G2M5IHrsjp+xrGT73RZf23xA1z5wB1ZzzEAWSZKvRwhWxTFp1rvkvCfwcvAUNAP31bhKTTGfDA==",
"version": "8.23.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-8.23.0.tgz",
"integrity": "sha512-pBG/XOn0MsJcKcTRLr27S5HpzQo4kLr+HjLQIyK4EiCsijDl/TB+h5uEuJU6bQ8Edvwz1XWOjpaP2qgnXGpTcA==",
"dev": true,
"dependencies": {
"@eslint/eslintrc": "^1.3.0",
"@eslint/eslintrc": "^1.3.1",
"@humanwhocodes/config-array": "^0.10.4",
"@humanwhocodes/gitignore-to-minimatch": "^1.0.2",
"@humanwhocodes/module-importer": "^1.0.1",
"ajv": "^6.10.0",
"chalk": "^4.0.0",
"cross-spawn": "^7.0.2",
@@ -2026,7 +2022,7 @@
"eslint-scope": "^7.1.1",
"eslint-utils": "^3.0.0",
"eslint-visitor-keys": "^3.3.0",
"espree": "^9.3.3",
"espree": "^9.4.0",
"esquery": "^1.4.0",
"esutils": "^2.0.2",
"fast-deep-equal": "^3.1.3",
@@ -2051,8 +2047,7 @@
"regexpp": "^3.2.0",
"strip-ansi": "^6.0.1",
"strip-json-comments": "^3.1.0",
"text-table": "^0.2.0",
"v8-compile-cache": "^2.0.3"
"text-table": "^0.2.0"
},
"bin": {
"eslint": "bin/eslint.js"
@@ -2299,9 +2294,9 @@
}
},
"node_modules/eslint-plugin-react": {
"version": "7.30.1",
"resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.30.1.tgz",
"integrity": "sha512-NbEvI9jtqO46yJA3wcRF9Mo0lF9T/jhdHqhCHXiXtD+Zcb98812wvokjWpU7Q4QH5edo6dmqrukxVvWWXHlsUg==",
"version": "7.31.1",
"resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.31.1.tgz",
"integrity": "sha512-j4/2xWqt/R7AZzG8CakGHA6Xa/u7iR8Q3xCxY+AUghdT92bnIDOBEefV456OeH0QvBcroVc0eyvrrLSyQGYIfg==",
"dev": true,
"dependencies": {
"array-includes": "^3.1.5",
@@ -2639,9 +2634,9 @@
}
},
"node_modules/espree": {
"version": "9.3.3",
"resolved": "https://registry.npmjs.org/espree/-/espree-9.3.3.tgz",
"integrity": "sha512-ORs1Rt/uQTqUKjDdGCyrtYxbazf5umATSf/K4qxjmZHORR6HJk+2s/2Pqe+Kk49HHINC/xNIrGfgh8sZcll0ng==",
"version": "9.4.0",
"resolved": "https://registry.npmjs.org/espree/-/espree-9.4.0.tgz",
"integrity": "sha512-DQmnRpLj7f6TgN/NYb0MTzJXL+vJF9h3pHy4JhCIs3zwcgez8xmGg3sXHcEO97BrmO2OSvCwMdfdlyl+E9KjOw==",
"dev": true,
"dependencies": {
"acorn": "^8.8.0",
@@ -2735,18 +2730,6 @@
"url": "https://github.com/sindresorhus/execa?sponsor=1"
}
},
"node_modules/execall": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/execall/-/execall-2.0.0.tgz",
"integrity": "sha512-0FU2hZ5Hh6iQnarpRtQurM/aAvp3RIbfvgLHrcqJYzhXyV2KFruhuChf9NC6waAhiUR7FFtlugkI4p7f2Fqlow==",
"dev": true,
"dependencies": {
"clone-regexp": "^2.1.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/executable": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/executable/-/executable-4.1.1.tgz",
@@ -2835,9 +2818,9 @@
"dev": true
},
"node_modules/fastest-levenshtein": {
"version": "1.0.14",
"resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.14.tgz",
"integrity": "sha512-tFfWHjnuUfKE186Tfgr+jtaFc0mZTApEgKDOeyN+FwOqRkO/zK/3h1AiRd8u8CY53owL3CUmGr/oI9p/RdyLTA==",
"version": "1.0.16",
"resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz",
"integrity": "sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==",
"dev": true,
"engines": {
"node": ">= 4.9.1"
@@ -3046,18 +3029,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/get-stdin": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-8.0.0.tgz",
"integrity": "sha512-sY22aA6xchAzprjyqmSEQv4UbAAzRN0L2dQB0NlN5acTTK9Don6nhoc3eAbUnpZiCANAMfd/+40kVdKfFygohg==",
"dev": true,
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/get-stream": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz",
@@ -5309,9 +5280,9 @@
}
},
"node_modules/postcss": {
"version": "8.4.14",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.14.tgz",
"integrity": "sha512-E398TUmfAYFPBSdzgeieK2Y1+1cpdxJx8yXbK/m57nRhKSmk1GB2tO4lbLBtlkfPQTDKfe4Xqv1ASWPpayPEig==",
"version": "8.4.16",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.16.tgz",
"integrity": "sha512-ipHE1XBvKzm5xI7hiHCZJCSugxvsdq2mPnsq5+UF+VHCjiBvtDrlxJfMBToWaP9D5XlgNmcFGqoHmUn0EYEaRQ==",
"dev": true,
"funding": [
{
@@ -6170,22 +6141,20 @@
"dev": true
},
"node_modules/stylelint": {
"version": "14.9.1",
"resolved": "https://registry.npmjs.org/stylelint/-/stylelint-14.9.1.tgz",
"integrity": "sha512-RdAkJdPiLqHawCSnu21nE27MjNXaVd4WcOHA4vK5GtIGjScfhNnaOuWR2wWdfKFAvcWQPOYe311iveiVKSmwsA==",
"version": "14.11.0",
"resolved": "https://registry.npmjs.org/stylelint/-/stylelint-14.11.0.tgz",
"integrity": "sha512-OTLjLPxpvGtojEfpESWM8Ir64Z01E89xsisaBMUP/ngOx1+4VG2DPRcUyCCiin9Rd3kPXPsh/uwHd9eqnvhsYA==",
"dev": true,
"dependencies": {
"@csstools/selector-specificity": "^2.0.1",
"@csstools/selector-specificity": "^2.0.2",
"balanced-match": "^2.0.0",
"colord": "^2.9.2",
"colord": "^2.9.3",
"cosmiconfig": "^7.0.1",
"css-functions-list": "^3.1.0",
"debug": "^4.3.4",
"execall": "^2.0.0",
"fast-glob": "^3.2.11",
"fastest-levenshtein": "^1.0.12",
"fastest-levenshtein": "^1.0.16",
"file-entry-cache": "^6.0.1",
"get-stdin": "^8.0.0",
"global-modules": "^2.0.0",
"globby": "^11.1.0",
"globjoin": "^0.1.4",
@@ -6200,7 +6169,7 @@
"micromatch": "^4.0.5",
"normalize-path": "^3.0.0",
"picocolors": "^1.0.0",
"postcss": "^8.4.14",
"postcss": "^8.4.16",
"postcss-media-query-parser": "^0.2.3",
"postcss-resolve-nested-selector": "^0.1.1",
"postcss-safe-parser": "^6.0.0",
@@ -6214,7 +6183,7 @@
"svg-tags": "^1.0.0",
"table": "^6.8.0",
"v8-compile-cache": "^2.3.0",
"write-file-atomic": "^4.0.1"
"write-file-atomic": "^4.0.2"
},
"bin": {
"stylelint": "bin/stylelint.js"
@@ -6811,16 +6780,16 @@
"dev": true
},
"node_modules/write-file-atomic": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.1.tgz",
"integrity": "sha512-nSKUxgAbyioruk6hU87QzVbY279oYT6uiwgDoujth2ju4mJ+TZau7SQBhtbTmUyuNYTuXnSyRn66FV0+eCgcrQ==",
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz",
"integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==",
"dev": true,
"dependencies": {
"imurmurhash": "^0.1.4",
"signal-exit": "^3.0.7"
},
"engines": {
"node": "^12.13.0 || ^14.15.0 || >=16"
"node": "^12.13.0 || ^14.15.0 || >=16.0.0"
}
},
"node_modules/y18n": {
@@ -6933,21 +6902,21 @@
"dev": true
},
"@babel/core": {
"version": "7.18.10",
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.18.10.tgz",
"integrity": "sha512-JQM6k6ENcBFKVtWvLavlvi/mPcpYZ3+R+2EySDEMSMbp7Mn4FexlbbJVrx2R7Ijhr01T8gyqrOaABWIOgxeUyw==",
"version": "7.18.13",
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.18.13.tgz",
"integrity": "sha512-ZisbOvRRusFktksHSG6pjj1CSvkPkcZq/KHD45LAkVP/oiHJkNBZWfpvlLmX8OtHDG8IuzsFlVRWo08w7Qxn0A==",
"dev": true,
"requires": {
"@ampproject/remapping": "^2.1.0",
"@babel/code-frame": "^7.18.6",
"@babel/generator": "^7.18.10",
"@babel/generator": "^7.18.13",
"@babel/helper-compilation-targets": "^7.18.9",
"@babel/helper-module-transforms": "^7.18.9",
"@babel/helpers": "^7.18.9",
"@babel/parser": "^7.18.10",
"@babel/parser": "^7.18.13",
"@babel/template": "^7.18.10",
"@babel/traverse": "^7.18.10",
"@babel/types": "^7.18.10",
"@babel/traverse": "^7.18.13",
"@babel/types": "^7.18.13",
"convert-source-map": "^1.7.0",
"debug": "^4.1.0",
"gensync": "^1.0.0-beta.2",
@@ -6967,12 +6936,12 @@
}
},
"@babel/generator": {
"version": "7.18.10",
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.18.10.tgz",
"integrity": "sha512-0+sW7e3HjQbiHbj1NeU/vN8ornohYlacAfZIaXhdoGweQqgcNy69COVciYYqEXJ/v+9OBA7Frxm4CVAuNqKeNA==",
"version": "7.18.13",
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.18.13.tgz",
"integrity": "sha512-CkPg8ySSPuHTYPJYo7IRALdqyjM9HCbt/3uOBEFbzyGVP6Mn8bwFPB0jX6982JVNBlYzM1nnPkfjuXSOPtQeEQ==",
"dev": true,
"requires": {
"@babel/types": "^7.18.10",
"@babel/types": "^7.18.13",
"@jridgewell/gen-mapping": "^0.3.2",
"jsesc": "^2.5.1"
},
@@ -7117,9 +7086,9 @@
}
},
"@babel/parser": {
"version": "7.18.11",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.18.11.tgz",
"integrity": "sha512-9JKn5vN+hDt0Hdqn1PiJ2guflwP+B6Ga8qbDuoF0PzzVhrzsKIJo8yGqVk6CmMHiMei9w1C1Bp9IMJSIK+HPIQ==",
"version": "7.18.13",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.18.13.tgz",
"integrity": "sha512-dgXcIfMuQ0kgzLB2b9tRZs7TTFFaGM2AbtA4fJgUUYukzGH4jwsS7hzQHEGs67jdehpm22vkgKwvbU+aEflgwg==",
"dev": true
},
"@babel/plugin-syntax-jsx": {
@@ -7162,27 +7131,27 @@
}
},
"@babel/traverse": {
"version": "7.18.11",
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.18.11.tgz",
"integrity": "sha512-TG9PiM2R/cWCAy6BPJKeHzNbu4lPzOSZpeMfeNErskGpTJx6trEvFaVCbDvpcxwy49BKWmEPwiW8mrysNiDvIQ==",
"version": "7.18.13",
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.18.13.tgz",
"integrity": "sha512-N6kt9X1jRMLPxxxPYWi7tgvJRH/rtoU+dbKAPDM44RFHiMH8igdsaSBgFeskhSl/kLWLDUvIh1RXCrTmg0/zvA==",
"dev": true,
"requires": {
"@babel/code-frame": "^7.18.6",
"@babel/generator": "^7.18.10",
"@babel/generator": "^7.18.13",
"@babel/helper-environment-visitor": "^7.18.9",
"@babel/helper-function-name": "^7.18.9",
"@babel/helper-hoist-variables": "^7.18.6",
"@babel/helper-split-export-declaration": "^7.18.6",
"@babel/parser": "^7.18.11",
"@babel/types": "^7.18.10",
"@babel/parser": "^7.18.13",
"@babel/types": "^7.18.13",
"debug": "^4.1.0",
"globals": "^11.1.0"
}
},
"@babel/types": {
"version": "7.18.10",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.18.10.tgz",
"integrity": "sha512-MJvnbEiiNkpjo+LknnmRrqbY1GPUUggjv+wQVjetM/AONoupqRALB7I6jGqNUAZsKcRIEu2J6FRFvsczljjsaQ==",
"version": "7.18.13",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.18.13.tgz",
"integrity": "sha512-ePqfTihzW0W6XAU+aMw2ykilisStJfDnsejDCXRchCcMJ4O0+8DhPXf2YUbZ6wjBlsEmZwLK/sPweWtu8hcJYQ==",
"dev": true,
"requires": {
"@babel/helper-string-parser": "^7.18.10",
@@ -7252,14 +7221,14 @@
}
},
"@eslint/eslintrc": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.3.0.tgz",
"integrity": "sha512-UWW0TMTmk2d7hLcWD1/e2g5HDM/HQ3csaLSqXCfqwh4uNDuNqlaKWXmEsL4Cs41Z0KnILNvwbHAah3C2yt06kw==",
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.3.1.tgz",
"integrity": "sha512-OhSY22oQQdw3zgPOOwdoj01l/Dzl1Z+xyUP33tkSN+aqyEhymJCcPHyXt+ylW8FSe0TfRC2VG+ROQOapD0aZSQ==",
"dev": true,
"requires": {
"ajv": "^6.12.4",
"debug": "^4.3.2",
"espree": "^9.3.2",
"espree": "^9.4.0",
"globals": "^13.15.0",
"ignore": "^5.2.0",
"import-fresh": "^3.2.1",
@@ -7302,6 +7271,12 @@
"integrity": "sha512-rSqmMJDdLFUsyxR6FMtD00nfQKKLFb1kv+qBbOVKqErvloEIJLo5bDTJTQNTYgeyp78JsA7u/NPi5jT1GR/MuA==",
"dev": true
},
"@humanwhocodes/module-importer": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz",
"integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==",
"dev": true
},
"@humanwhocodes/object-schema": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz",
@@ -7829,23 +7804,6 @@
"wrap-ansi": "^7.0.0"
}
},
"clone-regexp": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/clone-regexp/-/clone-regexp-2.2.0.tgz",
"integrity": "sha512-beMpP7BOtTipFuW8hrJvREQ2DrRu3BE7by0ZpibtfBA+qfHYvMGTc2Yb1JMYPKg/JUw0CHYvpg796aNTSW9z7Q==",
"dev": true,
"requires": {
"is-regexp": "^2.0.0"
},
"dependencies": {
"is-regexp": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/is-regexp/-/is-regexp-2.1.0.tgz",
"integrity": "sha512-OZ4IlER3zmRIoB9AqNhEggVxqIH4ofDns5nRrPS6yQxXE1TPCUpFznBfRQmQa8uC+pXqjMnukiJBxCisIxiLGA==",
"dev": true
}
}
},
"color-convert": {
"version": "1.9.3",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
@@ -7862,9 +7820,9 @@
"dev": true
},
"colord": {
"version": "2.9.2",
"resolved": "https://registry.npmjs.org/colord/-/colord-2.9.2.tgz",
"integrity": "sha512-Uqbg+J445nc1TKn4FoDPS6ZZqAvEDnwrH42yo8B40JSOgSLxMZ/gt3h4nmCtPLQeXhjJJkqBx7SCY35WnIixaQ==",
"version": "2.9.3",
"resolved": "https://registry.npmjs.org/colord/-/colord-2.9.3.tgz",
"integrity": "sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==",
"dev": true
},
"colorette": {
@@ -8043,9 +8001,9 @@
"dev": true
},
"cypress": {
"version": "10.4.0",
"resolved": "https://registry.npmjs.org/cypress/-/cypress-10.4.0.tgz",
"integrity": "sha512-OM7F8MRE01SHQRVVzunid1ZK1m90XTxYnl+7uZfIrB4CYqUDCrZEeSyCXzIbsS6qcaijVCAhqDL60SxG8N6hew==",
"version": "10.6.0",
"resolved": "https://registry.npmjs.org/cypress/-/cypress-10.6.0.tgz",
"integrity": "sha512-6sOpHjostp8gcLO34p6r/Ci342lBs8S5z9/eb3ZCQ22w2cIhMWGUoGKkosabPBfKcvRS9BE4UxybBtlIs8gTQA==",
"dev": true,
"requires": {
"@cypress/request": "^2.88.10",
@@ -8382,14 +8340,15 @@
"dev": true
},
"eslint": {
"version": "8.21.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-8.21.0.tgz",
"integrity": "sha512-/XJ1+Qurf1T9G2M5IHrsjp+xrGT73RZf23xA1z5wB1ZzzEAWSZKvRwhWxTFp1rvkvCfwcvAUNAP31bhKTTGfDA==",
"version": "8.23.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-8.23.0.tgz",
"integrity": "sha512-pBG/XOn0MsJcKcTRLr27S5HpzQo4kLr+HjLQIyK4EiCsijDl/TB+h5uEuJU6bQ8Edvwz1XWOjpaP2qgnXGpTcA==",
"dev": true,
"requires": {
"@eslint/eslintrc": "^1.3.0",
"@eslint/eslintrc": "^1.3.1",
"@humanwhocodes/config-array": "^0.10.4",
"@humanwhocodes/gitignore-to-minimatch": "^1.0.2",
"@humanwhocodes/module-importer": "^1.0.1",
"ajv": "^6.10.0",
"chalk": "^4.0.0",
"cross-spawn": "^7.0.2",
@@ -8399,7 +8358,7 @@
"eslint-scope": "^7.1.1",
"eslint-utils": "^3.0.0",
"eslint-visitor-keys": "^3.3.0",
"espree": "^9.3.3",
"espree": "^9.4.0",
"esquery": "^1.4.0",
"esutils": "^2.0.2",
"fast-deep-equal": "^3.1.3",
@@ -8424,8 +8383,7 @@
"regexpp": "^3.2.0",
"strip-ansi": "^6.0.1",
"strip-json-comments": "^3.1.0",
"text-table": "^0.2.0",
"v8-compile-cache": "^2.0.3"
"text-table": "^0.2.0"
},
"dependencies": {
"ansi-styles": {
@@ -8743,9 +8701,9 @@
}
},
"eslint-plugin-react": {
"version": "7.30.1",
"resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.30.1.tgz",
"integrity": "sha512-NbEvI9jtqO46yJA3wcRF9Mo0lF9T/jhdHqhCHXiXtD+Zcb98812wvokjWpU7Q4QH5edo6dmqrukxVvWWXHlsUg==",
"version": "7.31.1",
"resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.31.1.tgz",
"integrity": "sha512-j4/2xWqt/R7AZzG8CakGHA6Xa/u7iR8Q3xCxY+AUghdT92bnIDOBEefV456OeH0QvBcroVc0eyvrrLSyQGYIfg==",
"dev": true,
"requires": {
"array-includes": "^3.1.5",
@@ -8833,9 +8791,9 @@
"dev": true
},
"espree": {
"version": "9.3.3",
"resolved": "https://registry.npmjs.org/espree/-/espree-9.3.3.tgz",
"integrity": "sha512-ORs1Rt/uQTqUKjDdGCyrtYxbazf5umATSf/K4qxjmZHORR6HJk+2s/2Pqe+Kk49HHINC/xNIrGfgh8sZcll0ng==",
"version": "9.4.0",
"resolved": "https://registry.npmjs.org/espree/-/espree-9.4.0.tgz",
"integrity": "sha512-DQmnRpLj7f6TgN/NYb0MTzJXL+vJF9h3pHy4JhCIs3zwcgez8xmGg3sXHcEO97BrmO2OSvCwMdfdlyl+E9KjOw==",
"dev": true,
"requires": {
"acorn": "^8.8.0",
@@ -8904,15 +8862,6 @@
"strip-final-newline": "^2.0.0"
}
},
"execall": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/execall/-/execall-2.0.0.tgz",
"integrity": "sha512-0FU2hZ5Hh6iQnarpRtQurM/aAvp3RIbfvgLHrcqJYzhXyV2KFruhuChf9NC6waAhiUR7FFtlugkI4p7f2Fqlow==",
"dev": true,
"requires": {
"clone-regexp": "^2.1.0"
}
},
"executable": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/executable/-/executable-4.1.1.tgz",
@@ -8984,9 +8933,9 @@
"dev": true
},
"fastest-levenshtein": {
"version": "1.0.14",
"resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.14.tgz",
"integrity": "sha512-tFfWHjnuUfKE186Tfgr+jtaFc0mZTApEgKDOeyN+FwOqRkO/zK/3h1AiRd8u8CY53owL3CUmGr/oI9p/RdyLTA==",
"version": "1.0.16",
"resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz",
"integrity": "sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==",
"dev": true
},
"fastq": {
@@ -9147,12 +9096,6 @@
"has-symbols": "^1.0.3"
}
},
"get-stdin": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-8.0.0.tgz",
"integrity": "sha512-sY22aA6xchAzprjyqmSEQv4UbAAzRN0L2dQB0NlN5acTTK9Don6nhoc3eAbUnpZiCANAMfd/+40kVdKfFygohg==",
"dev": true
},
"get-stream": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz",
@@ -10742,9 +10685,9 @@
"dev": true
},
"postcss": {
"version": "8.4.14",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.14.tgz",
"integrity": "sha512-E398TUmfAYFPBSdzgeieK2Y1+1cpdxJx8yXbK/m57nRhKSmk1GB2tO4lbLBtlkfPQTDKfe4Xqv1ASWPpayPEig==",
"version": "8.4.16",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.16.tgz",
"integrity": "sha512-ipHE1XBvKzm5xI7hiHCZJCSugxvsdq2mPnsq5+UF+VHCjiBvtDrlxJfMBToWaP9D5XlgNmcFGqoHmUn0EYEaRQ==",
"dev": true,
"requires": {
"nanoid": "^3.3.4",
@@ -11375,22 +11318,20 @@
"dev": true
},
"stylelint": {
"version": "14.9.1",
"resolved": "https://registry.npmjs.org/stylelint/-/stylelint-14.9.1.tgz",
"integrity": "sha512-RdAkJdPiLqHawCSnu21nE27MjNXaVd4WcOHA4vK5GtIGjScfhNnaOuWR2wWdfKFAvcWQPOYe311iveiVKSmwsA==",
"version": "14.11.0",
"resolved": "https://registry.npmjs.org/stylelint/-/stylelint-14.11.0.tgz",
"integrity": "sha512-OTLjLPxpvGtojEfpESWM8Ir64Z01E89xsisaBMUP/ngOx1+4VG2DPRcUyCCiin9Rd3kPXPsh/uwHd9eqnvhsYA==",
"dev": true,
"requires": {
"@csstools/selector-specificity": "^2.0.1",
"@csstools/selector-specificity": "^2.0.2",
"balanced-match": "^2.0.0",
"colord": "^2.9.2",
"colord": "^2.9.3",
"cosmiconfig": "^7.0.1",
"css-functions-list": "^3.1.0",
"debug": "^4.3.4",
"execall": "^2.0.0",
"fast-glob": "^3.2.11",
"fastest-levenshtein": "^1.0.12",
"fastest-levenshtein": "^1.0.16",
"file-entry-cache": "^6.0.1",
"get-stdin": "^8.0.0",
"global-modules": "^2.0.0",
"globby": "^11.1.0",
"globjoin": "^0.1.4",
@@ -11405,7 +11346,7 @@
"micromatch": "^4.0.5",
"normalize-path": "^3.0.0",
"picocolors": "^1.0.0",
"postcss": "^8.4.14",
"postcss": "^8.4.16",
"postcss-media-query-parser": "^0.2.3",
"postcss-resolve-nested-selector": "^0.1.1",
"postcss-safe-parser": "^6.0.0",
@@ -11419,7 +11360,7 @@
"svg-tags": "^1.0.0",
"table": "^6.8.0",
"v8-compile-cache": "^2.3.0",
"write-file-atomic": "^4.0.1"
"write-file-atomic": "^4.0.2"
},
"dependencies": {
"balanced-match": {
@@ -11861,9 +11802,9 @@
"dev": true
},
"write-file-atomic": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.1.tgz",
"integrity": "sha512-nSKUxgAbyioruk6hU87QzVbY279oYT6uiwgDoujth2ju4mJ+TZau7SQBhtbTmUyuNYTuXnSyRn66FV0+eCgcrQ==",
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz",
"integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==",
"dev": true,
"requires": {
"imurmurhash": "^0.1.4",
+5 -5
View File
@@ -29,12 +29,12 @@
},
"homepage": "https://github.com/UniversityOfHelsinkiCS/oodikone#readme",
"devDependencies": {
"@babel/core": "^7.18.10",
"@babel/core": "^7.18.13",
"@babel/eslint-parser": "^7.17.0",
"@babel/plugin-syntax-jsx": "^7.16.7",
"concurrently": "^7.3.0",
"cypress": "^10.4.0",
"eslint": "^8.21.0",
"cypress": "^10.6.0",
"eslint": "^8.23.0",
"eslint-config-airbnb": "^19.0.4",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-babel": "^5.3.1",
@@ -43,13 +43,13 @@
"eslint-plugin-jsx-a11y": "^6.5.1",
"eslint-plugin-prefer-object-spread": "^1.2.1",
"eslint-plugin-prettier": "^4.2.1",
"eslint-plugin-react": "^7.29.3",
"eslint-plugin-react": "^7.31.1",
"eslint-plugin-react-hooks": "^4.3.0",
"husky": "^8.0.1",
"lint-staged": "^13.0.3",
"node-actionlint": "^1.2.2",
"prettier": "^2.5.1",
"stylelint": "^14.9.1",
"stylelint": "^14.11.0",
"stylelint-config-prettier": "^9.0.3",
"stylelint-config-standard": "^26.0.0",
"stylelint-prettier": "^2.0.0"
+30 -30
View File
@@ -19,11 +19,11 @@
"lodash": "^4.17.21",
"lru-cache": "^5.1.1",
"moment": "^2.29.1",
"moment-timezone": "^0.5.34",
"moment-timezone": "^0.5.37",
"morgan": "^1.10.0",
"pg": "^8.7.3",
"pg": "^8.8.0",
"redis": "^3.1.2",
"sequelize": "^6.17.0",
"sequelize": "^6.21.4",
"umzug": "^2.3.0",
"unfuck-utf8-headers-middleware": "^1.0.1",
"uuid": "^3.4.0",
@@ -2209,9 +2209,9 @@
}
},
"node_modules/moment-timezone": {
"version": "0.5.34",
"resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.34.tgz",
"integrity": "sha512-3zAEHh2hKUs3EXLESx/wsgw6IQdusOT8Bxm3D9UrHPQR7zlMmzwybC8zHEM1tQ4LJwP7fcxrWr8tuBg05fFCbg==",
"version": "0.5.37",
"resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.37.tgz",
"integrity": "sha512-uEDzDNFhfaywRl+vwXxffjjq1q0Vzr+fcQpQ1bU0kbzorfS7zVtZnCnGc8mhWmF39d4g4YriF6kwA75mJKE/Zg==",
"dependencies": {
"moment": ">= 2.9.0"
},
@@ -2619,14 +2619,14 @@
"integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ=="
},
"node_modules/pg": {
"version": "8.7.3",
"resolved": "https://registry.npmjs.org/pg/-/pg-8.7.3.tgz",
"integrity": "sha512-HPmH4GH4H3AOprDJOazoIcpI49XFsHCe8xlrjHkWiapdbHK+HLtbm/GQzXYAZwmPju/kzKhjaSfMACG+8cgJcw==",
"version": "8.8.0",
"resolved": "https://registry.npmjs.org/pg/-/pg-8.8.0.tgz",
"integrity": "sha512-UXYN0ziKj+AeNNP7VDMwrehpACThH7LUl/p8TDFpEUuSejCUIwGSfxpHsPvtM6/WXFy6SU4E5RG4IJV/TZAGjw==",
"dependencies": {
"buffer-writer": "2.0.0",
"packet-reader": "1.0.0",
"pg-connection-string": "^2.5.0",
"pg-pool": "^3.5.1",
"pg-pool": "^3.5.2",
"pg-protocol": "^1.5.0",
"pg-types": "^2.1.0",
"pgpass": "1.x"
@@ -2635,7 +2635,7 @@
"node": ">= 8.0.0"
},
"peerDependencies": {
"pg-native": ">=2.0.0"
"pg-native": ">=3.0.1"
},
"peerDependenciesMeta": {
"pg-native": {
@@ -2657,9 +2657,9 @@
}
},
"node_modules/pg-pool": {
"version": "3.5.1",
"resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.5.1.tgz",
"integrity": "sha512-6iCR0wVrro6OOHFsyavV+i6KYL4lVNyYAB9RD18w66xSzN+d8b66HiwuP30Gp1SH5O9T82fckkzsRjlrhD0ioQ==",
"version": "3.5.2",
"resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.5.2.tgz",
"integrity": "sha512-His3Fh17Z4eg7oANLob6ZvH8xIVen3phEZh2QuyrIl4dQSDVEabNducv6ysROKpDNPSD+12tONZVWfSgMvDD9w==",
"peerDependencies": {
"pg": ">=8.0"
}
@@ -3108,9 +3108,9 @@
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
},
"node_modules/sequelize": {
"version": "6.21.3",
"resolved": "https://registry.npmjs.org/sequelize/-/sequelize-6.21.3.tgz",
"integrity": "sha512-cJPrTTVCofUxaaNKoIETiXCYh2xJ+OFq5jMHJQqftp34M4kNoLpTfUMPSwYtRUeTcSh1/5HodfJXIBi7troIFA==",
"version": "6.21.4",
"resolved": "https://registry.npmjs.org/sequelize/-/sequelize-6.21.4.tgz",
"integrity": "sha512-5A0+giRhGHerTDRMsZ54TYRB8oQPWxeVscbc4USG9wRtw2Eqik0Vk0p2EVDrhoq7tmNBh2nHpd9YMfvGdwPEJw==",
"funding": [
{
"type": "opencollective",
@@ -5884,9 +5884,9 @@
"integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w=="
},
"moment-timezone": {
"version": "0.5.34",
"resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.34.tgz",
"integrity": "sha512-3zAEHh2hKUs3EXLESx/wsgw6IQdusOT8Bxm3D9UrHPQR7zlMmzwybC8zHEM1tQ4LJwP7fcxrWr8tuBg05fFCbg==",
"version": "0.5.37",
"resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.37.tgz",
"integrity": "sha512-uEDzDNFhfaywRl+vwXxffjjq1q0Vzr+fcQpQ1bU0kbzorfS7zVtZnCnGc8mhWmF39d4g4YriF6kwA75mJKE/Zg==",
"requires": {
"moment": ">= 2.9.0"
}
@@ -6207,14 +6207,14 @@
"integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ=="
},
"pg": {
"version": "8.7.3",
"resolved": "https://registry.npmjs.org/pg/-/pg-8.7.3.tgz",
"integrity": "sha512-HPmH4GH4H3AOprDJOazoIcpI49XFsHCe8xlrjHkWiapdbHK+HLtbm/GQzXYAZwmPju/kzKhjaSfMACG+8cgJcw==",
"version": "8.8.0",
"resolved": "https://registry.npmjs.org/pg/-/pg-8.8.0.tgz",
"integrity": "sha512-UXYN0ziKj+AeNNP7VDMwrehpACThH7LUl/p8TDFpEUuSejCUIwGSfxpHsPvtM6/WXFy6SU4E5RG4IJV/TZAGjw==",
"requires": {
"buffer-writer": "2.0.0",
"packet-reader": "1.0.0",
"pg-connection-string": "^2.5.0",
"pg-pool": "^3.5.1",
"pg-pool": "^3.5.2",
"pg-protocol": "^1.5.0",
"pg-types": "^2.1.0",
"pgpass": "1.x"
@@ -6231,9 +6231,9 @@
"integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw=="
},
"pg-pool": {
"version": "3.5.1",
"resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.5.1.tgz",
"integrity": "sha512-6iCR0wVrro6OOHFsyavV+i6KYL4lVNyYAB9RD18w66xSzN+d8b66HiwuP30Gp1SH5O9T82fckkzsRjlrhD0ioQ==",
"version": "3.5.2",
"resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.5.2.tgz",
"integrity": "sha512-His3Fh17Z4eg7oANLob6ZvH8xIVen3phEZh2QuyrIl4dQSDVEabNducv6ysROKpDNPSD+12tONZVWfSgMvDD9w==",
"requires": {}
},
"pg-protocol": {
@@ -6580,9 +6580,9 @@
}
},
"sequelize": {
"version": "6.21.3",
"resolved": "https://registry.npmjs.org/sequelize/-/sequelize-6.21.3.tgz",
"integrity": "sha512-cJPrTTVCofUxaaNKoIETiXCYh2xJ+OFq5jMHJQqftp34M4kNoLpTfUMPSwYtRUeTcSh1/5HodfJXIBi7troIFA==",
"version": "6.21.4",
"resolved": "https://registry.npmjs.org/sequelize/-/sequelize-6.21.4.tgz",
"integrity": "sha512-5A0+giRhGHerTDRMsZ54TYRB8oQPWxeVscbc4USG9wRtw2Eqik0Vk0p2EVDrhoq7tmNBh2nHpd9YMfvGdwPEJw==",
"requires": {
"@types/debug": "^4.1.7",
"@types/validator": "^13.7.1",
+3 -3
View File
@@ -20,11 +20,11 @@
"lodash": "^4.17.21",
"lru-cache": "^5.1.1",
"moment": "^2.29.1",
"moment-timezone": "^0.5.34",
"moment-timezone": "^0.5.37",
"morgan": "^1.10.0",
"pg": "^8.7.3",
"pg": "^8.8.0",
"redis": "^3.1.2",
"sequelize": "^6.17.0",
"sequelize": "^6.21.4",
"umzug": "^2.3.0",
"unfuck-utf8-headers-middleware": "^1.0.1",
"uuid": "^3.4.0",
@@ -0,0 +1,17 @@
module.exports = {
up: queryInterface => {
return queryInterface.bulkInsert('access_groups', [
{
group_code: 'facultyStatistics',
group_info: 'grants access to faculty statistics',
},
])
},
down: queryInterface => {
return queryInterface.bulkDelete('access_groups', [
{
group_code: 'facultyStatistics',
},
])
},
}
+21 -1
View File
@@ -5,6 +5,8 @@ const { refreshAssociationsInRedis } = require('./services/studyrights')
const { getAllProgrammes } = require('./services/studyrights')
const { updateBasicView, updateStudytrackView } = require('./services/studyprogrammeUpdates')
const { findAndSaveTeachers } = require('./services/topteachers')
const { faculties } = require('./services/organisations')
const { updateFacultyOverview } = require('./services/faculty/facultyUpdates')
const { isProduction } = require('./conf-backend')
const { getCurrentSemester } = require('./services/semesters')
const logger = require('./util/logger')
@@ -16,6 +18,23 @@ const refreshStudyrightAssociations = async () => {
logger.info('Studyright associations refreshed!')
}
const refreshFaculties = async () => {
logger.info('Refreshing faculty overview statistics for all faculties')
const facultyList = (await faculties()).filter(f => !['Y', 'H99', 'Y01', 'H92', 'H930'].includes(f.code))
let ready = 0
for (const faculty of facultyList) {
try {
await updateFacultyOverview(faculty.code, 'ALL')
ready += 1
} catch (e) {
logger.error({ message: `Failed to update stats for faculty ${faculty?.code} with type ALL`, meta: e })
}
logger.info(`${ready}/${facultyList.length} faculties done`)
}
logger.info('Faculty stats refreshed!')
}
const refreshNewOverviews = async () => {
logger.info('Refreshing studyprogramme and studytrack overview statistics for all programmes')
const codes = (await getAllProgrammes())
@@ -100,7 +119,7 @@ const startCron = () => {
logger.info('Cronjob for refreshing stats started: runs at 3am every day.')
// refresh 3am every day
schedule('0 3 * * *', async () => {
for (const func of [refreshStatistics, refreshTrends, refreshNewOverviews]) {
for (const func of [refreshStatistics, refreshTrends, refreshNewOverviews, refreshFaculties]) {
await func()
}
})
@@ -111,4 +130,5 @@ module.exports = {
startCron,
refreshStatistics,
refreshTrends,
refreshFaculties,
}
@@ -27,11 +27,16 @@ const accessLogger = morgan((tokens, req, res) => {
'ms',
].join(' ')
const usingIamRights = user.iamRights.some(programmeCode => tokens['url'](req, res).includes(programmeCode))
const onlyIamRights = !user.isAdmin && user.rights.length === 0
logger.info(message, {
// don't log student list which might be huge
..._.omit(meta, ['studentsUserCanAccess']),
// pass this as a custom field so we can filter by it in graylog
isUsageStats: true,
// needed for Grafana IAM users panel
isIamRights: onlyIamRights || usingIamRights,
})
})
+1 -1
View File
@@ -57,7 +57,7 @@ module.exports = (app, url) => {
app.use(url, tags)
app.use(url, programmeModules)
app.use(url, studyProgramme)
app.use(url, faculties)
app.use(`${url}/faculties`, auth.roles(['facultyStatistics']), faculties)
app.use(`${url}/updater`, auth.roles(['admin']), updater)
app.use(`${url}/teachers`, auth.roles(['teachers']), teachers)
app.use(`${url}/users`, auth.roles(['admin']), users)
+139 -75
View File
@@ -1,103 +1,167 @@
const router = require('express').Router()
const { faculties, degreeProgrammeCodesOfFaculty } = require('../services/organisations')
const { combineFacultyBasics } = require('../services/facultyBasics')
const { combineFacultyCredits } = require('../services/facultyCredits')
const { degreeProgrammesOfFaculty } = require('../services/faculty')
const { faculties } = require('../services/organisations')
const { combineFacultyBasics } = require('../services/faculty/facultyBasics')
const { combineFacultyCredits } = require('../services/faculty/facultyCredits')
const { findFacultyProgrammeCodes } = require('../services/faculty/faculty')
const { combineFacultyThesisWriters } = require('../services/faculty/facultyThesisWriters')
const { countGraduationTimes } = require('../services/faculty/facultyGraduationTimes')
const { updateFacultyOverview } = require('../services/faculty/facultyUpdates')
const { getFacultyStudentProgress } = require('../services/faculty/facultyStudentProgress')
//const { getFacultyStudents } = require('../services/faculty/facultyStudents')
const {
getFacultyProgrammes,
setFacultyProgrammes,
getBasicStats,
setBasicStats,
getCreditStats,
setCreditStats,
getThesisWritersStats,
setThesisWritersStats,
} = require('../services/faculty/facultyService')
const logger = require('../util/logger')
// Faculty uses a lot of tools designed for Study programme.
// Some of them have been copied here and slightly edited for faculty purpose.
router.get('/faculties', async (req, res) => {
const facultyList = await faculties()
const getProgrammes = async (code, programmeFilter) => {
const programmes = await getFacultyProgrammes(code, programmeFilter)
if (programmes) return programmes
let updatedProgrammes = await findFacultyProgrammeCodes(code, programmeFilter)
if (updatedProgrammes) updatedProgrammes = await setFacultyProgrammes(code, updatedProgrammes, programmeFilter)
return updatedProgrammes
}
router.get('/', async (req, res) => {
const ignore = ['Y', 'H99', 'Y01', 'H92', 'H930']
const facultyList = (await faculties()).filter(f => !ignore.includes(f.code))
facultyList.sort((a, b) => (a.name.fi > b.name.fi ? 1 : -1))
res.json(facultyList)
})
router.get('/faculties/:id/basicstats', async (req, res) => {
router.get('/:id/basicstats', async (req, res) => {
const code = req.params.id
const yearType = req.query?.year_type
const programmeFilter = req.query?.programme_filter
const specialGroups = req.query?.special_groups
if (!code) return res.status(422).end()
let allBasics = {
id: code,
years: [],
programmeNames: {},
studentInfo: {
tableStats: [],
graphStats: [],
titles: ['', 'Started studying', 'Graduated', 'Transferred inside', 'Transferred away', 'Transferred to'],
programmeTableStats: {},
},
graduationInfo: {
tableStats: [],
graphStats: [],
titles: ['', 'All graduations', 'Bachelors', 'Masters', 'Doctors', 'Others'],
programmeTableStats: {},
},
status: 'DONE',
lastUpdated: '',
const data = await getBasicStats(code, yearType, programmeFilter, specialGroups)
if (data) return res.json(data)
const wantedProgrammes = await getProgrammes(code, programmeFilter)
if (!wantedProgrammes) return res.status(422).end()
//all programmes are required for correct sorting of transfers
let allProgrammeCodes = []
if (programmeFilter === 'NEW_STUDY_PROGRAMMES') {
const allProgs = await getProgrammes(code, 'ALL_PROGRAMMES')
allProgs?.data.forEach(prog => allProgrammeCodes.push(prog.code))
} else {
wantedProgrammes?.data.forEach(prog => allProgrammeCodes.push(prog.code))
}
const programmes = await degreeProgrammeCodesOfFaculty(code)
if (programmes) {
await combineFacultyBasics(allBasics, code, programmes, yearType)
let updatedStats = await combineFacultyBasics(
code,
wantedProgrammes.data,
yearType,
allProgrammeCodes,
programmeFilter,
specialGroups
)
if (updatedStats) {
updatedStats = await setBasicStats(updatedStats, yearType, programmeFilter, specialGroups)
}
return res.json(allBasics)
return res.json(updatedStats)
})
router.get('/faculties/:id/creditstats', async (req, res) => {
router.get('/:id/creditstats', async (req, res) => {
const code = req.params.id
const yearType = req.query?.year_type
const specialGroups = 'SPECIAL_INCLUDED' // req.query?.special_groups
const specialGroups = req.query?.special_groups
const programmeFilter = req.query?.programme_filter
if (!code) return res.status(422).end()
let counts = {}
let years = []
let allCredits = {
id: code,
years: [],
tableStats: [],
graphStats: [],
programmeTableStats: {},
programmeNames: {},
titles: [
'',
'Total',
'Major students credits',
'Non-major faculty students credits',
'Non-major other faculty students credits',
'Non-degree student credits',
],
status: 'DONE',
lastUpdated: '',
const data = await getCreditStats(code, yearType, programmeFilter, specialGroups)
if (data) return res.json(data)
const programmes = await getProgrammes(code, programmeFilter)
if (!programmes) return res.status(422).end()
let updatedStats = await combineFacultyCredits(code, programmes.data, yearType, specialGroups)
if (updatedStats) {
updatedStats = await setCreditStats(updatedStats, yearType, programmeFilter, specialGroups)
}
const programmes = await degreeProgrammesOfFaculty(code)
if (programmes) {
await combineFacultyCredits(allCredits, programmes, yearType, specialGroups, counts, years)
}
let majors = []
let facultyNonMajor = []
let otherNonMajor = []
let nonDegree = []
years.forEach(year => {
majors.push(counts[year][1])
facultyNonMajor.push(counts[year][2])
otherNonMajor.push(counts[year][3])
nonDegree.push(counts[year][4])
})
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 res.json(allCredits)
return res.json(updatedStats)
})
router.get('/:id/thesisstats', 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 getThesisWritersStats(code, yearType, programmeFilter, specialGroups)
if (data) return res.json(data)
const programmes = await getProgrammes(code, programmeFilter)
if (!programmes) return res.status(422).end()
let updateStats = await combineFacultyThesisWriters(code, programmes.data, yearType, specialGroups)
if (updateStats) {
updateStats = await setThesisWritersStats(updateStats, yearType, programmeFilter, specialGroups)
}
return res.json(updateStats)
})
router.get('/:id/graduationtimes', async (req, res) => {
const code = req.params.id
const programmeFilter = req.query?.programme_filter
if (!code) return res.status(422).end()
// const programmes = await getProgrammes(code, programmeFilter)
const result = await countGraduationTimes(code, programmeFilter)
// currently counts all programmes
return res.json(result)
})
router.get('/:id/progressstats', async (req, res) => {
const faculty = req.params.id
const programmeFilter = req.query.programme_filter
if (!faculty) return res.status(422).end()
const programmes = await getProgrammes(faculty, programmeFilter)
const progressStats = await getFacultyStudentProgress(faculty, programmes)
return res.json(progressStats)
})
// router.get('/:id/studentstats', async (req, res) => {
// const code = req.params.id
// const programmeFilter = req.query.programme_filter
// if (!code) return res.status(422).end()
// const programmes = await getProgrammes(code, programmeFilter)
// const studentStats = await getFacultyStudents(code, programmes)
// return res.json(studentStats)
// })
router.get('/:id/update_basicview', async (req, res) => {
const code = req.params.id
const statsType = req.query?.stats_type
if (code) {
let result = null
try {
result = await updateFacultyOverview(code, statsType)
} catch (e) {
logger.error(`Failed to update faculty ${code} basic tab stats for ${statsType}: ${e}`)
}
return res.json(result)
}
return res.status(422).end()
})
module.exports = router
+19 -1
View File
@@ -48,7 +48,11 @@ router.post('/v2/populationstatistics/courses', async (req, res) => {
if (req.body.selectedStudentsByYear) {
req.body.selectedStudents = encrypted
? req.body.selectedStudents
.filter(({ encryptedData }) => req.body.selectedStudentsByYear[year].includes(encryptedData))
.filter(
({ encryptedData }) =>
req.body.selectedStudentsByYear[year] &&
req.body.selectedStudentsByYear[year].includes(encryptedData)
)
.map(decrypt)
: req.body.selectedStudentsByYear[year]
}
@@ -77,6 +81,20 @@ 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)
}
})
+1 -1
View File
@@ -450,7 +450,7 @@ const codeLikeTerm = code =>
: {
code: {
// Starts with code or has AY/A in front of the code
[Op.regexp]: `^(AY|ay|A|a)?${code}`,
[Op.iRegexp]: `^(AY|ay|A|a)?${code}`,
},
}
-144
View File
@@ -1,144 +0,0 @@
const Sequelize = require('sequelize')
const { Op } = Sequelize
const {
ElementDetail,
Organization,
ProgrammeModule,
Studyright,
StudyrightElement,
Student,
Transfer,
} = require('../models')
const { facultyFormatStudyright, facultyFormatProgramme, formatFacultyTransfer } = require('./facultyHelpers')
const startedStudyrights = async (faculty, since) =>
(
await Studyright.findAll({
include: [
{
model: Student,
attributes: ['studentnumber'],
required: true,
},
{
model: StudyrightElement,
required: true,
include: {
model: ElementDetail,
required: true,
},
},
],
where: {
faculty_code: faculty,
studystartdate: {
[Op.gte]: since,
},
student_studentnumber: { [Op.not]: null },
},
})
).map(facultyFormatStudyright)
const graduatedStudyrights = async (faculty, since) =>
(
await Studyright.findAll({
include: [
{
model: Student,
attributes: ['studentnumber'],
required: true,
},
{
model: StudyrightElement,
required: true,
include: {
model: ElementDetail,
required: true,
},
},
],
where: {
faculty_code: faculty,
enddate: {
[Op.gte]: since,
},
graduated: 1,
student_studentnumber: { [Op.not]: null },
},
})
).map(facultyFormatStudyright)
const transferredInsideFaculty = async (programmes, since) =>
(
await Transfer.findAll({
where: {
transferdate: {
[Op.gte]: since,
},
sourcecode: programmes,
targetcode: programmes,
},
})
).map(formatFacultyTransfer)
const transferredAway = async (programmes, since) =>
(
await Transfer.findAll({
where: {
transferdate: {
[Op.gte]: since,
},
sourcecode: programmes,
targetcode: {
[Op.notIn]: programmes,
},
},
})
).map(formatFacultyTransfer)
const transferredTo = async (programmes, since) =>
(
await Transfer.findAll({
where: {
transferdate: {
[Op.gte]: since,
},
sourcecode: {
[Op.notIn]: programmes,
},
targetcode: programmes,
},
})
).map(formatFacultyTransfer)
const degreeProgrammesOfFaculty = async facultyCode =>
(
await ProgrammeModule.findAll({
attributes: ['code', 'name'],
include: {
model: Organization,
where: {
code: facultyCode,
},
},
})
).map(facultyFormatProgramme)
const getProgrammeName = async programme => {
return await ElementDetail.findOne({
attributes: ['name'],
where: {
code: programme,
},
})
}
module.exports = {
startedStudyrights,
graduatedStudyrights,
transferredInsideFaculty,
transferredAway,
transferredTo,
degreeProgrammesOfFaculty,
getProgrammeName,
}
@@ -0,0 +1,338 @@
const Sequelize = require('sequelize')
const { Op } = Sequelize
const {
ElementDetail,
Organization,
ProgrammeModule,
Semester,
SemesterEnrollment,
Studyright,
StudyrightElement,
Student,
Transfer,
Credit,
Course,
} = require('../../models')
const {
facultyFormatStudyright,
facultyFormatProgramme,
formatFacultyTransfer,
formatFacultyThesisWriter,
formatOrganization,
formatAbsence,
isNewProgramme,
} = require('./facultyHelpers')
const getTransferredToAndAway = async (programmeCodes, allProgrammeCodes, since) => {
const awayTransfers = (await transferredAway(programmeCodes, allProgrammeCodes, since)).map(t => t.studyrightid)
const toTransfers = (await transferredTo(programmeCodes, allProgrammeCodes, since)).map(t => t.studyrightid)
const transferredToOrAway = [...toTransfers, ...awayTransfers]
return transferredToOrAway
}
const getTransferredInside = async (programmeCodes, allProgrammeCodes, since) => {
const transfersInside = (await transferredInsideFaculty(programmeCodes, allProgrammeCodes, since)).map(
t => t.studyrightid
)
return transfersInside
}
const startedStudyrights = async (faculty, since, studyRightWhere) =>
(
await Studyright.findAll({
include: [
{
model: Student,
attributes: ['studentnumber'],
required: true,
},
{
model: StudyrightElement,
required: true,
include: {
model: ElementDetail,
required: true,
},
},
],
where: {
faculty_code: faculty,
studystartdate: {
[Op.gte]: since,
},
student_studentnumber: { [Op.not]: null },
...studyRightWhere,
},
})
).map(facultyFormatStudyright)
const graduatedStudyrights = async (faculty, since, studyrightWhere) =>
(
await Studyright.findAll({
include: [
{
model: Student,
attributes: ['studentnumber'],
required: true,
},
{
model: StudyrightElement,
required: true,
include: {
model: ElementDetail,
required: true,
},
},
],
where: {
faculty_code: faculty,
enddate: {
[Op.gte]: since,
},
graduated: 1,
student_studentnumber: { [Op.not]: null },
...studyrightWhere,
},
})
).map(facultyFormatStudyright)
const graduatedStudyrightsByStartYear = async (faculty, since) =>
(
await Studyright.findAll({
include: [
{
model: Student,
attributes: ['studentnumber'],
required: true,
},
{
model: StudyrightElement,
required: true,
include: {
model: ElementDetail,
required: true,
},
},
],
where: {
faculty_code: faculty,
startdate: {
[Op.gte]: since,
},
graduated: 1,
student_studentnumber: { [Op.not]: null },
},
})
).map(facultyFormatStudyright)
const bachelorStudyright = async id => {
return await Studyright.findOne({
attributes: ['studystartdate'],
where: {
studyrightid: id,
extentcode: 1,
},
})
}
const transferredInsideFaculty = async (programmes, allProgrammeCodes, since) =>
(
await Transfer.findAll({
where: {
transferdate: {
[Op.gte]: since,
},
sourcecode: allProgrammeCodes,
targetcode: programmes,
},
})
).map(formatFacultyTransfer)
const transferredAway = async (programmes, allProgrammeCodes, since) =>
(
await Transfer.findAll({
where: {
transferdate: {
[Op.gte]: since,
},
sourcecode: programmes,
targetcode: {
[Op.notIn]: allProgrammeCodes,
},
},
})
).map(formatFacultyTransfer)
const transferredTo = async (programmes, allProgrammeCodes, since) =>
(
await Transfer.findAll({
where: {
transferdate: {
[Op.gte]: since,
},
sourcecode: {
[Op.notIn]: allProgrammeCodes,
},
targetcode: programmes,
},
})
).map(formatFacultyTransfer)
const degreeProgrammesOfFaculty = async facultyCode =>
(
await ProgrammeModule.findAll({
attributes: ['code', 'name'],
include: {
model: Organization,
where: {
code: {
[Op.startsWith]: facultyCode,
},
},
},
})
).map(facultyFormatProgramme)
const facultyOgranizationId = async faculty => {
return await Organization.findOne({
attributes: ['id'],
where: {
code: faculty,
},
})
}
const getChildOrganizations = async facultyId =>
(
await Organization.findAll({
attributes: ['id', 'name', 'code', 'parent_id'],
where: {
parent_id: facultyId,
},
})
).map(formatOrganization)
const thesisWriters = async (provider, since, thesisTypes, students) =>
(
await Credit.findAll({
attributes: ['id', 'course_code', 'attainment_date', 'student_studentnumber'],
include: {
model: Course,
attributes: ['course_unit_type'],
required: true,
where: {
course_unit_type: {
[Op.in]: thesisTypes,
},
},
include: {
model: Organization,
attributes: [],
required: true,
where: {
code: provider,
},
},
},
where: {
credittypecode: 4,
isStudyModule: {
[Op.not]: true,
},
attainment_date: {
[Op.gte]: since,
},
student_studentnumber: students.length > 0 ? students : { [Op.not]: null },
},
})
).map(formatFacultyThesisWriter)
const statutoryAbsences = async (studentnumber, startdate, enddate) =>
(
await SemesterEnrollment.findAll({
attributes: ['semestercode'],
include: {
model: Semester,
attributes: ['startdate', 'enddate'],
where: {
startdate: {
[Op.gte]: startdate,
},
enddate: {
[Op.lte]: enddate,
},
},
},
where: {
studentnumber: studentnumber,
statutory_absence: true,
},
})
).map(formatAbsence)
// Some programme modules are not directly associated to a faculty (organization).
// Some have intermediate organizations, such as department, so the connection must be digged up
const findFacultyProgrammeCodes = async (faculty, programmeFilter) => {
let allProgrammes = []
let allProgrammeCodes = []
const directAssociationProgrammes = await degreeProgrammesOfFaculty(faculty)
allProgrammes = allProgrammes.concat(directAssociationProgrammes)
allProgrammeCodes = allProgrammeCodes.concat(directAssociationProgrammes.map(prog => prog.code))
// find faculty's organization id and its direct child organizations
const { id } = await facultyOgranizationId(faculty)
const facultyChildOrganizations = await getChildOrganizations(id)
// get programme modules that have a faculty child as organization(_id)
for (const org of facultyChildOrganizations) {
const childAssociationProgrammes = await degreeProgrammesOfFaculty(org.code)
if (childAssociationProgrammes.length > 0) {
childAssociationProgrammes.forEach(prog => {
if (!(prog.code in allProgrammeCodes)) {
allProgrammes = allProgrammes.concat([prog])
allProgrammeCodes = allProgrammeCodes.concat([prog.code])
}
})
} else {
// dig deeper
const grandChildren = await getChildOrganizations(org.id)
if (grandChildren.length > 0) {
for (const gcOrg of grandChildren) {
const associatedProgrammes = await degreeProgrammesOfFaculty(gcOrg.code)
if (associatedProgrammes.length > 0) {
associatedProgrammes.forEach(prog => {
if (!(prog.code in allProgrammeCodes)) {
allProgrammes = allProgrammes.concat([prog])
allProgrammeCodes = allProgrammeCodes.concat([prog.code])
}
})
}
}
}
}
}
if (programmeFilter === 'NEW_STUDY_PROGRAMMES') {
allProgrammes = allProgrammes.filter(prog => isNewProgramme(prog.code))
}
return allProgrammes
}
module.exports = {
startedStudyrights,
graduatedStudyrights,
graduatedStudyrightsByStartYear,
bachelorStudyright,
transferredInsideFaculty,
transferredAway,
transferredTo,
degreeProgrammesOfFaculty,
thesisWriters,
findFacultyProgrammeCodes,
facultyOgranizationId,
statutoryAbsences,
getTransferredToAndAway,
getTransferredInside,
}
@@ -0,0 +1,432 @@
const { indexOf } = require('lodash')
const {
startedStudyrights,
graduatedStudyrights,
transferredInsideFaculty,
transferredAway,
transferredTo,
getTransferredToAndAway,
getTransferredInside,
} = require('./faculty')
const { getStatsBasis, getYearsArray, defineYear } = require('../studyprogrammeHelpers')
const { findRightProgramme, isNewProgramme, checkTransfers, getExtentFilter } = require('./facultyHelpers')
const filterDuplicateStudyrights = studyrights => {
// bachelor+master students have two studyrights (separated by two last digits in studyrightid)
// choose only the earlier started one, so we don't count start of masters as starting in faculty
let rightsToCount = {}
studyrights.forEach(right => {
const id = right.studyrightid.slice(0, -2)
const start = new Date(right.studystartdate)
if (id in rightsToCount) {
if (new Date(rightsToCount[id].studystartdate) > start) {
rightsToCount[id] = right
}
} else {
rightsToCount[id] = right
}
})
return Object.values(rightsToCount)
}
const addProgramme = async (
stats,
programme,
tableStats,
transferYear,
index,
allBasics,
facultyProgrammes,
includeAllSpecials
) => {
if (!(programme in stats)) {
stats[programme] = {}
if (includeAllSpecials) {
Object.keys(tableStats).forEach(year => (stats[programme][year] = [0, 0, 0]))
} else {
Object.keys(tableStats).forEach(year => (stats[programme][year] = [0]))
}
}
stats[programme][transferYear][index] += 1
if (!(programme in allBasics.programmeNames)) {
const name = facultyProgrammes.filter(prog => prog.code === programme)[0]?.name
allBasics.programmeNames[programme] = name
}
}
const getFacultyStarters = async (
faculty,
transfersToOrAwayStudyrights,
insideTransfersStudyrights,
since,
isAcademicYear,
yearsArray,
graphStats,
tableStats,
counts,
allBasics,
programmeData,
programmeFilter,
includeAllSpecials
) => {
const startedGraphStats = [...graphStats]
const startedTableStats = { ...tableStats }
const studyrightWhere = getExtentFilter(includeAllSpecials)
const studyrights = await startedStudyrights(faculty, since, studyrightWhere)
let filteredStudyrights = filterDuplicateStudyrights(studyrights)
const programmeTableStats = {}
if (!includeAllSpecials) {
filteredStudyrights = filteredStudyrights.filter(
s => !checkTransfers(s, insideTransfersStudyrights, transfersToOrAwayStudyrights)
)
}
filteredStudyrights.forEach(({ studystartdate, studyrightElements }) => {
const { programme, programmeName } = findRightProgramme(studyrightElements, 'started')
if (programmeFilter === 'ALL_PROGRAMMES' || isNewProgramme(programme)) {
const startYear = defineYear(studystartdate, isAcademicYear)
startedGraphStats[indexOf(yearsArray, startYear)] += 1
startedTableStats[startYear] += 1
if (!(programme in programmeTableStats)) {
programmeTableStats[programme] = { ...tableStats }
}
programmeTableStats[programme][startYear] += 1
if (!(programme in allBasics.programmeNames)) {
allBasics.programmeNames[programme] = programmeName
}
}
})
allBasics.studentInfo.graphStats.push({ name: 'Started studying', data: startedGraphStats })
programmeData['started'] = programmeTableStats
Object.keys(startedTableStats).forEach(year => {
counts[year] = [startedTableStats[year]]
})
}
const getFacultyGraduates = async (
faculty,
transfersToOrAwayStudyrights,
insideTransfersStudyrights,
since,
isAcademicYear,
yearsArray,
graphStats,
tableStats,
allBasics,
counts,
countsGraduations,
programmeData,
programmeFilter,
includeAllSpecials
) => {
const graduatedGraphStats = [[...graphStats], [...graphStats], [...graphStats], [...graphStats], [...graphStats]]
const graduatedTableStats = {}
let studyrightWhere = getExtentFilter(includeAllSpecials)
let graduatedRights = await graduatedStudyrights(faculty, since, studyrightWhere)
const programmeTableStats = {}
Object.keys(tableStats).forEach(year => (graduatedTableStats[year] = [0, 0, 0, 0, 0]))
if (!includeAllSpecials) {
graduatedRights = graduatedRights.filter(
s => !checkTransfers(s, insideTransfersStudyrights, transfersToOrAwayStudyrights)
)
}
graduatedRights.forEach(({ enddate, extentcode, studyrightElements }) => {
const { programme, programmeName } = findRightProgramme(studyrightElements, 'graduated')
if (programmeFilter === 'ALL_PROGRAMMES' || isNewProgramme(programme)) {
const endYear = defineYear(enddate, isAcademicYear)
graduatedGraphStats[0][indexOf(yearsArray, endYear)] += 1
graduatedTableStats[endYear][0] += 1
if (!(programme in programmeTableStats)) {
programmeTableStats[programme] = {}
Object.keys(tableStats).forEach(year => (programmeTableStats[programme][year] = [0, 0, 0, 0, 0]))
}
programmeTableStats[programme][endYear][0] += 1
if (extentcode === 1) {
graduatedGraphStats[1][indexOf(yearsArray, endYear)] += 1
graduatedTableStats[endYear][1] += 1
programmeTableStats[programme][endYear][1] += 1
} else if (extentcode === 2) {
graduatedGraphStats[2][indexOf(yearsArray, endYear)] += 1
graduatedTableStats[endYear][2] += 1
programmeTableStats[programme][endYear][2] += 1
} else if (extentcode === 4) {
graduatedGraphStats[3][indexOf(yearsArray, endYear)] += 1
graduatedTableStats[endYear][3] += 1
programmeTableStats[programme][endYear][3] += 1
} else {
graduatedGraphStats[4][indexOf(yearsArray, endYear)] += 1
graduatedTableStats[endYear][4] += 1
programmeTableStats[programme][endYear][4] += 1
}
if (!(programme in allBasics.programmeNames)) {
allBasics.programmeNames[programme] = programmeName
}
}
})
allBasics.studentInfo.graphStats.push({ name: 'Graduated', data: graduatedGraphStats[0] })
allBasics.graduationInfo.graphStats.push({ name: 'Bachelors', data: graduatedGraphStats[1] })
allBasics.graduationInfo.graphStats.push({ name: 'Masters', data: graduatedGraphStats[2] })
allBasics.graduationInfo.graphStats.push({ name: 'Doctors', data: graduatedGraphStats[3] })
allBasics.graduationInfo.graphStats.push({ name: 'Others', data: graduatedGraphStats[4] })
programmeData['graduated'] = programmeTableStats
Object.keys(graduatedTableStats).forEach(year => {
counts[year] = counts[year].concat(graduatedTableStats[year][0])
countsGraduations[year] = graduatedTableStats[year]
})
}
const getFacultyTransfers = async (
programmes,
programmeCodes,
allProgrammeCodes,
since,
isAcademicYear,
yearsArray,
graphStats,
tableStats,
allBasics,
counts,
programmeData,
includeAllSpecials
) => {
const insideTransfers = await transferredInsideFaculty(programmeCodes, allProgrammeCodes, since)
// Filter still away those that have started studying before 1.8.2017, when include all specials false.
let transferGraphStats = []
let transferTableStats = {}
if (includeAllSpecials) {
transferGraphStats = [[...graphStats], [...graphStats], [...graphStats]]
Object.keys(tableStats).forEach(year => (transferTableStats[year] = [0, 0, 0]))
} else {
transferGraphStats = [[...graphStats]]
Object.keys(tableStats).forEach(year => (transferTableStats[year] = [0]))
}
const programmeTableStats = {}
// inside transfer is counted towards target
for (const { transferdate, targetcode } of insideTransfers) {
const transferYear = defineYear(transferdate, isAcademicYear)
transferGraphStats[0][indexOf(yearsArray, transferYear)] += 1
transferTableStats[transferYear][0] += 1
await addProgramme(
programmeTableStats,
targetcode,
tableStats,
transferYear,
0,
allBasics,
programmes,
includeAllSpecials
)
}
if (includeAllSpecials) {
const awayTransfers = await transferredAway(programmeCodes, allProgrammeCodes, since)
const toTransfers = await transferredTo(programmeCodes, allProgrammeCodes, since)
for (const { transferdate, sourcecode } of awayTransfers) {
const transferYear = defineYear(transferdate, isAcademicYear)
transferGraphStats[1][indexOf(yearsArray, transferYear)] += 1
transferTableStats[transferYear][1] += 1
await addProgramme(
programmeTableStats,
sourcecode,
tableStats,
transferYear,
1,
allBasics,
programmes,
includeAllSpecials
)
}
for (const { transferdate, targetcode } of toTransfers) {
const transferYear = defineYear(transferdate, isAcademicYear)
transferGraphStats[2][indexOf(yearsArray, transferYear)] += 1
transferTableStats[transferYear][2] += 1
await addProgramme(
programmeTableStats,
targetcode,
tableStats,
transferYear,
2,
allBasics,
programmes,
includeAllSpecials
)
}
}
allBasics.studentInfo.graphStats.push({ name: 'Transferred inside', data: transferGraphStats[0] })
if (includeAllSpecials) {
allBasics.studentInfo.graphStats.push({ name: 'Transferred away', data: transferGraphStats[1] })
allBasics.studentInfo.graphStats.push({ name: 'Transferred to', data: transferGraphStats[2] })
}
programmeData['transferred'] = programmeTableStats
Object.keys(transferTableStats).forEach(year => {
counts[year] = counts[year].concat(transferTableStats[year])
})
}
const combineFacultyBasics = async (faculty, programmes, yearType, allProgrammeCodes, programmeFilter, special) => {
let counts = {}
let countsGraduations = {}
let years = []
let programmeData = {}
const isAcademicYear = yearType === 'ACADEMIC_YEAR'
const includeAllSpecials = special === 'SPECIAL_INCLUDED'
const since = isAcademicYear ? new Date('2017-08-01') : new Date('2017-01-01')
const yearsArray = getYearsArray(since.getFullYear(), isAcademicYear)
const { graphStats, tableStats } = getStatsBasis(yearsArray)
Object.keys(tableStats).forEach(year => years.push(year))
const wantedProgrammeCodes = programmes.map(prog => prog.code)
let allBasics = {
id: faculty,
years: [],
programmeNames: {},
studentInfo: {
tableStats: [],
graphStats: [],
titles: includeAllSpecials
? ['', 'Started studying', 'Graduated', 'Transferred inside', 'Transferred away', 'Transferred to']
: ['', 'Started studying', 'Graduated', 'Transferred inside'],
programmeTableStats: {},
},
graduationInfo: {
tableStats: [],
graphStats: [],
titles: ['', 'All', 'Bachelors', 'Masters', 'Doctors', 'Others'],
programmeTableStats: {},
},
}
const transfersToAwayStudyrights = await getTransferredToAndAway(wantedProgrammeCodes, allProgrammeCodes, since)
const transferInsideStudyrights = await getTransferredInside(wantedProgrammeCodes, allProgrammeCodes, since)
// Started studying in faculty
await getFacultyStarters(
faculty,
transfersToAwayStudyrights,
transferInsideStudyrights,
since,
isAcademicYear,
yearsArray,
graphStats,
tableStats,
counts,
allBasics,
programmeData,
programmeFilter,
includeAllSpecials
)
// Graduated in faculty
await getFacultyGraduates(
faculty,
transfersToAwayStudyrights,
transferInsideStudyrights,
since,
isAcademicYear,
yearsArray,
graphStats,
tableStats,
allBasics,
counts,
countsGraduations,
programmeData,
programmeFilter,
includeAllSpecials
)
// Transfers
await getFacultyTransfers(
programmes,
wantedProgrammeCodes,
allProgrammeCodes,
since,
isAcademicYear,
yearsArray,
graphStats,
tableStats,
allBasics,
counts,
programmeData,
includeAllSpecials
)
// combine tableStats from all categories
allBasics.years = years
years.forEach(year => {
allBasics.studentInfo.tableStats.push([year, ...counts[year]])
allBasics.graduationInfo.tableStats.push([year, ...countsGraduations[year]])
})
allBasics.studentInfo.tableStats.reverse()
allBasics.graduationInfo.tableStats.reverse()
//combine programme level tablestats
// all programmes are not present in all data types, check all found programmes and patch missing details with 0
let allCodes = Object.keys(programmeData.started).concat(
Object.keys(programmeData.graduated),
Object.keys(programmeData.transferred)
)
allCodes = [...new Set(allCodes)]
let studentInfo = {}
let graduationInfo = {}
allCodes.forEach(code => {
if (!(code in studentInfo)) studentInfo[code] = {}
for (const year of years) {
// started
if (code in programmeData.started) {
studentInfo[code][year] = [year, programmeData.started[code][year]]
} else {
studentInfo[code][year] = [year, 0]
}
// graduated total
if (code in programmeData.graduated) {
studentInfo[code][year] = studentInfo[code][year].concat([programmeData.graduated[code][year][0]])
if (!(code in graduationInfo)) graduationInfo[code] = {}
graduationInfo[code][year] = [year, ...programmeData.graduated[code][year]]
} else {
studentInfo[code][year] = studentInfo[code][year].concat([0])
}
// trasferred
if (code in programmeData.transferred) {
studentInfo[code][year] = studentInfo[code][year].concat(programmeData.transferred[code][year])
} else {
studentInfo[code][year] = studentInfo[code][year].concat([0, 0, 0])
}
}
})
Object.keys(studentInfo).forEach(prog => {
allBasics.studentInfo.programmeTableStats[prog] = [...Object.values(studentInfo[prog])].reverse()
})
Object.keys(graduationInfo).forEach(prog => {
allBasics.graduationInfo.programmeTableStats[prog] = Object.values(graduationInfo[prog]).reverse()
})
return allBasics
}
module.exports = { combineFacultyBasics }
@@ -1,5 +1,5 @@
const { indexOf } = require('lodash')
const { mapToProviders } = require('../util/utils')
const { mapToProviders } = require('../../util/utils')
const {
getStatsBasis,
defineYear,
@@ -8,14 +8,13 @@ const {
getYearsArray,
getStartDate,
isSpecialGroupCredit,
tableTitles,
} = require('./studyprogrammeHelpers')
} = require('../studyprogrammeHelpers')
const {
getStudyRights,
getCreditsForStudyProgramme,
allTransfers,
getCourseCodesForStudyProgramme,
} = require('./studyprogramme')
} = require('../studyprogramme')
const isFacultyNonMajorCredit = (studyrights, attainment_date, facultyProgrammes) => {
let right = ''
@@ -56,8 +55,8 @@ const getFacultyRegularCreditStats = async ({
const credits = await getCreditsForStudyProgramme(courses, since)
const students = [...new Set(credits.map(({ student_studentnumber }) => student_studentnumber))]
let studyrights = await getStudyRights(students, since)
const transfers = (await allTransfers(studyprogramme, since)).map(t => t.studyrightid)
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))
}
@@ -96,7 +95,6 @@ const getFacultyRegularCreditStats = async ({
nonDegree.tableStats[attainmentYear] += credits || 0
}
})
if (
majors.graphStats.every(year => year === 0) &&
facultyNonMajors.graphStats.every(year => year === 0) &&
@@ -126,7 +124,6 @@ const getFacultyCreditStatsForStudytrack = async ({ studyprogramme, facultyProgr
const { majors, facultyNonMajors, otherNonMajors, nonDegree } = await getFacultyRegularCreditStats(queryParameters)
const reversedYears = getYearsArray(since.getFullYear(), isAcademicYear).reverse()
const titles = tableTitles['credits'][includeAllSpecials ? 'SPECIAL_INCLUDED' : 'SPECIAL_EXCLUDED']
const dataFound = [majors, facultyNonMajors, otherNonMajors].some(d => d.graphStats.length) // , transferred
if (!dataFound) return null
@@ -144,48 +141,44 @@ const getFacultyCreditStatsForStudytrack = async ({ studyprogramme, facultyProgr
otherNonMajors.tableStats[year],
nonDegree.tableStats[year],
]
: [
year,
majors.tableStats[year] +
facultyNonMajors.tableStats[year] +
otherNonMajors.tableStats[year] +
nonDegree.tableStats[year],
majors.tableStats[year],
]
: [year, majors.tableStats[year], majors.tableStats[year]]
)
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,
},
]
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,
},
]
return {
id: studyprogramme,
years,
tableStats,
graphStats,
titles,
}
}
const getProgrammeCredits = async (code, yearType, specialGroups, facultyProgrammes) => {
// redis for faculty? Create faculty versions for out-commented functions
// const data = await getCreditStats(code, yearType, specialGroups)
// if (data) return data
const updatedStats = await getFacultyCreditStatsForStudytrack({
studyprogramme: code,
facultyProgrammes,
@@ -195,11 +188,34 @@ const getProgrammeCredits = async (code, yearType, specialGroups, facultyProgram
},
})
// if (updatedStats) await setCreditStats(updatedStats, yearType, specialGroups)
return updatedStats
}
const combineFacultyCredits = async (allCredits, programmes, yearType, specialGroups, counts, years) => {
const combineFacultyCredits = async (faculty, programmes, yearType, specialGroups) => {
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']
let allCredits = {
id: faculty,
years: [],
tableStats: [],
graphStats: [],
programmeTableStats: {},
programmeNames: {},
titles,
}
const progCodes = programmes.map(p => p.code)
for (const prog of programmes) {
const data = await getProgrammeCredits(prog.code, yearType, specialGroups, progCodes)
@@ -228,6 +244,30 @@ const combineFacultyCredits = async (allCredits, programmes, yearType, specialGr
years.sort()
allCredits.years = years
let majors = []
let facultyNonMajor = []
let otherNonMajor = []
let nonDegree = []
years.forEach(year => {
majors.push(counts[year][1])
facultyNonMajor.push(counts[year][2])
otherNonMajor.push(counts[year][3])
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 }]
return allCredits
}
module.exports = { combineFacultyCredits, getFacultyCreditStatsForStudytrack }
@@ -0,0 +1,278 @@
const moment = require('moment')
const {
graduatedStudyrights,
graduatedStudyrightsByStartYear,
bachelorStudyright,
statutoryAbsences,
} = require('./faculty')
const { findRightProgramme, isNewProgramme } = require('./facultyHelpers')
const { getYearsArray, getYearsObject, getMean, getMedian, defineYear } = require('../studyprogrammeHelpers')
const findBachelorStartdate = async id => {
const studyright = await bachelorStudyright(id)
if (studyright) return studyright.studystartdate
return null
}
const getProgrammeObjectBasis = (years, levels) => {
return levels.reduce(
(acc, level) => ({ ...acc, [level]: years.reduce((acc, year) => ({ ...acc, [year]: {} }), {}) }),
{}
)
}
const getStatutoryAbsences = async (studentnumber, startdate, enddate) => {
const absences = await statutoryAbsences(studentnumber, startdate, enddate)
if (absences.length) {
const absentMonths = absences.reduce((sum, ab) => sum + moment(ab.end).diff(moment(ab.start), 'months'), 0)
return absentMonths
}
return 0
}
const addGraduation = async (
extentcode,
startdate,
studyrightid,
enddate,
studentnumber,
graduationAmounts,
graduationTimes,
year,
programmes,
programme
) => {
let actualStartdate = startdate
let level = null
if (extentcode == 1) {
level = 'bachelor'
} else if (extentcode === 2) {
if (studyrightid.slice(-2) === '-2') {
level = 'bcMsCombo'
actualStartdate = await findBachelorStartdate(studyrightid.replace(/-2$/, '-1'))
} else {
level = 'master'
}
} else if (extentcode === 4) {
level = 'doctor'
} else if (extentcode === 3) {
level = 'licentiate'
} else {
return
}
const absoluteTimeToGraduation = moment(enddate).diff(moment(actualStartdate), 'months')
const statutoryAbsences = await getStatutoryAbsences(studentnumber, actualStartdate, enddate)
const timeToGraduation = absoluteTimeToGraduation - statutoryAbsences
graduationAmounts[level][year] += 1
graduationTimes[level][year] = [...graduationTimes[level][year], timeToGraduation]
if (!(programme in programmes[level][year]))
programmes[level][year][programme] = { graduationTimes: [], graduationAmounts: 0 }
programmes[level][year][programme].graduationAmounts += 1
programmes[level][year][programme].graduationTimes = [
...programmes[level][year][programme].graduationTimes,
timeToGraduation,
]
}
const countByStartYear = async (faculty, since, years, yearsList, levels, programmeFilter) => {
let graduationAmounts = {}
let graduationTimes = {}
let programmes = getProgrammeObjectBasis(yearsList, levels)
let byStartYear = {
means: {},
medians: {},
programmes: {
medians: getProgrammeObjectBasis(yearsList, levels),
means: getProgrammeObjectBasis(yearsList, levels),
},
}
levels.forEach(level => {
graduationAmounts[level] = getYearsObject({ years: yearsList })
graduationTimes[level] = getYearsObject({ years: yearsList, emptyArrays: true })
byStartYear.medians[level] = []
byStartYear.means[level] = []
})
const graduatedRights = await graduatedStudyrightsByStartYear(faculty, since)
for (const right of graduatedRights) {
const { enddate, startdate, studyrightid, extentcode, studyrightElements, studentnumber } = right
const { programme } = findRightProgramme(studyrightElements, 'graduated')
if (programmeFilter === 'NEW_STUDY_PROGRAMMES' && !isNewProgramme(programme)) continue
const startYear = defineYear(startdate, false)
await addGraduation(
extentcode,
startdate,
studyrightid,
enddate,
studentnumber,
graduationAmounts,
graduationTimes,
startYear,
programmes,
programme
)
}
years.forEach(year => {
levels.forEach(level => {
const median = getMedian(graduationTimes[level][year])
const mean = getMean(graduationTimes[level][year])
byStartYear.medians[level] = [
...byStartYear.medians[level],
{ y: median, amount: graduationAmounts[level][year] },
]
byStartYear.means[level] = [...byStartYear.means[level], { y: mean, amount: graduationAmounts[level][year] }]
// Programme level breakdown
byStartYear.programmes.medians[level][year] = { programmes: [], data: [] }
byStartYear.programmes.means[level][year] = { programmes: [], data: [] }
const programmeCodes = Object.keys(programmes[level][year])
programmeCodes.sort()
if (programmeCodes.length > 0) {
for (const prog of programmeCodes) {
const progMedian = getMedian(programmes[level][year][prog].graduationTimes)
const progMean = getMean(programmes[level][year][prog].graduationTimes)
byStartYear.programmes.medians[level][year].programmes = [
...byStartYear.programmes.medians[level][year].programmes,
prog,
]
byStartYear.programmes.medians[level][year].data = [
...byStartYear.programmes.medians[level][year].data,
{ y: progMedian, amount: programmes[level][year][prog].graduationAmounts },
]
byStartYear.programmes.means[level][year].programmes = [
...byStartYear.programmes.means[level][year].programmes,
prog,
]
byStartYear.programmes.means[level][year].data = [
...byStartYear.programmes.means[level][year].data,
{ y: progMean, amount: programmes[level][year][prog].graduationAmounts },
]
}
}
})
})
return byStartYear
}
const countByGraduationYear = async (faculty, since, years, yearsList, levels, programmeFilter) => {
let graduationAmounts = {}
let graduationTimes = {}
let programmes = getProgrammeObjectBasis(yearsList, levels)
let byGradYear = {
means: {},
medians: {},
programmes: {
medians: getProgrammeObjectBasis(yearsList, levels),
means: getProgrammeObjectBasis(yearsList, levels),
},
}
levels.forEach(level => {
graduationAmounts[level] = getYearsObject({ years: yearsList })
graduationTimes[level] = getYearsObject({ years: yearsList, emptyArrays: true })
byGradYear.medians[level] = []
byGradYear.means[level] = []
})
const graduatedRights = await graduatedStudyrights(faculty, since)
for (const right of graduatedRights) {
const { enddate, startdate, studyrightid, extentcode, studyrightElements, studentnumber } = right
const { programme } = findRightProgramme(studyrightElements, 'graduated')
if (programmeFilter === 'NEW_STUDY_PROGRAMMES' && !isNewProgramme(programme)) continue
const graduationYear = defineYear(enddate, false)
await addGraduation(
extentcode,
startdate,
studyrightid,
enddate,
studentnumber,
graduationAmounts,
graduationTimes,
graduationYear,
programmes,
programme
)
}
years.forEach(year => {
levels.forEach(level => {
const median = getMedian(graduationTimes[level][year])
const mean = getMean(graduationTimes[level][year])
byGradYear.medians[level] = [...byGradYear.medians[level], { y: median, amount: graduationAmounts[level][year] }]
byGradYear.means[level] = [...byGradYear.means[level], { y: mean, amount: graduationAmounts[level][year] }]
// Programme level breakdown
byGradYear.programmes.medians[level][year] = { programmes: [], data: [] }
byGradYear.programmes.means[level][year] = { programmes: [], data: [] }
const programmeCodes = Object.keys(programmes[level][year])
programmeCodes.sort()
if (programmeCodes.length > 0) {
for (const prog of programmeCodes) {
const progMedian = getMedian(programmes[level][year][prog].graduationTimes)
const progMean = getMean(programmes[level][year][prog].graduationTimes)
byGradYear.programmes.medians[level][year].programmes = [
...byGradYear.programmes.medians[level][year].programmes,
prog,
]
byGradYear.programmes.medians[level][year].data = [
...byGradYear.programmes.medians[level][year].data,
{ y: progMedian, amount: programmes[level][year][prog].graduationAmounts },
]
byGradYear.programmes.means[level][year].programmes = [
...byGradYear.programmes.means[level][year].programmes,
prog,
]
byGradYear.programmes.means[level][year].data = [
...byGradYear.programmes.means[level][year].data,
{ y: progMean, amount: programmes[level][year][prog].graduationAmounts },
]
}
}
})
})
return byGradYear
}
const countGraduationTimes = async (faculty, programmeFilter) => {
const isAcademicYear = false // = yearType === 'ACADEMIC_YEAR'
const since = isAcademicYear ? new Date('2017-08-01') : new Date('2017-01-01')
const yearsList = getYearsArray(since.getFullYear(), isAcademicYear)
const levels = ['bachelor', 'bcMsCombo', 'master', 'doctor', 'licentiate']
const years = [...yearsList].reverse()
const goals = {
bachelor: 36,
bcMsCombo: 60,
master: 24,
doctor: 48,
// Fix differrent cases?
licentiate: 78,
}
// We count studyrights (vs. studyright_elements)
// This way we get the whole time for a degree, even if the student was transferred to a new programme
// E.g. started 8/2016 in old Bc, transferred to new 10/2020, graduated from new 1/2021 --> total 53 months (not 3)
const byGradYear = await countByGraduationYear(faculty, since, years, yearsList, levels, programmeFilter)
const byStartYear = await countByStartYear(faculty, since, years, yearsList, levels, programmeFilter)
return { years, goals, byGradYear, byStartYear }
}
module.exports = { countGraduationTimes }
@@ -0,0 +1,137 @@
const Sequelize = require('sequelize')
const { Op } = Sequelize
const findRightProgramme = (studyrightElements, mode) => {
let programme = ''
let programmeName = ''
let studyRightElement = null
if (studyrightElements) {
if (mode === 'started') {
studyRightElement = studyrightElements
.filter(sre => sre.element_detail.type === 20)
.sort((a, b) => new Date(a.startdate) - new Date(b.startdate))[0]
// this way round counts to old programmes, other way around to new programmes
} else {
// graduated
studyRightElement = studyrightElements
.filter(sre => sre.element_detail.type === 20)
.sort((a, b) => new Date(b.startdate) - new Date(a.startdate))[0]
}
if (studyRightElement) {
programme = studyRightElement.code
programmeName = studyRightElement.element_detail.name
}
}
return { programme, programmeName }
}
const facultyFormatStudyright = studyright => {
const {
studyrightid,
studystartdate,
enddate,
givendate,
graduated,
active,
prioritycode,
extentcode,
student,
studyright_elements,
startdate,
} = studyright
return {
studyrightid,
studystartdate,
startdate,
enddate,
givendate,
graduated,
active,
prioritycode,
extentcode,
studentnumber: student.studentnumber,
studyrightElements: studyright_elements,
}
}
const formatFacultyTransfer = transfer => {
const { sourcecode, targetcode, transferdate, studyrightid } = transfer
return {
sourcecode,
targetcode,
transferdate,
studyrightid,
}
}
const facultyFormatProgramme = programme => {
const { code, name } = programme
return { code, name }
}
const formatFacultyThesisWriter = credit => {
const { course_code, credits, attainment_date, student_studentnumber, course } = credit
return {
course_code,
credits,
attainment_date,
student_studentnumber,
courseUnitType: course.course_unit_type,
}
}
const formatOrganization = org => {
const { id, name, code, parent_id } = org
return { id, name, code, parentId: parent_id }
}
const formatAbsence = absence => {
const { semestercode, semester } = absence
return { semestercode, start: semester.startdate, end: semester.enddate }
}
const newProgrammes = [/^KH/, /^MH/, /^T/, /^LI/, /^K-/, /^FI/, /^00901$/, /^00910$/]
const isNewProgramme = code => {
for (let i = 0; i < newProgrammes.length; i++) {
if (newProgrammes[i].test(code)) {
return true
}
}
return false
}
const checkTransfers = (s, insideTransfersStudyrights, transfersToOrAwayStudyrights) => {
if (transfersToOrAwayStudyrights.includes(s.studyrightid)) return true
if (insideTransfersStudyrights.includes(s.studyrightid) && new Date(s.studystartdate) < new Date('2017-08-01'))
return true
return false
}
const getExtentFilter = includeAllSpecials => {
const filteredExtents = [16] // always filter out secondary subject students
if (!includeAllSpecials) {
filteredExtents.push(7, 34, 22, 99, 14, 13, 9)
}
let studyrightWhere = {
extentcode: {
[Op.notIn]: filteredExtents,
},
}
return studyrightWhere
}
module.exports = {
findRightProgramme,
facultyFormatStudyright,
facultyFormatProgramme,
formatFacultyTransfer,
formatFacultyThesisWriter,
formatOrganization,
formatAbsence,
isNewProgramme,
checkTransfers,
getExtentFilter,
}
@@ -0,0 +1,101 @@
const { redisClient } = require('../redis')
const moment = require('moment')
const createRedisKeyForFacultyProgrammes = (id, programmeFilter) => `FACULTY_PROGRAMMES_${id}_${programmeFilter}`
const createRedisKeyForBasicStats = (id, yearType, programmeFilter, specialGroups) =>
`FACULTY_BASIC_STATS_${id}_${yearType}_${programmeFilter}_${specialGroups}`
const createRedisKeyForCreditStats = (id, yearType, programmeFilter, specialGroups) =>
`FACULTY_CREDIT_STATS_${id}_${yearType}_${programmeFilter}_${specialGroups}`
// const createRedisKeyForGraduationTimeStats = (id, mode, programmeFilter) =>
// `FACULTY_GRADUATION_TIME_STATS_${id}_${mode}__${programmeFilter}`
const createRedisKeyForThesiswriters = (id, yearType, programmeFilter, specialGroups) =>
`FACULTY_THESIS_WRITERS_STATS_${id}_${yearType}_${programmeFilter}_${specialGroups}`
const setFacultyProgrammes = async (id, data, programmeFilter) => {
const redisKey = createRedisKeyForFacultyProgrammes(id, programmeFilter)
const dataToRedis = {
data: data,
status: 'DONE',
lastUpdated: moment().format(),
}
const setOperationStatus = await redisClient.setAsync(redisKey, JSON.stringify(dataToRedis))
if (setOperationStatus !== 'OK') return null
return dataToRedis
}
const getFacultyProgrammes = async (id, programmeFilter) => {
const redisKey = createRedisKeyForFacultyProgrammes(id, programmeFilter)
const dataFromRedis = await redisClient.getAsync(redisKey)
if (!dataFromRedis) return null
return JSON.parse(dataFromRedis)
}
const setBasicStats = async (data, yearType, programmeFilter, specialGroups) => {
const { id } = data
const redisKey = createRedisKeyForBasicStats(id, yearType, programmeFilter, specialGroups)
const dataToRedis = {
...data,
status: 'DONE',
lastUpdated: moment().format(),
}
const setOperationStatus = await redisClient.setAsync(redisKey, JSON.stringify(dataToRedis))
if (setOperationStatus !== 'OK') return null
return dataToRedis
}
const getBasicStats = async (id, yearType, programmeFilter, specialGroups) => {
const redisKey = createRedisKeyForBasicStats(id, yearType, programmeFilter, specialGroups)
const dataFromRedis = await redisClient.getAsync(redisKey)
if (!dataFromRedis) return null
return JSON.parse(dataFromRedis)
}
const setCreditStats = async (data, yearType, programmeFilter, specialGroups) => {
const { id } = data
const redisKey = createRedisKeyForCreditStats(id, yearType, programmeFilter, specialGroups)
const dataToRedis = {
...data,
status: 'DONE',
lastUpdated: moment().format(),
}
const setOperationStatus = await redisClient.setAsync(redisKey, JSON.stringify(dataToRedis))
if (setOperationStatus !== 'OK') return null
return dataToRedis
}
const getCreditStats = async (id, yearType, programmeFilter, specialGroups) => {
const redisKey = createRedisKeyForCreditStats(id, yearType, programmeFilter, specialGroups)
const dataFromRedis = await redisClient.getAsync(redisKey)
if (!dataFromRedis) return null
return JSON.parse(dataFromRedis)
}
const setThesisWritersStats = async (data, yearType, programmeFilter, specialGroups) => {
const { id } = data
const redisKey = createRedisKeyForThesiswriters(id, yearType, programmeFilter, specialGroups)
const dataToRedis = {
...data,
status: 'DONE',
lastUpdated: moment().format(),
}
const setOperationStatus = await redisClient.setAsync(redisKey, JSON.stringify(dataToRedis))
if (setOperationStatus !== 'OK') return null
return dataToRedis
}
const getThesisWritersStats = async (id, yearType, programmeFilter, specialGroups) => {
const redisKey = createRedisKeyForThesiswriters(id, yearType, programmeFilter, specialGroups)
const dataFromRedis = await redisClient.getAsync(redisKey)
if (!dataFromRedis) return null
return JSON.parse(dataFromRedis)
}
module.exports = {
setFacultyProgrammes,
getFacultyProgrammes,
setBasicStats,
getBasicStats,
setCreditStats,
getCreditStats,
setThesisWritersStats,
getThesisWritersStats,
}
@@ -0,0 +1,214 @@
const { indexOf } = require('lodash')
const moment = require('moment')
const { getAcademicYearDates } = require('../../util/semester')
const {
getCreditThresholds,
getBachelorCreditGraphStats,
getMasterCreditGraphStats,
getDoctoralCreditGraphStats,
getYearsArray,
getCorrectStudentnumbers,
} = require('../studyprogrammeHelpers')
const { studytrackStudents, allStudyrights } = require('../studyprogramme')
const getStudentData = (startDate, students, thresholdKeys, thresholdAmounts, studentStats = false) => {
let data = {}
if (studentStats) {
data = { female: 0, male: 0, finnish: 0 }
}
thresholdKeys.forEach(t => (data[t] = 0))
students.forEach(({ gender_code, home_country_en, credits }) => {
const creditcount = credits
.filter(credit => moment(credit.attainment_date).isAfter(startDate))
.reduce((prev, curr) => (prev += curr.credits), 0)
if (studentStats) {
data.male += gender_code === '1' ? 1 : 0
data.female += gender_code === '2' ? 1 : 0
data.finnish += home_country_en === 'Finland' ? 1 : 0
}
data[thresholdKeys[0]] += creditcount < thresholdAmounts[0] ? 1 : 0
data[thresholdKeys[1]] += creditcount >= thresholdAmounts[0] && creditcount < thresholdAmounts[1] ? 1 : 0
data[thresholdKeys[2]] += creditcount >= thresholdAmounts[1] && creditcount < thresholdAmounts[2] ? 1 : 0
data[thresholdKeys[3]] += creditcount >= thresholdAmounts[2] && creditcount < thresholdAmounts[3] ? 1 : 0
data[thresholdKeys[4]] += creditcount >= thresholdAmounts[3] && creditcount < thresholdAmounts[4] ? 1 : 0
data[thresholdKeys[5]] += creditcount >= thresholdAmounts[4] && creditcount < thresholdAmounts[5] ? 1 : 0
if (thresholdKeys.length > 7) {
data[thresholdKeys[6]] += creditcount >= thresholdAmounts[5] && creditcount < thresholdAmounts[6] ? 1 : 0
data[thresholdKeys[7]] += creditcount >= thresholdAmounts[6] ? 1 : 0
} else {
data[thresholdKeys[6]] += creditcount >= thresholdAmounts[5] ? 1 : 0
}
})
return data
}
const getFacultyStudentProgress = async (faculty, programmes) => {
const since = new Date('2017-08-01')
const isAcademicYear = true
const includeYearsCombined = true
const yearsArray = getYearsArray(since.getFullYear(), isAcademicYear, includeYearsCombined)
const facultyProgressStats = {
id: faculty,
years: yearsArray,
bachelorsTableStats: [],
mastersTableStats: [],
doctoralTableStats: [],
bachelorsGraphStats: {},
mastersGraphStats: {},
doctoralGraphStats: {},
bachelorTitles: [
'',
'All',
'< 15 credits',
'15-30 credits',
'30-59 credits',
'60-89 credits',
'90-119 credits',
'120-149 credits',
'150-179 credits',
'> 180 credits',
],
mastersTitles: [
'',
'All',
'< 200 credits',
'200-219 credits',
'220-239 credits',
'240-259 credits',
'260-279 credits',
'280-299 credits',
'> 300 credits',
],
doctoralTitles: [
'',
'All',
'< 50 credits',
'50-99 credits',
'100-149 credits',
'150-199 credits',
'200-249 credits',
'250-299 credits',
'> 300 credits',
],
programmeNames: programmes.data.reduce((obj, dataItem) => ({ ...obj, [dataItem.code]: dataItem.name }), {}),
bachelorsProgrammeStats: {},
mastersProgrammeStats: {},
doctoralProgrammeStats: {},
}
const bachelorstats = getBachelorCreditGraphStats(yearsArray)
let allBachelorGraphStats = {
lte15: {
name: 'Less than 15 credits',
data: new Array(yearsArray.length).fill(0),
},
...bachelorstats,
}
let allMastersGraphStats = getMasterCreditGraphStats(yearsArray)
let allDoctoralGraphStats = getDoctoralCreditGraphStats(yearsArray)
let bachelorsProgrammeStats = {}
let mastersProgrammeStats = {}
let doctoralProgrammeStats = {}
let bachelorsTableStats = new Array(yearsArray.length)
let mastersTableStats = new Array(yearsArray.length)
let doctoralTableStats = new Array(yearsArray.length)
const reversedYears = [...yearsArray].reverse()
reversedYears.forEach(year => {
bachelorsTableStats[indexOf(reversedYears, year)] = [year, 0, 0, 0, 0, 0, 0, 0, 0, 0]
mastersTableStats[indexOf(reversedYears, year)] = [year, 0, 0, 0, 0, 0, 0, 0, 0]
doctoralTableStats[indexOf(reversedYears, year)] = [year, 0, 0, 0, 0, 0, 0, 0, 0]
})
for (const programme of programmes.data) {
let { creditThresholdKeys, creditThresholdAmounts } = getCreditThresholds(programme.code)
if (!creditThresholdKeys) return
if (programme.code.includes('KH')) {
creditThresholdKeys = ['lte15', ...creditThresholdKeys]
creditThresholdAmounts = [15, ...creditThresholdAmounts]
}
for (const year of reversedYears) {
const { startDate, endDate } = getAcademicYearDates(year, since)
const specialGroups = true
const includeGraduated = true
const studentnumbers = await getCorrectStudentnumbers({
codes: [programme.code],
startDate,
endDate,
specialGroups,
includeGraduated,
})
const all = await allStudyrights(programme.code, studentnumbers)
const students = await studytrackStudents(studentnumbers)
if (programme.code.includes('KH')) {
if (!(programme.code in bachelorsProgrammeStats)) {
bachelorsProgrammeStats[programme.code] = new Array(reversedYears.length - 1)
}
const studentData = getStudentData(startDate, students, creditThresholdKeys, creditThresholdAmounts)
bachelorsTableStats[indexOf(reversedYears, year)][1] += all.length || 0
if (year !== 'Total') {
bachelorsProgrammeStats[programme.code][indexOf(reversedYears, year)] = creditThresholdKeys.map(
key => studentData[key]
)
}
for (const key of Object.keys(studentData)) {
bachelorsTableStats[indexOf(reversedYears, year)][indexOf(Object.keys(studentData), key) + 2] +=
studentData[key] || 0
allBachelorGraphStats[key].data[indexOf(yearsArray, year)] += studentData[key]
}
} else if (programme.code.includes('MH')) {
const studentData = getStudentData(startDate, students, creditThresholdKeys, creditThresholdAmounts)
mastersTableStats[indexOf(reversedYears, year)][1] += all.length || 0
if (!(programme.code in mastersProgrammeStats)) {
mastersProgrammeStats[programme.code] = new Array(reversedYears.length - 1)
}
if (year !== 'Total') {
mastersProgrammeStats[programme.code][indexOf(reversedYears, year)] = creditThresholdKeys.map(
key => studentData[key]
)
}
for (const key of Object.keys(studentData)) {
mastersTableStats[indexOf(reversedYears, year)][indexOf(Object.keys(studentData), key) + 2] +=
studentData[key] || 0
allMastersGraphStats[key].data[indexOf(yearsArray, year)] += studentData[key]
}
} else {
const studentData = getStudentData(startDate, students, creditThresholdKeys, creditThresholdAmounts)
doctoralTableStats[indexOf(reversedYears, year)][1] += all.length || 0
if (!(programme.code in doctoralProgrammeStats)) {
doctoralProgrammeStats[programme.code] = new Array(reversedYears.length - 1)
}
if (year !== 'Total') {
doctoralProgrammeStats[programme.code][indexOf(reversedYears, year)] = creditThresholdKeys.map(
key => studentData[key]
)
}
for (const key of Object.keys(studentData)) {
doctoralTableStats[indexOf(reversedYears, year)][indexOf(Object.keys(studentData), key) + 2] +=
studentData[key] || 0
allDoctoralGraphStats[key].data[indexOf(yearsArray, year)] += studentData[key]
}
}
}
}
facultyProgressStats.bachelorsTableStats = bachelorsTableStats
facultyProgressStats.mastersTableStats = mastersTableStats
facultyProgressStats.doctoralTableStats = doctoralTableStats
facultyProgressStats.doctoralProgrammeStats = doctoralProgrammeStats
facultyProgressStats.mastersProgrammeStats = mastersProgrammeStats
facultyProgressStats.bachelorsProgrammeStats = bachelorsProgrammeStats
facultyProgressStats.bachelorsGraphStats = allBachelorGraphStats
facultyProgressStats.mastersGraphStats = allMastersGraphStats
facultyProgressStats.doctoralGraphStats = allDoctoralGraphStats
return facultyProgressStats
}
module.exports = { getFacultyStudentProgress }
@@ -0,0 +1,134 @@
const { indexOf } = require('lodash')
const { mapToProviders } = require('../../util/utils')
const { thesisWriters } = require('./faculty')
const {
getStatsBasis,
getYearsArray,
defineYear,
getCorrectStudentnumbers,
alltimeEndDate,
alltimeStartDate,
} = require('../studyprogrammeHelpers')
const getFacultyThesisWriters = async ({ since, years, isAcademicYear, facultyProgrammes, includeAllSpecials }) => {
const thesisTypes = [
'urn:code:course-unit-type:bachelors-thesis',
'urn:code:course-unit-type:masters-thesis',
'urn:code:course-unit-type:doctors-thesis',
'urn:code:course-unit-type:licentiate-thesis',
]
let bachelors = getStatsBasis(years)
let masters = getStatsBasis(years)
let doctors = getStatsBasis(years)
let licentiates = getStatsBasis(years)
let programmeCounts = {}
let programmeNames = {}
for (const { code, name } of facultyProgrammes) {
const provider = mapToProviders([code])[0]
const students = await getCorrectStudentnumbers({
codes: [code],
startDate: alltimeStartDate,
endDate: alltimeEndDate,
includeAllSpecials,
})
const thesisCourseCodes = await thesisWriters(provider, since, thesisTypes, students)
thesisCourseCodes?.forEach(({ attainment_date, courseUnitType }) => {
const thesisYear = defineYear(attainment_date, isAcademicYear)
if (!(code in programmeCounts)) {
programmeCounts[code] = {}
Object.keys(bachelors.tableStats).forEach(year => (programmeCounts[code][year] = [0, 0, 0, 0, 0]))
programmeNames[code] = name
}
programmeCounts[code][thesisYear][0] += 1
if (courseUnitType === thesisTypes[0]) {
bachelors.graphStats[indexOf(years, thesisYear)] += 1
bachelors.tableStats[thesisYear] += 1
programmeCounts[code][thesisYear][1] += 1
} else if (courseUnitType === thesisTypes[1]) {
masters.graphStats[indexOf(years, thesisYear)] += 1
masters.tableStats[thesisYear] += 1
programmeCounts[code][thesisYear][2] += 1
} else if (courseUnitType === thesisTypes[2]) {
doctors.graphStats[indexOf(years, thesisYear)] += 1
doctors.tableStats[thesisYear] += 1
programmeCounts[code][thesisYear][3] += 1
} else if (courseUnitType === thesisTypes[3]) {
licentiates.graphStats[indexOf(years, thesisYear)] += 1
licentiates.tableStats[thesisYear] += 1
programmeCounts[code][thesisYear][4] += 1
}
})
}
return { bachelors, masters, doctors, licentiates, programmeCounts, programmeNames }
}
const getFacultyThesisWritersForStudyTrack = async (
allThesisWriters,
facultyProgrammes,
isAcademicYear,
includeAllSpecials
) => {
const since = isAcademicYear ? new Date('2017-08-01') : new Date('2017-01-01')
const years = getYearsArray(since.getFullYear(), isAcademicYear)
const queryParameters = { since, years, isAcademicYear, facultyProgrammes, includeAllSpecials }
const { bachelors, masters, doctors, licentiates, programmeCounts, programmeNames } = await getFacultyThesisWriters(
queryParameters
)
allThesisWriters.years = years.map(year => year.toString())
const reversedYears = years.reverse()
allThesisWriters.tableStats = reversedYears.map(year => [
year,
bachelors.tableStats[year] + masters.tableStats[year] + doctors.tableStats[year] + licentiates.tableStats[year],
bachelors.tableStats[year],
masters.tableStats[year],
doctors.tableStats[year],
licentiates.tableStats[year],
])
allThesisWriters.graphStats.push({ name: 'Bachelors', data: bachelors.graphStats })
allThesisWriters.graphStats.push({ name: 'Masters', data: masters.graphStats })
allThesisWriters.graphStats.push({ name: 'Doctors', data: doctors.graphStats })
allThesisWriters.graphStats.push({ name: 'Licentiates', data: licentiates.graphStats })
const programmes = programmeNames ? Object.keys(programmeNames) : facultyProgrammes.map(programme => programme.code)
programmes.forEach(programmeCode => {
reversedYears.forEach(year => {
if (programmeCounts && programmeCode in programmeCounts) {
if (!(programmeCode in allThesisWriters.programmeTableStats)) {
allThesisWriters.programmeTableStats[programmeCode] = []
}
allThesisWriters.programmeTableStats[programmeCode].push([year, ...programmeCounts[programmeCode][year]])
}
})
})
allThesisWriters.programmeNames = programmeNames
}
const combineFacultyThesisWriters = async (faculty, facultyProgrammes, yearType, specialGroups) => {
let allThesisWriters = {
id: faculty,
years: [],
tableStats: [],
graphStats: [],
programmeTableStats: {},
titles: ['', 'All', 'Bachelors', 'Masters', 'Doctors', 'Licentiates'],
programmeNames: {},
status: 'Done',
lastUpdated: '',
}
const isAcademicYear = yearType === 'ACADEMIC_YEAR'
const includeAllSpecials = specialGroups === 'SPECIAL_INCLUDED'
await getFacultyThesisWritersForStudyTrack(allThesisWriters, facultyProgrammes, isAcademicYear, includeAllSpecials)
return allThesisWriters
}
module.exports = { combineFacultyThesisWriters }
@@ -0,0 +1,117 @@
const logger = require('../../util/logger')
const { findFacultyProgrammeCodes } = require('./faculty')
const { combineFacultyBasics } = require('./facultyBasics')
const { combineFacultyCredits } = require('./facultyCredits')
const { combineFacultyThesisWriters } = require('./facultyThesisWriters')
const { setFacultyProgrammes, setBasicStats, setCreditStats, setThesisWritersStats } = require('./facultyService')
const updateFacultyOverview = async (faculty, statsType) => {
const calendarNewSpecial = {
yearType: 'CALENDAR_YEAR',
programmeFilter: 'NEW_STUDY_PROGRAMMES',
specialGroups: 'SPECIAL_INCLUDED',
}
const calendarNewSpecialOut = {
yearType: 'CALENDAR_YEAR',
programmeFilter: 'NEW_STUDY_PROGRAMMES',
specialGroups: 'SPECIAL_EXCLUDED',
}
const calendarAllSpecial = {
yearType: 'CALENDAR_YEAR',
programmeFilter: 'ALL_PROGRAMMES',
specialGroups: 'SPECIAL_INCLUDED',
}
const calendarAllSpecialOut = {
yearType: 'CALENDAR_YEAR',
programmeFilter: 'ALL_PROGRAMMES',
specialGroups: 'SPECIAL_EXCLUDED',
}
const academicNewSpecial = {
yearType: 'ACADEMIC_YEAR',
programmeFilter: 'NEW_STUDY_PROGRAMMES',
specialGroups: 'SPECIAL_INCLUDED',
}
const academicAllSpecial = {
yearType: 'ACADEMIC_YEAR',
programmeFilter: 'ALL_PROGRAMMES',
specialGroups: 'SPECIAL_INCLUDED',
}
const academicNewSpecialOut = {
yearType: 'ACADEMIC_YEAR',
programmeFilter: 'NEW_STUDY_PROGRAMMES',
specialGroups: 'SPECIAL_EXCLUDED',
}
const academicAllSpecialOut = {
yearType: 'ACADEMIC_YEAR',
programmeFilter: 'ALL_PROGRAMMES',
specialGroups: 'SPECIAL_EXCLUDED',
}
const options = [
calendarNewSpecial,
calendarAllSpecial,
academicNewSpecial,
academicAllSpecial,
calendarNewSpecialOut,
calendarAllSpecialOut,
academicNewSpecialOut,
academicAllSpecialOut,
]
let allProgrammes = []
let newProgrammes = []
let allProgrammeCodes = []
try {
const all = await findFacultyProgrammeCodes(faculty, 'ALL_PROGRAMMES')
const onlyNew = await findFacultyProgrammeCodes(faculty, 'NEW_STUDY_PROGRAMMES')
allProgrammes = await setFacultyProgrammes(faculty, all, 'ALL_PROGRAMMES')
newProgrammes = await setFacultyProgrammes(faculty, onlyNew, 'NEW_STUDY_PROGRAMMES')
allProgrammes?.data.forEach(prog => allProgrammeCodes.push(prog.code))
} catch (e) {
logger.error(e)
}
for (const option of options) {
const { yearType, programmeFilter, specialGroups } = option
try {
if (statsType === 'ALL' || statsType === 'STUDENT') {
const updatedStudentInfo = await combineFacultyBasics(
faculty,
programmeFilter === 'ALL_PROGRAMMES' ? allProgrammes.data : newProgrammes.data,
yearType,
allProgrammeCodes,
programmeFilter,
specialGroups
)
await setBasicStats(updatedStudentInfo, option.yearType, option.programmeFilter, option.specialGroups)
}
if (statsType === 'ALL' || statsType === 'CREDITS') {
const updatedCredits = await combineFacultyCredits(
faculty,
programmeFilter === 'ALL_PROGRAMMES' ? allProgrammes.data : newProgrammes.data,
yearType,
specialGroups
)
await setCreditStats(updatedCredits, yearType, programmeFilter, specialGroups)
}
if (statsType === 'ALL' || statsType === 'THESIS') {
const updateThesisWriters = await combineFacultyThesisWriters(
faculty,
programmeFilter === 'ALL_PROGRAMMES' ? allProgrammes.data : newProgrammes.data,
yearType,
specialGroups
)
await setThesisWritersStats(updateThesisWriters, yearType, programmeFilter, specialGroups)
}
} catch (e) {
logger.error(e)
}
}
return 'OK'
}
module.exports = { updateFacultyOverview }
@@ -1,316 +0,0 @@
const { indexOf } = require('lodash')
const {
startedStudyrights,
graduatedStudyrights,
transferredInsideFaculty,
transferredAway,
transferredTo,
getProgrammeName,
} = require('./faculty')
const { getStatsBasis, getYearsArray, defineYear } = require('./studyprogrammeHelpers')
const { findRightProgramme } = require('./facultyHelpers')
const filterDuplicateStudyrights = studyrights => {
// bachelor+master students have two studyrights (separated by two last digits in studyrightid)
// choose only the earlier started one, so we don't count start of masters as starting in faculty
let rightsToCount = {}
studyrights.forEach(right => {
const id = right.studyrightid.slice(0, -2)
const start = new Date(right.studystartdate)
if (id in rightsToCount) {
if (new Date(rightsToCount[id].studystartdate) > start) {
rightsToCount[id] = right
}
} else {
rightsToCount[id] = right
}
})
return Object.values(rightsToCount)
}
const addProgramme = async (stats, programme, tableStats, transferYear, index, allBasics) => {
if (!(programme in stats)) {
stats[programme] = {}
Object.keys(tableStats).forEach(year => (stats[programme][year] = [0, 0, 0]))
}
stats[programme][transferYear][index] += 1
if (!(programme in allBasics.programmeNames)) {
const { name } = await getProgrammeName(programme)
allBasics.programmeNames[programme] = name
}
}
const getFacultyStarters = async (
faculty,
since,
isAcademicYear,
yearsArray,
graphStats,
tableStats,
counts,
allBasics,
programmeData
) => {
const startedGraphStats = [...graphStats]
const startedTableStats = { ...tableStats }
const studyrights = await startedStudyrights(faculty, since)
const filteredStudyrights = filterDuplicateStudyrights(studyrights)
const programmeTableStats = {}
filteredStudyrights.forEach(({ studystartdate, studyrightElements }) => {
const { programme, programmeName } = findRightProgramme(studyrightElements, 'started')
const startYear = defineYear(studystartdate, isAcademicYear)
startedGraphStats[indexOf(yearsArray, startYear)] += 1
startedTableStats[startYear] += 1
if (!(programme in programmeTableStats)) {
programmeTableStats[programme] = { ...tableStats }
}
programmeTableStats[programme][startYear] += 1
if (!(programme in allBasics.programmeNames)) {
allBasics.programmeNames[programme] = programmeName
}
})
allBasics.studentInfo.graphStats.push({ name: 'Started studying', data: startedGraphStats })
programmeData['started'] = programmeTableStats
Object.keys(startedTableStats).forEach(year => {
counts[year] = [startedTableStats[year]]
})
}
const getFacultyGraduates = async (
faculty,
since,
isAcademicYear,
yearsArray,
graphStats,
tableStats,
allBasics,
counts,
countsGraduations,
programmeData
) => {
const graduatedGraphStats = [[...graphStats], [...graphStats], [...graphStats], [...graphStats], [...graphStats]]
const graduatedTableStats = {}
Object.keys(tableStats).forEach(year => (graduatedTableStats[year] = [0, 0, 0, 0, 0]))
const programmeTableStats = {}
const graduatedRights = await graduatedStudyrights(faculty, since)
graduatedRights.forEach(({ enddate, extentcode, studyrightElements }) => {
const { programme, programmeName } = findRightProgramme(studyrightElements, 'graduated')
const endYear = defineYear(enddate, isAcademicYear)
graduatedGraphStats[0][indexOf(yearsArray, endYear)] += 1
graduatedTableStats[endYear][0] += 1
if (!(programme in programmeTableStats)) {
programmeTableStats[programme] = {}
Object.keys(tableStats).forEach(year => (programmeTableStats[programme][year] = [0, 0, 0, 0, 0]))
}
programmeTableStats[programme][endYear][0] += 1
if (extentcode === 1) {
graduatedGraphStats[1][indexOf(yearsArray, endYear)] += 1
graduatedTableStats[endYear][1] += 1
programmeTableStats[programme][endYear][1] += 1
} else if (extentcode === 2) {
graduatedGraphStats[2][indexOf(yearsArray, endYear)] += 1
graduatedTableStats[endYear][2] += 1
programmeTableStats[programme][endYear][2] += 1
} else if (extentcode === 4) {
graduatedGraphStats[3][indexOf(yearsArray, endYear)] += 1
graduatedTableStats[endYear][3] += 1
programmeTableStats[programme][endYear][3] += 1
} else {
graduatedGraphStats[4][indexOf(yearsArray, endYear)] += 1
graduatedTableStats[endYear][4] += 1
programmeTableStats[programme][endYear][4] += 1
}
if (!(programme in allBasics.programmeNames)) {
allBasics.programmeNames[programme] = programmeName
}
})
allBasics.studentInfo.graphStats.push({ name: 'Graduated', data: graduatedGraphStats[0] })
allBasics.graduationInfo.graphStats.push({ name: 'All graduations', data: graduatedGraphStats[0] })
allBasics.graduationInfo.graphStats.push({ name: 'Bachelors', data: graduatedGraphStats[1] })
allBasics.graduationInfo.graphStats.push({ name: 'Masters', data: graduatedGraphStats[2] })
allBasics.graduationInfo.graphStats.push({ name: 'Doctors', data: graduatedGraphStats[3] })
allBasics.graduationInfo.graphStats.push({ name: 'Others', data: graduatedGraphStats[4] })
programmeData['graduated'] = programmeTableStats
Object.keys(graduatedTableStats).forEach(year => {
counts[year] = counts[year].concat(graduatedTableStats[year][0])
countsGraduations[year] = graduatedTableStats[year]
})
}
const getFacultyTransfers = async (
programmes,
since,
isAcademicYear,
yearsArray,
graphStats,
tableStats,
allBasics,
counts,
programmeData
) => {
const insideTransfers = await transferredInsideFaculty(programmes, since)
const awayTransfers = await transferredAway(programmes, since)
const toTransfers = await transferredTo(programmes, since)
const transferGraphStats = [[...graphStats], [...graphStats], [...graphStats]]
const transferTableStats = {}
Object.keys(tableStats).forEach(year => (transferTableStats[year] = [0, 0, 0]))
const programmeTableStats = {}
// inside transfer is counted towards target
for (const { transferdate, targetcode } of insideTransfers) {
const transferYear = defineYear(transferdate, isAcademicYear)
transferGraphStats[0][indexOf(yearsArray, transferYear)] += 1
transferTableStats[transferYear][0] += 1
await addProgramme(programmeTableStats, targetcode, tableStats, transferYear, 0, allBasics)
}
for (const { transferdate, sourcecode } of awayTransfers) {
const transferYear = defineYear(transferdate, isAcademicYear)
transferGraphStats[1][indexOf(yearsArray, transferYear)] += 1
transferTableStats[transferYear][1] += 1
await addProgramme(programmeTableStats, sourcecode, tableStats, transferYear, 1, allBasics)
}
for (const { transferdate, targetcode } of toTransfers) {
const transferYear = defineYear(transferdate, isAcademicYear)
transferGraphStats[2][indexOf(yearsArray, transferYear)] += 1
transferTableStats[transferYear][2] += 1
await addProgramme(programmeTableStats, targetcode, tableStats, transferYear, 2, allBasics)
}
allBasics.studentInfo.graphStats.push({ name: 'Transferred inside', data: transferGraphStats[0] })
allBasics.studentInfo.graphStats.push({ name: 'Transferred away', data: transferGraphStats[1] })
allBasics.studentInfo.graphStats.push({ name: 'Transferred to', data: transferGraphStats[2] })
programmeData['transferred'] = programmeTableStats
Object.keys(transferTableStats).forEach(year => {
counts[year] = counts[year].concat(transferTableStats[year])
})
}
const combineFacultyBasics = async (allBasics, faculty, programmes, yearType) => {
let counts = {}
let countsGraduations = {}
let years = []
let programmeData = {}
const isAcademicYear = yearType === 'ACADEMIC_YEAR'
const since = isAcademicYear ? new Date('2017-08-01') : new Date('2017-01-01')
const yearsArray = getYearsArray(since.getFullYear(), isAcademicYear)
const { graphStats, tableStats } = getStatsBasis(yearsArray)
Object.keys(tableStats).forEach(year => years.push(year))
// Started studying in faculty
await getFacultyStarters(
faculty,
since,
isAcademicYear,
yearsArray,
graphStats,
tableStats,
counts,
allBasics,
programmeData
)
// Graduated in faculty
await getFacultyGraduates(
faculty,
since,
isAcademicYear,
yearsArray,
graphStats,
tableStats,
allBasics,
counts,
countsGraduations,
programmeData
)
// Transfers
await getFacultyTransfers(
programmes,
since,
isAcademicYear,
yearsArray,
graphStats,
tableStats,
allBasics,
counts,
programmeData
)
// combine tableStats from all categories
allBasics.years = years
years.forEach(year => {
allBasics.studentInfo.tableStats.push([year, ...counts[year]])
allBasics.graduationInfo.tableStats.push([year, ...countsGraduations[year]])
})
allBasics.studentInfo.tableStats.reverse()
allBasics.graduationInfo.tableStats.reverse()
//combine programme level tablestats
// all programmes are not present in all data types, check all found programmes and patch missing details with 0
let allCodes = Object.keys(programmeData.started).concat(
Object.keys(programmeData.graduated),
Object.keys(programmeData.transferred)
)
allCodes = [...new Set(allCodes)]
let studentInfo = {}
let graduationInfo = {}
allCodes.forEach(code => {
if (!(code in studentInfo)) studentInfo[code] = {}
for (const year of years) {
// started
if (code in programmeData.started) {
studentInfo[code][year] = [year, programmeData.started[code][year]]
} else {
studentInfo[code][year] = [year, 0]
}
// graduated total
if (code in programmeData.graduated) {
studentInfo[code][year] = studentInfo[code][year].concat([programmeData.graduated[code][year][0]])
if (!(code in graduationInfo)) graduationInfo[code] = {}
graduationInfo[code][year] = [year, ...programmeData.graduated[code][year].slice(1)]
} else {
studentInfo[code][year] = studentInfo[code][year].concat([0])
}
// trasferred
if (code in programmeData.transferred) {
studentInfo[code][year] = studentInfo[code][year].concat(programmeData.transferred[code][year])
} else {
studentInfo[code][year] = studentInfo[code][year].concat([0, 0, 0])
}
}
})
Object.keys(studentInfo).forEach(prog => {
allBasics.studentInfo.programmeTableStats[prog] = [...Object.values(studentInfo[prog])].reverse()
})
Object.keys(graduationInfo).forEach(prog => {
allBasics.graduationInfo.programmeTableStats[prog] = Object.values(graduationInfo[prog]).reverse()
})
}
module.exports = { combineFacultyBasics }
@@ -1,69 +0,0 @@
const findRightProgramme = (studyrightElements, mode) => {
let programme = ''
let programmeName = ''
let studyRightElement = null
if (studyrightElements) {
if (mode === 'started') {
studyRightElement = studyrightElements
.filter(sre => sre.element_detail.type === 20)
.sort((a, b) => new Date(a.startdate) - new Date(b.startdate))[0]
// this way round counts to old programmes, other way around to new programmes
} else {
// graduated
studyRightElement = studyrightElements
.filter(sre => sre.element_detail.type === 20)
.sort((a, b) => new Date(b.startdate) - new Date(a.startdate))[0]
}
if (studyRightElement) {
programme = studyRightElement.code
programmeName = studyRightElement.element_detail.name
}
}
return { programme, programmeName }
}
const facultyFormatStudyright = studyright => {
const {
studyrightid,
studystartdate,
enddate,
givendate,
graduated,
active,
prioritycode,
extentcode,
student,
studyright_elements,
} = studyright
return {
studyrightid,
studystartdate,
enddate,
givendate,
graduated,
active,
prioritycode,
extentcode,
studentnumber: student.studentnumber,
studyrightElements: studyright_elements,
}
}
const formatFacultyTransfer = transfer => {
const { sourcecode, targetcode, transferdate, studyrightid } = transfer
return {
sourcecode,
targetcode,
transferdate,
studyrightid,
}
}
const facultyFormatProgramme = programme => {
const { code, name } = programme
return { code, name }
}
module.exports = { findRightProgramme, facultyFormatStudyright, facultyFormatProgramme, formatFacultyTransfer }
+3 -3
View File
@@ -210,7 +210,7 @@ const getStudentsIncludeCoursesBetween = async (studentnumbers, startDate, endDa
raw: true,
}),
Enrollment.findAll({
attributes: ['course_code', 'state', 'enrollment_date_time', 'studentnumber'],
attributes: ['course_code', 'state', 'enrollment_date_time', 'studentnumber', 'semestercode'],
where: {
enrollment_date_time: {
[Op.between]: [attainmentDateFrom, endDate],
@@ -414,7 +414,7 @@ const studentnumbersWithAllStudyrightElements = async (
// db startdate is formatted to utc so need to change it when querying
const formattedStartDate = new Date(moment.tz(startDate, 'Europe/Helsinki').format()).toUTCString()
const filteredExtents = [10] // always filter out secondary subject students
const filteredExtents = [16] // always filter out secondary subject students
let studyrightWhere = {
extentcode: {
[Op.notIn]: filteredExtents,
@@ -424,7 +424,7 @@ const studentnumbersWithAllStudyrightElements = async (
filteredExtents.push(7, 34)
}
if (!nondegreeStudents) {
filteredExtents.push(33, 99, 14, 13)
filteredExtents.push(22, 99, 14, 13)
}
let studentWhere = {}
+3 -3
View File
@@ -121,7 +121,7 @@ const getUnifyStatus = unifyCourses => {
}
const findByCourseAndSemesters = async (coursecodes, from, to, separate, unifyCourses = 'unifyStats') => {
const { startdate } = await Semester.findOne({
const { startdate, semestercode: fromSemester } = await Semester.findOne({
where: {
[separate ? 'semestercode' : 'yearcode']: from,
},
@@ -129,7 +129,7 @@ const findByCourseAndSemesters = async (coursecodes, from, to, separate, unifyCo
limit: 1,
raw: true,
})
const { enddate } = await Semester.findOne({
const { enddate, semestercode: toSemester } = await Semester.findOne({
where: {
[separate ? 'semestercode' : 'yearcode']: to,
},
@@ -164,7 +164,7 @@ const findByCourseAndSemesters = async (coursecodes, from, to, separate, unifyCo
WHERE
e.studentnumber = s.studentnumber
AND e.course_code IN (:coursecodes)
AND e.enrollment_date_time BETWEEN '${startdate.toISOString()}' AND '${enddate.toISOString()}'
AND e.semestercode BETWEEN ${fromSemester} AND ${toSemester}
AND e.enrollment_date_time >= '2021-05-31'
AND e.state IN ('ENROLLED', 'CONFIRMED')
);
@@ -31,6 +31,19 @@ const sinceDate = since => {
const studytrackStudents = async studentnumbers =>
(
await Student.findAll({
include: {
model: Credit,
separate: true,
attributes: ['credits', 'attainment_date'],
where: {
isStudyModule: {
[Op.eq]: null,
},
credittypecode: {
[Op.in]: [4, 9],
},
},
},
where: {
studentnumber: {
[Op.in]: studentnumbers,
@@ -577,6 +590,36 @@ const getOtherStudentsForProgrammeCourses = async (from, to, programmeCourses, s
}))
}
const getTransferStudentsForProgrammeCourses = async (from, to, programmeCourses) => {
const res = await sequelize.query(
`
WITH Dist AS (
SELECT DISTINCT cr.student_studentnumber AS student, cr.credits AS credits,
co.code AS code, co.name AS course_name FROM credit cr
INNER JOIN course co ON cr.course_code = co.code
WHERE cr.attainment_date BETWEEN :from AND :to
AND (cr."isStudyModule" = false OR cr."isStudyModule" IS NULL)
AND cr.course_code IN (:programmeCourses)
AND cr.credittypecode = 9
)
SELECT COUNT(student) AS total_students, SUM(credits) AS total_credits, code, course_name
FROM Dist
GROUP BY dist.code, course_name;
`,
{
type: sequelize.QueryTypes.SELECT,
replacements: { from, to, programmeCourses },
}
)
return res.map(course => ({
code: course.code,
name: course.course_name,
totalTransferStudents: parseInt(course.total_students),
totalTransferCredits: parseInt(course.total_credits),
type: 'transfer',
}))
}
const getStudentsWithoutStudyrightForProgrammeCourses = async (from, to, programmeCourses) => {
const res = await sequelize.query(
`
@@ -716,4 +759,5 @@ module.exports = {
getAllProgrammeCourses,
getStudyRights,
getCourseCodesForStudyProgramme,
getTransferStudentsForProgrammeCourses,
}
@@ -5,6 +5,7 @@ const {
getCurrentStudyYearStartDate,
getOwnStudentsForProgrammeCourses,
getStudentsWithoutStudyrightForProgrammeCourses,
getTransferStudentsForProgrammeCourses,
getOtherStudentsForProgrammeCourses,
getAllProgrammeCourses,
} = require('./studyprogramme')
@@ -50,6 +51,9 @@ const makeYearlyPromises = (years, academicYear, type, programmeCourses, studypr
case 'otherStudents':
result = await getOtherStudentsForProgrammeCourses(from, to, programmeCourses, studyprogramme)
break
case 'transfer':
result = await getTransferStudentsForProgrammeCourses(from, to, programmeCourses)
break
default:
result = await getStudentsForProgrammeCourses(from, to, programmeCourses)
}
@@ -92,16 +96,26 @@ const getStudyprogrammeCoursesForStudytrack = async (unixMillis, studyprogramme,
studyprogramme
)
const yearlyTransferStudentsPromises = makeYearlyPromises(
yearRange,
academicYear,
'transfer',
programmeCourses,
studyprogramme
)
const [
yearlyStudentByCourse,
yearlyProgrammeStudents,
yearlyStudentsWithoutStudyright,
yearlyOtherProgrammeStudents,
yearlyTransferStudents,
] = await Promise.all([
Promise.all(yearlyStudentByCoursePromises),
Promise.all(yearlyProgrammeStudentsPromises),
Promise.all(yearlyStudentsWithoutStudyrightPromises),
Promise.all(yearlyOtherProgrammeStudentsPromises),
Promise.all(yearlyTransferStudentsPromises),
])
let maxYear = 0
@@ -110,6 +124,7 @@ const getStudyprogrammeCoursesForStudytrack = async (unixMillis, studyprogramme,
...yearlyProgrammeStudents.flat(),
...yearlyStudentsWithoutStudyright.flat(),
...yearlyOtherProgrammeStudents.flat(),
...yearlyTransferStudents.flat(),
].reduce((acc, curr) => {
if (curr.year > maxYear) maxYear = curr.year
if (!acc[curr.code]) {
@@ -129,6 +144,8 @@ const getStudyprogrammeCoursesForStudytrack = async (unixMillis, studyprogramme,
totalOtherProgrammeCredits: 0,
totalWithoutStudyrightStudents: 0,
totalWithoutStudyrightCredits: 0,
totalTransferStudents: 0,
totalTransferCredits: 0,
}
}
switch (curr.type) {
@@ -140,16 +157,17 @@ const getStudyprogrammeCoursesForStudytrack = async (unixMillis, studyprogramme,
acc[curr.code]['years'][curr.year]['totalProgrammeStudents'] += curr.totalProgrammeStudents
acc[curr.code]['years'][curr.year]['totalProgrammeCredits'] += curr.totalProgrammeCredits
break
case 'otherProgramme':
acc[curr.code]['years'][curr.year]['totalOtherProgrammeStudents'] += curr.totalOtherProgrammeStudents
acc[curr.code]['years'][curr.year]['totalOtherProgrammeCredits'] += curr.totalOtherProgrammeCredits
break
case 'noStudyright':
acc[curr.code]['years'][curr.year]['totalWithoutStudyrightStudents'] += curr.totalWithoutStudyrightStudents
acc[curr.code]['years'][curr.year]['totalWithoutStudyrightCredits'] += curr.totalWithoutStudyrightCredits
break
case 'transfer':
acc[curr.code]['years'][curr.year]['totalTransferStudents'] += curr.totalTransferStudents
acc[curr.code]['years'][curr.year]['totalTransferCredits'] += curr.totalTransferCredits
}
return acc
@@ -164,6 +182,8 @@ const getStudyprogrammeCoursesForStudytrack = async (unixMillis, studyprogramme,
'totalOtherProgrammeCredits',
'totalWithoutStudyrightStudents',
'totalWithoutStudyrightCredits',
'totalTransferCredits',
'totalTransferStudents',
]
ayCourses.forEach(ayCourse => {
const normCode = isOpenUniCourseCode(ayCourse)[1]
@@ -187,6 +207,8 @@ const getStudyprogrammeCoursesForStudytrack = async (unixMillis, studyprogramme,
totalOtherProgrammeCredits: 0,
totalWithoutStudyrightStudents: 0,
totalWithoutStudyrightCredits: 0,
totalTransferCredits: 0,
totalTransferStudents: 0,
}
} else {
mergedCourse['years'][year] = { ...allCourses[normCode]['years'][year] }
@@ -76,12 +76,13 @@ const formatStudyright = studyright => {
}
const formatStudent = student => {
const { studentnumber, gender_code, home_country_en, creditcount } = student
const { studentnumber, gender_code, home_country_en, creditcount, credits } = student
return {
studentnumber,
gender_code,
home_country_en,
creditcount,
credits,
}
}
@@ -155,7 +156,7 @@ const isSpecialGroupCredit = (studyrights, attainment_date, transfers) =>
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 ([7, 9, 16, 34, 33, 99, 14, 13].includes(studyright.extentcode)) return true // Excludes non-degree studyrights and exchange students
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
})
@@ -349,8 +350,8 @@ const tableTitles = {
'',
'Total',
'Major students credits',
'Non major students credits',
'Non degree students credits',
'Non-major students credits',
'Non-degree students credits',
'Transferred credits',
],
},
@@ -26,11 +26,15 @@ const {
} = require('./studyprogramme')
const { getAcademicYearDates } = require('../util/semester')
const getStudentData = (students, thresholdKeys, thresholdAmounts) => {
const getStudentData = (startDate, students, thresholdKeys, thresholdAmounts) => {
let data = { female: 0, male: 0, finnish: 0 }
thresholdKeys.forEach(t => (data[t] = 0))
students.forEach(({ gender_code, home_country_en, creditcount }) => {
students.forEach(({ gender_code, home_country_en, credits }) => {
const creditcount = credits
.filter(credit => moment(credit.attainment_date).isAfter(startDate))
.reduce((prev, curr) => (prev += curr.credits), 0)
data.male += gender_code === '1' ? 1 : 0
data.female += gender_code === '2' ? 1 : 0
data.finnish += home_country_en === 'Finland' ? 1 : 0
@@ -121,7 +125,7 @@ const getStudytrackDataForTheYear = async ({
// Get all the studyrights and students for the calculations
const all = await allStudyrights(track, studentnumbers)
const students = await studytrackStudents(studentnumbers)
const studentData = getStudentData(students, creditThresholdKeys, creditThresholdAmounts)
const studentData = getStudentData(startDate, students, creditThresholdKeys, creditThresholdAmounts)
const started = await startedStudyrights(track, startDate, studentnumbers)
const enrolled = await enrolledStudents(track, startDate, studentnumbers)
const absent = await absentStudents(track, studentnumbers)
+5 -6
View File
@@ -43,7 +43,7 @@ const getCategoriesAndYears = async () => {
const creditsWithTeachersForYear = yearcode =>
Credit.findAll({
attributes: ['id', 'credits', 'credittypecode', 'isStudyModule'],
attributes: ['id', 'credits', 'credittypecode', 'isStudyModule', 'is_open'],
include: [
{
model: Semester,
@@ -106,21 +106,20 @@ const findTopTeachers = async yearcode => {
credits
.filter(isRegularCourse)
.map(credit => {
const { credits, course, credittypecode } = credit
const { credits, credittypecode, is_open } = credit
const teachers = credit.teachers
.filter(({ id }) => !id.includes('hy-hlo-org')) // Remove faculties from the leaderboards
.map(({ id, name }) => ({ id, name }))
const passed = Credit.passed(credit) || Credit.improved(credit)
const failed = Credit.failed(credit)
const transferred = credittypecode === 9
const isOpenUni = course.code[0] === 'A'
return { passed, failed, credits, teachers, isOpenUni, transferred }
return { passed, failed, credits, teachers, is_open, transferred }
})
.forEach(credit => {
const { passed, failed, credits, teachers, isOpenUni, transferred } = credit
const { passed, failed, credits, teachers, is_open, transferred } = credit
teachers.forEach(teacher => {
all[teacher.id] = updatedStats(all, teacher, passed, failed, credits, transferred)
if (isOpenUni) {
if (is_open) {
openuni[teacher.id] = updatedStats(openuni, teacher, passed, failed, credits, transferred)
}
})
+6 -1
View File
@@ -10,6 +10,7 @@ const { User, UserElementDetails, AccessGroup, UserFaculties, sequelizeUser } =
const { getOrganizationAccess } = require('../util/organizationAccess')
const courseStatisticsGroup = 'grp-oodikone-basic-users'
const facultyStatisticsGroup = 'grp-oodikone-users'
const hyOneGroup = 'hy-one'
const toskaGroup = 'grp-toska'
@@ -191,7 +192,9 @@ const formatUser = async userFromDb => {
const rights = _.uniqBy([...programmes, ...enrichProgrammesFromFaculties(faculties)])
const iamRights = Object.keys(await getOrganizationAccess(userFromDb))
const iamRights = Object.keys(await getOrganizationAccess(userFromDb)).filter(
programmeCode => !rights.includes(programmeCode)
)
const {
dataValues: {
@@ -265,6 +268,8 @@ const getUser = async ({ username, name, email, iamGroups, sisId }) => {
let newAccessGroups = []
if (await checkStudyGuidanceGroupsAccess(sisId)) newAccessGroups.push('studyGuidanceGroups')
if (iamGroups.includes(courseStatisticsGroup)) newAccessGroups.push('courseStatistics')
if (iamGroups.includes(facultyStatisticsGroup) || iamGroups.some(element => /-jory$/.test(element)))
newAccessGroups.push('facultyStatistics')
if (iamGroups.includes(hyOneGroup) || currentAccessGroups.includes('teachers')) newAccessGroups.push('teachers')
if (iamGroups.includes(toskaGroup) || currentAccessGroups.includes('admin')) newAccessGroups.push('admin')
if (_.difference(newAccessGroups, currentAccessGroups).length > 0) {
+8 -8
View File
@@ -6,7 +6,7 @@
"": {
"name": "frontend",
"dependencies": {
"@reduxjs/toolkit": "^1.8.0",
"@reduxjs/toolkit": "^1.8.5",
"@sentry/browser": "^6.18.2",
"@sentry/tracing": "^6.18.2",
"axios": "^0.21.4",
@@ -18,7 +18,7 @@
"history": "^4.10.1",
"immer": "^9.0.12",
"lodash": "^4.17.21",
"moment": "^2.29.1",
"moment": "^2.29.4",
"path-to-regexp": "^0.1.7",
"piwik-react-router": "^0.12.1",
"prop-types": "^15.8.1",
@@ -2841,9 +2841,9 @@
}
},
"node_modules/@reduxjs/toolkit": {
"version": "1.8.3",
"resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-1.8.3.tgz",
"integrity": "sha512-lU/LDIfORmjBbyDLaqFN2JB9YmAT1BElET9y0ZszwhSBa5Ef3t6o5CrHupw5J1iOXwd+o92QfQZ8OJpwXvsssg==",
"version": "1.8.5",
"resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-1.8.5.tgz",
"integrity": "sha512-f4D5EXO7A7Xq35T0zRbWq5kJQyXzzscnHKmjnu2+37B3rwHU6mX9PYlbfXdnxcY6P/7zfmjhgan0Z+yuOfeBmA==",
"dependencies": {
"immer": "^9.0.7",
"redux": "^4.1.2",
@@ -24283,9 +24283,9 @@
"integrity": "sha512-9X2obfABZuDVLCgPK9aX0a/x4jaOEweTTWE2+9sr0Qqqevj2Uv5XorvusThmc9XGYpS9yI+fhh8RTafBtGposw=="
},
"@reduxjs/toolkit": {
"version": "1.8.3",
"resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-1.8.3.tgz",
"integrity": "sha512-lU/LDIfORmjBbyDLaqFN2JB9YmAT1BElET9y0ZszwhSBa5Ef3t6o5CrHupw5J1iOXwd+o92QfQZ8OJpwXvsssg==",
"version": "1.8.5",
"resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-1.8.5.tgz",
"integrity": "sha512-f4D5EXO7A7Xq35T0zRbWq5kJQyXzzscnHKmjnu2+37B3rwHU6mX9PYlbfXdnxcY6P/7zfmjhgan0Z+yuOfeBmA==",
"requires": {
"immer": "^9.0.7",
"redux": "^4.1.2",
+1 -1
View File
@@ -6,7 +6,7 @@
"build": "DISABLE_ESLINT_PLUGIN=true react-scripts build"
},
"dependencies": {
"@reduxjs/toolkit": "^1.8.0",
"@reduxjs/toolkit": "^1.8.5",
"@sentry/browser": "^6.18.2",
"@sentry/tracing": "^6.18.2",
"axios": "^0.21.4",
@@ -0,0 +1,70 @@
export default {
ProgrammeToggle: `
New study programmes: Näyttää kaikissa luvuissa ja kuvaajissa pelkästään uuden opetussuunnitelman ohjelmat.
All programmes: Näyttää kaikissa luvuissa ja kuvaajissa kuvaajissa myös vanhan opetussuunnitelman ohjelmat.`,
YearToggle: `
Academic year: Näyttää tilastot akateemisille vuosille jaoteltuna, esimerkiksi 1.8.2019-31.7.2020
Calendar year: Näyttää tilastot kalenterivuosille jaoteltuna, esimerkiksi 1.1.2019-31.12.2019
`,
StudentsOfTheFaculty: `
**Started studying**: Sisältää kyseisenä vuonna tiedekunnassa aloittaneet opiskelijat (aloitetut opinto-oikeudet) riippumatta siitä, minä vuonna kyseinen opiskeluoikeus on myönnetty. Opiskelija on myös voinut myöhemmin keskeyttää opintonsa tai siirtyä pois ohjelmasta.
Kandi-maisteri-opinto-oikeudellisista opiskelijoista lasketaan vain kandissa ts. tiedekunnassa aloitus, ei toistamiseen enää maisteriopintojen aloitusta.\n
**Graduated**: Sisältää kyseisenä vuonna valmistuneet opiskelijat riippumatta siitä, minä vuonna opiskeluoikeus ohjelmaan on myönnetty. Jaottelu on tehty opinto-oikeuden päättymispäivän (=valmistumispäivän) perusteella.
**Transferred inside**: Sisältää kyseisenä vuonna tiedekunnan sisällä siirtyneet opiskelijat (opinto-oikeudet). Ohjelmakohtaisissa tilastoissa siirto on laskettu kohdeohjelman tilastoihin.
**Transferred away**: Sisältää kyseisenä vuonna pois tiedekunnasta siirtyneet opiskelijat (opinto-oikeudet). Sama opiskelija voi esiintyä tilastoissa useana vuonna, mikäli hän on siirtynyt tiedekuntaan ja siitä pois useasti.
**Transferred to**: Sisältää kyseisenä vuonna tiedekuntaan siirtyneet opiskelijat (opinto-oikeudet).
HUOM! Siirtyneiden määrä on erityisen suuri vuonna 2020, jolloin vanhoista koulutusohjelmista luovuttiin ja opiskelijat siirtyivät uusiin ohjelmiin.
`,
CreditsProducedByTheFaculty: `
Sisältää opintopisteet suoritusvuosittain. Suoritukset on jaoteltu Sisussa näkyvän kurssin suorituspäivän mukaan.
**Major students credits**: Sisältää ne opintopisteet, joiden suorittajalla on suoritushetkellä ollut ensisijainen opinto-oikeus johonkin tiedekunnan koulutusohjelmaan. Sisältää myös tiedekunnan ohjelmaan siirtyneet ensisijaiset opiskelijat.
**Non-major faculty students credits**: Sisältää tiedekunnan koulutusohjelmien tuottamat opintopisteet, joiden suorittajalla EI ole ollut suoritushetkellä ensisijaista opinto-oikeutta kyseiseen koulutusohjelmaan,
MUTTA hänellä on sellainen johonkin toiseen tiedekunnan ohjelmaan. Sisältää esimerkiksi vaihto-opiskelijat ja erillis-opinto-oikeudella opiskelevat.
Sisältää myös opintopisteet, jotka opiskelija on suorittanut koulutusohjelmaan ennen opintojen aloittamista koulutusohjelmassa tai opintojen päätyttyä.
**Non-major other faculty students credits**: Sisältää tiedekunnan koulutusohjelmien tuottamat opintopisteet, joiden suorittajalla EI ole ollut suoritushetkellä ensisijaista opinto-oikeutta kyseiseen koulutusohjelmaan tai mihinkään muuhun kyseisen tiedekunnan koulutusohjelmaan.
Sisältää esimerkiksi vaihto-opiskelijat ja erillis-opinto-oikeudella opiskelevat. Sisältää myös opintopisteet, jotka opiskelija on suorittanut koulutusohjelmaan ennen opintojen aloittamista koulutusohjelmassa tai opintojen päätyttyä.
**Non-degree student credits**: Sisältää tiedekuntaan suoritetut opintopisteet, joiden suorittajilla ei ole tutkinto-oikeutta Helsingin yliopistossa.
`,
GraduatedOfTheFaculty: `Sisältää kyseisenä vuonna tiedekunnasta valmistuneet opiskelijat riippumatta siitä, minä vuonna opiskeluoikeus ohjelmaan on myönnetty. Jaottelu on tehty opinto-oikeuden päättymispäivän (=valmistumispäivän) perusteella.`,
ThesisWritersOfTheFaculty: `Sisältää kyseisenä vuonna kandidaatintutkielman, pro gradu -tutkielman, väitöskirjan tai lisensiaatintyön hyväksytysti suorittaneet opiskelijat. Mukana ei ole mahdollisia arvosanan korotuksia.`,
StudentToggle: `
All studyrights: Valinnassa ovat mukana vaihto-opiskelijat, erillisoikeudella opiskelevat ja ohjelmaan siirtyneet opiskelijat sekä ohjelmasta pois siirtyneet opiskelijat.\n
Special studyrights excluded: Valinnasta ja kaikista luvuista on poissuljettu kaikki edellä mainitut erikoisryhmät.
`,
// Graduation times
GroupByToggle: `Group by: Starting year: Valmistuneet opiskelijat on jaoteltu opinto-oikeuden alkuvuoden mukaan.\n
Graduation year: Valmistuneet opiskelijat on ryhmitelty opintojen valmistumisvuoden perusteella.`,
AverageGraduationTimes: `
Opiskelijoiden keskimääräiset valmistumisajat tutkintotasoittain.
- Bachelor: Kandidaatin tutkinnon suoritusajat
- Bachelor + Master: Kandi+maisteriopinto-oikeudella opiskelleiden ja siitä maisteriksi valmistuneiden suoritusajat
- Master: Pelkällä maisterintutkinnon opinto-oikeudella opiskelleiden suoritusajat
- Doctor: Tohtorin tutkinnon suoritusajat
- Licentiate: Lisensiaatin tutkinnon suoritusajat
Valmistumisajoista on **vähennetty lakisääteiset poissaolot**. Vihreä katkoviiva kuvastaa tavoiteaikaa.
Vihreä palkki merkitsee, että keskimääräinen valmistumisaika on alle tai yhtä kuin tavoiteaika, keltainen palkki merkitsee korkeintaan vuoden ylitystä
ja punainen yli vuoden tavoiteajan ylitystä.
Säädöt:
**Starting year**. Ryhmittelee tulokset opiskelijoiden opinto-oikeuden alkamisvuosittain. Näyttää siis vuonna xxxx aloittaneiden opiskelijoiden keskimääräiset valmistumisajat. Luvut voivat elää, koska osa vuonna xxxx
aloittaneiden opiskelijoiden opinnoista voi olla edelleen kesken.
**Graduation year**: Ryhmittelee tulokset valmistumisvuosittain. Näyttää siis vuonna xxxx valmistuneiden opiskelijoiden keskimääräiset valmistumisajat.
Luvut ovat lopullisia (kuluva vuosi poislukien), sillä edeltäville vuosille ei tule uusia valmistumisia.
**Median time**: Näyttää kyseisenä vuonna valmistuneiden opiskelijoiden opintojen keston **mediaanin** kuukausina.
**Mean time**: Näyttää kyseisenä vuonna valmistuneiden opiskelijoiden opintojen keston **keskiarvon** kuukausina.`,
}
@@ -3,6 +3,7 @@ import CoolDataScience from './coolDataScience'
import CourseStatistics from './courseStatistics'
import CoursePopulation from './coursePopulation'
import Studyprogramme from './studyprogramme'
import Faculty from './faculty'
export default {
PopulationStatistics,
@@ -10,4 +11,5 @@ export default {
CourseStatistics,
CoursePopulation,
Studyprogramme,
Faculty,
}
@@ -80,4 +80,10 @@ export default {
NonDegree: `
Opiskelijat, joilla ei ole tutkinto-oikeutta Helsingin yliopistossa.
`,
TransferredCourses: `
Sisältää opiskelijat, jotka ovat suorittaneet kyseisenä vuonna hyväksiluettuja opintopisteitä tähän koulutusohjelmaan. Mukana ovat kaikenlaisilla opinto-oikeuksilla hyväksiluetut opintopisteet
`,
TransferredCredits: `
Sisältää opintopisteet, jotka ovat suoritettu kyseisenä vuonna, ja hyväksiluettu tähän koulutusohjelmaan. Mukana ovat kaikenlaisilla opinto-oikeuksilla hyväksiluetut opintopisteet
`,
}
@@ -65,7 +65,7 @@ const countTotalStats = (formattedStats, userHasAccessToAllStats) => {
categories: { passed, failed },
grades: cgrades,
enrollmentsByState: { ...acc.attempts.enrollmentsByState },
totalEnrollments: acc.students.totalEnrollments + (curr.students.totalEnrollments || 0),
totalEnrollments: acc.attempts.totalEnrollments + (curr.attempts.totalEnrollments || 0),
},
students: {
totalEnrollments: acc.students.totalEnrollments + (curr.students.totalEnrollments || 0),
@@ -1,7 +1,11 @@
import React from 'react'
import { Divider, Loader } from 'semantic-ui-react'
import { useGetFacultyCreditStatsQuery, useGetFacultyBasicStatsQuery } from 'redux/facultyStats'
import { Divider, Loader, Popup, Button, Message } from 'semantic-ui-react'
import xlsx from 'xlsx'
import {
useGetFacultyCreditStatsQuery,
useGetFacultyBasicStatsQuery,
useGetFacultyThesisStatsQuery,
} from 'redux/facultyStats'
import LineGraph from 'components/StudyProgramme/BasicOverview/LineGraph'
import StackedBarChart from 'components/StudyProgramme/BasicOverview/StackedBarChart'
import InteractiveDataTable from '../InteractiveDataView'
@@ -10,33 +14,154 @@ import InfoBox from '../../Info/InfoBox'
import InfotoolTips from '../../../common/InfoToolTips'
import '../faculty.css'
const Overview = ({ faculty, academicYear, setAcademicYear }) => {
const toolTips = InfotoolTips.Studyprogramme
const Overview = ({
faculty,
academicYear,
setAcademicYear,
studyProgrammes,
setStudyProgrammes,
specialGroups,
setSpecialGroups,
language,
}) => {
const toolTips = InfotoolTips.Faculty
const yearType = academicYear ? 'ACADEMIC_YEAR' : 'CALENDAR_YEAR'
const special = 'SPECIAL_INCLUDED' // specialGroups ? 'SPECIAL_EXCLUDED' : 'SPECIAL_INCLUDED'
const credits = useGetFacultyCreditStatsQuery({ id: faculty?.code, yearType, specialGroups: special })
const basics = useGetFacultyBasicStatsQuery({ id: faculty?.code, yearType, specialGroups: special })
// basic.data.studentsInfo --> data for students of faculty table
// basics.data.graduationInfo --> data for graduated and thesis writers table
const studyProgrammeFilter = studyProgrammes ? 'ALL_PROGRAMMES' : 'NEW_STUDY_PROGRAMMES'
const special = specialGroups ? 'SPECIAL_EXCLUDED' : 'SPECIAL_INCLUDED'
const credits = useGetFacultyCreditStatsQuery({
id: faculty?.code,
yearType,
studyProgrammeFilter,
specialGroups: special,
})
const basics = useGetFacultyBasicStatsQuery({
id: faculty?.code,
yearType,
studyProgrammeFilter,
specialGroups: special,
})
const thesisWriters = useGetFacultyThesisStatsQuery({
id: faculty?.code,
yearType,
studyProgrammeFilter,
specialGroups: special,
})
const getDivider = (title, toolTipText) => (
/*
Order of the programme keys: KH -> MH -> T -> FI -> K- -> Numbers containing letters at end -> Y- -> Numbers
*/
const regexValuesAll = [
/^KH/,
/^MH/,
/^T/,
/^LI/,
/^K-/,
/^FI/,
/^00901$/,
/^00910$/,
/^\d.*a$/,
/^Y/,
/\d$/,
/^\d.*e$/,
]
const testKey = value => {
for (let i = 0; i < regexValuesAll.length; i++) {
if (regexValuesAll[i].test(value)) {
return i
}
}
return 6
}
const sortProgrammeKeys = programmeKeys => {
return programmeKeys.sort((a, b) => {
if (testKey(a) - testKey(b) === 0) {
return a.localeCompare(b)
}
return testKey(a) - testKey(b)
})
}
const downloadCsv = (titles, tableStats, programmeStats, programmeNames, toolTipText) => {
const headers = titles.map(title => ({ label: title === '' ? 'Year' : title, key: title === '' ? 'Year' : title }))
const csvData = sortProgrammeKeys(Object.keys(programmeStats)).reduce(
(results, programme) => [
...results,
...programmeStats[programme].map(yearRow => {
return {
Programme: programme,
Name: programmeNames[programme][language]
? programmeNames[programme][language]
: programmeNames[programme].fi,
...yearRow.reduce((result, value, valueIndex) => ({ ...result, [headers[valueIndex].key]: value }), {}),
}
}),
],
[]
)
const tableStatsAsCsv = tableStats.map(yearArray =>
yearArray.reduce((result, value, yearIndex) => ({ ...result, [headers[yearIndex].key]: value }), {})
)
const book = xlsx.utils.book_new()
const tableSheet = xlsx.utils.json_to_sheet(tableStatsAsCsv)
xlsx.utils.book_append_sheet(book, tableSheet, 'TableStats')
const sheet = xlsx.utils.json_to_sheet(csvData)
xlsx.utils.book_append_sheet(book, sheet, 'ProgrammeStats')
xlsx.writeFile(book, `${faculty.code}-${toolTipText}.xlsx`)
}
const getDivider = (title, toolTipText, titles, tableStats, programmeStats, programmeNames) => (
<>
<div className="divider">
<Divider data-cy={`Section-${toolTipText}`} horizontal>
{title}
</Divider>
</div>
<InfoBox content="Sisältää opintopisteet suoritusvuosittain. Suoritukset on jaoteltu Sisussa näkyvän kurssin suorituspäivän mukaan. Kaikki valmistuneet -lukumäärä sisältää kaikki mahdolliset suoritetuksi merkityt opiskeluoikeudet." />
{/* <InfoBox content={toolTips[toolTipText]} /> */}
<InfoBox content={toolTips[toolTipText]} cypress={toolTipText} />
<Popup
content="Download statistics as csv"
trigger={
<Button
icon="download"
floated="right"
onClick={() => downloadCsv(titles, tableStats, programmeStats, programmeNames, toolTipText)}
style={{ backgroundColor: 'white', borderRadius: 0 }}
/>
}
/>
</>
)
const isFetchingOrLoading = credits.isLoading || credits.isFetching || basics.isLoading || basics.isFetching
const isFetchingOrLoading =
credits.isLoading ||
credits.isFetching ||
basics.isLoading ||
basics.isFetching ||
thesisWriters.isLoading ||
thesisWriters.isFetching
const isError =
(basics.isError && credits.isError) || (basics.isSuccess && !basics.data && credits.isSuccess && !credits.data)
(basics.isError && credits.isError && thesisWriters.isError) ||
(basics.isSuccess &&
!basics.data &&
credits.isSuccess &&
!credits.data &&
thesisWriters.isSuccess &&
!thesisWriters.data)
if (isError) return <h3>Something went wrong, please try refreshing the page.</h3>
let creditShortTitles = []
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']
}
return (
<div className="faculty-overview">
<div className="toggle-container">
@@ -48,70 +173,174 @@ const Overview = ({ faculty, academicYear, setAcademicYear }) => {
value={academicYear}
setValue={setAcademicYear}
/>
<Toggle
cypress="ProgrammeToggle"
toolTips={toolTips.ProgrammeToggle}
firstLabel="New study programmes"
secondLabel="All study programmes"
value={studyProgrammes}
setValue={setStudyProgrammes}
/>
<Toggle
cypress="StudentToggle"
toolTips={toolTips.StudentToggle}
firstLabel="All studyrights"
secondLabel="Special studyrights excluded"
value={specialGroups}
setValue={setSpecialGroups}
/>
</div>
{isFetchingOrLoading ? (
<Loader active style={{ marginTop: '10em' }} />
<Loader active style={{ marginTop: '15em' }} />
) : (
<>
{studyProgrammeFilter === 'ALL_PROGRAMMES' && (
<Message data-cy="FacultyProgrammesShownInfo">
Please note that the data is complete only for current Bachelor, Masters and Doctoral programmes.
Especially, credits and thesis writers contain only data for current programmes.
</Message>
)}
{special === 'SPECIAL_EXCLUDED' && (
<Message data-cy="FacultyExcludeSpecialsInfo">
Please note that the data might be inaccurate in some fields. Checking the values is in progress.
</Message>
)}
{basics.isSuccess && basics.data && (
<>
{getDivider('Students of the faculty', 'StudentsOfTheFaculty')}
{getDivider(
'Students of the faculty',
'StudentsOfTheFaculty',
basics?.data?.studentInfo.titles,
basics?.data?.studentInfo.tableStats,
basics?.data?.studentInfo.programmeTableStats,
basics?.data?.programmeNames
)}
<div className="section-container">
<LineGraph
cypress="StudentsOfTheFaculty"
data={{ ...basics?.data.studentInfo, years: basics.data.years }}
/>
<InteractiveDataTable
cypress="StudentsOfTheFaculty"
dataStats={basics?.data?.studentInfo.tableStats}
dataProgrammeStats={basics?.data?.studentInfo.programmeTableStats}
programmeNames={basics?.data?.programmeNames}
titles={basics?.data?.studentInfo.titles}
sliceStart={1}
language="fi"
/>
<div className="graph-container-narrow">
<LineGraph
cypress="StudentsOfTheFaculty"
data={{ ...basics?.data.studentInfo, years: basics.data.years }}
/>
</div>
<div className="table-container-wide">
<InteractiveDataTable
cypress="StudentsOfTheFaculty"
dataStats={basics?.data?.studentInfo.tableStats}
dataProgrammeStats={basics?.data?.studentInfo.programmeTableStats}
programmeNames={basics?.data?.programmeNames}
sortedKeys={sortProgrammeKeys(Object.keys(basics?.data?.studentInfo.programmeTableStats))}
titles={basics?.data?.studentInfo.titles}
sliceStart={1}
language={language}
yearsVisible={Array(basics?.data?.studentInfo.tableStats.length).fill(false)}
shortNames={transferShortTitles}
/>
</div>
</div>
</>
)}
{credits.isSuccess && credits.data && (
<>
{getDivider('Credits produced by the faculty', 'CreditsProducedByTheFaculty')}
{getDivider(
'Credits produced by the faculty',
'CreditsProducedByTheFaculty',
credits?.data?.titles,
credits?.data?.tableStats,
credits?.data?.programmeTableStats,
credits?.data?.programmeNames
)}
<div className="section-container">
<StackedBarChart
cypress="CreditsProducedByTheSFaculty"
data={credits?.data?.graphStats}
labels={credits?.data?.years}
/>
<InteractiveDataTable
cypress="CreditsProducedByTheFaculty"
dataStats={credits?.data?.tableStats}
dataProgrammeStats={credits?.data?.programmeTableStats}
programmeNames={credits?.data?.programmeNames}
titles={credits?.data?.titles}
sliceStart={2}
language="fi"
/>
<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))}
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('Graduated and thesis writers of the faculty', 'GraduatedAndThesisWritersOfTheFaculty')}
{getDivider(
'Graduated of the faculty',
'GraduatedOfTheFaculty',
basics?.data?.graduationInfo.titles,
basics?.data?.graduationInfo.tableStats,
basics?.data?.graduationInfo.programmeTableStats,
basics?.data?.programmeNames
)}
<div className="section-container">
<LineGraph
cypress="GraduatedAndThesisWritersOfTheFaculty"
data={{ ...basics?.data.graduationInfo, years: basics.data.years }}
/>
<InteractiveDataTable
cypress="GraduatedAndThesisWritersOfTheFaculty"
dataStats={basics?.data?.graduationInfo.tableStats}
dataProgrammeStats={basics?.data?.graduationInfo.programmeTableStats}
programmeNames={basics?.data?.programmeNames}
titles={basics?.data?.graduationInfo.titles}
sliceStart={2}
language="fi"
/>
<div className="graph-container-narrow">
<LineGraph
cypress="GraduatedOfTheFaculty"
data={{ ...basics?.data.graduationInfo, years: basics.data.years }}
/>
</div>
<div className="table-container-wide">
<InteractiveDataTable
cypress="GraduatedOfTheFaculty"
dataStats={basics?.data?.graduationInfo.tableStats}
dataProgrammeStats={basics?.data?.graduationInfo.programmeTableStats}
programmeNames={basics?.data?.programmeNames}
sortedKeys={sortProgrammeKeys(Object.keys(basics?.data?.graduationInfo.programmeTableStats))}
titles={basics?.data?.graduationInfo.titles}
sliceStart={2}
language={language}
yearsVisible={Array(basics?.data?.graduationInfo.tableStats.length).fill(false)}
/>
</div>
</div>
</>
)}
{thesisWriters.isSuccess && thesisWriters.data && (
<>
{getDivider(
'Thesis writers of the faculty',
'ThesisWritersOfTheFaculty',
thesisWriters?.data?.titles,
thesisWriters?.data.tableStats,
thesisWriters?.data.programmeTableStats,
thesisWriters?.data?.programmeNames
)}
<div className="section-container">
<div className="graph-container-narrow">
<LineGraph
cypress="ThesisWritersOfTheFaculty"
data={{ ...thesisWriters?.data, years: thesisWriters?.data.years }}
/>
</div>
<div className="table-container-wide">
<InteractiveDataTable
cypress="ThesisWritersOfTheFaculty"
dataStats={thesisWriters?.data.tableStats}
dataProgrammeStats={thesisWriters?.data.programmeTableStats}
programmeNames={thesisWriters?.data.programmeNames}
sortedKeys={sortProgrammeKeys(Object.keys(thesisWriters?.data.programmeTableStats))}
titles={thesisWriters?.data?.titles}
sliceStart={2}
extraHeight="EXTRA HEIGHT"
language={language}
yearsVisible={Array(thesisWriters?.data.tableStats.length).fill(false)}
/>
</div>
</div>
</>
)}
@@ -0,0 +1,70 @@
import React from 'react'
const ReactHighcharts = require('react-highcharts')
const getColors = len => {
if (len < 8) return ['#f57368', '#fb8c6e', '#fba678', '#dbda7d', '#9ec27c', '#60a866', '#008c59']
return ['#e66067', '#f57368', '#fb8c6e', '#fba678', '#dbda7d', '#9ec27c', '#60a866', '#008c59']
}
const FacultyBarChart = ({ cypress, data }) => {
if (!data) return null
const colors = getColors(Object.keys(data.stats).length)
const dataWithColors = Object.values(data?.stats).map((series, index) => ({ ...series, color: colors[index] }))
const getFileName = () => {
return `oodikone_progress_of_students_in_${data?.id}_by_study_start_year`
}
const defaultConfig = {
title: {
text: '',
},
series: dataWithColors,
credits: {
text: 'oodikone | TOSKA',
},
xAxis: {
categories: data?.years,
},
chart: {
type: 'column',
height: '450px',
},
plotOptions: {
column: {
stacking: 'percent',
dataLabels: {
enabled: true,
format: '{point.percentage:.1f}%',
},
},
},
exporting: {
filename: getFileName(),
width: 2200,
height: 1400,
sourceWidth: 1200,
sourceHeight: 600,
buttons: {
contextButton: {
menuItems: ['viewFullscreen', 'downloadPNG', 'downloadSVG', 'downloadPDF'],
},
},
},
yAxis: {
allowDecimals: false,
min: 0,
reversed: false,
title: '',
},
}
return (
<div className="graph-container" data-cy={`Graph-${cypress}`}>
<ReactHighcharts config={defaultConfig} />
</div>
)
}
export default FacultyBarChart
@@ -0,0 +1,81 @@
import React, { useState } from 'react'
import { Table } from 'semantic-ui-react'
import ProgrammeProgressChart from './ProgrammeProgressChart'
import BasicRow from '../InteractiveDataView/BasicRow'
const FacultyProgressTable = ({
data,
programmeStats,
titles,
sortedKeys,
progressYearsVisible,
programmeNames,
language,
cypress,
}) => {
const [visible, setVisible] = useState(progressYearsVisible)
if (!data) return null
const toggleVisibility = yearIndex => {
const arrayToModify = [...visible]
arrayToModify[yearIndex] = !visible[yearIndex]
setVisible(arrayToModify)
}
const lenOfYearArray = data.length - 1
return (
<div>
<Table data-cy={cypress} compact celled>
<Table.Header>
<Table.Row key={`${cypress}-${Math.random()}`} textAlign="center">
{titles?.map(title => (
<Table.HeaderCell key={title}>{title}</Table.HeaderCell>
))}
</Table.Row>
</Table.Header>
<Table.Body>
{data.map((yearArray, yearIndex) => (
<React.Fragment key={`random-fragment-key-${Math.random()}`}>
{yearIndex < lenOfYearArray ? (
<BasicRow
icon={visible[yearIndex] ? 'chevron down' : 'chevron right'}
yearArray={yearArray}
cypress={visible[yearIndex] ? `Hide-${cypress}` : `Show-${cypress}`}
yearIndex={yearIndex}
toggleVisibility={() => toggleVisibility(yearIndex)}
/>
) : (
<Table.Row>
{yearArray.map(value => (
<Table.Cell key={`last-row-${Math.random()}`}>{value}</Table.Cell>
))}
</Table.Row>
)}
{yearIndex < lenOfYearArray ? (
<Table.Row
key={`${cypress}-row-${Math.random()}`}
style={{ display: visible[yearIndex] ? '' : 'none' }}
>
<Table.Cell data-cy={`Cell-${cypress}-${yearIndex}`} colSpan={100}>
<ProgrammeProgressChart
data={sortedKeys?.map(programme => programmeStats[programme][yearIndex])}
longLabels={programmeNames}
labels={sortedKeys}
language={language}
names={titles?.slice(2)}
/>
</Table.Cell>
</Table.Row>
) : (
<></>
)}
</React.Fragment>
))}
</Table.Body>
</Table>
</div>
)
}
export default FacultyProgressTable
@@ -0,0 +1,102 @@
/* eslint-disable react/no-this-in-sfc */
import React from 'react'
import Highcharts from 'highcharts'
import ReactHighcharts from 'react-highcharts'
const getColors = len => {
if (len < 8) return ['#f57368', '#fb8c6e', '#fba678', '#dbda7d', '#9ec27c', '#60a866', '#008c59']
return ['#e66067', '#f57368', '#fb8c6e', '#fba678', '#dbda7d', '#9ec27c', '#60a866', '#008c59']
}
const ProgrammeProgressChart = ({ data, labels, longLabels, names, language }) => {
if (!data || data.length === 0)
return (
<>
<b>No data available</b>
</>
)
const transpose = matrix => {
return matrix.reduce((prev, next) => next.map((_item, i) => (prev[i] || []).concat(next[i])), [])
}
const colors = getColors(data[0]?.length || 6)
const dataTranspose = transpose(data)
.map((obj, idx) => ({ name: names[idx], data: obj, color: colors[idx] }))
.reverse()
// Tooltip formatter function: this.<some value> needs to be used in order to obtain access to the values.
const defaultConfig = {
chart: {
type: 'bar',
marginTop: 60,
},
credits: {
text: 'oodikone | TOSKA',
},
title: {
text: '',
style: {
display: 'none',
},
},
series: dataTranspose,
xAxis: {
categories: labels,
fontSize: '24px',
},
yAxis: {
allowDecimals: false,
min: 0,
reversed: true,
title: '',
},
legend: {
layout: 'horizontal',
align: 'left',
fontSize: '24px',
x: 10,
verticalAlign: 'top',
y: -10,
floating: true,
backgroundColor: Highcharts.defaultOptions.legend.backgroundColor || 'white',
borderColor: '#CCC',
borderWidth: 1,
shadow: false,
},
tooltip: {
shared: true,
backgroundColor: 'white',
fontSize: '25px',
formatter() {
let tooltipString = `<b>${
longLabels[this.x] && longLabels[this.x][language] ? longLabels[this.x][language] : longLabels[this.x].fi
}</b><br /><p>${this.x}</p><br />`
this.points.forEach(point => {
tooltipString += `<span style="color:${point.color}">●</span> <b>${point.series.name}: ${point.y}</b>
</span><br />`
})
return tooltipString
},
},
plotOptions: {
series: {
stacking: 'percent',
pointWidth: 24,
dataLabels: {
enabled: true,
format: '{point.percentage:.1f}%',
fontSize: '24px',
filter: {
property: 'y',
operator: '>',
value: 2,
},
},
},
},
}
return <ReactHighcharts config={defaultConfig} />
}
export default ProgrammeProgressChart
@@ -1,7 +1,175 @@
import React from 'react'
import { Divider, Loader } from 'semantic-ui-react'
import { useGetFacultyProgressStatsQuery } from 'redux/facultyStats'
import FacultyProgressTable from './FacultyProgressTable'
import FacultyBarChart from './FacultyBarChart'
// import Toggle from '../../StudyProgramme/Toggle'
import InfoBox from '../../Info/InfoBox'
// import InfotoolTips from '../../../common/InfoToolTips'
import '../faculty.css'
const FacultyProgrammeOverview = () => {
return <div className="programmes-overview">Faculty programmes</div>
// const toolTips = InfotoolTips.Faculty
const getDivider = (title, toolTipText) => (
<>
<div className="divider">
<Divider data-cy={`Section-${toolTipText}`} horizontal>
{title}
</Divider>
</div>
<InfoBox content="To be done" />
</>
)
const FacultyProgrammeOverview = ({ faculty, language }) => {
// const toolTipsProgramme = InfotoolTips.Faculty
// const [programme, setProgramme] = useState(faculty)
const studyProgrammeFilter = 'NEW_STUDY_PROGRAMMES'
const progressStats = useGetFacultyProgressStatsQuery({ id: faculty?.code, studyProgrammeFilter })
const isFetchingOrLoading = progressStats.isLoading || progressStats.isFetching
const isError = progressStats.isError || (progressStats.isSuccess && !progressStats.data)
if (isError) return <h3>Something went wrong, please try refreshing the page.</h3>
/*
Order of the programme keys: KH -> MH -> T -> FI -> K- -> Numbers containing letters at end -> Y- -> Numbers
*/
const regexValuesAll = [
/^KH/,
/^MH/,
/^T/,
/^LI/,
/^K-/,
/^FI/,
/^00901$/,
/^00910$/,
/^\d.*a$/,
/^Y/,
/\d$/,
/^\d.*e$/,
]
const testKey = value => {
for (let i = 0; i < regexValuesAll.length; i++) {
if (regexValuesAll[i].test(value)) {
return i
}
}
return 6
}
const sortProgrammeKeys = programmeKeys => {
return programmeKeys.sort((a, b) => {
if (testKey(a) - testKey(b) === 0) {
return a.localeCompare(b)
}
return testKey(a) - testKey(b)
})
}
// Toggles in studytrans ans student populations
return (
<div className="programmes-overview">
{isFetchingOrLoading ? (
<Loader active style={{ marginTop: '15em' }} />
) : (
<div className="programmes-overview">
{/* {placeholderdata && (
<>{getDivider('Students of the faculty By Starting year', 'StudentsOfTheFacultyByStartingYear')}</>
)} */}
{progressStats.isSuccess && progressStats.data && (
<>
{getDivider(
'Progress of bachelor students of the faculty by starting year',
'ProgressOfBachelorStudentsOfTheFacultyByStartingYear'
)}
<div className="section-container">
<div className="graph-container">
<FacultyBarChart
cypress="FacultyBachelorsProgress"
data={{
id: faculty.code,
stats: progressStats?.data.bachelorsGraphStats,
years: progressStats?.data.years,
}}
/>
</div>
<div className="table-container">
<FacultyProgressTable
data={progressStats?.data.bachelorsTableStats}
programmeStats={progressStats?.data.bachelorsProgrammeStats}
titles={progressStats?.data.bachelorTitles}
sortedKeys={sortProgrammeKeys(Object.keys(progressStats?.data.bachelorsProgrammeStats))}
progressYearsVisible={Array(progressStats?.data.years.slice(1).length).fill(false)}
programmeNames={progressStats?.data.programmeNames}
language={language}
cypress="FacultyBachelorsProgressTable"
/>
</div>
</div>
{getDivider(
'Progress of master students of the faculty by starting year',
'ProgressOfMasterStudentsOfTheFacultyByStartingYear'
)}
<div className="section-container">
<div className="graph-container">
<FacultyBarChart
cypress="FacultyMastersProgress"
data={{
id: faculty.code,
stats: progressStats?.data.mastersGraphStats,
years: progressStats?.data.years,
}}
/>
</div>
<div className="table-container">
<FacultyProgressTable
data={progressStats?.data.mastersTableStats}
programmeStats={progressStats?.data.mastersProgrammeStats}
titles={progressStats?.data.mastersTitles}
sortedKeys={sortProgrammeKeys(Object.keys(progressStats?.data.mastersProgrammeStats))}
progressYearsVisible={Array(progressStats?.data.years.slice(1).length).fill(false)}
programmeNames={progressStats?.data.programmeNames}
language={language}
cypress="FacultyBachelorsProgressTable"
/>
</div>
</div>
{getDivider(
'Progress of doctoral students of the faculty by starting year',
'ProgressOfDoctoralStudentsOfTheFacultyByStartingYear'
)}
<div className="section-container">
<div className="graph-container">
<FacultyBarChart
cypress="FacultyDoctoralProgress"
data={{
id: faculty.code,
stats: progressStats?.data.doctoralGraphStats,
years: progressStats?.data.years,
}}
/>
</div>
<div className="table-container">
<FacultyProgressTable
data={progressStats?.data.doctoralTableStats}
programmeStats={progressStats?.data.doctoralProgrammeStats}
titles={progressStats?.data.doctoralTitles}
sortedKeys={sortProgrammeKeys(Object.keys(progressStats?.data.doctoralProgrammeStats))}
progressYearsVisible={Array(progressStats?.data.years.slice(1).length).fill(false)}
programmeNames={progressStats?.data.programmeNames}
language={language}
cypress="FacultyBachelorsProgressTable"
/>
</div>
</div>
</>
)}
</div>
)}
</div>
)
}
export default FacultyProgrammeOverview
@@ -0,0 +1,37 @@
import React from 'react'
import { Button, Icon, Table, Label } from 'semantic-ui-react'
const roundValue = value => {
return Number.isInteger(value) ? value : value.toFixed(1)
}
const BasicRow = ({ icon, yearArray, cypress, yearIndex, toggleVisibility, styles }) => {
return (
<Table.Row style={styles} key={`Basic-row-${cypress}-${yearIndex}`}>
{yearArray?.map((value, idx) => (
<Table.Cell key={`random-button-cell-key-${Math.random()}`}>
{idx === 0 ? (
<Button
as="div"
onClick={toggleVisibility}
labelPosition="right"
style={{ backgroundColor: 'white', borderRadius: 0 }}
data-cy={`Button-${cypress}-${yearIndex}`}
>
<Button icon style={{ backgroundColor: 'white', borderRadius: 0 }}>
<Icon name={icon} />
</Button>
<Label as="a" style={{ backgroundColor: 'white', borderRadius: 0 }}>
{value}
</Label>
</Button>
) : (
roundValue(value)
)}
</Table.Cell>
))}
</Table.Row>
)
}
export default BasicRow
@@ -3,13 +3,21 @@ import React from 'react'
import Highcharts from 'highcharts'
import ReactHighcharts from 'react-highcharts'
const colors = ['#7cb5ec', '#434348', '#90ed7d', '#f7a35c', '#8085e9', '#2b908f', '#f45b5b', '#91e8e1']
const colors = ['#7cb5ec', '#90ed7d', '#434348', '#f7a35c', '#FFF000', '#2b908f', '#f45b5b', '#91e8e1']
const CollapsedStackedBar = ({ data, labels, longLabels, names, language, differenceData }) => {
const CollapsedStackedBar = ({ data, labels, longLabels, names, language, differenceData, extraHeight }) => {
const transpose = matrix => {
return matrix.reduce((prev, next) => next.map((_item, i) => (prev[i] || []).concat(next[i])), [])
}
const needsExtra = extraHeight === 'EXTRA HEIGHT'
const manyProgrammes =
labels.includes('KH57_001') ||
labels.includes('KH50_001') ||
labels.includes('KH70_001') ||
labels.includes('KH40_001') ||
labels.includes('KH30_001') ||
labels.includes('KH80_001')
const dataTranspose = transpose(data)
.map((obj, idx) => ({ name: names[idx], data: obj, color: colors[idx] }))
.reverse()
@@ -31,7 +39,14 @@ const CollapsedStackedBar = ({ data, labels, longLabels, names, language, differ
if (change > 0) return `+${change.toString()}`
return change
}
const flexHeight = labels.length < 6 ? `${(1 / 3) * 100}%` : `${labels.length * (1 / 22) * 100}%`
// Point width is 24 px different multipliers adjusts the height.
const getFlexHeight = (len, needsExtra, manyProgrammes, labels) => {
if (labels.includes('KH74_001')) return `${len * 24 * 5}px`
if (needsExtra && manyProgrammes) return `${len * 24 * 1.5}px`
if (needsExtra) return `${len * 24 * 2.5}px`
return `${len * 24 * 1.5}px`
}
const getColor = change => {
if (change > 0) return '#6ab04c'
@@ -44,11 +59,7 @@ const CollapsedStackedBar = ({ data, labels, longLabels, names, language, differ
chart: {
type: 'bar',
marginTop: 60,
height: flexHeight,
scrollablePlotArea: {
minWidth: 700,
},
padding: 2,
height: getFlexHeight(labels.length, needsExtra, manyProgrammes, labels),
},
credits: {
text: 'oodikone | TOSKA',
@@ -98,7 +109,9 @@ const CollapsedStackedBar = ({ data, labels, longLabels, names, language, differ
backgroundColor: 'white',
fontSize: '25px',
formatter() {
let tooltipString = `<b>${longLabels[this.x][language]}</b><br /><p>${this.x}</p><br />`
let tooltipString = `<b>${
longLabels[this.x] && longLabels[this.x][language] ? longLabels[this.x][language] : longLabels[this.x].fi
}</b><br /><p>${this.x}</p><br />`
const diffArray = differenceArray[this.x]
this.points.forEach(point => {
tooltipString += `<span style="color:${point.color}">●</span> <b>${point.series.name}: ${point.y}</b>
@@ -106,16 +119,26 @@ const CollapsedStackedBar = ({ data, labels, longLabels, names, language, differ
diffArray[point.series.name]
)}</span>)<br />`
})
tooltipString += `<b>Total: ${this.points.reduce((prev, current) => prev + current.y, 0)}</b>`
return tooltipString
},
},
plotOptions: {
series: {
stacking: 'normal',
pointWidth: 25,
pointWidth: 24,
dataLabels: {
enabled: true,
formatter() {
if (Number.isInteger(this.y)) return `${this.y}`
return `${this.y.toFixed(1)}`
},
fontSize: '24px',
filter: {
property: 'y',
operator: '>',
value: 2,
},
},
},
},
@@ -1,76 +0,0 @@
import React, { useState, useImperativeHandle } from 'react'
import { Button, Icon, Table, Label } from 'semantic-ui-react'
const BasicRow = ({ icon, yearArray, cypress, yearIndex, toggleVisibility, styles }) => {
return (
<Table.Row style={styles} key={`Basic-row-${cypress}-${yearIndex}`}>
{yearArray?.map((value, idx) => (
<Table.Cell key={`random-button-cell-key-${Math.random()}`}>
{idx === 0 ? (
<Button
as="div"
onClick={toggleVisibility}
labelPosition="right"
style={{ backgroundColor: 'white', borderRadius: 0 }}
data-cy={`Button-${cypress}-${yearIndex}`}
>
<Button icon style={{ backgroundColor: 'white', borderRadius: 0 }}>
<Icon name={icon} />
</Button>
<Label as="a" style={{ backgroundColor: 'white', borderRadius: 0 }}>
{value}
</Label>
</Button>
) : (
value
)}
</Table.Cell>
))}
</Table.Row>
)
}
const ToggleTableView = React.forwardRef((props, ref) => {
const [visible, setVisible] = useState(false)
const toggleVisibility = () => {
setVisible(!visible)
}
useImperativeHandle(ref, () => {
return toggleVisibility
})
const chartRowStyles = {
rowSpan: 100,
display: visible ? '' : 'none',
}
const basicRowStyles = {
display: visible ? 'none' : '',
}
return (
<React.Fragment key={`random-fragment-key-${Math.random()}`}>
<BasicRow
icon="chevron right"
yearArray={props.yearArray}
cypress={`Show-${props.cypress}`}
yearIndex={props.yearIndex}
toggleVisibility={toggleVisibility}
styles={basicRowStyles}
/>
<BasicRow
icon="chevron down"
yearArray={props.yearArray}
cypress={`Hide-${props.cypress}`}
yearIndex={props.yearIndex}
toggleVisibility={toggleVisibility}
styles={chartRowStyles}
/>
<Table.Row key={`stack-row-key-${Math.random()}`} style={chartRowStyles}>
{props.children}
</Table.Row>
</React.Fragment>
)
})
ToggleTableView.displayName = 'ToggleTableView'
export default ToggleTableView
@@ -1,20 +1,50 @@
import React from 'react'
import { Table } from 'semantic-ui-react'
import React, { useEffect, useState } from 'react'
import { Table, Menu } from 'semantic-ui-react'
import CollapsedStackedBar from './CollapsedStackedBar'
import ToggleTableView from './ToggleTableView'
import BasicRow from './BasicRow'
const InteractiveDataTable = ({
cypress,
dataStats,
dataProgrammeStats,
programmeNames,
sortedKeys,
titles,
wideTable,
language,
sliceStart,
extraHeight,
yearsVisible,
shortNames,
studentsTable,
}) => {
if (!dataStats || !titles || !dataProgrammeStats) return null
const [keyOrder, setkeyOrder] = useState({})
const [sorter, setSorter] = useState('Code')
const [sortDir, setSortDir] = useState(1)
const [sortbyColumn, setSortByColumn] = useState(0)
const [columnIndex, setSelectedIndex] = useState(0)
const [visible, setVisible] = useState(yearsVisible)
const sortBySelectedColumn = () => {
const keys = {}
if (!(columnIndex === 0)) {
const numbersOfYears = dataStats.length
const data = Object.entries(dataProgrammeStats)
for (let yearIdx = 0; yearIdx < numbersOfYears; yearIdx++) {
const yearlySortedKeys = data
.sort((a, b) => {
if (sortDir === -1) return a[1][yearIdx][columnIndex] - b[1][yearIdx][columnIndex]
return b[1][yearIdx][columnIndex] - a[1][yearIdx][columnIndex]
})
.map(yearlyProgrammes => yearlyProgrammes[0])
keys[yearIdx] = yearlySortedKeys
}
}
setkeyOrder(keys)
setSortByColumn(columnIndex)
}
useEffect(sortBySelectedColumn, [columnIndex, dataStats, sortDir])
if (!dataStats || !titles || !dataProgrammeStats) return null
/* Calculation works as follows
1. Calculate difference between current and previous year: current year - previous year
2. Calculations are done for the values starting from the index 2 (index 0: year, index 1: total)
@@ -37,50 +67,87 @@ const InteractiveDataTable = ({
)
return differenceMatrix
}
const sortKeysAlphabeticallyAsc = programmeKeys => {
return programmeKeys.sort((a, b) => a.localeCompare(b))
const handleClick = (sorterName, nameIndex) => {
if (sorterName === sorter) setSortDir(-1 * sortDir)
setSorter(sorterName)
setSelectedIndex(nameIndex)
}
const toggleVisibility = yearIndex => {
const arrayToModify = [...visible]
arrayToModify[yearIndex] = !visible[yearIndex]
setVisible(arrayToModify)
}
const sorterNames = shortNames || titles.map(title => (title === '' ? 'Code' : title))
const differenceToPrevYears = calculatDiffToPrevYear(dataProgrammeStats)
const yearRef = React.createRef()
return (
<div className={`table-container${wideTable ? '-wide' : ''}`}>
<div>
<Menu compact attached="top">
<Menu.Item style={{ cursor: 'default', borderRadius: '1px', padding: '5px' }} active color="black">
Sort by:
</Menu.Item>
{sorterNames.map((sorterName, nameIndex) => (
<Menu.Item
data-cy={`Menu-${cypress}-${sorterName}`}
color={sorter === sorterName ? 'blue' : 'black'}
key={sorterName}
active={sorter === sorterName}
onClick={() => handleClick(sorterName, nameIndex)}
style={{ borderRadius: '1px', fontSize: '14px', padding: '5px' }}
icon={sortDir === 1 ? 'triangle down' : 'triangle up'}
content={sorterName}
/>
))}
</Menu>
<Table data-cy={`Table-${cypress}`} celled>
<Table.Header>
<Table.Row key={`randow-header-row-${Math.random()}`}>
<Table.Row key={`randow-header-row-${Math.random()}`} textAlign="center">
{titles?.map(title => (
<Table.HeaderCell key={title}>{title}</Table.HeaderCell>
))}
</Table.Row>
</Table.Header>
<Table.Body>
{dataStats?.map((yearArray, yearIndex) => (
<ToggleTableView
yearArray={yearArray}
cypress={cypress}
yearIndex={yearIndex}
ref={yearRef}
key={`togglable-${Math.random()}`}
>
<Table.Cell data-cy={`Cell-${cypress}-${yearIndex}`} key={`stack-cell${Math.random()}`} colSpan={100}>
<CollapsedStackedBar
data={sortKeysAlphabeticallyAsc(Object.keys(dataProgrammeStats))?.map(programme =>
dataProgrammeStats[programme][yearIndex].slice(sliceStart)
)}
labels={sortKeysAlphabeticallyAsc(Object.keys(dataProgrammeStats))}
differenceData={Object.keys(differenceToPrevYears)?.reduce(
(yearlyObject, programme) => ({
...yearlyObject,
[programme]: differenceToPrevYears[programme][yearIndex],
}),
{}
)}
longLabels={programmeNames}
language={language}
names={titles?.slice(sliceStart)}
/>
</Table.Cell>
</ToggleTableView>
<React.Fragment key={`random-fragment-key-${Math.random()}`}>
<BasicRow
icon={visible[yearIndex] ? 'chevron down' : 'chevron right'}
yearArray={yearArray}
cypress={visible[yearIndex] ? `Hide-${cypress}` : `Show-${cypress}`}
yearIndex={yearIndex}
toggleVisibility={() => toggleVisibility(yearIndex)}
/>
<Table.Row key={`stack-row-key-${Math.random()}`} style={{ display: visible[yearIndex] ? '' : 'none' }}>
<Table.Cell data-cy={`Cell-${cypress}-${yearIndex}`} key={`stack-cell${Math.random()}`} colSpan={100}>
<CollapsedStackedBar
data={
sortbyColumn === 0
? sortedKeys?.map(programme => dataProgrammeStats[programme][yearIndex].slice(sliceStart))
: keyOrder[yearIndex]?.map(programme =>
dataProgrammeStats[programme][yearIndex].slice(sliceStart)
)
}
labels={sortbyColumn === 0 ? sortedKeys : keyOrder[yearIndex]}
differenceData={Object.keys(differenceToPrevYears)?.reduce(
(yearlyObject, programme) => ({
...yearlyObject,
[programme]: differenceToPrevYears[programme][yearIndex],
}),
{}
)}
longLabels={programmeNames}
language={language}
names={titles?.slice(sliceStart)}
extraHeight={extraHeight}
studentsTable={studentsTable}
/>
</Table.Cell>
</Table.Row>
</React.Fragment>
))}
</Table.Body>
</Table>
@@ -0,0 +1,118 @@
import React from 'react'
import ReactHighcharts from 'react-highcharts'
const BarChart = ({ data, categories, goal, handleClick, facultyGraph = true, year, label }) => {
const maxValue = data.reduce((max, { y }) => {
return y > max ? y : max
}, goal * 2)
const config = {
chart: {
type: 'bar',
width: 700,
margin: [70, 0],
},
title: { text: ' ' },
tooltip: {
// eslint-disable-next-line
formatter: function() {
// eslint-disable-next-line
return this.x + ': ' + this.y + ' months' ;
},
},
plotOptions: {
series: {
dataLabels: {
enabled: true,
inside: true,
},
pointPadding: 0.0,
// groupPadding: 0.1,
},
},
series: [
{
data,
dataLabels: [
{
align: 'left',
format: '{point.amount} students',
color: '#EBECF0',
},
{
align: 'right',
format: '{y}',
color: '#EBECF0',
},
],
showInLegend: false,
zones: [
{
value: goal + 0.1,
color: '#90A959',
},
{
value: goal + 12.1,
color: '#FEE191',
},
{
color: '#FB6962',
},
],
point: {
events: {
click(e) {
handleClick(e, facultyGraph)
},
},
},
},
],
xAxis: {
categories,
title: {
text: facultyGraph ? label : 'Programme',
align: 'high',
offset: 0,
rotation: 0,
y: -10,
},
},
yAxis: {
min: 0,
max: maxValue,
title: { text: 'Graduation time (months)' },
labels: {
overflow: 'justify',
},
allowDecimals: false,
showFirstLabel: false,
plotLines: [
{
color: '#90A959',
width: 2,
value: goal,
dashStyle: 'shortDash',
// label: {
// text: `${goal}`, // 'optimal',
// verticalAlign: 'bottom',
// },
},
],
},
credits: {
text: 'oodikone | TOSKA',
},
}
if (!facultyGraph) config.title.text = `Year ${year} by ${label.toLowerCase()}`
return (
<div>
<ReactHighcharts config={config} />
</div>
)
}
export default BarChart
@@ -0,0 +1,51 @@
import React, { useState } from 'react'
import { Divider, Message } from 'semantic-ui-react'
import BarChart from './BarChart'
const GraduationTimes = ({ title, years, data, level, goal, label, levelProgrammeData }) => {
const [programmeData, setProgrammeData] = useState(null)
if (!data.some(a => a.amount > 0)) return null
const handleClick = (e, isFacultyGraph) => {
if (isFacultyGraph) {
const year = e.point.category
setProgrammeData({
data: levelProgrammeData[year]?.data,
categories: levelProgrammeData[year]?.programmes,
year,
})
} else {
setProgrammeData(null)
}
}
// TODO clean up inline styling
return (
<>
<Divider data-cy={`Section-${level}`} horizontal>
{title}
</Divider>
<div>
<div style={{ display: 'flex', justifyContent: 'space-between', paddingBottom: '100px' }}>
<BarChart categories={years} data={data} goal={goal} handleClick={handleClick} label={label} />
{programmeData === null ? (
<div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center' }}>
<Message compact>Click a bar to view that year's programme level breakdown</Message>
</div>
) : (
<BarChart
categories={programmeData?.categories}
data={programmeData?.data}
goal={goal}
facultyGraph={false}
handleClick={handleClick}
year={programmeData?.year}
label={label}
/>
)}
</div>
</div>
</>
)
}
export default GraduationTimes
@@ -0,0 +1,34 @@
import React from 'react'
import { Dropdown } from 'semantic-ui-react'
import '../../StudyProgramme/studyprogramme.css'
const ProgrammeSelector = ({ programme, setProgramme, programmes }) => {
if (!programmes) return null
const handleStudytrackChange = (event, { value }) => {
event.preventDefault()
setProgramme(value)
}
return (
<div className="programme-selector">
<h4>Choose programme</h4>
<Dropdown
fluid
selection
name="programme"
placeholder="All programmes of the faculty"
value={programme}
onChange={handleStudytrackChange}
options={programmes.map(({ code, name }) => ({
key: code,
value: code,
text: code === 'faculty' ? name?.fi : `${name?.fi}, ${code}`,
}))}
/>
</div>
)
}
export default ProgrammeSelector
@@ -0,0 +1,135 @@
import React, { useState } from 'react'
import { Divider, Loader } from 'semantic-ui-react'
import { useGetFacultyGraduationTimesQuery } from 'redux/facultyStats'
import Toggle from '../../StudyProgramme/Toggle'
import InfoBox from '../../Info/InfoBox'
import GraduationTimes from './GraduationTimes'
import InfotoolTips from '../../../common/InfoToolTips'
import '../faculty.css'
const TimesAndPathsView = ({ faculty, studyProgrammes, setStudyProgrammes }) => {
const toolTips = InfotoolTips.Faculty
const [showMeanTime, setShowMeanTime] = useState(false)
const [groupByGradYear, setGroupByGradYear] = useState(false)
const studyProgrammeFilter = studyProgrammes ? 'ALL_PROGRAMMES' : 'NEW_STUDY_PROGRAMMES'
const graduationStats = useGetFacultyGraduationTimesQuery({ id: faculty?.code, studyProgrammeFilter })
const groupBy = groupByGradYear ? 'byGradYear' : 'byStartYear'
const label = groupByGradYear ? 'Graduation year' : 'Start year'
const data = showMeanTime ? graduationStats?.data?.[groupBy].means : graduationStats?.data?.[groupBy].medians
const years = graduationStats?.data?.years
const goals = graduationStats?.data?.goals
const programmeData = showMeanTime
? graduationStats?.data?.[groupBy].programmes.means
: graduationStats?.data?.[groupBy].programmes.medians
const isFetchingOrLoading = graduationStats.isLoading || graduationStats.isFetching
const isError = graduationStats.isError || (graduationStats.isSuccess && !graduationStats.data)
if (isError) return <h3>Something went wrong, please try refreshing the page.</h3>
const getDivider = (title, toolTipText) => (
<>
<div className="divider">
<Divider data-cy={`Section-${toolTipText}`} horizontal>
{title}
</Divider>
</div>
<InfoBox content={toolTips[toolTipText]} />
</>
)
return (
<div className="programmes-overview">
<div className="toggle-container">
<Toggle
cypress="ProgrammeToggle"
toolTips={toolTips.ProgrammeToggle}
firstLabel="New study programmes"
secondLabel="All study programmes"
value={studyProgrammes}
setValue={setStudyProgrammes}
/>
</div>
{isFetchingOrLoading ? (
<Loader active style={{ marginTop: '15em' }} />
) : (
<>
{graduationStats.isSuccess && graduationStats.data && (
<>
{getDivider('Average graduation times', 'AverageGraduationTimes')}
<div className="toggle-container">
<Toggle
cypress="GroupByToggle"
firstLabel="Group by: Starting year"
secondLabel="Graduation year"
value={groupByGradYear}
setValue={setGroupByGradYear}
/>
<Toggle
cypress="GraduationTimeToggle"
firstLabel="Median time"
secondLabel="Mean time"
value={showMeanTime}
setValue={setShowMeanTime}
/>
</div>
<div>
<GraduationTimes
level="bachelor"
title="Bachelor"
data={data?.bachelor}
years={years}
goal={goals?.bachelor}
label={label}
levelProgrammeData={programmeData?.bachelor}
/>
<GraduationTimes
level="bcMsCombo"
title="Bachelor + Master"
data={data?.bcMsCombo}
years={years}
goal={goals?.bcMsCombo}
label={label}
levelProgrammeData={programmeData?.bcMsCombo}
/>
<GraduationTimes
level="master"
title="Master"
data={data?.master}
years={years}
goal={goals?.master}
label={label}
levelProgrammeData={programmeData?.master}
/>
<GraduationTimes
level="doctor"
title="Doctor"
data={data?.doctor}
years={years}
goal={goals?.doctor}
label={label}
levelProgrammeData={programmeData?.doctor}
/>
<GraduationTimes
level="licentiate"
title="Licentiate"
data={data?.licentiate}
years={years}
goal={goals?.licentiate}
label={label}
levelProgrammeData={programmeData?.licentiate}
/>
</div>
</>
)}
</>
)}
</div>
)
}
export default TimesAndPathsView
@@ -0,0 +1,51 @@
import React, { useState } from 'react'
import { Button, Icon, Loader } from 'semantic-ui-react'
import { useUpdateFacultyBasicViewQuery } from '../../../redux/facultyStats'
const getStatusIcon = stats => {
if (stats.isLoading) return <Loader active />
if (stats.isSuccess) return <Icon name="check" color="green" />
if (stats.isError) return <Icon name="close" color="red" />
return ''
}
const UpdateView = ({ faculty }) => {
const [skipBasic, setSkipBasic] = useState(true)
const [skipCredits, setSkipCredits] = useState(true)
const [skipThesis, setSkipThesis] = useState(true)
const basicBasicTabStats = useUpdateFacultyBasicViewQuery({ id: faculty, statsType: 'STUDENT' }, { skip: skipBasic })
const creditsBasicTabStats = useUpdateFacultyBasicViewQuery(
{ id: faculty, statsType: 'CREDITS' },
{ skip: skipCredits }
)
const thesisBasicTabStats = useUpdateFacultyBasicViewQuery({ id: faculty, statsType: 'THESIS' }, { skip: skipThesis })
return (
<div className="update-view">
<div className="button-container">
<h3>Update data</h3>
<p>
<i>(For large faculties updates can take a couple of minutes)</i>
</p>
<h4>Update students data on Basic information -view</h4>
<Button disabled={basicBasicTabStats.isLoading} color="blue" onClick={() => setSkipBasic(false)}>
Update
</Button>
{getStatusIcon(basicBasicTabStats)}
<h4>Update credits data on Basic information -view</h4>
<Button disabled={creditsBasicTabStats.isLoading} color="blue" onClick={() => setSkipCredits(false)}>
Update
</Button>
{getStatusIcon(creditsBasicTabStats)}
<h4>Update thesiswriters data on Basic information -view</h4>
<Button disabled={thesisBasicTabStats.isLoading} color="blue" onClick={() => setSkipThesis(false)}>
Update
</Button>
{getStatusIcon(thesisBasicTabStats)}
</div>
</div>
)
}
export default UpdateView
@@ -2,30 +2,34 @@ import React, { useCallback, useEffect, useState } from 'react'
import { withRouter, useHistory } from 'react-router-dom'
import { Segment, Header, Tab, Loader } from 'semantic-ui-react'
import { useGetFacultiesQuery } from 'redux/facultyStats'
import { useGetAuthorizedUserQuery } from 'redux/auth'
import { getTextIn } from 'common'
import { useTabs, useTitle } from '../../common/hooks'
import FacultySelector from './facultySelector'
import BasicOverview from './BasicOverview'
import ProgrammeOverview from './FacultyProgrammeOverview'
import TimesAndPathsView from './TimesAndPaths'
import UpdateView from './UpdateView'
import useLanguage from '../LanguagePicker/useLanguage'
import TSA from '../../common/tsa'
const ignore = ['Y', 'H99', 'Y01', 'H92', 'H930']
const FacultyStatistics = props => {
useTitle('Faculties')
const history = useHistory()
const { language } = useLanguage()
const allFaculties = useGetFacultiesQuery()
const faculties = allFaculties?.data && allFaculties.data.filter(f => !ignore.includes(f.code))
const faculties = allFaculties?.data
const { match } = props
const { facultyCode } = match.params
const faculty = faculties && facultyCode && faculties.find(f => f.code === facultyCode)
const facultyName = faculty && getTextIn(faculty.name, language)
const { isAdmin } = useGetAuthorizedUserQuery()
const [tab, setTab] = useTabs('p_tab', 0, history)
const [academicYear, setAcademicYear] = useState(false)
const [studyProgrammes, setStudyProgrammes] = useState(false)
const [specialGroups, setSpecialGroups] = useState(false)
useEffect(() => {
if (!facultyName) {
@@ -56,13 +60,49 @@ const FacultyStatistics = props => {
const panes = [
{
menuItem: 'Basic information',
render: () => <BasicOverview faculty={faculty} academicYear={academicYear} setAcademicYear={setAcademicYear} />,
},
{
menuItem: 'Programmes and student populations',
render: () => <ProgrammeOverview />,
render: () => (
<BasicOverview
faculty={faculty}
academicYear={academicYear}
setAcademicYear={setAcademicYear}
studyProgrammes={studyProgrammes}
setStudyProgrammes={setStudyProgrammes}
specialGroups={specialGroups}
setSpecialGroups={setSpecialGroups}
language={language}
/>
),
},
]
// REMEMBER to change cypress test 'Correct tabs are shown', when visibility of
// Graduation times and Programmes and student Populations -tabs is changed for facultyStatistic users
if (isAdmin) {
panes.push({
menuItem: 'Programmes and student populations',
render: () => <ProgrammeOverview faculty={faculty} language={language} />,
})
}
if (isAdmin) {
panes.push({
menuItem: 'Graduation times',
render: () => (
<TimesAndPathsView
faculty={faculty}
studyProgrammes={studyProgrammes}
setStudyProgrammes={setStudyProgrammes}
/>
),
})
}
if (isAdmin) {
panes.push({
menuItem: 'Update statistics',
render: () => <UpdateView faculty={faculty?.code} />,
})
}
return panes
}
@@ -81,7 +121,7 @@ const FacultyStatistics = props => {
)
return (
<div className="segmentContainer">
<div className="segmentContainer" data-cy="FacultySegmentContainer">
<Segment className="contentSegment">
<div align="center" style={{ padding: '30px' }}>
<Header textAlign="center">{facultyName}</Header>
@@ -10,9 +10,19 @@ const filterTexts = {
[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.NOT_PARTICIPATED]: { label: 'Not Participated' },
[FilterType.DID_NOT_PASS]: { label: "Didn't Pass" },
}
// 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',
}
const CourseCard = ({ course, filterType, onChange }) => {
const { language } = useLanguage()
@@ -27,7 +37,7 @@ const CourseCard = ({ course, filterType, onChange }) => {
return (
<>
<Label style={{ marginTop: '0.5rem' }}>
{getTextIn(course.name, language)}
{getTextIn(course.course.name, language)}
<Dropdown
text={filterTexts[filterType].label}
@@ -35,7 +45,7 @@ const CourseCard = ({ course, filterType, onChange }) => {
fluid
className="mini"
button
data-cy={`${name}-${course.code}-dropdown`}
data-cy={`${name}-${course.course.code}-dropdown`}
style={{ marginTop: '0.5rem' }}
>
<Dropdown.Menu>
@@ -50,6 +60,8 @@ const CourseCard = ({ course, filterType, onChange }) => {
/>
)
}
if (Object.keys(course?.students[translate[type]]).length === 0)
return <Dropdown.Item key={label} text={label} value={type} onClick={onClick} disabled />
return <Dropdown.Item key={label} text={label} value={type} onClick={onClick} />
})}
</Dropdown.Menu>
@@ -60,7 +72,7 @@ const CourseCard = ({ course, filterType, onChange }) => {
size="tiny"
onClick={clear}
icon
data-cy={`${name}-${course.code}-clear`}
data-cy={`${name}-${course.course.code}-clear`}
style={{ marginTop: '0.5rem' }}
>
<Icon name="close" />
@@ -62,7 +62,7 @@ const CourseFilterCard = ({ courseStats, options, onOptionsChange }) => {
/>
{Object.entries(courseFilters).map(([code]) => (
<CourseCard
course={courseStats[code].course}
course={courseStats[code]}
filterType={courseFilters[code] ?? FilterType.ALL}
onChange={type => setCourseFilter(code, type)}
key={`course-filter-selected-course-${code}`}
@@ -83,9 +83,10 @@ const filterFunctions = {
[FilterType.PASSED_AFTER_FAILURE]: createFilterFunc('retryPassed'),
[FilterType.FAILED]: createFilterFunc('failed'),
[FilterType.FAILED_MANY_TIMES]: createFilterFunc('failedMany'),
[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.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),
}
const filter = createFilter({
@@ -4,24 +4,32 @@ import { Message, Icon, Button } from 'semantic-ui-react'
import ReactMarkdown from 'react-markdown'
import './infoBox.css'
const InfoBox = ({ content }) => {
const InfoBox = ({ content, cypress }) => {
const [open, setOpen] = useState(false)
const toggleOpen = () => setOpen(prev => !prev)
return open ? (
<Message icon className="ok-infobox" color="green">
<div className="content-container">
<div className="content-container" data-cy={`${cypress}-info-content`}>
<Icon name="info circle" size="huge" />
<Message.Content className="ok-infobox content">
<ReactMarkdown escapeHtml={false}>{content}</ReactMarkdown>
</Message.Content>
</div>
<Button onClick={toggleOpen} className="ok-infobox-close">
<Button onClick={toggleOpen} className="ok-infobox-close" data-cy={`${cypress}-close-info`}>
Close Info
</Button>
</Message>
) : (
<Button icon labelPosition="left" className="ok-infobox-collapsed" onClick={toggleOpen} basic color="green">
<Button
icon
labelPosition="left"
className="ok-infobox-collapsed"
onClick={toggleOpen}
basic
color="green"
data-cy={`${cypress}-open-info`}
>
<Icon name="info circle" />
Show Info
</Button>
@@ -9,7 +9,8 @@ import LanguagePicker from '../LanguagePicker'
import { isDev, adminerUrls } from '../../conf'
const allNavigationItems = {
faculty: { path: '/faculties', key: 'faculties', label: 'Faculties', reqRights: ['admin'] },
trends: { path: '/trends', key: 'trends', label: 'Trends' },
faculty: { path: '/faculties', key: 'faculties', label: 'Faculties' },
populations: {
key: 'studyProgramme',
label: 'Study programme',
@@ -18,11 +19,10 @@ const allNavigationItems = {
{ path: '/study-programme', key: 'overview', label: 'Overview' },
],
},
students: { path: '/students', key: 'students', label: 'Student statistics' },
courseStatistics: { path: '/coursestatistics', key: 'courseStatistics', label: 'Course statistics' },
students: { path: '/students', key: 'students', label: 'Student statistics' },
teachers: { path: '/teachers', key: 'teachers', label: 'Teachers', reqRights: ['teachers'] },
users: { path: '/users', key: 'users', label: 'Users', reqRights: ['admin'] },
trends: { path: '/trends', key: 'trends', label: 'Trends' },
studyGuidanceGroups: {
path: '/studyguidancegroups',
key: 'studyGuidanceGroups',
@@ -54,6 +54,8 @@ const NavigationBar = () => {
if (!checkUserAccess(['admin', 'studyGuidanceGroups'], roles) && rights.length === 0) return
} else if (key === 'courseStatistics') {
if (!checkUserAccess(['courseStatistics', 'admin'], roles) && rights.length === 0) return
} else if (key === 'faculty') {
if (!checkUserAccess(['facultyStatistics', 'admin'], roles)) return
}
const { reqRights } = allNavigationItems[key]
if (!reqRights || reqRights.every(r => roles.includes(r))) {
@@ -29,11 +29,11 @@ const mapCourseData = course => ({
},
})
const GradeDistribution = ({ flat }) => {
const GradeDistribution = ({ flat, onlyIamRights }) => {
const { modules, courseStatistics, onGoToCourseStatisticsClick } = UsePopulationCourseContext()
const columns = useMemo(
() => [
const columns = useMemo(() => {
const columns = [
{
key: 'name-parent',
mergeHeader: true,
@@ -110,9 +110,12 @@ const GradeDistribution = ({ flat }) => {
},
],
},
],
[onGoToCourseStatisticsClick]
)
]
if (onlyIamRights) columns[0].children.pop()
return columns
}, [onGoToCourseStatisticsClick])
const data = useMemo(() => {
if (flat) {
@@ -23,11 +23,11 @@ const createModuleAggregateRow = ({ definition, children }) => ({
},
})
const PassFail = ({ flat }) => {
const PassFail = ({ flat, onlyIamRights }) => {
const { modules, courseStatistics, onGoToCourseStatisticsClick } = UsePopulationCourseContext()
const columns = useMemo(
() => [
const columns = useMemo(() => {
const columns = [
{
key: 'course',
title: 'Course',
@@ -197,9 +197,11 @@ const PassFail = ({ flat }) => {
},
],
},
],
[onGoToCourseStatisticsClick]
)
]
if (onlyIamRights) columns[0].children[0].children.pop()
return columns
}, [onGoToCourseStatisticsClick])
const data = useMemo(() => {
if (flat) {
@@ -26,11 +26,11 @@ const createModuleAggregateRow = ({ definition, children }) => ({
},
})
const PassFailEnrollments = ({ flat }) => {
const PassFailEnrollments = ({ flat, onlyIamRights }) => {
const { modules, courseStatistics, onGoToCourseStatisticsClick } = UsePopulationCourseContext()
const columns = useMemo(
() => [
const columns = useMemo(() => {
const columns = [
{
key: 'course',
title: 'Course',
@@ -204,9 +204,11 @@ const PassFailEnrollments = ({ flat }) => {
},
],
},
],
[onGoToCourseStatisticsClick]
)
]
if (onlyIamRights) columns[0].children[0].children.pop()
return columns
}, [onGoToCourseStatisticsClick])
const data = useMemo(() => {
if (flat) {
@@ -34,12 +34,12 @@ const yearColumn = (year, cumulative) => ({
children: [semesterColumn(year, 'Fall', cumulative), semesterColumn(year, 'Spring', cumulative)],
})
const PassingSemesters = () => {
const PassingSemesters = ({ onlyIamRights }) => {
const { modules, onGoToCourseStatisticsClick } = UsePopulationCourseContext()
const [cumulativeStats, setCumulativeStats] = useState(false)
const columns = useMemo(
() => [
const columns = useMemo(() => {
const columns = [
{
key: 'course-name',
mergeHeader: true,
@@ -118,9 +118,12 @@ const PassingSemesters = () => {
..._.range(0, 6).map(i => yearColumn(i, cumulativeStats)),
],
},
],
[cumulativeStats, onGoToCourseStatisticsClick]
)
]
if (onlyIamRights) columns[0].children.pop()
return columns
}, [cumulativeStats, onGoToCourseStatisticsClick])
const data = useMemo(
() =>
@@ -294,7 +294,11 @@ const PopulationCourseStats = props => {
menuItem: 'pass/fail',
render: () => (
<Tab.Pane className="menuTab">
<PassFail expandedGroups={expandedGroups} toggleGroupExpansion={toggleGroupExpansion} />
<PassFail
expandedGroups={expandedGroups}
toggleGroupExpansion={toggleGroupExpansion}
onlyIamRights={props.onlyIamRights}
/>
</Tab.Pane>
),
},
@@ -302,7 +306,11 @@ const PopulationCourseStats = props => {
menuItem: 'pass/fail w/ enrollments ',
render: () => (
<Tab.Pane className="menuTab">
<PassFailEnrollments expandedGroups={expandedGroups} toggleGroupExpansion={toggleGroupExpansion} />
<PassFailEnrollments
expandedGroups={expandedGroups}
toggleGroupExpansion={toggleGroupExpansion}
onlyIamRights={props.onlyIamRights}
/>
</Tab.Pane>
),
},
@@ -310,7 +318,11 @@ const PopulationCourseStats = props => {
menuItem: 'grades',
render: () => (
<Tab.Pane className="menuTab">
<GradeDistribution expandedGroups={expandedGroups} toggleGroupExpansion={toggleGroupExpansion} />
<GradeDistribution
expandedGroups={expandedGroups}
toggleGroupExpansion={toggleGroupExpansion}
onlyIamRights={props.onlyIamRights}
/>
</Tab.Pane>
),
},
@@ -323,6 +335,7 @@ const PopulationCourseStats = props => {
courseStatistics={courseStatistics}
expandedGroups={expandedGroups}
toggleGroupExpansion={toggleGroupExpansion}
onlyIamRights={props.onlyIamRights}
/>
</Tab.Pane>
),
@@ -84,7 +84,9 @@ const PopulationCourses = ({ query = {}, filteredStudents, selectedStudentsByYea
<Segment basic>
<InfoBox content={infotooltips.PopulationStatistics.CoursesOfPopulation} />
{renderToggleStructureButton()}
{query.studyRights.programme && <FilterDegreeCoursesModal studyProgramme={query.studyRights.programme} />}
{query.studyRights.programme && !onlyIamRights && (
<FilterDegreeCoursesModal studyProgramme={query.studyRights.programme} />
)}
<SegmentDimmer isLoading={pending} />
{showByStudytrack ? (
@@ -143,8 +143,8 @@ export default ({ students }) => {
studytrack: studytrack(s.studyrights).map(st => st.name)[0],
tags: tags(s.tags),
'start year at university': reformatDate(s.started, 'YYYY'),
'start of studyright': getStartOfStudyright(s.studyrights),
'started in studyright': getStartedInStudyright(s.studyrights),
'start of studyright': reformatDate(getStartOfStudyright(s.studyrights), 'YYYY-MM-DD'),
'started in studyright': reformatDate(getStartedInStudyright(s.studyrights), 'YYYY-MM-DD'),
'admission type': parseInt(queryYear, 10 >= 2020) ? getAdmissionType(s.studyrights) : undefined,
'updated at': reformatDate(s.updatedAt, 'YYYY-MM-DD hh:mm:ss'),
'mandatory total passed': totalMandatoryPassed(s.studentNumber),
@@ -17,6 +17,7 @@ import {
} from 'common'
import { useGetStudyGuidanceGroupPopulationQuery } from 'redux/studyGuidanceGroups'
import { useGetAuthorizedUserQuery } from 'redux/auth'
import { useGetSemestersQuery } from 'redux/semesters'
import { PRIORITYCODE_TEXTS } from '../../../constants'
import sendEvent from '../../../common/sendEvent'
import useLanguage from '../../LanguagePicker/useLanguage'
@@ -35,6 +36,19 @@ const GeneralTab = ({
const { useFilterSelector } = useFilters()
const [popupStates, setPopupStates] = useState({})
const sendAnalytics = sendEvent.populationStudents
const { data: semesterData } = useGetSemestersQuery()
const fromSemester = from
? Object.values(semesterData.semesters)
.filter(({ startdate }) => new Date(startdate) <= new Date(from))
.sort((a, b) => new Date(b.startdate) - new Date(a.startdate))[0]?.semestercode
: null
const toSemester = to
? Object.values(semesterData.semesters)
.filter(({ enddate }) => new Date(enddate) >= new Date(to))
.sort((a, b) => new Date(a.enddate) - new Date(b.enddate))[0]?.semestercode
: null
const creditDateFilterOptions = useFilterSelector(creditDateFilter.selectors.selectOptions)
@@ -210,9 +224,7 @@ const GeneralTab = ({
const enrollments =
s.enrollments
?.filter(e => coursecode.includes(e.course_code))
?.filter(
e => new Date(e.enrollment_date_time) >= new Date(from) && new Date(e.enrollment_date_time) < new Date(to)
) ?? null
?.filter(e => e.semestercode >= fromSemester && e.semestercode <= toSemester) ?? null
if (!enrollments || !enrollments.length) return ''
return enrollments[0].enrollment_date_time
}
@@ -62,7 +62,12 @@ const Routes = () => (
<Route exact path={routes.feedback} component={Feedback} />
<Route path={routes.trends} component={Trends} />
<ProtectedRoute requireUserHasRights exact path="/populations" component={Populations} />
<ProtectedRoute requiredRoles={['admin']} exact path="/faculties/:facultyCode?" component={FacultyStatistics} />
<ProtectedRoute
requiredRoles={['admin', 'facultyStatistics']}
exact
path="/faculties/:facultyCode?"
component={FacultyStatistics}
/>
<ProtectedRoute
requireUserHasRights
exact
@@ -45,7 +45,7 @@ export const getColumnValue = (ctx, column, exportMode = false) => {
return column.getRowExportVal(ctx.item, ctx.isGroup, ctx.parents)
}
if (column.getRowVal) {
if (column && column.getRowVal) {
return column.getRowVal(ctx.item, ctx.isGroup, ctx.parents)
}
@@ -286,6 +286,8 @@ const SingleStudyGroupViewWrapper = ({ group, language, isLoading, studyProgramm
history.push('/studyguidancegroups')
}
if (!group) return null
return (
<>
<Wrapper isLoading={isLoading}>
@@ -293,7 +295,7 @@ const SingleStudyGroupViewWrapper = ({ group, language, isLoading, studyProgramm
<Divider />
<div style={{ display: 'flex', gap: '8px', alignItems: 'baseline' }}>
<Header size="medium" style={{ marginRight: 'auto' }}>
{group.name && language && getTextIn(group.name, language)}
{group && group.name && language && getTextIn(group.name, language)}
</Header>
{group.tags?.studyProgramme && (
<Label tag content={studyProgrammes.find(p => p.value === group.tags.studyProgramme)?.text} color="blue" />
@@ -316,21 +318,15 @@ const SingleStudyGuidanceGroupContainer = ({ group }) => {
if (!group) {
return (
<SingleStudyGroupViewWrapper>
<StyledMessage>
Couldn't find group with this id! Please check that id is correct and you have rights to this study guidance
group.
</StyledMessage>
</SingleStudyGroupViewWrapper>
<StyledMessage>
Couldn't find group with this id! Please check that id is correct and you have rights to this study guidance
group.
</StyledMessage>
)
}
if (groupStudentNumbers.length === 0) {
return (
<SingleStudyGroupViewWrapper>
<StyledMessage>This study guidance group doesn't contain any students.</StyledMessage>
</SingleStudyGroupViewWrapper>
)
if (!groupStudentNumbers.length) {
return <StyledMessage>This study guidance group doesn't contain any students.</StyledMessage>
}
return (
@@ -2,7 +2,7 @@ import React from 'react'
import { useSelector } from 'react-redux'
import { Link, useHistory } from 'react-router-dom'
import { Formik } from 'formik'
import { Form, Button, Icon } from 'semantic-ui-react'
import { Form, Button, Icon, Modal } from 'semantic-ui-react'
import Datetime from 'react-datetime'
import { getTextIn, textAndDescriptionSearch } from 'common'
import { useChangeStudyGuidanceGroupTagsMutation } from 'redux/studyGuidanceGroups'
@@ -40,85 +40,113 @@ const prettifyCamelCase = str => {
const cellWrapper = { display: 'flex', gap: '8px', width: '100%' }
const cellContent = { flexGrow: 1 }
const AssociateTagForm = ({ group, tagName, toggleEdit, selectFieldItems }) => {
const EditTagModal = ({ group, tagName, toggleEdit, selectFieldItems, open }) => {
const [changeStudyGuidanceGroupTags, { isLoading }] = useChangeStudyGuidanceGroupTagsMutation()
const { language } = useSelector(({ settings }) => settings)
const onSubmit = values => {
changeStudyGuidanceGroupTags({ groupId: group.id, tags: values })
toggleEdit()
}
return (
<div>
<Modal onClose={toggleEdit} open={open}>
<Formik
initialValues={{ [tagName]: '' }}
onSubmit={values => {
changeStudyGuidanceGroupTags({ groupId: group.id, tags: values })
if (group.tags?.[tagName]) toggleEdit()
}}
onSubmit={values => onSubmit(values)}
validate={values => (!values[tagName] ? { [tagName]: `${tagName} is required` } : {})}
>
{formik => (
<Form onSubmit={formik.handleSubmit} style={{ ...cellWrapper, alignItems: 'center' }}>
<div style={cellContent}>
{tagName === 'studyProgramme' ? (
<Form.Select
name={tagName}
search={textAndDescriptionSearch}
fluid
placeholder={
selectFieldItems.find(p => p.value === group.tags?.[tagName])?.text || 'Select study programme'
}
options={selectFieldItems}
closeOnChange
value={formik.values[tagName]}
onChange={(_, value) => formik.setFieldValue(tagName, value?.value)}
/>
) : (
<Datetime
className="studyguidancegroupoverview__yeartagselector"
name={tagName}
dateFormat="YYYY"
timeFormat={false}
initialvalue={group.tags?.[tagName]}
inputProps={{ readOnly: true }}
renderYear={(props, year) => {
const shiftBy = 2 // fix to start from 2017 instead of 2019
const shiftedProps = {
...props,
key: props.key - shiftBy,
'data-value': props['data-value'] - shiftBy,
}
const formattedAndShiftedYear = startYearToAcademicYear(year - shiftBy)
return <td {...shiftedProps}> {formattedAndShiftedYear}</td>
}}
renderInput={({ value, ...rest }) => {
return (
<div>
<input value={startYearToAcademicYear(value)} placeholder="Select year" {...rest} />
</div>
)
}}
closeOnSelect
value={formik.values[tagName]}
onChange={value => formik.setFieldValue(tagName, value?.format('YYYY'))}
/>
)}
</div>
<div style={{ ...cellContent, flexGrow: 0 }}>
<Button type="submit" style={{ margin: '0' }} disabled={isLoading}>
Add {prettifyCamelCase(tagName)}
</Button>
</div>
{group.tags?.[tagName] && (
<div style={cellContent}>
<Button icon type="button" style={{ margin: '0' }} onClick={() => toggleEdit()}>
Close
<Icon name="close" />
</Button>
</div>
)}
</Form>
<>
<Modal.Header>{getTextIn(group.name, language)}</Modal.Header>
<Modal.Content>
<AssociateTagForm group={group} tagName={tagName} selectFieldItems={selectFieldItems} formik={formik} />
</Modal.Content>
<Modal.Actions>
<Button content="Cancel" labelPosition="right" icon="trash" onClick={toggleEdit} negative />
<Button
content="Save"
type="submit"
labelPosition="right"
icon="checkmark"
onClick={formik.handleSubmit}
disabled={isLoading}
positive
/>
</Modal.Actions>
</>
)}
</Formik>
</div>
</Modal>
)
}
const AssociateTagForm = ({ group, tagName, selectFieldItems, formik }) => (
<>
<p>
{tagName === 'studyProgramme'
? `Edit associated study programme for this group:`
: `Edit associated starting year for this group:`}
</p>
<Form onSubmit={formik.handleSubmit} style={{ ...cellWrapper, alignItems: 'center' }}>
<div style={cellContent}>
{tagName === 'studyProgramme' ? (
<Form.Select
name={tagName}
search={textAndDescriptionSearch}
fluid
placeholder={
selectFieldItems.find(p => p.value === group.tags?.[tagName])?.text || 'Select study programme'
}
options={selectFieldItems}
closeOnChange
value={formik.values[tagName]}
onChange={(_, value) => formik.setFieldValue(tagName, value?.value)}
/>
) : (
<Datetime
className="studyguidancegroupoverview__yeartagselector"
name={tagName}
dateFormat="YYYY"
timeFormat={false}
initialvalue={group.tags?.[tagName]}
inputProps={{ readOnly: true }}
renderYear={(props, year) => {
const shiftBy = 2 // fix to start from 2017 instead of 2019
const formattedAndShiftedYear = startYearToAcademicYear(year - shiftBy)
const shiftedProps = {
...props,
key: props.key - shiftBy,
className:
`${formik.values[tagName]}` === formattedAndShiftedYear.substring(0, 4)
? 'rdtYear rdtActive'
: 'rdtYear',
'data-value': props['data-value'] - shiftBy,
}
return <td {...shiftedProps}> {formattedAndShiftedYear}</td>
}}
renderInput={({ value, ...rest }) => {
return (
<div>
<input
value={startYearToAcademicYear(value)}
placeholder="Select year"
style={{ maxWidth: 400 }}
{...rest}
/>
</div>
)
}}
closeOnSelect
value={formik.values[tagName]}
onChange={value => formik.setFieldValue(tagName, value?.format('YYYY'))}
/>
)}
</div>
</Form>
</>
)
const TagCell = ({ tagName, group, studyProgrammes }) => {
const [showEdit, toggleEdit] = useToggle()
const getText = () => {
@@ -131,17 +159,26 @@ const TagCell = ({ tagName, group, studyProgrammes }) => {
throw Error(`Wrong tagname: ${tagName}`)
}
}
return group.tags?.[tagName] && !showEdit ? (
<div style={{ ...cellWrapper, alignItems: 'baseline' }}>
<p style={{ ...cellContent, textAlign: 'center' }}>{getText()}</p>
<div style={{ ...cellContent, flexGrow: 0 }}>
<Button type="button" onClick={() => toggleEdit()}>
Edit {prettifyCamelCase(tagName)}
</Button>
</div>
</div>
) : (
<AssociateTagForm group={group} tagName={tagName} toggleEdit={toggleEdit} selectFieldItems={studyProgrammes} />
return (
<>
<EditTagModal
group={group}
tagName={tagName}
toggleEdit={toggleEdit}
selectFieldItems={studyProgrammes}
open={showEdit}
/>
{group.tags?.[tagName] ? (
<div style={{ ...cellWrapper, alignItems: 'baseline' }}>
<p style={{ ...cellContent, textAlign: 'center' }}>{getText()}</p>
<div style={{ ...cellContent, flexGrow: 0 }}>
<Button icon="pencil" onClick={() => toggleEdit()} size="tiny" />
</div>
</div>
) : (
<Button content={`Add ${prettifyCamelCase(tagName)}`} icon="add" onClick={() => toggleEdit()} size="tiny" />
)}
</>
)
}
@@ -203,7 +240,7 @@ const StudyGuidanceGroupOverview = ({ groups }) => {
lisäominaisuuksia.{' '}
</p>
</StyledMessage>
<SortableTable figure={false} columns={headers} getRowKey={group => group.id} data={groups} />
<SortableTable figure={false} columns={headers} getRowKey={group => group.id} data={groups} singleLine={false} />
</>
)
}
@@ -5,8 +5,7 @@ import '../studyprogramme.css'
const ReactHighcharts = require('react-highcharts')
require('highcharts-exporting')(ReactHighcharts.Highcharts)
// const colors = ['#003E65', '#1392c2', '#E68825', '#333737', '#036415']
const colors = ['#7cb5ec', '#434348', '#90ed7d', '#f7a35c', '#8085e9', '#2b908f', '#f45b5b', '#91e8e1']
const colors = ['#7cb5ec', '#90ed7d', '#434348', '#f7a35c', '#FFF000', '#2b908f', '#f45b5b', '#91e8e1']
const LineGraph = ({ cypress, data }) => {
const dataWithColors = data?.graphStats.map((series, index) => ({
@@ -1,9 +1,9 @@
/* eslint-disable react/no-this-in-sfc */
import React from 'react'
const ReactHighcharts = require('react-highcharts')
// const colors = ['#003E65', '#1392c2', '#036415', '#bdc2c7', '#E68825', '#333737', '#54997b', '#195f8a']
const colors = ['#7cb5ec', '#434348', '#90ed7d', '#f7a35c', '#8085e9', '#2b908f', '#f45b5b', '#91e8e1']
const colors = ['#7cb5ec', '#90ed7d', '#434348', '#f7a35c', '#FFF000', '#2b908f', '#f45b5b', '#91e8e1']
const StackedBarChart = ({ cypress, data, labels, wideTable }) => {
const dataWithColors = data?.map((series, index) => ({ ...series, color: colors[index] }))
@@ -18,6 +18,7 @@ const StackedBarChart = ({ cypress, data, labels, wideTable }) => {
},
xAxis: {
categories: labels,
allowDecimals: false,
},
chart: {
type: 'column',
@@ -40,6 +41,10 @@ const StackedBarChart = ({ cypress, data, labels, wideTable }) => {
stacking: 'normal',
dataLabels: {
enabled: true,
formatter() {
if (Number.isInteger(this.y)) return `${this.y}`
return `${this.y.toFixed(1)}`
},
},
},
},
@@ -20,6 +20,10 @@ const StudyProgrammeSelector = ({ studyprogrammes, selected }) => {
const { language } = useLanguage()
useEffect(() => {
if (studyprogrammes?.length > 0) studyprogrammes.sort((a, b) => (a.code > b.code ? 1 : -1))
}, [studyprogrammes])
useEffect(() => {
if (studyprogrammes) {
const filteredBachelorProgrammes = []
@@ -3,7 +3,7 @@ import NoDataMessage from '../NoDataMessage'
const ReactHighcharts = require('react-highcharts')
const colors = ['#333737', '#bdc2c7', '#79b0e8', '#1392c2', '#195f8a', '#54997b', '#036415']
const colors = ['#f57368', '#fb8c6e', '#fba678', '#dbda7d', '#9ec27c', '#60a866', '#008c59']
const BarChart = ({ cypress, data, track }) => {
if (!data || !data.creditGraphStats || !data.creditGraphStats[track])
@@ -59,7 +59,6 @@ const getColumns = (language, showStudents) => {
getRowVal: course => course.totalAllStudents,
getRowContent: course => course.totalAllStudents,
},
{
key: 'totalOwnProgramme',
title: 'Major students',
@@ -68,7 +67,6 @@ const getColumns = (language, showStudents) => {
getRowVal: course => course.totalProgrammeStudents,
getRowContent: course => course.totalProgrammeStudents,
},
{
key: 'totalOtherProgramme',
title: 'Non-major students',
@@ -77,7 +75,6 @@ const getColumns = (language, showStudents) => {
getRowVal: course => course.totalOtherProgrammeStudents,
getRowContent: course => course.totalOtherProgrammeStudents,
},
{
key: 'totalWithoutStudyright',
title: 'Non-degree students',
@@ -87,6 +84,15 @@ const getColumns = (language, showStudents) => {
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,
},
]
} else {
columns = [
@@ -160,6 +166,15 @@ const getColumns = (language, showStudents) => {
getRowVal: course => course.totalWithoutStudyrightCredits,
getRowContent: course => course.totalWithoutStudyrightCredits,
},
{
key: 'totalTransfer',
title: 'Transferred credits',
helpText: toolTips.TransferredCredits,
cellStyle: { textAlign: 'right' },
filterType: 'range',
getRowVal: course => course.totalTransferCredits,
getRowContent: course => course.totalTransferCredits,
},
]
}
@@ -63,6 +63,8 @@ const ProgrammeCoursesOverview = ({ studyProgramme, academicYear, setAcademicYea
acc.totalOtherProgrammeCredits += curr[1].totalOtherProgrammeCredits
acc.totalWithoutStudyrightStudents += curr[1].totalWithoutStudyrightStudents
acc.totalWithoutStudyrightCredits += curr[1].totalWithoutStudyrightCredits
acc.totalTransferCredits += curr[1].totalTransferCredits
acc.totalTransferStudents += curr[1].totalTransferStudents
}
return acc
@@ -76,6 +78,8 @@ const ProgrammeCoursesOverview = ({ studyProgramme, academicYear, setAcademicYea
totalOtherProgrammeCredits: 0,
totalWithoutStudyrightStudents: 0,
totalWithoutStudyrightCredits: 0,
totalTransferCredits: 0,
totalTransferStudents: 0,
}
)
return {
+32 -5
View File
@@ -6,15 +6,42 @@ const facultystatsApi = RTKApi.injectEndpoints({
query: () => '/faculties',
}),
getFacultyBasicStats: builder.query({
query: ({ id, yearType, specialGroups }) =>
`/faculties/${id}/basicstats?year_type=${yearType}&special_groups=${specialGroups}`,
query: ({ id, yearType, studyProgrammeFilter, specialGroups }) =>
`/faculties/${id}/basicstats?year_type=${yearType}&programme_filter=${studyProgrammeFilter}&special_groups=${specialGroups}`,
}),
getFacultyCreditStats: builder.query({
query: ({ id, yearType, specialGroups }) =>
`/faculties/${id}/creditstats?year_type=${yearType}&special_groups=${specialGroups}`,
query: ({ id, yearType, studyProgrammeFilter, specialGroups }) =>
`/faculties/${id}/creditstats?year_type=${yearType}&programme_filter=${studyProgrammeFilter}&special_groups=${specialGroups}`,
}),
getFacultyThesisStats: builder.query({
query: ({ id, yearType, studyProgrammeFilter, specialGroups }) =>
`/faculties/${id}/thesisstats?year_type=${yearType}&programme_filter=${studyProgrammeFilter}&special_groups=${specialGroups}`,
}),
getFacultyGraduationTimes: builder.query({
query: ({ id, studyProgrammeFilter }) =>
`/faculties/${id}/graduationtimes?programme_filter=${studyProgrammeFilter}`,
}),
getFacultyProgressStats: builder.query({
query: ({ id, studyProgrammeFilter }) =>
`/faculties/${id}/progressstats?programme_filter=${studyProgrammeFilter}`,
}),
// getFacultyStudentStats: builder.query({
// query: ({ id, studyProgrammeFilter }) =>
// `/faculties/${id}/studentstats?programme_filter=${studyProgrammeFilter}`,
// }),
updateFacultyBasicView: builder.query({
query: ({ id, statsType }) => `/faculties/${id}/update_basicview?stats_type=${statsType}`,
}),
}),
overrideExisting: false,
})
export const { useGetFacultiesQuery, useGetFacultyBasicStatsQuery, useGetFacultyCreditStatsQuery } = facultystatsApi
export const {
useGetFacultiesQuery,
useGetFacultyBasicStatsQuery,
useGetFacultyCreditStatsQuery,
useGetFacultyThesisStatsQuery,
useGetFacultyGraduationTimesQuery,
useGetFacultyProgressStatsQuery,
useUpdateFacultyBasicViewQuery,
} = facultystatsApi