Compare commits

...
214 Commits
Author SHA1 Message Date
Laura Sinikallio 1626f76e3a Open programme charts update on toggling 2022-09-20 18:28:10 +03:00
Laura Sinikallio d597b9e67f Show how many graduated on time for programmes 2022-09-20 17:58:51 +03:00
Laura Sinikallio e1dbcd0e7e Show how many graduated on time for faculty 2022-09-20 17:33:51 +03:00
outisa 70b33a3ae4 Merge branch 'master' of github.com:UniversityOfHelsinkiCS/oodikone 2022-09-20 16:26:56 +03:00
outisa 4834515b4c Fix studyprogramme update problem 2022-09-20 16:26:07 +03:00
Laura Sinikallio fa0bf87307 Graduation times: show programme names, improve tooltip 2022-09-20 16:22:11 +03:00
outisa 4b9fe9d6a0 Fix cypress test values 2022-09-20 13:59:36 +03:00
outisa a674c4257d Faculty/Study programme unify student progress views 2022-09-20 13:06:05 +03:00
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
Miko Keskimäki e8f3d9f601 Fix error with encryption when no students selected 2022-08-12 08:45:48 +03:00
outisa 6f3af6f135 Merge branch 'master' of github.com:UniversityOfHelsinkiCS/oodikone 2022-08-11 17:02:52 +03:00
outisa 3e08836d4e Faculty: Make study programme level colors to match faculty level ones 2022-08-11 17:00:21 +03:00
Miko Keskimäki efea7cdc9e Encrypt studentnumbers in population's course statistics if iam user 2022-08-11 16:04:27 +03:00
Miko Keskimäki c6232e617f Hide students tab of courses of population from iam users 2022-08-11 16:03:08 +03:00
outisa f360716b8e FacultyStats: Show difference to previous year 2022-08-11 15:02:45 +03:00
Laura Sinikallio 55bf6b0457 General refactoring 2022-08-11 14:29:58 +03:00
Miko Keskimäki 3067b37298 Merge branch 'master' of github.com:UniversityOfHelsinkiCS/oodikone 2022-08-11 13:38:03 +03:00
Miko Keskimäki a19a343feb Make courses of population visible to iam users 2022-08-11 13:36:45 +03:00
Miko Keskimäki edb5ca5422 Fix selectedStudentsByYear not being decrypted 2022-08-11 13:04:52 +03:00
Miko Keskimäki 2eb5f10eee Skip iam tests for now 2022-08-11 12:55:32 +03:00
Miko Keskimäki 7ce269d8be Decrypt student numbers 2022-08-11 12:30:58 +03:00
Miko Keskimäki 42554bba7a Send decryptable student selection to backend 2022-08-11 12:29:54 +03:00
Laura Sinikallio be77832f72 Edit column names 2022-08-11 11:58:19 +03:00
Miko Keskimäki f32d29ca34 Encrypt studentnumbers for iam user instead of hashing
Need to decrypt in backend to make courses of population tab work
2022-08-11 11:48:34 +03:00
Laura Sinikallio bb3652f9c4 Fix missing programme names 2022-08-11 10:51:17 +03:00
Laura Sinikallio a6272bff4f [Faculty] programme names for the rest of the tables 2022-08-10 15:27:20 +03:00
Miko Keskimäki 29f8cf110b Fix type error in IAMConfig 2022-08-10 12:06:57 +03:00
Laura Sinikallio 05ccbd6b16 Merge branch 'master' of github.com:UniversityOfHelsinkiCS/oodikone 2022-08-10 11:41:19 +03:00
outisa 608f2a921e [Faculty] sort programmes alphabetically by default 2022-08-10 10:53:28 +03:00
Miko Keskimäki 7bd40f7030 Align study track with correct programme in student statistics 2022-08-10 10:32:40 +03:00
Laura Sinikallio 0075a5f994 Merge branch 'master' of github.com:UniversityOfHelsinkiCS/oodikone 2022-08-10 09:41:16 +03:00
outisa 06414288f8 Merge branch 'master' of github.com:UniversityOfHelsinkiCS/oodikone 2022-08-10 09:29:02 +03:00
outisa ac9b32408c Disable eslint in tooltip formatter() for 'no-this' 2022-08-09 18:26:39 +03:00
Laura Sinikallio 4ad1e4237c Programme details for rest of current faculty tables 2022-08-09 16:37:14 +03:00
Miko Keskimäki 2f0ebd04d0 Remove transfered studytracks from studyright associations 2022-08-09 15:17:55 +03:00
Miko Keskimäki 40264f28d1 Filter transfered students' studytracks from population stats 2022-08-09 15:17:41 +03:00
outisa 95eaef7f8d Faculty: Change button to 'array' 2022-08-09 14:50:21 +03:00
Laura Sinikallio 7a1fa68c94 Provide programme names for faculty credit table 2022-08-09 13:01:56 +03:00
Laura Sinikallio 1d86557ed4 Merge branch 'master' of github.com:UniversityOfHelsinkiCS/oodikone 2022-08-09 09:07:53 +03:00
outisa d013809645 Facultystatistics: Add years to x-axis (linegraphs) 2022-08-08 17:25:34 +03:00
Laura Sinikallio 32b860bf0b [Faculty] programme-level numbers for graduations 2022-08-08 16:11:19 +03:00
Laura Sinikallio 02ea189fcb Fix table titles 2022-08-08 15:39:13 +03:00
outisa d38b647fc3 FacultyStatistics: Add new table views 2022-08-08 14:25:42 +03:00
Miko Keskimäki 0eb31cc8ea Reload to maybe make iam test user work 2022-08-08 13:52:29 +03:00
Miko Keskimäki 7aef916e70 Simple tests for iam user studyprogramme and population statistics access 2022-08-08 12:30:57 +03:00
Miko Keskimäki fc84ef78fd Create new test user with only IAM access 2022-08-08 12:28:11 +03:00
Laura Sinikallio 7c31425114 Faculty - separate graduations breakdown from students -table 2022-08-08 11:56:19 +03:00
Laura Sinikallio c88ad2201d Refactor faculty basics data structure 2022-08-08 11:07:38 +03:00
Miko Keskimäki e936510754 Update searching by course code #3457 2022-08-08 10:12:55 +03:00
outisa 825e549b49 Fix faculty stats cypress tests 2022-08-08 09:32:50 +03:00
outisa b8b0ffe34b Disable cypress tests for toggle credits for further investigation 2022-08-05 16:52:16 +03:00
outisa e2018d3e4c Modify faculty statistics cypress tests 2022-08-05 15:58:52 +03:00
outisa c3dbc9682e Add cypress tests for toggling CreditsCharts 2022-08-05 15:06:54 +03:00
Miko Keskimäki 54195f0a85 Add babel/core to dev dependencies 2022-08-05 12:50:30 +03:00
Miko Keskimäki 600795ca48 Fix more lint errors 2022-08-05 12:33:56 +03:00
Miko Keskimäki 3b2b04b10d Update eslint, eslintrc and fix errors 2022-08-05 12:08:29 +03:00
Miko Keskimäki 57ddbcaa36 Update stylelint config and fix errors 2022-08-05 12:00:52 +03:00
Miko Keskimäki 1185793e4d Update dev package major versions 2022-08-05 12:00:39 +03:00
outisa d8a5989ba1 FacultyStatistics: Make barchart look better 2022-08-05 10:54:30 +03:00
Miko Keskimäki 7a8ba85071 Run tests when actions updated 2022-08-05 09:32:12 +03:00
Miko Keskimäki 378b886947 Use newer Cypress action 2022-08-05 09:22:43 +03:00
Miko Keskimäki 167fa3b979 Update Cypress paths in test workflow and readme 2022-08-05 08:53:28 +03:00
Miko Keskimäki 7ddcf94b57 Update Cypress and migrate tests and config to v10 2022-08-05 08:48:52 +03:00
outisa f71c406180 InteractiveTAble: Fix key props 2022-08-04 17:44:47 +03:00
outisa c1a94acb92 FacultyStatistics: Add toggleTableView 2022-08-04 17:11:42 +03:00
Miko Keskimäki 1264d4f3b9 Use run-tests action for all Cypress tests 2022-08-04 15:19:43 +03:00
Miko Keskimäki 3ab87a18e3 Test composite action with failing test 2022-08-04 15:07:58 +03:00
Miko Keskimäki 2a68a9cc9b Pass registry username and password to action 2022-08-04 14:49:31 +03:00
Miko Keskimäki 15f86a85d4 Test composite action for custom population tests 2022-08-04 14:37:53 +03:00
Miko Keskimäki 16952cece6 Try moving repeatitive Cypress test job config into composite action 2022-08-04 14:35:24 +03:00
Miko Keskimäki 4c71092e7f Use actionlint npm package instead of broken docker version 2022-08-04 13:43:59 +03:00
Miko Keskimäki d2558a40d8 [skip ci] Remove test comment 2022-08-04 13:10:40 +03:00
Miko Keskimäki c918c5ffc9 Test that workflow activates 2022-08-04 13:02:49 +03:00
Miko Keskimäki 54ecddc46e Run test-oodikone.yaml when sis-db migrations updated #3729 2022-08-04 13:00:01 +03:00
Miko Keskimäki f2e655bb05 Update remaining actions 2022-08-04 12:36:48 +03:00
Miko Keskimäki 5ee3e7310e Update dependabot.yml 2022-08-04 12:30:33 +03:00
Miko Keskimäki 8bdb30bca5 Fix wrong login-action version 2022-08-04 10:54:45 +03:00
Miko Keskimäki bc06295f97 Update github actions versions
See if anything breaks
2022-08-04 10:49:51 +03:00
outisa 522e9f851e Merge branch 'master' of github.com:UniversityOfHelsinkiCS/oodikone 2022-08-04 10:27:45 +03:00
outisa 28e798ff28 FacultyStatistics: Add interactive data view 2022-08-04 10:26:51 +03:00
Miko Keskimäki ca4576e155 Allow usage without oodikone iams if user has iam rights to any programme 2022-08-03 13:58:36 +03:00
Laura Sinikallio 90309143fd Start of programme-level breakdown of faculty stats 2022-08-01 12:42:26 +03:00
Miko KeskimäkiandGitHub 8f4fcb4364 Merge pull request #3632 from UniversityOfHelsinkiCS/dependabot/npm_and_yarn/services/frontend/async-2.6.4
Bump async from 2.6.3 to 2.6.4 in /services/frontend
2022-08-01 10:55:07 +03:00
Miko KeskimäkiandGitHub facd2c15b9 Merge pull request #3538 from UniversityOfHelsinkiCS/dependabot/npm_and_yarn/services/frontend/minimist-1.2.6
Bump minimist from 1.2.5 to 1.2.6 in /services/frontend
2022-08-01 10:54:56 +03:00
Miko KeskimäkiandGitHub 756cadd0e6 Merge pull request #3539 from UniversityOfHelsinkiCS/dependabot/npm_and_yarn/updater/sis-updater-worker/minimist-1.2.6
Bump minimist from 1.2.5 to 1.2.6 in /updater/sis-updater-worker
2022-08-01 10:54:44 +03:00
Miko KeskimäkiandGitHub 842a561f94 Merge pull request #3540 from UniversityOfHelsinkiCS/dependabot/npm_and_yarn/minimist-1.2.6
Bump minimist from 1.2.5 to 1.2.6
2022-08-01 10:54:26 +03:00
Miko KeskimäkiandGitHub 9346b1e245 Merge pull request #3775 from UniversityOfHelsinkiCS/dependabot/npm_and_yarn/moment-2.29.4
Bump moment from 2.29.1 to 2.29.4
2022-08-01 10:54:09 +03:00
Miko KeskimäkiandGitHub b93b430554 Merge pull request #3777 from UniversityOfHelsinkiCS/dependabot/npm_and_yarn/updater/sis-updater-worker/moment-2.29.4
Bump moment from 2.29.1 to 2.29.4 in /updater/sis-updater-worker
2022-08-01 10:53:54 +03:00
Miko KeskimäkiandGitHub a90dd779d5 Merge pull request #3778 from UniversityOfHelsinkiCS/dependabot/npm_and_yarn/updater/sis-updater-scheduler/moment-2.29.4
Bump moment from 2.29.3 to 2.29.4 in /updater/sis-updater-scheduler
2022-08-01 10:53:39 +03:00
Miko KeskimäkiandGitHub dafec6bd6b Merge pull request #3779 from UniversityOfHelsinkiCS/dependabot/npm_and_yarn/services/frontend/moment-2.29.4
Bump moment from 2.29.1 to 2.29.4 in /services/frontend
2022-08-01 10:53:17 +03:00
Miko Keskimäki b27e23e7fb [skip ci] Ignore major semver updates with Dependabot
Too many ignored PRs
2022-08-01 10:16:51 +03:00
Miko Keskimäki 6fff1f15fb Expand Cypress viewport 2022-08-01 09:46:50 +03:00
Miko Keskimäki 85bfc6041e Fix breaking test 2022-08-01 09:46:22 +03:00
Miko Keskimäki eb1a92e96a Update nonbreaking package versions 2022-08-01 09:18:03 +03:00
Miko Keskimäki 8c7a94b476 Npm audit fix 2022-08-01 09:10:56 +03:00
Miko Keskimäki f683d33769 Update all package-locks to lockfile v2 2022-08-01 09:10:53 +03:00
Miko Keskimäki 0c84cbf08f Show search by class to iam users 2022-07-29 13:11:39 +03:00
Miko Keskimäki 358f629285 Hide tabs without data from iam users 2022-07-29 13:10:53 +03:00
Miko Keskimäki af77bc500e Return obfuscated population statistics for iam rights 2022-07-29 13:03:52 +03:00
dependabot[bot]andGitHub 7608f62869 Bump moment from 2.29.1 to 2.29.4 in /services/frontend
Bumps [moment](https://github.com/moment/moment) from 2.29.1 to 2.29.4.
- [Release notes](https://github.com/moment/moment/releases)
- [Changelog](https://github.com/moment/moment/blob/develop/CHANGELOG.md)
- [Commits](https://github.com/moment/moment/compare/2.29.1...2.29.4)

---
updated-dependencies:
- dependency-name: moment
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-07-07 07:41:06 +00:00
dependabot[bot]andGitHub 03c3b99f3d Bump moment from 2.29.3 to 2.29.4 in /updater/sis-updater-scheduler
Bumps [moment](https://github.com/moment/moment) from 2.29.3 to 2.29.4.
- [Release notes](https://github.com/moment/moment/releases)
- [Changelog](https://github.com/moment/moment/blob/develop/CHANGELOG.md)
- [Commits](https://github.com/moment/moment/compare/2.29.3...2.29.4)

---
updated-dependencies:
- dependency-name: moment
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-07-07 07:40:51 +00:00
dependabot[bot]andGitHub af08a0b681 Bump moment from 2.29.1 to 2.29.4 in /updater/sis-updater-worker
Bumps [moment](https://github.com/moment/moment) from 2.29.1 to 2.29.4.
- [Release notes](https://github.com/moment/moment/releases)
- [Changelog](https://github.com/moment/moment/blob/develop/CHANGELOG.md)
- [Commits](https://github.com/moment/moment/compare/2.29.1...2.29.4)

---
updated-dependencies:
- dependency-name: moment
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-07-07 07:40:43 +00:00
dependabot[bot]andGitHub 44ee47c34d Bump moment from 2.29.1 to 2.29.4
Bumps [moment](https://github.com/moment/moment) from 2.29.1 to 2.29.4.
- [Release notes](https://github.com/moment/moment/releases)
- [Changelog](https://github.com/moment/moment/blob/develop/CHANGELOG.md)
- [Commits](https://github.com/moment/moment/compare/2.29.1...2.29.4)

---
updated-dependencies:
- dependency-name: moment
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-07-07 04:29:55 +00:00
dependabot[bot]andGitHub f79aafa6cf Bump minimist from 1.2.5 to 1.2.6 in /services/frontend
Bumps [minimist](https://github.com/substack/minimist) from 1.2.5 to 1.2.6.
- [Release notes](https://github.com/substack/minimist/releases)
- [Commits](https://github.com/substack/minimist/compare/1.2.5...1.2.6)

---
updated-dependencies:
- dependency-name: minimist
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-04-25 11:24:38 +00:00
dependabot[bot]andGitHub 0b5b4b5ebd Bump async from 2.6.3 to 2.6.4 in /services/frontend
Bumps [async](https://github.com/caolan/async) from 2.6.3 to 2.6.4.
- [Release notes](https://github.com/caolan/async/releases)
- [Changelog](https://github.com/caolan/async/blob/v2.6.4/CHANGELOG.md)
- [Commits](https://github.com/caolan/async/compare/v2.6.3...v2.6.4)

---
updated-dependencies:
- dependency-name: async
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-04-25 11:24:29 +00:00
dependabot[bot]andGitHub e61a4cf09c Bump minimist from 1.2.5 to 1.2.6
Bumps [minimist](https://github.com/substack/minimist) from 1.2.5 to 1.2.6.
- [Release notes](https://github.com/substack/minimist/releases)
- [Commits](https://github.com/substack/minimist/compare/1.2.5...1.2.6)

---
updated-dependencies:
- dependency-name: minimist
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-04-25 11:23:36 +00:00
dependabot[bot]andGitHub 11d58a9fc4 Bump minimist from 1.2.5 to 1.2.6 in /updater/sis-updater-worker
Bumps [minimist](https://github.com/substack/minimist) from 1.2.5 to 1.2.6.
- [Release notes](https://github.com/substack/minimist/releases)
- [Commits](https://github.com/substack/minimist/compare/1.2.5...1.2.6)

---
updated-dependencies:
- dependency-name: minimist
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-04-25 11:23:22 +00:00
126 changed files with 56540 additions and 10578 deletions
+2 -1
View File
@@ -14,6 +14,7 @@
"no-console": "error",
"react/no-unescaped-entities": "off",
"no-async-promise-executor": "off",
"prettier/prettier": "error"
"prettier/prettier": "error",
"cypress/no-assigning-return-values": "off"
}
}
+52
View File
@@ -0,0 +1,52 @@
name: 'Run tests'
description: 'Runs Cypress tests for individual spec'
inputs:
username:
required: true
description: 'Toska registry username'
password:
required: true
description: 'Toska registry password'
spec:
required: true
description: 'Path to Cypress file'
runs:
using: composite
steps:
- name: 'Download images'
uses: actions/download-artifact@v3
with:
path: /tmp/
- name: 'Load built images'
shell: bash
run: |
docker load --input /tmp/artifact/backend.tar
docker load --input /tmp/artifact/frontend.tar
- name: 'Login to toska docker registry'
uses: docker/login-action@v2
with:
registry: registry.toska.cs.helsinki.fi
username: ${{ inputs.username }}
password: ${{ inputs.password }}
- name: 'Run services, pull rest of the needed images'
shell: bash
run: docker-compose --file docker-compose.ci.yml up --detach --no-recreate
- name: 'Run cypress'
uses: cypress-io/github-action@v4
with:
browser: chrome
spec: ${{ inputs.spec }}
env:
CYPRESS_baseUrl: http://localhost:1337
- name: 'Upload video if tests fail'
uses: actions/upload-artifact@v3
if: failure()
with:
name: Cypress videos
path: ./cypress/videos/*
- name: 'Print logs on failure'
shell: bash
if: failure()
run: docker-compose --file docker-compose.ci.yml logs
+21
View File
@@ -8,6 +8,10 @@ updates:
labels:
- 'dependencies'
- 'infra'
ignore:
- dependency-name: '*'
update-types: ['version-update:semver-major']
- package-ecosystem: npm
directory: '/updater/sis-updater-scheduler'
schedule:
@@ -16,6 +20,10 @@ updates:
labels:
- 'dependencies'
- 'updater'
ignore:
- dependency-name: '*'
update-types: ['version-update:semver-major']
- package-ecosystem: npm
directory: '/updater/sis-updater-worker'
schedule:
@@ -24,6 +32,10 @@ updates:
labels:
- 'dependencies'
- 'updater'
ignore:
- dependency-name: '*'
update-types: ['version-update:semver-major']
- package-ecosystem: npm
directory: '/services/backend'
schedule:
@@ -32,6 +44,10 @@ updates:
labels:
- 'dependencies'
- 'back'
ignore:
- dependency-name: '*'
update-types: ['version-update:semver-major']
- package-ecosystem: npm
directory: '/services/frontend'
schedule:
@@ -40,8 +56,13 @@ updates:
labels:
- 'dependencies'
- 'front'
ignore:
- dependency-name: '*'
update-types: ['version-update:semver-major']
- package-ecosystem: 'github-actions'
directory: '/'
schedule:
interval: weekly
time: '03:00'
open-pull-requests-limit: 10
+8 -8
View File
@@ -9,10 +9,10 @@ jobs:
name: 'Build and release backend'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: docker/setup-buildx-action@v1
- uses: actions/checkout@v3
- uses: docker/setup-buildx-action@v2
- name: 'Login to DockerHub'
uses: docker/login-action@v1
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
@@ -23,7 +23,7 @@ jobs:
id: extract_version
run: echo ::set-output name=VERSION::"${GITHUB_REF#refs/tags/}"
- name: 'Build and push'
uses: docker/build-push-action@v2
uses: docker/build-push-action@v3
with:
build-args: |
SENTRY_RELEASE=${{ steps.extract_version.outputs.VERSION }}
@@ -41,10 +41,10 @@ jobs:
name: 'Build and release frontend'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: docker/setup-buildx-action@v1
- uses: actions/checkout@v3
- uses: docker/setup-buildx-action@v2
- name: 'Login to DockerHub'
uses: docker/login-action@v1
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
@@ -58,7 +58,7 @@ jobs:
id: extract_version
run: echo ::set-output name=VERSION::"${GITHUB_REF#refs/tags/}"
- name: 'Build and push'
uses: docker/build-push-action@v2
uses: docker/build-push-action@v3
with:
build-args: |
SENTRY_RELEASE=${{ steps.extract_version.outputs.VERSION }}
+16 -16
View File
@@ -14,10 +14,10 @@ jobs:
name: 'Build sis-updater-worker'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: docker/setup-buildx-action@v1
- uses: actions/checkout@v3
- uses: docker/setup-buildx-action@v2
- name: 'Build the image'
uses: docker/build-push-action@v2
uses: docker/build-push-action@v3
with:
build-args: |
SENTRY_RELEASE=${{ github.sha }}
@@ -30,7 +30,7 @@ jobs:
cache-from: type=registry,ref=toska/sis-updater-worker:latest
cache-to: type=inline
- name: 'Store built image'
uses: actions/upload-artifact@v2
uses: actions/upload-artifact@v3
with:
path: /tmp/worker.tar
@@ -38,10 +38,10 @@ jobs:
name: 'Build sis-updater-scheduler'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: docker/setup-buildx-action@v1
- uses: actions/checkout@v3
- uses: docker/setup-buildx-action@v2
- name: 'Build the image'
uses: docker/build-push-action@v2
uses: docker/build-push-action@v3
with:
build-args: |
SENTRY_RELEASE=${{ github.sha }}
@@ -54,7 +54,7 @@ jobs:
cache-from: type=registry,ref=toska/sis-updater-scheduler:latest
cache-to: type=inline
- name: 'Store built image'
uses: actions/upload-artifact@v2
uses: actions/upload-artifact@v3
with:
path: /tmp/scheduler.tar
@@ -65,15 +65,15 @@ jobs:
- build_worker
- build_scheduler
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- name: 'Login to toska docker registry'
uses: docker/login-action@v1
uses: docker/login-action@v2
with:
registry: registry.toska.cs.helsinki.fi
username: ${{ secrets.TOSKAREGISTRY_USERNAME }}
password: ${{ secrets.TOSKAREGISTRY_PASSWORD }}
- name: 'Download built images'
uses: actions/download-artifact@v2
uses: actions/download-artifact@v3
with:
path: /tmp/
- name: 'Load worker image'
@@ -114,15 +114,15 @@ jobs:
needs:
- test_updater
steps:
- uses: actions/checkout@v2
- uses: docker/setup-buildx-action@v1
- uses: actions/checkout@v3
- uses: docker/setup-buildx-action@v2
- name: 'Login to DockerHub'
uses: docker/login-action@v1
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: 'Build and push scheduler'
uses: docker/build-push-action@v2
uses: docker/build-push-action@v3
with:
cache-from: type=registry,ref=toska/sis-updater-scheduler:latest
cache-to: type=inline
@@ -131,7 +131,7 @@ jobs:
tags: toska/sis-updater-scheduler:latest
target: production
- name: 'Build and push worker'
uses: docker/build-push-action@v2
uses: docker/build-push-action@v3
with:
cache-from: type=registry,ref=toska/sis-updater-worker:latest
cache-to: type=inline
+63 -269
View File
@@ -6,14 +6,16 @@ on:
- master
paths:
- '.github/workflows/test-oodikone.yaml'
- '.github/actions/**'
- 'cypress/**'
- 'cypress.json'
- 'cypress.config.json'
- 'services/**'
- '!services/backend/src/rapodiff.js'
- 'docker-compose.ci.yml'
- 'docker-compose.test.yml'
- 'nginx.conf'
- 'package*.json'
- 'updater/sis-updater-worker/src/db/migrations/**'
jobs:
# === Static tests / linting ===
@@ -22,8 +24,8 @@ jobs:
name: 'Lint backend'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
- uses: actions/checkout@v3
- uses: actions/setup-node@v3.4.1
with:
node-version: 14
- name: 'Install dependencies'
@@ -35,8 +37,8 @@ jobs:
name: 'Lint frontend'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
- uses: actions/checkout@v3
- uses: actions/setup-node@v3.4.1
with:
node-version: 14
- name: 'Install dependencies'
@@ -51,10 +53,10 @@ jobs:
name: 'Build backend'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: docker/setup-buildx-action@v1
- uses: actions/checkout@v3
- uses: docker/setup-buildx-action@v2
- name: 'Build image and export it'
uses: docker/build-push-action@v2
uses: docker/build-push-action@v3
with:
build-args: |
SENTRY_RELEASE=${{ github.sha }}
@@ -69,7 +71,7 @@ jobs:
target: production
outputs: type=docker,dest=/tmp/backend.tar
- name: 'Upload image for other jobs'
uses: actions/upload-artifact@v2
uses: actions/upload-artifact@v3
with:
path: /tmp/backend.tar
@@ -77,13 +79,13 @@ jobs:
name: 'Build frontend'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: docker/setup-buildx-action@v1
- uses: actions/checkout@v3
- uses: docker/setup-buildx-action@v2
- name: 'Get current time'
id: get_current_time
run: echo ::set-output name=NOW::"$(date --iso-8601=seconds)"
- name: 'Build image and export it'
uses: docker/build-push-action@v2
uses: docker/build-push-action@v3
with:
build-args: |
SENTRY_RELEASE=${{ github.sha }}
@@ -99,7 +101,7 @@ jobs:
target: production
outputs: type=docker,dest=/tmp/frontend.tar
- name: 'Upload image for other jobs'
uses: actions/upload-artifact@v2
uses: actions/upload-artifact@v3
with:
path: /tmp/frontend.tar
@@ -107,7 +109,7 @@ jobs:
name: 'Build anon sis-db and push to toska hub if new migrations have been added'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- uses: dorny/paths-filter@v2
id: check_migrations
with:
@@ -116,7 +118,7 @@ jobs:
- 'updater/sis-updater-worker/src/db/migrations/**'
- '.github/workflows/test-oodikone.yaml'
- name: 'Login to toska docker registry'
uses: docker/login-action@v1
uses: docker/login-action@v2
with:
registry: registry.toska.cs.helsinki.fi
username: ${{ secrets.TOSKAREGISTRY_USERNAME }}
@@ -134,7 +136,7 @@ jobs:
- build_backend
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- uses: dorny/paths-filter@v2
id: check_migrations
with:
@@ -143,13 +145,13 @@ jobs:
- 'services/backend/src/database/migrations_kone/**'
- '.github/workflows/test-oodikone.yaml'
- name: 'Download images'
uses: actions/download-artifact@v2
uses: actions/download-artifact@v3
with:
path: /tmp/
- name: 'Load built backend image'
run: docker load --input /tmp/artifact/backend.tar
- name: 'Login to toska docker registry'
uses: docker/login-action@v1
uses: docker/login-action@v2
with:
registry: registry.toska.cs.helsinki.fi
username: ${{ secrets.TOSKAREGISTRY_USERNAME }}
@@ -167,7 +169,7 @@ jobs:
- build_backend
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- uses: dorny/paths-filter@v2
id: check_migrations
with:
@@ -176,13 +178,13 @@ jobs:
- 'services/backend/src/database/migrations_user/**'
- '.github/workflows/test-oodikone.yaml'
- name: 'Download images'
uses: actions/download-artifact@v2
uses: actions/download-artifact@v3
with:
path: /tmp/
- name: 'Load built backend image'
run: docker load --input /tmp/artifact/backend.tar
- name: 'Login to toska docker registry'
uses: docker/login-action@v1
uses: docker/login-action@v2
with:
registry: registry.toska.cs.helsinki.fi
username: ${{ secrets.TOSKAREGISTRY_USERNAME }}
@@ -205,39 +207,13 @@ jobs:
- build_and_push_anon_user_db
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: 'Download images'
uses: actions/download-artifact@v2
- uses: actions/checkout@v3
- name: 'Run tests'
uses: ./.github/actions/run-tests
with:
path: /tmp/
- name: 'Load built images'
run: |
docker load --input /tmp/artifact/backend.tar
docker load --input /tmp/artifact/frontend.tar
- name: 'Login to toska docker registry'
uses: docker/login-action@v1
with:
registry: registry.toska.cs.helsinki.fi
username: ${{ secrets.TOSKAREGISTRY_USERNAME }}
password: ${{ secrets.TOSKAREGISTRY_PASSWORD }}
- name: 'Run services, pull rest of the needed images'
run: docker-compose --file docker-compose.ci.yml up --detach --no-recreate
- name: 'Run cypress'
uses: cypress-io/github-action@v2
with:
browser: chrome
spec: cypress/integration/Course_statistics.js
env:
CYPRESS_baseUrl: http://localhost:1337
- name: 'Upload video if tests fail'
uses: actions/upload-artifact@v2
if: failure()
with:
name: Cypress videos
path: ./cypress/videos/*
- name: 'Print logs on failure'
if: failure()
run: docker-compose --file docker-compose.ci.yml logs
spec: cypress/e2e/Course_statistics.js
test_custom_population:
name: 'Test custom population'
@@ -249,39 +225,13 @@ jobs:
- build_and_push_anon_user_db
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: 'Download images'
uses: actions/download-artifact@v2
- uses: actions/checkout@v3
- name: 'Run tests'
uses: ./.github/actions/run-tests
with:
path: /tmp/
- name: 'Load built images'
run: |
docker load --input /tmp/artifact/backend.tar
docker load --input /tmp/artifact/frontend.tar
- name: 'Login to toska docker registry'
uses: docker/login-action@v1
with:
registry: registry.toska.cs.helsinki.fi
username: ${{ secrets.TOSKAREGISTRY_USERNAME }}
password: ${{ secrets.TOSKAREGISTRY_PASSWORD }}
- name: 'Run services, pull rest of the needed images'
run: docker-compose --file docker-compose.ci.yml up --detach --no-recreate
- name: 'Run cypress'
uses: cypress-io/github-action@v2
with:
browser: chrome
spec: cypress/integration/Custom_population.js
env:
CYPRESS_baseUrl: http://localhost:1337
- name: 'Upload video if tests fail'
uses: actions/upload-artifact@v2
if: failure()
with:
name: Cypress videos
path: ./cypress/videos/*
- name: 'Print logs on failure'
if: failure()
run: docker-compose --file docker-compose.ci.yml logs
spec: cypress/e2e/Custom_population.js
test_population_statistics:
name: 'Test population statistics'
@@ -293,39 +243,13 @@ jobs:
- build_and_push_anon_user_db
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: 'Download images'
uses: actions/download-artifact@v2
- uses: actions/checkout@v3
- name: 'Run tests'
uses: ./.github/actions/run-tests
with:
path: /tmp/
- name: 'Load built images'
run: |
docker load --input /tmp/artifact/backend.tar
docker load --input /tmp/artifact/frontend.tar
- name: 'Login to toska docker registry'
uses: docker/login-action@v1
with:
registry: registry.toska.cs.helsinki.fi
username: ${{ secrets.TOSKAREGISTRY_USERNAME }}
password: ${{ secrets.TOSKAREGISTRY_PASSWORD }}
- name: 'Run services, pull rest of the needed images'
run: docker-compose --file docker-compose.ci.yml up --detach --no-recreate
- name: 'Run cypress'
uses: cypress-io/github-action@v2
with:
browser: chrome
spec: cypress/integration/Population_statistics.js
env:
CYPRESS_baseUrl: http://localhost:1337
- name: 'Upload video if tests fail'
uses: actions/upload-artifact@v2
if: failure()
with:
name: Cypress videos
path: ./cypress/videos/*
- name: 'Print logs on failure'
if: failure()
run: docker-compose --file docker-compose.ci.yml logs
spec: cypress/e2e/Population_statistics.js
test_filters:
name: 'Test filters'
@@ -337,39 +261,13 @@ jobs:
- build_and_push_anon_user_db
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: 'Download images'
uses: actions/download-artifact@v2
- uses: actions/checkout@v3
- name: 'Run tests'
uses: ./.github/actions/run-tests
with:
path: /tmp/
- name: 'Load built images'
run: |
docker load --input /tmp/artifact/backend.tar
docker load --input /tmp/artifact/frontend.tar
- name: 'Login to toska docker registry'
uses: docker/login-action@v1
with:
registry: registry.toska.cs.helsinki.fi
username: ${{ secrets.TOSKAREGISTRY_USERNAME }}
password: ${{ secrets.TOSKAREGISTRY_PASSWORD }}
- name: 'Run services, pull rest of the needed images'
run: docker-compose --file docker-compose.ci.yml up --detach --no-recreate
- name: 'Run cypress'
uses: cypress-io/github-action@v2
with:
browser: chrome
spec: cypress/integration/Filters.js
env:
CYPRESS_baseUrl: http://localhost:1337
- name: 'Upload video if tests fail'
uses: actions/upload-artifact@v2
if: failure()
with:
name: Cypress videos
path: ./cypress/videos/*
- name: 'Print logs on failure'
if: failure()
run: docker-compose --file docker-compose.ci.yml logs
spec: cypress/e2e/Filters.js
test_student_statistics:
name: 'Test student statistics'
@@ -381,39 +279,13 @@ jobs:
- build_and_push_anon_user_db
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: 'Download images'
uses: actions/download-artifact@v2
- uses: actions/checkout@v3
- name: 'Run tests'
uses: ./.github/actions/run-tests
with:
path: /tmp/
- name: 'Load built images'
run: |
docker load --input /tmp/artifact/backend.tar
docker load --input /tmp/artifact/frontend.tar
- name: 'Login to toska docker registry'
uses: docker/login-action@v1
with:
registry: registry.toska.cs.helsinki.fi
username: ${{ secrets.TOSKAREGISTRY_USERNAME }}
password: ${{ secrets.TOSKAREGISTRY_PASSWORD }}
- name: 'Run services, pull rest of the needed images'
run: docker-compose --file docker-compose.ci.yml up --detach --no-recreate
- name: 'Run cypress'
uses: cypress-io/github-action@v2
with:
browser: chrome
spec: cypress/integration/Student_statistics.js
env:
CYPRESS_baseUrl: http://localhost:1337
- name: 'Upload video if tests fail'
uses: actions/upload-artifact@v2
if: failure()
with:
name: Cypress videos
path: ./cypress/videos/*
- name: 'Print logs on failure'
if: failure()
run: docker-compose --file docker-compose.ci.yml logs
spec: cypress/e2e/Student_statistics.js
test_studyprogramme_overview:
name: 'Test studyprogramme overview'
@@ -425,39 +297,13 @@ jobs:
- build_and_push_anon_user_db
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: 'Download images'
uses: actions/download-artifact@v2
- uses: actions/checkout@v3
- name: 'Run tests'
uses: ./.github/actions/run-tests
with:
path: /tmp/
- name: 'Load built images'
run: |
docker load --input /tmp/artifact/backend.tar
docker load --input /tmp/artifact/frontend.tar
- name: 'Login to toska docker registry'
uses: docker/login-action@v1
with:
registry: registry.toska.cs.helsinki.fi
username: ${{ secrets.TOSKAREGISTRY_USERNAME }}
password: ${{ secrets.TOSKAREGISTRY_PASSWORD }}
- name: 'Run services, pull rest of the needed images'
run: docker-compose --file docker-compose.ci.yml up --detach --no-recreate
- name: 'Run cypress'
uses: cypress-io/github-action@v2
with:
browser: chrome
spec: cypress/integration/Studyprogramme_overview.js
env:
CYPRESS_baseUrl: http://localhost:1337
- name: 'Upload video if tests fail'
uses: actions/upload-artifact@v2
if: failure()
with:
name: Cypress videos
path: ./cypress/videos/*
- name: 'Print logs on failure'
if: failure()
run: docker-compose --file docker-compose.ci.yml logs
spec: cypress/e2e/Studyprogramme_overview.js
test_teachers_and_users:
name: 'Test teachers and users'
@@ -469,41 +315,15 @@ jobs:
- build_and_push_anon_user_db
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: 'Download images'
uses: actions/download-artifact@v2
- uses: actions/checkout@v3
- name: 'Run tests'
uses: ./.github/actions/run-tests
with:
path: /tmp/
- name: 'Load built images'
run: |
docker load --input /tmp/artifact/backend.tar
docker load --input /tmp/artifact/frontend.tar
- name: 'Login to toska docker registry'
uses: docker/login-action@v1
with:
registry: registry.toska.cs.helsinki.fi
username: ${{ secrets.TOSKAREGISTRY_USERNAME }}
password: ${{ secrets.TOSKAREGISTRY_PASSWORD }}
- name: 'Run services, pull rest of the needed images'
run: docker-compose --file docker-compose.ci.yml up --detach --no-recreate
- name: 'Run cypress'
uses: cypress-io/github-action@v2
with:
browser: chrome
spec: |
cypress/integration/Teachers.js
cypress/integration/Users.js
env:
CYPRESS_baseUrl: http://localhost:1337
- name: 'Upload video if tests fail'
uses: actions/upload-artifact@v2
if: failure()
with:
name: Cypress videos
path: ./cypress/videos/*
- name: 'Print logs on failure'
if: failure()
run: docker-compose --file docker-compose.ci.yml logs
cypress/e2e/Teachers.js
cypress/e2e/Users.js
test_faculty_statistics:
name: 'Test faculty statistics'
@@ -515,39 +335,13 @@ jobs:
- build_and_push_anon_user_db
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: 'Download images'
uses: actions/download-artifact@v2
- uses: actions/checkout@v3
- name: 'Run tests'
uses: ./.github/actions/run-tests
with:
path: /tmp/
- name: 'Load built images'
run: |
docker load --input /tmp/artifact/backend.tar
docker load --input /tmp/artifact/frontend.tar
- name: 'Login to toska docker registry'
uses: docker/login-action@v1
with:
registry: registry.toska.cs.helsinki.fi
username: ${{ secrets.TOSKAREGISTRY_USERNAME }}
password: ${{ secrets.TOSKAREGISTRY_PASSWORD }}
- name: 'Run services, pull rest of the needed images'
run: docker-compose --file docker-compose.ci.yml up --detach --no-recreate
- name: 'Run cypress'
uses: cypress-io/github-action@v2
with:
browser: chrome
spec: cypress/integration/Faculty_statistics.js
env:
CYPRESS_baseUrl: http://localhost:1337
- name: 'Upload video if tests fail'
uses: actions/upload-artifact@v2
if: failure()
with:
name: Cypress videos
path: ./cypress/videos/*
- name: 'Print logs on failure'
if: failure()
run: docker-compose --file docker-compose.ci.yml logs
spec: cypress/e2e/Faculty_statistics.js
# === Other tests ===
@@ -561,9 +355,9 @@ jobs:
- build_and_push_anon_user_db
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- name: 'Download images'
uses: actions/download-artifact@v2
uses: actions/download-artifact@v3
with:
path: /tmp/
- name: 'Load built images'
@@ -571,7 +365,7 @@ jobs:
docker load --input /tmp/artifact/backend.tar
docker load --input /tmp/artifact/frontend.tar
- name: 'Login to toska docker registry'
uses: docker/login-action@v1
uses: docker/login-action@v2
with:
registry: registry.toska.cs.helsinki.fi
username: ${{ secrets.TOSKAREGISTRY_USERNAME }}
@@ -607,9 +401,9 @@ jobs:
- test_faculty_statistics
- test_service_exit_statuses
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- name: 'Download images'
uses: actions/download-artifact@v2
uses: actions/download-artifact@v3
with:
path: /tmp/
- name: 'Load built images'
@@ -617,7 +411,7 @@ jobs:
docker load --input /tmp/artifact/backend.tar
docker load --input /tmp/artifact/frontend.tar
- name: 'Login to DockerHub'
uses: docker/login-action@v1
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
@@ -675,7 +469,7 @@ jobs:
- test_service_exit_statuses
if: failure()
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- name: 'Send notification to slack'
uses: UniversityOfHelsinkiCS/slackbot-action@v1.4.2
with:
+1 -1
View File
@@ -9,7 +9,7 @@ module.exports = {
'*.css': files => `stylelint --fix ${files.join(' ')}`,
Dockerfile: files => `${dockerCmdBase} hadolint/hadolint:v2.7.0 hadolint ${relativeFilePaths(files)}`,
'*.sh': files => `${dockerCmdBase} koalaman/shellcheck:v0.7.2 ${relativeFilePaths(files)} -x`,
'.github/workflows/*': files => `${dockerCmdBase} rhysd/actionlint:1.6.2 ${relativeFilePaths(files)}`,
'.github/{workflows,actions}/*': files => `npm run actionlint ${relativeFilePaths(files)}`,
'docker-compose*': files => {
const composeFiles = file => {
if (['docker-compose.ci.yml', 'docker-compose.test.yml'].some(f => file.includes(f))) {
+1 -1
View File
@@ -5,5 +5,5 @@
"printWidth": 120,
"tabWidth": 2,
"useTabs": false,
"jsxSingleQuote": false,
"jsxSingleQuote": false
}
+5 -1
View File
@@ -13,6 +13,10 @@
"ignorePseudoClasses": ["global"]
}
],
"prettier/prettier": true
"prettier/prettier": true,
"selector-class-pattern": null,
"custom-property-pattern": null,
"value-no-vendor-prefix": null,
"property-no-vendor-prefix": null
}
}
+1 -1
View File
@@ -108,7 +108,7 @@ There are three types of tests in this project: static tests with eslint/prettie
- Cypress
- can be launched in interactive mode with `npm run cypress open`. `package.json` defines entrypoint `npm run cypress` so you can basically run cypress with any arguments you want
- are defined in cypress -folder and cypress.json
- are defined in cypress -folder and cypress.config.json
- whole cypress test stack takes about 15 to 20 mins to run. Since tests are ran in our Github actions CI pipe, you're encouraged to take advantage of this instead of running all tests locally.
- There are some different user types and cypress commands defined for testing. Take a look at these when debugging tests.
- Jest
+19
View File
@@ -0,0 +1,19 @@
const { defineConfig } = require('cypress')
module.exports = defineConfig({
projectId: 'c3jsph',
defaultCommandTimeout: 30000,
requestTimeout: 30000,
videoCompression: false,
viewportWidth: 1800,
viewportHeight: 1200,
e2e: {
// We've imported your old cypress plugins here.
// You may want to clean this up later by importing these.
setupNodeEvents(on, config) {
return require('./cypress/plugins/index.js')(on, config)
},
baseUrl: 'http://localhost:3000',
specPattern: 'cypress/e2e/**/*.{js,jsx,ts,tsx}',
},
})
-7
View File
@@ -1,7 +0,0 @@
{
"projectId": "c3jsph",
"baseUrl": "http://localhost:3000",
"defaultCommandTimeout": 30000,
"requestTimeout": 30000,
"videoCompression": false
}
+139
View File
@@ -0,0 +1,139 @@
/// <reference types="Cypress" />
// Change admin to regular user when feature is ready for general use
describe('Faculty overview', () => {
describe('Faculty can be selected', () => {
it('Faculties are listed and one can be chosen', () => {
cy.init('/faculties', 'admin')
cy.get('[data-cy=select-faculty]').contains('td', 'H10')
cy.contains('td', 'H99').should('not.exist')
cy.contains('td', 'H90').click()
cy.contains('.header', 'Eläinlääketieteellinen tiedekunta')
})
})
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"]').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('Faculty basic information: basic user', () => {
beforeEach(() => {
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('[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('[data-cy="Table-CreditsProducedByTheFaculty"]').should('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')
})
})
})
@@ -217,4 +217,30 @@ describe('Population Statistics tests', () => {
})
})
})
describe('when using IAM user', () => {
beforeEach(() => {
cy.init(pathToCSBach2017, 'onlyiamrights')
})
it('Population statistics is visible', () => {
cy.reload()
cy.get('.card').within(() => {
cy.contains('Tietojenkäsittelytieteen kandiohjelma')
cy.contains('Sample size: 170 students')
cy.contains('Excludes exchange students')
cy.contains('Excludes students with non-degree study right')
cy.contains('Excludes students who have transferred out of this programme')
})
cy.contains('Credit accumulation (for 170 students)')
cy.contains('Credit statistics')
})
it('Students tab is not available', () => {
cy.contains('Students (170)').should('not.exist')
// TODO: somehow check that obfuscation works
})
})
})
@@ -249,8 +249,8 @@ describe('Studyprogramme overview', () => {
'0 %',
0,
'0 %',
0,
'0 %',
1,
'1.2 %',
1,
'1.2 %',
65,
@@ -291,8 +291,8 @@ describe('Studyprogramme overview', () => {
'0 %',
0,
'0 %',
3,
'1.8 %',
4,
'2.3 %',
42,
'24.6 %',
135,
@@ -312,8 +312,8 @@ describe('Studyprogramme overview', () => {
'0 %',
0,
'0 %',
3,
'0.7 %',
5,
'1.2 %',
54,
'12.5 %',
328,
@@ -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, 0],
['2019 - 2020', 86, 10, 16, 34, 21, 5, 0, 0, 0],
['2018 - 2019', 162, 2, 8, 27, 47, 36, 26, 12, 4],
['2017 - 2018', 171, 5, 15, 21, 26, 22, 22, 33, 27],
['Total', 431, 14, 37, 64, 97, 79, 55, 51, 34],
]
cy.checkTableStats(tableContents, 'StudytrackProgress')
@@ -347,12 +347,12 @@ describe('Studyprogramme overview', () => {
cy.get('.attached').contains('Studytracks and student populations').click()
cy.get('[data-cy=Graph-StudytrackProgress]')
.should('contain', 'Less than 30 credits')
.should('contain', 'Less than 15 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', '2.9%') // The percentage for less than 15 credits in 2017-2018, to check that the graph renders
.should('contain', '4.9%') // The percentage for 15-29 credits in 2018-2019
.should('contain', '11.6%') // The percentage for less than 15 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()
@@ -499,4 +499,42 @@ describe('Studyprogramme overview', () => {
cy.contains(name).should('not.exist')
})
})
describe('IAM user', () => {
beforeEach(() => {
cy.init('/study-programme', 'onlyiamrights')
})
it('can access programme and correct tabs are visible', () => {
cy.reload()
cy.contains('a', 'Tietojenkäsittelytieteen kandiohjelma').click()
cy.contains('Basic information')
cy.contains('Studytracks and student populations')
cy.contains('Update statistics').should('not.exist')
cy.contains('Degree Courses').should('not.exist')
})
it('can access basic information', () => {
cy.contains('a', 'Tietojenkäsittelytieteen kandiohjelma').click()
cy.contains('Basic information').click()
cy.get('[data-cy=Section-StudentsOfTheStudyprogramme]')
cy.get('[data-cy=Section-CreditsProducedByTheStudyprogramme]')
cy.get('[data-cy=Section-GraduatedAndThesisWritersOfTheProgramme]')
cy.get('[data-cy=Section-ProgrammesBeforeOrAfter]')
cy.get('[data-cy=Section-AverageGraduationTimes]')
})
it('can access studytracks', () => {
cy.contains('a', 'Tietojenkäsittelytieteen kandiohjelma').click()
cy.get('.attached').contains('Studytracks and student populations').click()
cy.get('[data-cy=Section-StudytrackOverview]')
cy.get('[data-cy=Section-StudytrackProgress]')
cy.get('[data-cy=Section-AverageGraduationTimes]')
})
})
})
-26
View File
@@ -1,26 +0,0 @@
/// <reference types="Cypress" />
// Change admin to regular user when feature is ready for general use
describe('Faculty overview', () => {
describe('Faculty can be selected', () => {
it('Faculties are listed and one can be chosen', () => {
cy.init('/faculties', 'admin')
cy.get('[data-cy=select-faculty]').contains('td', 'H10')
cy.contains('td', 'H99').should('not.exist')
cy.contains('td', 'H90').click()
cy.contains('.header', 'Eläinlääketieteellinen tiedekunta')
})
})
describe('Faculty basic information', () => {
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'])
})
})
})
+17 -1
View File
@@ -43,7 +43,23 @@ const norightsUserHeaders = {
hypersonsisuid: 'hy-hlo-0000000',
}
const userHeaders = [adminUserHeaders, basicUserHeaders, onlycoursestatisticsUserHeaders, norightsUserHeaders]
const onlyIamRightsUserHeaders = {
uid: 'onlyiamrights',
displayname: 'Only IAM rights user',
'shib-session-id': 'mock-cypress-session',
hygroupcn: 'hy-employees;hy-mltdk-tkt-jory',
edupersonaffiliation: 'member',
mail: 'grp-toska+mockonlyiamrightsuser@helsinki.fi',
hypersonsisuid: 'hy-hlo-1111111',
}
const userHeaders = [
adminUserHeaders,
basicUserHeaders,
onlycoursestatisticsUserHeaders,
norightsUserHeaders,
onlyIamRightsUserHeaders,
]
/**
Set up headers to login, set up correct user (admin / basic / etc.) and open given path.
+9128 -2710
View File
File diff suppressed because it is too large Load Diff
+16 -13
View File
@@ -6,6 +6,7 @@
"concurrently": "concurrently",
"eslint": "eslint",
"stylelint": "stylelint",
"actionlint": "node-actionlint",
"prettier": "prettier",
"cli": "./cli.sh",
"oodikone": "./run.sh oodikone anon up --detach",
@@ -28,31 +29,33 @@
},
"homepage": "https://github.com/UniversityOfHelsinkiCS/oodikone#readme",
"devDependencies": {
"@babel/core": "^7.18.13",
"@babel/eslint-parser": "^7.17.0",
"@babel/plugin-syntax-jsx": "^7.16.7",
"concurrently": "^6.5.1",
"cypress": "^8.7.0",
"eslint": "^7.32.0",
"eslint-config-airbnb": "^18.2.1",
"concurrently": "^7.3.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",
"eslint-plugin-cypress": "^2.12.1",
"eslint-plugin-import": "^2.25.4",
"eslint-plugin-jsx-a11y": "^6.5.1",
"eslint-plugin-prefer-object-spread": "^1.2.1",
"eslint-plugin-prettier": "^3.4.1",
"eslint-plugin-react": "^7.29.3",
"eslint-plugin-prettier": "^4.2.1",
"eslint-plugin-react": "^7.31.1",
"eslint-plugin-react-hooks": "^4.3.0",
"husky": "^7.0.4",
"lint-staged": "^11.2.6",
"husky": "^8.0.1",
"lint-staged": "^13.0.3",
"node-actionlint": "^1.2.2",
"prettier": "^2.5.1",
"stylelint": "^13.13.1",
"stylelint-config-prettier": "^8.0.2",
"stylelint-config-standard": "^22.0.0",
"stylelint-prettier": "^1.2.0"
"stylelint": "^14.11.0",
"stylelint-config-prettier": "^9.0.3",
"stylelint-config-standard": "^26.0.0",
"stylelint-prettier": "^2.0.0"
},
"dependencies": {
"lodash": "^4.17.21",
"moment": "^2.29.1"
"moment": "^2.29.4"
}
}
+1 -1
View File
@@ -393,7 +393,7 @@ const iamToOrganisationCode = iam => {
const kosuIamToFaculties = iam => {
const faculties = kosuFacultyMap[iam]
if (!faculties?.length > 0) return []
if (!Array.isArray(faculties) || !faculties.length) return []
return faculties.map(f => facultyMap[f])
}
+5303 -1349
View File
File diff suppressed because it is too large Load Diff
+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,
})
})
@@ -4,10 +4,12 @@ const { getUser, getMockedUser } = require('../services/userService')
const { requiredGroup } = require('../conf-backend')
const _ = require('lodash')
const { relevantIAMs } = require('../../config/IAMConfig')
const { getOrganizationAccess } = require('../util/organizationAccess')
const parseIamGroups = iamGroups => iamGroups?.split(';').filter(Boolean) ?? []
const hasRequiredIamGroup = iamGroups => _.intersection(iamGroups, requiredGroup).length > 0
const hasRequiredIamGroup = (iamGroups, iamRights) =>
_.intersection(iamGroups, requiredGroup).length > 0 || iamRights.length > 0
const currentUserMiddleware = async (req, _res, next) => {
const {
@@ -27,7 +29,9 @@ const currentUserMiddleware = async (req, _res, next) => {
const iamGroups = parseIamGroups(hygroupcn).filter(iam => relevantIAMs.includes(iam))
if (!hasRequiredIamGroup(iamGroups)) {
const iamRights = Object.keys(await getOrganizationAccess({ iamGroups }))
if (!hasRequiredIamGroup(iamGroups, iamRights)) {
throw new ApplicationError('User does not have required iam group', 403, { logoutUrl })
}
+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 -73
View File
@@ -1,101 +1,167 @@
const router = require('express').Router()
const { faculties, degreeProgrammeCodesOfFaculty } = require('../services/organisations')
const { combineFacultyBasics } = require('../services/facultyBasics')
const { combineFacultyCredits } = require('../services/facultyCredits')
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: [],
tableStats: [],
graphStats: [],
titles: [
'',
'Started studying',
'All graduated',
'Graduated bachelors',
'Graduated masters',
'Graduated doctors',
'Other graduations',
'Transferred inside',
'Transferred away',
'Transferred to',
],
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: [],
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 degreeProgrammeCodesOfFaculty(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
+57 -9
View File
@@ -9,6 +9,7 @@ const TagService = require('../services/tags')
const CourseService = require('../services/courses')
const StatMergeService = require('../services/statMerger')
const { mapToProviders } = require('../util/utils')
const { encrypt, decrypt } = require('../services/encrypt')
const { ApplicationError } = require('../util/customErrors')
@@ -38,16 +39,26 @@ router.post('/v2/populationstatistics/courses', async (req, res) => {
req.body.months = 12
}
const encrypted = req.body.selectedStudents[0]?.encryptedData
if (req.body.years) {
const upperYearBound = new Date().getFullYear() + 1
const multicoursestatPromises = Promise.all(
req.body.years.map(year => {
if (req.body.selectedStudentsByYear) {
req.body.selectedStudents = req.body.selectedStudentsByYear[year]
req.body.selectedStudents = encrypted
? req.body.selectedStudents
.filter(
({ encryptedData }) =>
req.body.selectedStudentsByYear[year] &&
req.body.selectedStudentsByYear[year].includes(encryptedData)
)
.map(decrypt)
: req.body.selectedStudentsByYear[year]
}
const newMonths = (upperYearBound - Number(year)) * 12
const query = { ...req.body, year, months: newMonths }
const coursestatistics = Population.bottlenecksOf(query)
const coursestatistics = Population.bottlenecksOf(query, null, encrypted)
return coursestatistics
})
)
@@ -60,7 +71,9 @@ router.post('/v2/populationstatistics/courses', async (req, res) => {
res.json(result)
} else {
const result = await Population.bottlenecksOf(req.body)
if (encrypted) req.body.selectedStudents = req.body.selectedStudents.map(decrypt)
const result = await Population.bottlenecksOf(req.body, null, encrypted)
if (result.error) {
Sentry.captureException(new Error(result.error))
@@ -68,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)
}
})
@@ -180,13 +207,13 @@ router.get('/v3/populationstatistics', async (req, res) => {
return
}
let studyRights = null
try {
studyRights = JSON.parse(studyRightsJSON)
const {
user: { rights, isAdmin },
} = req
studyRights = JSON.parse(studyRightsJSON)
const {
user: { rights, iamRights, isAdmin },
} = req
if (!isAdmin && !rights.includes(studyRights.programme)) {
try {
if (!isAdmin && !rights.includes(studyRights.programme) && !iamRights.includes(studyRights.programme)) {
res.status(403).json([])
return
}
@@ -232,6 +259,27 @@ router.get('/v3/populationstatistics', async (req, res) => {
return
}
// Obfuscate if user has only iam rights
if (!isAdmin && !rights.includes(studyRights.programme)) {
result.students = result.students.map(student => {
const { iv, encryptedData } = encrypt(student.studentNumber)
const obfuscatedBirthDate = new Date(new Date(student.birthdate).setMonth(0, 0)) // only birth year for age distribution
return {
...student,
firstnames: '',
lastname: '',
started: null,
iv,
studentNumber: encryptedData,
name: '',
email: '',
tags: [],
birthdate: obfuscatedBirthDate,
obfuscated: true,
}
})
}
res.json(filterPersonalTags(result, user.id))
}
})
+2 -1
View File
@@ -449,7 +449,8 @@ const codeLikeTerm = code =>
? undefined
: {
code: {
[Op.iLike]: `%${code.trim()}%`,
// Starts with code or has AY/A in front of the code
[Op.iRegexp]: `^(AY|ay|A|a)?${code}`,
},
}
+28
View File
@@ -0,0 +1,28 @@
const { randomBytes, createCipheriv, createDecipheriv } = require('crypto')
const algorithm = 'aes-256-cbc'
const key = randomBytes(32)
const iv = randomBytes(16)
const encrypt = text => {
const cipher = createCipheriv(algorithm, Buffer.from(key), iv)
let encrypted = cipher.update(text)
encrypted = Buffer.concat([encrypted, cipher.final()])
return {
iv: iv.toString('hex'),
encryptedData: encrypted.toString('hex'),
}
}
const decrypt = text => {
const iv = Buffer.from(text.iv, 'hex')
const encryptedText = Buffer.from(text.encryptedData, 'hex')
const decipher = createDecipheriv(algorithm, Buffer.from(key), iv)
let decrypted = decipher.update(encryptedText)
decrypted = Buffer.concat([decrypted, decipher.final()])
return decrypted.toString()
}
module.exports = { encrypt, decrypt }
-90
View File
@@ -1,90 +0,0 @@
const Sequelize = require('sequelize')
const { Op } = Sequelize
const { Studyright, Student, Transfer } = require('../models')
const { formatStudyright, formatTransfer } = require('./studyprogrammeHelpers')
const startedStudyrights = async (faculty, since) =>
(
await Studyright.findAll({
include: [
{
model: Student,
attributes: ['studentnumber'],
required: true,
},
],
where: {
faculty_code: faculty,
studystartdate: {
[Op.gte]: since,
},
student_studentnumber: { [Op.not]: null },
},
})
).map(formatStudyright)
const graduatedStudyrights = async (faculty, since) =>
(
await Studyright.findAll({
include: [
{
model: Student,
attributes: ['studentnumber'],
required: true,
},
],
where: {
faculty_code: faculty,
enddate: {
[Op.gte]: since,
},
graduated: 1,
student_studentnumber: { [Op.not]: null },
},
})
).map(formatStudyright)
const transferredInsideFaculty = async (programmes, since) =>
(
await Transfer.findAll({
where: {
transferdate: {
[Op.gte]: since,
},
sourcecode: programmes,
targetcode: programmes,
},
})
).map(formatTransfer)
const transferredAway = async (programmes, since) =>
(
await Transfer.findAll({
where: {
transferdate: {
[Op.gte]: since,
},
sourcecode: programmes,
targetcode: {
[Op.notIn]: programmes,
},
},
})
).map(formatTransfer)
const transferredTo = async (programmes, since) =>
(
await Transfer.findAll({
where: {
transferdate: {
[Op.gte]: since,
},
sourcecode: {
[Op.notIn]: programmes,
},
targetcode: programmes,
},
})
).map(formatTransfer)
module.exports = { startedStudyrights, graduatedStudyrights, transferredInsideFaculty, transferredAway, transferredTo }
@@ -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) &&
@@ -116,7 +114,7 @@ const getFacultyRegularCreditStats = async ({
// Fetches all credits for the studytrack and combines them into statistics
// These are for the faculty view which differs from study programme overview
// (no tranfers, non majors split to in- and outside-faculty)
// (no transfers, non majors split to in- and outside-faculty)
const getFacultyCreditStatsForStudytrack = async ({ studyprogramme, facultyProgrammes, settings }) => {
const { isAcademicYear, includeAllSpecials } = settings
const since = getStartDate(studyprogramme, isAcademicYear)
@@ -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,14 +188,40 @@ 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, yearType, specialGroups, programmes)
const data = await getProgrammeCredits(prog.code, yearType, specialGroups, progCodes)
if (data) {
allCredits.programmeNames[prog.code] = prog.name
allCredits.programmeTableStats[prog.code] = data.tableStats
if (!allCredits.lastUpdated || new Date(data.lastUpdated) > new Date(allCredits.lastUpdated))
allCredits.lastUpdated = data.lastUpdated
@@ -225,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,338 @@
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 countTimeCategories = (times, goal) => {
const statistics = { onTime: 0, yearOver: 0, wayOver: 0 }
times.forEach(time => {
if (time <= goal) statistics.onTime += 1
else if (time <= goal + 12) statistics.yearOver += 1
else statistics.wayOver += 1
})
return statistics
}
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, programmeNames, goals) => {
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, programmeName } = findRightProgramme(studyrightElements, 'graduated')
if (programmeFilter === 'NEW_STUDY_PROGRAMMES' && !isNewProgramme(programme)) continue
if (!(programme in programmeNames)) {
programmeNames[programme] = programmeName
}
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])
const statistics = countTimeCategories(graduationTimes[level][year], goals[level])
byStartYear.medians[level] = [
...byStartYear.medians[level],
{ y: median, amount: graduationAmounts[level][year], statistics },
]
byStartYear.means[level] = [
...byStartYear.means[level],
{ y: mean, amount: graduationAmounts[level][year], statistics },
]
// 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)
const progStatistics = countTimeCategories(programmes[level][year][prog].graduationTimes, goals[level])
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, statistics: progStatistics },
]
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, statistics: progStatistics },
]
}
}
})
})
return byStartYear
}
const countByGraduationYear = async (
faculty,
since,
years,
yearsList,
levels,
programmeFilter,
programmeNames,
goals
) => {
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, programmeName } = findRightProgramme(studyrightElements, 'graduated')
if (programmeFilter === 'NEW_STUDY_PROGRAMMES' && !isNewProgramme(programme)) continue
if (!(programme in programmeNames)) {
programmeNames[programme] = programmeName
}
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])
const statistics = countTimeCategories(graduationTimes[level][year], goals[level])
byGradYear.medians[level] = [
...byGradYear.medians[level],
{ y: median, amount: graduationAmounts[level][year], statistics },
]
byGradYear.means[level] = [
...byGradYear.means[level],
{ y: mean, amount: graduationAmounts[level][year], statistics },
]
// 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)
const progStatistics = countTimeCategories(programmes[level][year][prog].graduationTimes, goals[level])
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, statistics: progStatistics },
]
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, statistics: progStatistics },
]
}
}
})
})
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 programmeNames = {}
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,
programmeNames,
goals
)
const byStartYear = await countByStartYear(
faculty,
since,
years,
yearsList,
levels,
programmeFilter,
programmeNames,
goals
)
return { years, goals, byGradYear, byStartYear, programmeNames }
}
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,174 @@
const { indexOf } = require('lodash')
const moment = require('moment')
const { getAcademicYearDates } = require('../../util/semester')
const {
getCreditThresholds,
getBachelorCreditGraphStats,
getMasterCreditGraphStats,
getDoctoralCreditGraphStats,
getYearsArray,
getCorrectStudentnumbers,
tableTitles,
} = 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: tableTitles.creditProgress.bachelor,
mastersTitles: tableTitles.creditProgress.master,
doctoralTitles: tableTitles.creditProgress.doctoral,
programmeNames: programmes.data.reduce((obj, dataItem) => ({ ...obj, [dataItem.code]: dataItem.name }), {}),
bachelorsProgrammeStats: {},
mastersProgrammeStats: {},
doctoralProgrammeStats: {},
}
let allBachelorGraphStats = getBachelorCreditGraphStats(yearsArray)
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
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)
const studentData = getStudentData(startDate, students, creditThresholdKeys, creditThresholdAmounts)
if (programme.code.includes('KH')) {
if (!(programme.code in bachelorsProgrammeStats)) {
bachelorsProgrammeStats[programme.code] = new Array(reversedYears.length - 1)
}
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(creditThresholdKeys, key) + 2] +=
studentData[key] || 0
allBachelorGraphStats[key].data[indexOf(yearsArray, year)] += studentData[key] || 0
}
} else if (programme.code.includes('MH')) {
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(creditThresholdKeys, key) + 2] +=
studentData[key] || 0
allMastersGraphStats[key].data[indexOf(yearsArray, year)] += studentData[key] || 0
}
} else {
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(creditThresholdKeys, key) + 2] +=
studentData[key] || 0
allDoctoralGraphStats[key].data[indexOf(yearsArray, year)] += studentData[key] || 0
}
}
}
}
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,175 +0,0 @@
const { indexOf } = require('lodash')
const {
startedStudyrights,
graduatedStudyrights,
transferredInsideFaculty,
transferredAway,
transferredTo,
} = require('./faculty')
const { getStatsBasis, getYearsArray, defineYear } = require('./studyprogrammeHelpers')
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 getFacultyStarters = async (
faculty,
since,
isAcademicYear,
yearsArray,
graphStats,
tableStats,
counts,
allBasics
) => {
const startedGraphStats = [...graphStats]
const startedTableStats = { ...tableStats }
const studyrights = await startedStudyrights(faculty, since)
const filteredStudyrights = filterDuplicateStudyrights(studyrights)
filteredStudyrights.forEach(({ studystartdate }) => {
const startYear = defineYear(studystartdate, isAcademicYear)
startedGraphStats[indexOf(yearsArray, startYear)] += 1
startedTableStats[startYear] += 1
})
allBasics.graphStats.push({ name: 'Started studying', data: startedGraphStats })
Object.keys(startedTableStats).forEach(year => {
counts[year] = [startedTableStats[year]]
})
}
const getFacultyGraduates = async (
faculty,
since,
isAcademicYear,
yearsArray,
graphStats,
tableStats,
allBasics,
counts
) => {
const graduatedGraphStats = [[...graphStats], [...graphStats], [...graphStats], [...graphStats], [...graphStats]]
const graduatedTableStats = {}
Object.keys(tableStats).forEach(year => (graduatedTableStats[year] = [0, 0, 0, 0, 0]))
const graduatedRights = await graduatedStudyrights(faculty, since)
graduatedRights.forEach(({ enddate, extentcode }) => {
const endYear = defineYear(enddate, isAcademicYear)
graduatedGraphStats[0][indexOf(yearsArray, endYear)] += 1
graduatedTableStats[endYear][0] += 1
if (extentcode === 1) {
graduatedGraphStats[1][indexOf(yearsArray, endYear)] += 1
graduatedTableStats[endYear][1] += 1
} else if (extentcode === 2) {
graduatedGraphStats[2][indexOf(yearsArray, endYear)] += 1
graduatedTableStats[endYear][2] += 1
} else if (extentcode === 4) {
graduatedGraphStats[3][indexOf(yearsArray, endYear)] += 1
graduatedTableStats[endYear][3] += 1
} else {
graduatedGraphStats[4][indexOf(yearsArray, endYear)] += 1
graduatedTableStats[endYear][4] += 1
}
})
allBasics.graphStats.push({ name: 'All graduated', data: graduatedGraphStats[0] })
allBasics.graphStats.push({ name: 'Graduated bachelors', data: graduatedGraphStats[1] })
allBasics.graphStats.push({ name: 'Graduated masters', data: graduatedGraphStats[2] })
allBasics.graphStats.push({ name: 'Graduated doctors', data: graduatedGraphStats[3] })
allBasics.graphStats.push({ name: 'Other graduations', data: graduatedGraphStats[4] })
Object.keys(graduatedTableStats).forEach(year => {
counts[year] = counts[year].concat(graduatedTableStats[year])
})
}
const getFacultyTransfers = async (
programmes,
since,
isAcademicYear,
yearsArray,
graphStats,
tableStats,
allBasics,
counts
) => {
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]))
insideTransfers.forEach(({ transferdate }) => {
const transferYear = defineYear(transferdate, isAcademicYear)
transferGraphStats[0][indexOf(yearsArray, transferYear)] += 1
transferTableStats[transferYear][0] += 1
})
awayTransfers.forEach(({ transferdate }) => {
const transferYear = defineYear(transferdate, isAcademicYear)
transferGraphStats[1][indexOf(yearsArray, transferYear)] += 1
transferTableStats[transferYear][1] += 1
})
toTransfers.forEach(({ transferdate }) => {
const transferYear = defineYear(transferdate, isAcademicYear)
transferGraphStats[2][indexOf(yearsArray, transferYear)] += 1
transferTableStats[transferYear][2] += 1
})
allBasics.graphStats.push({ name: 'Transferred inside', data: transferGraphStats[0] })
allBasics.graphStats.push({ name: 'Transferred away', data: transferGraphStats[1] })
allBasics.graphStats.push({ name: 'Transferred to', data: transferGraphStats[2] })
Object.keys(transferTableStats).forEach(year => {
counts[year] = counts[year].concat(transferTableStats[year])
})
}
const combineFacultyBasics = async (allBasics, faculty, programmes, yearType) => {
let counts = {}
let years = []
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)
// Graduated in faculty
await getFacultyGraduates(faculty, since, isAcademicYear, yearsArray, graphStats, tableStats, allBasics, counts)
// Transfers
await getFacultyTransfers(programmes, since, isAcademicYear, yearsArray, graphStats, tableStats, allBasics, counts)
// combine tableStats from all categories
allBasics.years = years
years.forEach(year => {
allBasics.tableStats.push([year, ...counts[year]])
})
allBasics.tableStats.reverse()
}
module.exports = { combineFacultyBasics }
+23 -4
View File
@@ -24,6 +24,7 @@ const { Tag, TagStudent } = require('../models/models_kone')
const { CourseStatsCounter } = require('./course_stats_counter')
const { getPassingSemester, semesterEnd, semesterStart } = require('../util/semester')
const { getAllProgrammes } = require('./studyrights')
const { encrypt } = require('../services/encrypt')
const enrolmentDates = () => {
const query = 'SELECT DISTINCT s.dateOfUniversityEnrollment as date FROM Student s'
@@ -209,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],
@@ -413,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,
@@ -423,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 = {}
@@ -981,7 +982,7 @@ const parseCreditInfo = credit => ({
date: credit.attainment_date,
})
const bottlenecksOf = async (query, studentnumberlist) => {
const bottlenecksOf = async (query, studentnumberlist, encryptdata = false) => {
const isValidRequest = async (query, params) => {
const { studyRights, startDate, endDate, exchangeStudents, nondegreeStudents, transferredStudents } = params
@@ -1055,6 +1056,22 @@ const bottlenecksOf = async (query, studentnumberlist) => {
return [allstudents, courses]
}
}
const encryptStudentnumbers = bottlenecks => {
for (const course in bottlenecks.coursestatistics) {
const encryptedStudentStats = {}
for (const data in bottlenecks.coursestatistics[course].students) {
encryptedStudentStats[data] = {}
const studentnumbers = Object.keys(bottlenecks.coursestatistics[course].students[data])
studentnumbers.forEach(studentnumber => {
encryptedStudentStats[data][encrypt(studentnumber).encryptedData] =
bottlenecks.coursestatistics[course].students[data][studentnumber]
})
}
bottlenecks.coursestatistics[course].students = encryptedStudentStats
}
}
const params = parseQueryParams(query)
const [[allstudents, courses], error] = await Promise.all([
getStudentsAndCourses(query.selectedStudents, studentnumberlist, query.courses),
@@ -1114,6 +1131,8 @@ const bottlenecksOf = async (query, studentnumberlist) => {
bottlenecks.coursestatistics = Object.values(stats).map(coursestatistics => coursestatistics.getFinalStats())
bottlenecks.allStudents = allstudentslength
if (encryptdata) encryptStudentnumbers(bottlenecks)
return bottlenecks
}
+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,
}
}
@@ -150,15 +151,16 @@ const isNonMajorCredit = (studyrights, attainment_date) =>
)
})
const isSpecialGroupCredit = (studyrights, attainment_date, transfers) =>
const isSpecialGroupCredit = (studyrights, attainment_date, transfers) => {
studyrights.some(studyright => {
if (!studyright) return true // If there is no studyright matching the credit, is not a major student credit
if (studyright.studystartdate > attainment_date) return true // Credits before the studyright started are not major student credits
if (studyright.enddate && attainment_date > studyright.enddate) return true // Credits after studyright are not major student credits
if ([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
})
}
const getMedian = values => {
if (values.length === 0) return 0
@@ -223,8 +225,12 @@ const getPercentage = (value, total) => {
const getEmptyArray = length => new Array(length).fill(0)
const getBachelorCreditGraphStats = years => ({
lte15: {
name: 'Less than 15 credits',
data: getEmptyArray(years.length),
},
lte30: {
name: 'Less than 30 credits',
name: '15-29 credits',
data: getEmptyArray(years.length),
},
lte60: {
@@ -284,6 +290,33 @@ const getMasterCreditGraphStats = years => ({
},
})
const getOnlyMasterCreditGraphStats = years => ({
lte15: {
name: 'Less than 15 credits',
data: getEmptyArray(years.length),
},
lte30: {
name: '15-29 credits',
data: getEmptyArray(years.length),
},
lte60: {
name: '30-59 credits',
data: getEmptyArray(years.length),
},
lte90: {
name: '60-89 credits',
data: getEmptyArray(years.length),
},
lte120: {
name: '90-119 credits',
data: getEmptyArray(years.length),
},
mte120: {
name: 'More than 120 credits',
data: getEmptyArray(years.length),
},
})
const getDoctoralCreditGraphStats = years => ({
lte50: {
name: 'Less than 50 credits',
@@ -321,11 +354,13 @@ const getCreditGraphStats = (studyprogramme, years) => {
return getDoctoralCreditGraphStats(years)
}
const bachelorCreditThresholds = ['lte30', 'lte60', 'lte90', 'lte120', 'lte150', 'lte180', 'mte180']
const bachelorCreditThresholds = ['lte15', 'lte30', 'lte60', 'lte90', 'lte120', 'lte150', 'lte180', 'mte180']
const masterCreditThresholds = ['lte200', 'lte220', 'lte240', 'lte260', 'lte280', 'lte300', 'mte300']
// const onlyMasterCredtThresholds = ['lte15', 'lte30', 'lte60', 'lte90', 'mte120']
const doctoralCreditThresholds = ['lte50', 'lte100', 'lte150', 'lte200', 'lte250', 'lte300', 'mte300']
const bachelorCreditAmounts = [30, 60, 90, 120, 150, 180, 180]
const bachelorCreditAmounts = [15, 30, 60, 90, 120, 150, 180, 180]
const masterCreditAmounts = [200, 220, 240, 260, 280, 300, 300]
// const onlyMasterCreditAmounts = [15, 30, 60, 90, 120, 120]
const doctoralCreditAmounts = [50, 100, 150, 200, 250, 300, 300]
const getCreditThresholds = studyprogramme => {
@@ -349,8 +384,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',
],
},
@@ -358,7 +393,8 @@ const tableTitles = {
bachelor: [
'',
'All',
'< 30 credits',
'< 15 credits',
'15-29 credits',
'30-59 credits',
'60-89 credits',
'90-119 credits',
@@ -377,6 +413,16 @@ const tableTitles = {
'280-299 credits',
'> 300 credits',
],
masterOnly: [
'',
'All',
'< 15 credits',
'15-29 credits',
'30-59 credits',
'60-89 credits',
'90-119 credits',
'> 120 credits',
],
doctoral: [
'',
'All',
@@ -431,6 +477,7 @@ module.exports = {
getBachelorCreditGraphStats,
getMasterCreditGraphStats,
getDoctoralCreditGraphStats,
getOnlyMasterCreditGraphStats,
getCreditGraphStats,
getCreditThresholds,
tableTitles,
@@ -30,7 +30,6 @@ const updateBasicView = async code => {
}
const options = [specialCalendar, specialAcademic, specialExcludedCalendar, specialExcludedAcademic]
for (const option of options) {
try {
const basicStats = await getBasicStatsForStudytrack({
@@ -62,8 +61,8 @@ const updateBasicView = async code => {
} catch (e) {
logger.error(e)
}
return 'OK'
}
return 'OK'
}
const updateStudytrackView = async code => {
+10 -2
View File
@@ -1,7 +1,7 @@
const {
dbConnections: { sequelize },
} = require('../database/connection')
const { Studyright, StudyrightElement, ElementDetail } = require('../models')
const { Studyright, StudyrightElement, ElementDetail, Transfer } = require('../models')
const moment = require('moment')
const { redisClient } = require('../services/redis')
const _ = require('lodash')
@@ -187,6 +187,10 @@ const StudyRightType = {
const calculateAssociationsFromDb = async (chunksize = 100000) => {
// bottlenecked by Studyright.findAll in associatedStudyrightElements()
const transfers = await Transfer.findAll({
attributes: ['studyrightid'],
})
const transferedStudyrightIds = transfers.map(transfer => transfer.studyrightid)
const getSemester = momentstartdate => {
if (momentstartdate < moment(`${momentstartdate.utc().year()}-07-31 21:00:00+00`)) return 'SPRING'
return 'FALL'
@@ -247,8 +251,12 @@ const calculateAssociationsFromDb = async (chunksize = 100000) => {
name: e.name,
code: e.code,
}
if (!associations.programmes[code].studytracks.includes(e.code))
if (
!associations.programmes[code].studytracks.includes(e.code) &&
!transferedStudyrightIds.includes(e.studyrightid)
) {
associations.programmes[code].studytracks.push(e.code)
}
}
associations.studyTracks[e.code] = associations.studyTracks[e.code] || {
type: e.type,
@@ -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
@@ -40,7 +44,13 @@ const getStudentData = (students, thresholdKeys, thresholdAmounts) => {
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
data[thresholdKeys[6]] += creditcount >= thresholdAmounts[6] ? 1 : 0
// bachelor programme has more columns than master and doctoral programmes
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
}
@@ -121,7 +131,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) {
+11 -1
View File
@@ -23,7 +23,14 @@
"no-shadow": "off",
"react/jsx-props-no-spreading": "off",
"react/jsx-filename-extension": "off",
"no-async-promise-executor": "off",
"react/function-component-definition": [
2,
{
"namedComponents": "arrow-function",
"unnamedComponents": "arrow-function"
}
],
"default-param-last": "off",
"no-implied-eval": "off",
"react/state-in-constructor": "off",
"react/static-property-placement": "off",
@@ -31,6 +38,9 @@
"react/no-unescaped-entities": "off",
"react/prop-types": "off",
"react/prefer-stateless-function": "error",
"react/no-unstable-nested-components": "off",
"react/jsx-no-constructed-context-values": "off",
"react/jsx-no-useless-fragment": "off",
"import/no-unresolved": "off",
"react/jsx-no-bind": "error",
"prefer-object-spread/prefer-object-spread": "error",
+25235 -3179
View File
File diff suppressed because it is too large Load Diff
+3 -2
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",
@@ -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",
@@ -35,6 +35,7 @@
"react-router-dom": "^5.3.0",
"react-scripts": "4.0.3",
"react-scroll-to-component": "^1.0.2",
"scheduler": "^0.23.0",
"semantic-ui-css": "^2.4.1",
"semantic-ui-react": "^2.1.2",
"unfuck-spa-shibboleth-session": "^3.1.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
`,
}
+2 -2
View File
@@ -41,7 +41,7 @@ export const byName = (a, b) => a.name.localeCompare(b.name)
export const byCodeDesc = (a, b) => b.code.localeCompare(a.code)
export const studyRightRegex = new RegExp(/.*master|bachelor|doctor|licentiate|specialist.*/)
export const studyRightRegex = /.*master|bachelor|doctor|licentiate|specialist.*/
export const studyrightElementTypes = { programme: 20, speciality: 30 }
@@ -150,8 +150,8 @@ export const getUnifyTextIn = unifyCourses => {
export const cancelablePromise = promise => {
let hasCanceled = false
// eslint-disable-next-line no-async-promise-executor
const wrappedPromise = new Promise(async (res, rej) => {
// eslint-disable-line no-async-promise-executor
try {
await promise
if (hasCanceled) res(false)
@@ -30,7 +30,7 @@
}
.cool-data-science-tile.cool-data-science-tile--active {
box-shadow: 0 3px 6px rgba(0, 0, 0, 0.16), 0 3px 6px rgba(0, 0, 0, 0.23);
box-shadow: 0 3px 6px rgb(0 0 0 / 16%), 0 3px 6px rgb(0 0 0 / 23%);
}
.cool-data-science-tile__title {
@@ -7,7 +7,7 @@
}
.status-card:hover {
box-shadow: 0 3px 6px rgba(0, 0, 0, 0.16), 0 3px 6px rgba(0, 0, 0, 0.23);
box-shadow: 0 3px 6px rgb(0 0 0 / 16%), 0 3px 6px rgb(0 0 0 / 23%);
}
.years {
@@ -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,3 +1,4 @@
/* eslint-disable camelcase */
import React, { useState, useRef, useMemo } from 'react'
import { renderToString } from 'react-dom/server'
import moment from 'moment'
@@ -261,9 +262,7 @@ const createGoalSeries = (starting, ending, absences) => {
}
}
// eslint-disable-next-line camelcase
const resolveStudyRightElement = ({ studyright_elements }) => {
// eslint-disable-next-line camelcase
if (!studyright_elements || !studyright_elements.length) return {}
return (
studyright_elements
@@ -1,40 +1,167 @@
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 DataTable from 'components/StudyProgramme/BasicOverview/DataTable'
import InteractiveDataTable from '../InteractiveDataView'
import Toggle from '../../StudyProgramme/Toggle'
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 })
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">
@@ -46,39 +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} />
<DataTable
cypress="StudentsOfTheFaculty"
data={basics?.data?.tableStats}
titles={basics?.data?.titles}
/>
<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}
/>
<DataTable
cypress="CreditsProducedByTheFaculty"
data={credits?.data?.tableStats}
titles={credits?.data?.titles}
/>
<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 of the faculty',
'GraduatedOfTheFaculty',
basics?.data?.graduationInfo.titles,
basics?.data?.graduationInfo.tableStats,
basics?.data?.graduationInfo.programmeTableStats,
basics?.data?.programmeNames
)}
<div className="section-container">
<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
@@ -0,0 +1,153 @@
/* eslint-disable react/no-this-in-sfc */
import React from 'react'
import Highcharts from 'highcharts'
import ReactHighcharts from 'react-highcharts'
const colors = ['#7cb5ec', '#90ed7d', '#434348', '#f7a35c', '#FFF000', '#2b908f', '#f45b5b', '#91e8e1']
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()
const differenceArray = Object.keys(differenceData).reduce(
(programmes, programme) => ({
...programmes,
[programme]: differenceData[programme].reduce(
(results, val, currentIdx) => ({
...results,
[names[currentIdx]]: val,
}),
{}
),
}),
{}
)
const getCorrectSign = change => {
if (change > 0) return `+${change.toString()}`
return change
}
// 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'
if (change < 0) return '#ff7979'
return '#7B9FCF'
}
// 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,
height: getFlexHeight(labels.length, needsExtra, manyProgrammes, labels),
},
credits: {
text: 'oodikone | TOSKA',
},
title: {
text: '',
style: {
display: 'none',
},
},
series: dataTranspose,
xAxis: {
categories: labels,
title: {
text: '',
},
fontSize: '24px',
},
yAxis: {
min: 0,
title: {
text: '',
},
stackLabels: {
enabled: true,
style: {
color: (Highcharts.defaultOptions.title.style && Highcharts.defaultOptions.title.style.color) || 'gray',
},
fontSize: '24px',
},
},
legend: {
layout: 'horizontal',
align: 'left',
fontSize: '24px',
x: 20,
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 />`
const diffArray = differenceArray[this.x]
this.points.forEach(point => {
tooltipString += `<span style="color:${point.color}">●</span> <b>${point.series.name}: ${point.y}</b>
(<span style="color:${getColor(diffArray[point.series.name])};font-weight:bold">${getCorrectSign(
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: 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,
},
},
},
},
}
if (!dataTranspose) {
return <>No data provided</>
}
return <ReactHighcharts config={defaultConfig} />
}
export default CollapsedStackedBar
@@ -0,0 +1,158 @@
import React, { useEffect, useState } from 'react'
import { Table, Menu } from 'semantic-ui-react'
import CollapsedStackedBar from './CollapsedStackedBar'
import BasicRow from './BasicRow'
const InteractiveDataTable = ({
cypress,
dataStats,
dataProgrammeStats,
programmeNames,
sortedKeys,
titles,
language,
sliceStart,
extraHeight,
yearsVisible,
shortNames,
studentsTable,
}) => {
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)
3. Last year, currently 2017 is initialized to zeros
4. A new object with programme names as keys and the difference array as value is created
*/
const calculatDiffToPrevYear = programmeData => {
const differenceMatrix = Object.keys(programmeData).reduce(
(differenceMatrix, programme) => ({
...differenceMatrix,
[programme]: programmeData[programme].map((_yearlyValues, yearIndex) =>
yearIndex < programmeData[programme].length - 1
? programmeData[programme][yearIndex]
.slice(sliceStart)
.map((val, idx) => val - programmeData[programme][yearIndex + 1][idx + sliceStart])
: new Array(programmeData[programme][yearIndex].length - sliceStart).fill(0)
),
}),
{}
)
return differenceMatrix
}
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)
return (
<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()}`} textAlign="center">
{titles?.map(title => (
<Table.HeaderCell key={title}>{title}</Table.HeaderCell>
))}
</Table.Row>
</Table.Header>
<Table.Body>
{dataStats?.map((yearArray, yearIndex) => (
<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>
</div>
)
}
export default InteractiveDataTable
@@ -0,0 +1,147 @@
/* eslint-disable react/no-this-in-sfc */
import React from 'react'
import ReactHighcharts from 'react-highcharts'
const BarChart = ({
data,
categories,
goal,
handleClick,
facultyGraph = true,
year,
label,
programmeNames,
language = null,
showMeanTime,
}) => {
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: {
backgroundColor: 'white',
fontSize: '25px',
// eslint-disable-next-line
formatter: function() {
const sortingText =
label === 'Start year'
? `<b>From class of ${facultyGraph ? this.x : year}, ${this.point.amount} students have graduated</b>`
: `<b>${this.point.amount} students graduated in year ${facultyGraph ? this.x : year}</b>`
// eslint-disable-next-line prettier/prettier
const timeText = `<br /><p>${sortingText}, <br /><b>${showMeanTime ? 'mean' : 'median'} study time: ${this.y} months</p></b>`
const statistics = `<br /><p>${this.point.statistics.onTime} graduated on time</p><br />
<p>${this.point.statistics.yearOver} graduated max year overtime</p>
<br /><p>${this.point.statistics.wayOver} graduated over year late</p>`
if (!facultyGraph)
return `<b>${
programmeNames[this.x]?.[language] ? programmeNames[this.x]?.[language] : programmeNames[this.x]?.fi
}</b><br />${this.x}${timeText}${statistics}`
// eslint-disable-next-line
return `${timeText}${statistics}` ;
},
},
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,72 @@
import React, { useState } from 'react'
import { Divider, Message } from 'semantic-ui-react'
import BarChart from './BarChart'
import useLanguage from '../../LanguagePicker/useLanguage'
const GraduationTimes = ({
title,
years,
data,
level,
goal,
label,
levelProgrammeData,
programmeNames,
showMeanTime,
}) => {
const [programmeData, setProgrammeData] = useState(false)
const [year, setYear] = useState(null)
const { language } = useLanguage()
if (!data.some(a => a.amount > 0)) return null
const handleClick = (e, isFacultyGraph) => {
if (isFacultyGraph) {
setYear(e.point.category)
setProgrammeData(true)
} else {
setProgrammeData(false)
setYear(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}
programmeNames={programmeNames}
showMeanTime={showMeanTime}
/>
{!programmeData ? (
<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={levelProgrammeData[year]?.programmes}
data={levelProgrammeData[year]?.data}
goal={goal}
facultyGraph={false}
handleClick={handleClick}
year={year} // programmeData?.year}
label={label}
programmeNames={programmeNames}
language={language}
showMeanTime={showMeanTime}
/>
)}
</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,132 @@
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 programmeNames = graduationStats?.data?.programmeNames
const commonProps = { years, label, programmeNames, showMeanTime }
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}
goal={goals?.bachelor}
levelProgrammeData={programmeData?.bachelor}
{...commonProps}
/>
<GraduationTimes
level="bcMsCombo"
title="Bachelor + Master"
data={data?.bcMsCombo}
goal={goals?.bcMsCombo}
levelProgrammeData={programmeData?.bcMsCombo}
{...commonProps}
/>
<GraduationTimes
level="master"
title="Master"
data={data?.master}
goal={goals?.master}
levelProgrammeData={programmeData?.master}
{...commonProps}
/>
<GraduationTimes
level="doctor"
title="Doctor"
data={data?.doctor}
goal={goals?.doctor}
levelProgrammeData={programmeData?.doctor}
{...commonProps}
/>
<GraduationTimes
level="licentiate"
title="Licentiate"
data={data?.licentiate}
goal={goals?.licentiate}
levelProgrammeData={programmeData?.licentiate}
{...commonProps}
/>
</div>
</>
)}
</>
)}
</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
@@ -27,13 +27,13 @@
.faculty-courses .radio-toggle {
display: flex;
justify-content: center;
margin: 20px 35px 20px 35px;
margin: 20px 35px;
}
.programmes-overview .radio-toggle {
display: flex;
justify-content: center;
margin: 0 35px 70px 35px;
margin: 0 35px 70px;
}
.programmes-overview .toggle-label,
@@ -61,7 +61,7 @@
.programmes-overview .ui.toggle.checkbox input:focus:checked ~ label::before,
.faculty-overview .ui.toggle.checkbox input:focus:checked ~ label::before,
.faculty-courses .ui.toggle.checkbox input:focus:checked ~ label::before {
background-color: rgba(0, 0, 0, 0.05) !important;
background-color: rgb(0 0 0 / 5%) !important;
}
.faculty-overview .divider {
@@ -110,12 +110,12 @@
.programmes-overview .datatable .total-row-cell,
.programmes-overview .table-container .total-row-cell {
border-top: 3px solid rgb(189, 180, 180);
border-top: 3px solid rgb(189 180 180);
font-weight: bold;
}
.programmes-overview .programmes-selector {
margin: 60px 60px 30px 60px;
margin: 60px 60px 30px;
}
.programmes-overview .header-row {
@@ -123,7 +123,7 @@
}
.programmes-overview .datatable {
margin: 2em 0 5em 0;
margin: 2em 0 5em;
}
.programmes-overview .no-data-message {
@@ -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>
@@ -35,7 +35,7 @@
}
.filter-tray-toggle .button .button-label > .label {
padding: 0.5em 0.7em 0.3em 0.7em;
padding: 0.5em 0.7em 0.3em;
margin-top: -2px;
margin-bottom: -2px;
}
@@ -68,7 +68,7 @@
margin: 0 0 0 3rem;
overflow-y: scroll;
overflow-x: hidden;
padding: 1.5rem 0 10rem 0;
padding: 1.5rem 0 10rem;
}
#filter-tray-container > .header {
@@ -112,7 +112,7 @@
}
#filter-tray .card {
background-color: rgba(255, 255, 255, 0.08);
background-color: rgb(255 255 255 / 8%);
border: none;
box-shadow: none;
margin-top: 0;
@@ -140,7 +140,7 @@
#filter-tray.cards .description,
#filter-tray.cards .extra {
padding: 0 0.8rem 0.8rem 0.8rem;
padding: 0 0.8rem 0.8rem;
margin: 0;
}
@@ -171,7 +171,7 @@
#filter-tray .description-text {
color: #9e9e9e;
font-style: italic;
margin: 1rem 0.2rem 0 0.2rem;
margin: 1rem 0.2rem 0;
}
#filter-tray .card-content {
@@ -1,6 +1,6 @@
.filter-card-header .filter-clear-icon {
margin-top: -0.2em;
color: rgb(136, 136, 136);
color: rgb(136 136 136);
}
.filter-card-header .filter-clear-icon:hover {
@@ -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({
@@ -1,3 +1,4 @@
/* eslint-disable camelcase */
import React from 'react'
import { Form, Dropdown } from 'semantic-ui-react'
import { getTextIn } from 'common'
@@ -11,6 +12,7 @@ const StudyTrackFilterCard = ({ options, onOptionsChange, withoutSelf, activeAt,
const activeAtMoment = activeAt && moment(activeAt)
const dropdownOptions = _.chain(withoutSelf())
.filter(student => !student.transferredStudyright)
.flatMap(student => student.studyrights)
.map(sr => sr.studyright_elements)
.filter(sre => sre.some(e => e.code === code))
@@ -76,27 +78,23 @@ export default createFilter({
filter: (student, { selected, args }) => {
const activeAt = _.get(args, 'activeAt', moment())
return (
student.studyrights
// eslint-disable-next-line camelcase
.filter(({ studyright_elements, graduated }) =>
studyright_elements.some(
sre =>
sre.element_detail.type === 30 &&
(!activeAt ||
moment(activeAt).isBetween(
moment(sre.startdate),
graduated ? moment() : moment(sre.enddate),
'day',
'[]'
))
)
return student.studyrights
.filter(({ studyright_elements, graduated }) =>
studyright_elements.some(
sre =>
sre.element_detail.type === 30 &&
(!activeAt ||
moment(activeAt).isBetween(
moment(sre.startdate),
graduated ? moment() : moment(sre.enddate),
'day',
'[]'
))
)
// eslint-disable-next-line camelcase
.flatMap(({ studyright_elements }) => studyright_elements)
.map(sre => sre.element_detail.code)
.some(code => selected.includes(code))
)
)
.flatMap(({ studyright_elements }) => studyright_elements)
.map(sre => sre.element_detail.code)
.some(code => selected.includes(code))
},
render: (props, { args }) => (
@@ -6,6 +6,7 @@ blockquote {
position: relative;
padding: 40px 20px;
}
blockquote::before {
content: open-quote;
top: 0;
@@ -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>
@@ -2,7 +2,7 @@
display: flex;
flex-direction: column;
padding: 0;
background-color: rgba(0, 0, 0, 0.05);
background-color: rgb(0 0 0 / 5%);
}
.ok-infobox.ok-infobox.ok-infobox .content-container {
@@ -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',
@@ -43,11 +43,6 @@ const NavigationBar = () => {
const showAsUser = useShowAsUser()
const [logout] = useLogoutMutation()
// Remove population tab for user with only IAM rights, delete when limited access ready
if (!isLoading && !isAdmin && rights.length === 0 && iamRights.length !== 0) {
delete allNavigationItems.populations.items[0]
}
const refreshNavigationRoutes = () => {
const visibleNavigationItems = {}
if (isLoading) return visibleNavigationItems
@@ -59,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>
),
@@ -352,6 +365,10 @@ const PopulationCourseStats = props => {
if (pending) {
return null
}
if (props.onlyIamRights) {
panes.pop()
}
return (
<div>
<PopulationCourseContext.Provider value={contextValue}>
@@ -9,7 +9,7 @@ import FilterDegreeCoursesModal from './FilterDegreeCoursesModal'
import { getPopulationSelectedStudentCourses } from '../../redux/populationSelectedStudentCourses'
import infotooltips from '../../common/InfoToolTips'
const PopulationCourses = ({ query = {}, filteredStudents, selectedStudentsByYear }) => {
const PopulationCourses = ({ query = {}, filteredStudents, selectedStudentsByYear, onlyIamRights }) => {
const [showByStudytrack, setShowByStudytrack] = useState(true)
const populationCourses = useSelector(({ populationCourses }) => populationCourses)
const mandatoryCourses = useSelector(({ populationMandatoryCourses }) => populationMandatoryCourses)
@@ -23,12 +23,17 @@ const PopulationCourses = ({ query = {}, filteredStudents, selectedStudentsByYea
return populationSelectedStudentCourses.query.selectedStudents?.length === filteredStudents.length
}
const getSelectedStudents = students =>
onlyIamRights
? students.map(({ studentNumber, iv }) => ({ encryptedData: studentNumber, iv }))
: students.map(student => student.studentNumber)
const fetch = courses => {
dispatch(
getPopulationSelectedStudentCourses({
...query,
studyRights: [query.studyRights.programme],
selectedStudents: filteredStudents.map(s => s.studentNumber),
selectedStudents: getSelectedStudents(filteredStudents),
selectedStudentsByYear,
courses,
})
@@ -79,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 ? (
@@ -88,6 +95,7 @@ const PopulationCourses = ({ query = {}, filteredStudents, selectedStudentsByYea
courses={selectedPopulationCourses.data}
pending={pending}
filteredStudents={filteredStudents}
onlyIamRights={onlyIamRights}
/>
) : (
<CustomPopulationCourses

Some files were not shown because too many files have changed in this diff Show More