Compare commits

...
281 Commits
Author SHA1 Message Date
outisa 3d552a5811 Fix typos 2023-02-03 22:19:14 +02:00
outisa 624a103f08 Add moar wating time 2023-02-03 21:26:19 +02:00
outisa 9f56554ed9 Fix criteria to show correctly after change 2023-02-03 19:31:28 +02:00
outisa 0d1bc3c519 Add betterment for placeholder credit criteria 2023-02-03 19:02:04 +02:00
outisa aa1d21bc09 Modify infotext 2023-02-03 18:13:00 +02:00
outisa eaf5d7574e Add frontend functionality to change criteria for studyprogramme 2023-02-03 17:53:39 +02:00
outisa 9db14a839f Add backend functionality to change criteria for studyprogrammes 2023-02-03 17:51:32 +02:00
outisa b55d649622 Create table for atudy programme criteria 2023-02-03 17:47:50 +02:00
Miko Keskimäki 47c792e1f1 Revert "Dependabot updates"
This reverts commit 2c76703085.
2023-01-30 18:55:29 +02:00
Miko Keskimäki fffa3d3ee1 Maybe fix package updates breaking dev env setup 2023-01-30 18:41:32 +02:00
Laura Sinikallio c9407ff4b0 Improve students table column layout 2023-01-26 12:55:48 +02:00
outisa 1b43d4fc39 [Faculty] Add (new in faculty) clarifiaction for started studying 2023-01-25 16:54:19 +02:00
Laura Sinikallio c0ce396d4d Try fixing last of duplicateprogramme id issue 2023-01-20 16:34:59 +02:00
Laura Sinikallio f15ffa95f5 Update infoboxes 2023-01-20 15:40:17 +02:00
Laura Sinikallio 765d2f771a Fix lint issue 2023-01-20 15:19:20 +02:00
Laura Sinikallio e1271d7d1f Adjust table header texts 2023-01-20 14:55:54 +02:00
Laura Sinikallio 7f632bde34 Enable line break in long column headers 2023-01-20 14:44:45 +02:00
Laura Sinikallio 9dd47d002d Make column name react to HOPS filter 2023-01-20 13:54:01 +02:00
Miko Keskimäki 16a07ace39 [skip ci] Update architecture diagram 2023-01-20 09:02:00 +02:00
outisa ac756cb35b Add message box for bachelor-master credits 2023-01-19 17:44:24 +02:00
outisa b9059eb058 [Faculty] Separate programmes with different colors 2023-01-19 16:55:08 +02:00
Miko Keskimäki 1f967a1f2c [skip ci] Update Oodikone architecture diagram 2023-01-19 16:21:54 +02:00
outisa e8ef944a9d Bring total population link back 2023-01-19 13:06:48 +02:00
outisa 0e9ea20235 [Study programme] Remove unnecessary code 2023-01-19 11:05:45 +02:00
outisa 8d4f1c7794 [studyprogramme] Fix master titles and remove unnecessary code 2023-01-19 10:46:46 +02:00
Laura Sinikallio 6f3e4b8f64 Update infobox 2023-01-19 10:45:29 +02:00
Laura Sinikallio 316ed2af2f Adjust students table column order 2023-01-19 10:37:23 +02:00
Laura Sinikallio 2f753d976b Adjust students table visibility 2023-01-19 10:24:21 +02:00
iPegii 9b6aa96531 Merge branch 'master' of github.com:UniversityOfHelsinkiCS/oodikone 2023-01-19 10:05:08 +02:00
iPegii 229a64aa45 Update text for "hyväksiluku"-icon 2023-01-19 10:04:51 +02:00
Miko Keskimäki 6456b84777 Get kosu and jory info from Jami, grant kosus full access to their programmes 2023-01-18 16:46:35 +02:00
Laura Sinikallio 9d34c34de7 Unify spelling, remove notification asterisk 2023-01-18 14:51:58 +02:00
Laura Sinikallio 2573976ea4 Update students table excel to match previous table changes 2023-01-18 14:04:25 +02:00
Laura Sinikallio 389b71cfbf Students table: Add start of studyright column and touch up started in programme column 2023-01-18 13:22:55 +02:00
Laura Sinikallio c0ee74b7f8 Population filters - Add passive to enrollment status filter 2023-01-18 10:07:40 +02:00
iPegii e3fbf91b99 fix frontend lint 2023-01-17 22:03:40 +02:00
iPegii 3d1f7cc655 Merge branch 'master' of github.com:UniversityOfHelsinkiCS/oodikone 2023-01-17 20:50:46 +02:00
iPegii 069bd97729 [student page] show different icon for courses 'approved' 2023-01-17 20:50:37 +02:00
outisa 96fded285b [study programme] Fix MH90_001 progress table and graph 2023-01-17 20:40:56 +02:00
iPegii 9c172c2f97 Merge branch 'master' of github.com:UniversityOfHelsinkiCS/oodikone 2023-01-17 20:39:48 +02:00
iPegii d37b037cc6 Column sticky on 'courses of class' 2023-01-17 20:39:35 +02:00
outisa a55eef0f34 [studyprogramme] Change credit limits for master programme 2023-01-17 19:35:26 +02:00
iPegii 01ec83d909 Merge branch 'master' of github.com:UniversityOfHelsinkiCS/oodikone 2023-01-17 19:26:01 +02:00
iPegii 81e8a0c77e [pop statistics - student] courses, column sticky 2023-01-17 19:25:33 +02:00
Miko Keskimäki 7782f4f863 Move iam logic from oodikone to jami special groups 2023-01-17 15:15:52 +02:00
outisa 80f52355bb [Studyprogramme] Remove LIC/DOC thesis writers 2023-01-17 10:53:19 +02:00
outisa 28cf6c855d [Faculty] Improve thesis writers infotext 2023-01-16 21:17:46 +02:00
outisa 6d446a1c05 Chance column name Finnish to Finland 2023-01-16 21:16:55 +02:00
outisa d8214e7432 Fix test 2023-01-16 21:15:42 +02:00
outisa a8b04d56d6 [Faculty-progress] Show academic year if data available 2023-01-16 21:14:43 +02:00
Miko Keskimäki 0fc7bfda51 Remove specialGroup data for now
Messes up current IAM logic and not yet used for anything
2023-01-16 16:57:17 +02:00
iPegii 5614a6eb3f [Custom pop] Accept student number w/o leading zero 2023-01-16 15:23:29 +02:00
iPegii dfde4adf87 Merge branch 'master' of github.com:UniversityOfHelsinkiCS/oodikone 2023-01-12 20:27:05 +02:00
iPegii 32a8c7c261 [class statistics] Show also grade for openUni 2023-01-12 20:26:56 +02:00
Laura Sinikallio b7390912f4 Fix typo 2023-01-12 10:27:20 +02:00
Laura Sinikallio 6af7a1a5d0 Add some documentation 2023-01-12 10:25:18 +02:00
Laura Sinikallio e24b63da74 Merge branch 'master' of github.com:UniversityOfHelsinkiCS/oodikone 2023-01-11 16:31:30 +02:00
Miko Keskimäki bb5bb8d04e [skip ci] Minor setup script fix 2023-01-11 16:13:26 +02:00
Laura Sinikallio a91fb5d7b7 Merge branch 'master' of github.com:UniversityOfHelsinkiCS/oodikone 2023-01-11 15:46:42 +02:00
Miko Keskimäki 5369e75b61 Update CI docker-compose 2023-01-11 14:40:46 +02:00
Laura Sinikallio 7a43433947 Handle duplicate programme name ATM 2023-01-11 14:37:12 +02:00
Miko Keskimäki 656870028e Get iams for login as from jami 2023-01-11 14:16:52 +02:00
Miko Keskimäki 1315d7ca81 Get iam groups for user table from jami 2023-01-11 13:53:31 +02:00
Miko Keskimäki 3b1f002ac8 Refactor jamiClient, test connection 2023-01-11 13:49:38 +02:00
Miko Keskimäki 00116544bf Update setup scripts 2023-01-11 13:12:22 +02:00
Miko Keskimäki 7bbccc9a81 Add jami-db to dev environment 2023-01-11 12:01:29 +02:00
outisa c18187d094 [Faculty] Modify sorting actually work in all use cases 2023-01-10 23:44:26 +02:00
outisa 7688a22c53 [Faculty programmes] Remove licentiates from doctoral view 2023-01-10 19:45:48 +02:00
outisa 49df0a0d21 [Faculty studentstats] Do not show programmes that have no data 2023-01-10 19:39:40 +02:00
outisa 449e8976d9 [Faculty basics] Remove doctors and licentiates from thesiswriters 2023-01-10 19:29:31 +02:00
outisa 6871502cb9 [Faculty progess] Modify info text 2023-01-10 19:24:50 +02:00
iPegii b72bedc49f Fix hover text when course code match 2023-01-10 10:44:00 +02:00
iPegii 9f300ccab0 [users] fix crashing on programme search 2023-01-09 18:29:59 +02:00
iPegii cb8aa00f6f [student statistics] show most recent in search 2023-01-09 17:41:40 +02:00
iPegii 4a05f136cc remove "new"-icon from Study Programme 2023-01-09 15:39:40 +02:00
iPegii 50840a3fe2 [Class statistics] improve term on credits column 2023-01-09 15:33:28 +02:00
iPegii ac440ff843 Merge branch 'master' of github.com:UniversityOfHelsinkiCS/oodikone 2023-01-09 15:08:37 +02:00
iPegii b6d28c6cff improve colunm order in [class statistics] student table 2023-01-09 15:08:21 +02:00
Laura Sinikallio f79539c2a6 Adjust chart size on small dataset 2023-01-09 14:09:17 +02:00
Laura Sinikallio c46173cc98 Merge branch 'master' of github.com:UniversityOfHelsinkiCS/oodikone 2023-01-09 14:07:52 +02:00
Laura Sinikallio a13c0e4628 Update info text 2023-01-09 13:51:24 +02:00
outisa a4887a30bc Fix progress tab faculty tests 2023-01-09 13:51:13 +02:00
Laura Sinikallio bf3adf446f Update tests 2023-01-09 13:44:30 +02:00
Laura Sinikallio a03d1c9707 Faculty - switch grad times default display 2023-01-09 13:27:43 +02:00
Laura Sinikallio 2d9fe72481 Update tests 2023-01-09 13:21:06 +02:00
Laura Sinikallio 15ef2f156e Merge branch 'master' of github.com:UniversityOfHelsinkiCS/oodikone 2023-01-09 13:20:07 +02:00
outisa 5470d53a19 [Faculty] Show coming academic year in studentstats only if data available 2023-01-09 13:13:50 +02:00
outisa 65eedf1de8 [Open Uni] Handle invalid dates better 2023-01-09 13:04:55 +02:00
Laura Sinikallio 1a9e51534b Studyprogramme - switch grad times default display 2023-01-09 13:03:19 +02:00
Miko Keskimäki 0d7d059541 Fix lint error 2023-01-09 12:04:34 +02:00
Miko Keskimäki 2c76703085 Dependabot updates 2023-01-09 11:52:13 +02:00
outisa 81c45c383f Fix test 2023-01-09 11:01:19 +02:00
outisa 9bb456e258 [Faculty - progress tab] Modify charts to be more compact 2023-01-09 10:45:00 +02:00
outisa c9641bb118 [Faculty - programmes tab] Add hover for programmenames and table titles 2023-01-08 13:11:51 +02:00
outisa 06d129b685 Handle cases when programmelist is empty 2023-01-05 19:33:35 +02:00
outisa b78446a11a [faculty basic view] Improve sorting 2023-01-05 19:09:53 +02:00
outisa 3511b947c4 [Faculty - programmes tab] Fix sorting 2023-01-05 16:18:23 +02:00
iPegii a338b2f4d5 Merge branch 'master' of github.com:UniversityOfHelsinkiCS/oodikone 2023-01-05 10:22:26 +02:00
iPegii 3de3bbab43 improve hover text on study programme 2023-01-05 10:22:13 +02:00
outisa b24b30b179 [faculty-basic view] Add horizontal lines programme breakdown graphs 2023-01-04 18:48:12 +02:00
outisa 8af06138a9 Update filtering in studytrack update function 2023-01-04 18:45:43 +02:00
outisa 8b30e2b416 Fix sorting order for studytracks 2023-01-04 16:04:24 +02:00
Laura Sinikallio 562ee801d7 Remove mean counting (studytrack) 2023-01-04 14:42:41 +02:00
Laura Sinikallio eb9b3ccaae Remove mean counting (studyprogramme) 2023-01-04 14:34:57 +02:00
Laura Sinikallio 3bf1c0e47d Remove mean counting (faculty) 2023-01-04 14:28:02 +02:00
Laura Sinikallio 2f028ff9e0 Remove old code, improve tooltip 2023-01-04 14:14:13 +02:00
Laura Sinikallio b529c219ea Update info box 2023-01-04 13:51:43 +02:00
Laura Sinikallio 6369bdb62c Studyprogramme-studytrack: switch graduation times mean display to breakdown display 2023-01-04 13:47:23 +02:00
Laura Sinikallio 69803e528f Update info texts 2023-01-04 12:57:44 +02:00
Laura Sinikallio e71f75bd72 Studyprogramme: switch graduation times mean display to breakdown display 2023-01-04 12:14:29 +02:00
Laura Sinikallio 964500fb52 Edit test 2023-01-03 20:46:34 +02:00
Laura Sinikallio 9241efb16a Update tests and infobox 2023-01-03 20:24:17 +02:00
Laura Sinikallio e585e59907 Faculty grad times: Replace mean stats with goal time based statistics 2023-01-03 20:14:21 +02:00
outisa 5a4e724021 Fix the fixed advanced filters 2023-01-03 13:59:29 +02:00
outisa 7adc101024 [faculty basics] Ignore (duplicate) MH70_008_2 2023-01-03 11:41:37 +02:00
Laura Sinikallio 57bfa879ae Remove unused component 2023-01-03 09:15:02 +02:00
outisa 38174d36d9 [class statistics] Fix advanced filter to keep correct year 2023-01-03 08:57:40 +02:00
outisa 062bdfbe0b Fix gender code to be string in the models 2023-01-02 20:41:30 +02:00
outisa d0540610c1 Remove copy paste 2023-01-02 20:30:42 +02:00
iPegii db43907ce3 Merge branch 'master' of github.com:UniversityOfHelsinkiCS/oodikone 2023-01-02 18:48:06 +02:00
iPegii be906d262e Fix filtering of last login in Users 2023-01-02 18:48:00 +02:00
outisa a28b7d8bc5 [class/ custom populations] Add temporary fix to courses filter 2023-01-02 17:55:41 +02:00
iPegii ba9c3ac3e3 Merge branch 'master' of github.com:UniversityOfHelsinkiCS/oodikone 2023-01-02 17:18:04 +02:00
iPegii 644ef5a031 SortableTable search to be case-insensitive 2023-01-02 17:15:10 +02:00
Laura Sinikallio 80e6e7f5dc Grad times: breakdown chart 2023-01-02 17:01:36 +02:00
Miko Keskimäki 962381e8b7 Fix user id being sent to jami 2023-01-02 16:09:29 +02:00
outisa 26f0eb80c1 [faculty - progress stats] Add betterments for calculating starters 2022-12-28 20:18:31 +02:00
outisa ff901f63b8 [Faculty updates] Fix given programmes data 2022-12-28 17:30:41 +02:00
outisa 0e601fa712 faculty betterments 2022-12-24 22:03:04 +02:00
outisa dde20472e4 [faculty-studyprogramme] Remove population link from Total (#4049) 2022-12-21 15:01:40 +02:00
outisa 9097a3e32d Fix tests and stats 2022-12-21 14:39:04 +02:00
outisa 533dd9aeba [faculty-studyprogramme] Fix totals + doctoral progress stats 2022-12-21 14:06:36 +02:00
Miko Keskimäki c2b6006580 Disable broken tests 2022-12-21 08:59:23 +02:00
Miko Keskimäki b051783ecf Try again 2022-12-20 15:18:35 +02:00
Miko Keskimäki a3730bb88f Try fixing iam tests 2022-12-20 14:35:07 +02:00
Miko Keskimäki 3deedff357 Get IAM access in CI 2022-12-20 14:05:21 +02:00
Miko Keskimäki 906d66e5cd Try to re-enable IAM tests 2022-12-20 14:00:31 +02:00
Miko Keskimäki 467009c598 Run Jami locally in Dev and CI enviroment 2022-12-20 13:59:07 +02:00
Miko Keskimäki ba233aba5e Remove no longer needed IAM logic 2022-12-20 12:27:18 +02:00
Miko Keskimäki 93656322b2 Skip iam tests for now
New microservice is not accessible from test runner
2022-12-20 10:41:01 +02:00
Miko Keskimäki 819c8cda22 Refactor user rights, only fetch iam access when needed 2022-12-20 10:25:35 +02:00
Miko Keskimäki ab76ee4349 Use jami microservice for getting iam rights 2022-12-20 10:24:02 +02:00
Laura Sinikallio 0ccb1de58b Add course code to CSV export 2022-12-18 15:12:15 +02:00
outisa 128a41c0ab Add missing line for openuni test 2022-12-17 12:35:22 +02:00
outisa 277f88a215 Add missing line for openuni test 2022-12-17 12:33:52 +02:00
outisa 354e97e100 Add openuni tests into workflow 2022-12-17 12:28:49 +02:00
outisa e65a96ced5 [Cypress] Add open uni and infobox tests 2022-12-17 12:07:50 +02:00
outisa e19a2b574d [class stats] Remove started in studyright -column 2022-12-16 15:51:24 +02:00
outisa 26dd92288e [cypress] Add moar cypress fun 2022-12-15 22:09:23 +02:00
outisa 9c83bae7af [Faculty -progress] Modify backend logic 2022-12-15 21:35:39 +02:00
Laura Sinikallio 57c6c500a2 Remove unneeded code 2022-12-15 12:08:56 +02:00
outisa 1ecd01d55f [faculty] Fix progress tab update 2022-12-15 10:35:32 +02:00
Laura Sinikallio 80248639df Update tests 2022-12-15 10:33:08 +02:00
Laura Sinikallio ad0bbf996c Merge branch 'master' of github.com:UniversityOfHelsinkiCS/oodikone 2022-12-15 10:12:12 +02:00
Laura Sinikallio f904f63b9a Text updates 2022-12-15 10:10:29 +02:00
outisa 3b82be53d0 Merge branch 'master' of github.com:UniversityOfHelsinkiCS/oodikone 2022-12-14 22:40:53 +02:00
outisa 6d1120f3a4 [faculty] Add betterments for progress tab 2022-12-14 22:40:18 +02:00
Laura Sinikallio 7180dd5a34 More exception handling 2022-12-14 18:04:08 +02:00
outisa 5806cd6baf [studytrack-programme] Fix bug of showing data for MH90_001 2022-12-14 17:06:17 +02:00
Laura Sinikallio e98df56fab Update tests 2022-12-14 15:50:44 +02:00
Laura Sinikallio 1b7f60c4a2 Update info texts 2022-12-14 15:29:00 +02:00
Laura Sinikallio 6a85dd10a1 Update tests 2022-12-14 15:14:56 +02:00
Laura Sinikallio 679175385f Remove retired code 2022-12-14 14:56:43 +02:00
Laura Sinikallio 84cfde0c39 Switch studytrack graduation times to bar chart 2022-12-14 14:44:26 +02:00
Laura Sinikallio 878b391aac Clean up 2022-12-13 13:40:24 +02:00
Laura Sinikallio 53129e7683 Studytrack graduation times: add new counting system 2022-12-13 13:38:28 +02:00
Laura Sinikallio 27eb8984b5 Studyprogramme: graduations by start year 2022-12-12 18:39:55 +02:00
Laura Sinikallio e1f5bc5ec2 Remove old code 2022-12-12 12:52:48 +02:00
Laura Sinikallio 3698b8d38b Update test 2022-12-11 19:05:14 +02:00
Laura Sinikallio 850a429165 Graduation times: unify faculty vs. studyprogramme 2022-12-11 18:48:31 +02:00
Miko Keskimäki 9e1c0751d3 Log user's iam groups when login fails 2022-12-09 13:15:07 +02:00
outisa 414495590f Merge branch 'master' of github.com:UniversityOfHelsinkiCS/oodikone 2022-12-08 19:16:49 +02:00
outisa 915128dce6 [Faculty] Add faculty fixes and updates 2022-12-08 19:16:40 +02:00
Laura Sinikallio 8cb8e3c595 Add course code to CSV 2022-12-08 18:43:28 +02:00
Laura Sinikallio 432fd7dbc0 Discount statutory absences from studyprogramme graduation times 2022-12-07 19:28:11 +02:00
outisa b07070dc7f [faculty - programme progress] Fix stats 2022-12-07 18:13:24 +02:00
outisa 2ef6c2a47f [merge] overlaping changes in faculty.js 2022-12-07 18:11:39 +02:00
outisa 67de743838 Fetch studyrights 2022-12-07 18:02:46 +02:00
Laura Sinikallio 37f7e06ba5 Refactor common code location 2022-12-07 15:01:34 +02:00
Laura Sinikallio 7e798821c8 Test commit 2022-12-07 12:00:46 +02:00
Laura Sinikallio 04692463f0 Add pre-commit check for 'only' tests 2022-12-07 11:58:27 +02:00
Laura Sinikallio c3fecddc61 Aaaaand remove 'only' from test 2022-12-07 11:38:29 +02:00
Laura Sinikallio bed3e64e1f Expand tests 2022-12-07 11:37:11 +02:00
Laura Sinikallio 98cb0f5e3e Studyprogramme: display grad times breakdown stats 2022-12-07 10:56:57 +02:00
outisa d062788864 [faculty] Modify laastarifix with transfers 2022-12-07 10:51:39 +02:00
Laura Sinikallio f2b19d5f74 Studyprogramme: display separate Ms and Bc+Ms grad times for master programmes 2022-12-07 10:47:57 +02:00
outisa e624152e3e [Faculty - progress] Refine stats for H90 2022-12-06 13:33:10 +02:00
outisa d296508eb5 Remove statistics until field from tests 2022-12-06 13:30:48 +02:00
outisa 88b00beb5d [faculty] Add laastarifix facultyBasics transfers 2022-12-06 11:55:42 +02:00
outisa a9057b949a [Class stats] Remove adv. settings filter statistics until 2022-12-05 15:29:30 +02:00
Laura Sinikallio d1744d9fb8 More studyprogramme grad times rewriting 2022-12-03 15:36:06 +02:00
outisa 3e83b800d7 [Course statistics] Fix bug 2022-12-02 15:47:23 +02:00
Laura Sinikallio 49891f3779 Programme overview - switch times to bar display 2022-11-27 16:18:13 +02:00
outisa 0aaa745763 [Class statistics] Fix graduation time in csv to show correctly 2022-11-24 19:35:33 +02:00
outisa e4edf31057 [Class statistics] Add phonenumber into excel 2022-11-24 19:32:33 +02:00
outisa 8c8b8ccb1d Add phone number column into student table 2022-11-24 18:23:13 +02:00
outisa dc339896a1 [Updater] Add phone number column to student table 2022-11-24 17:16:36 +02:00
Laura Sinikallio da0f76e2a8 Start transition from gauges to bars in studyprogramme grad times 2022-11-24 16:34:41 +02:00
outisa 0fa9d8a1ca [Open uni] Change icon color value to accepted one 2022-11-24 16:16:41 +02:00
outisa 95066dde6e [Open uni] Betterments for the view 2022-11-24 16:13:20 +02:00
outisa e17ff572bf [Open uni] Create extra columns shown only in csv 2022-11-22 18:04:13 +02:00
outisa 4eb748f76d Fix hook order problem 2022-11-22 13:13:29 +02:00
outisa e6147effdf [Open uni] Allow adding the course codes with different separators 2022-11-22 12:00:39 +02:00
outisa 7e333668c5 Refactor components 2022-11-21 18:27:25 +02:00
outisa 45fb6659a9 [Open uni] Possible fix for 'recursive fn exceeding maximum stack size' 2022-11-21 17:50:53 +02:00
outisa 0f73e5025b [Open uni] Add small modifications 2022-11-18 17:44:06 +02:00
outisa 69f0b4cb75 [Updater] Map unknown marketing allowance as null 2022-11-18 17:18:03 +02:00
Laura Sinikallio 2a96a72151 Fix faculty credit sorting bug 2022-11-16 17:16:52 +02:00
outisa 30c2cfbfa0 [Open uni] Add search into url 2022-11-16 17:15:15 +02:00
outisa e8c3a77ba6 [Open uni] Fix indentation 2022-11-16 14:13:58 +02:00
outisa e15894c2f5 [Course stats] Fix bug faculties not shown in credits gains 2022-11-16 14:12:26 +02:00
outisa 566fb861b0 [Open uni] Add UI betterments 2022-11-15 15:23:40 +02:00
outisa 80873a3227 [Open uni] Add possibility to save, update and delete searches 2022-11-12 17:43:33 +02:00
outisa 809c63ada9 [Open uni] Send usage data to grafana 2022-11-11 12:38:24 +02:00
outisa f0bf56f6c8 [Open uni] Add new columns 2022-11-10 18:01:04 +02:00
outisa e4214ef871 [Updater] Add open uni dissemination of information allowed field for student 2022-11-10 13:28:36 +02:00
outisa 88e6939347 [Open uni] Add possibility to choose dates 2022-11-10 00:16:25 +02:00
outisa dc73aabbd3 [Open uni] Add small adjustments 2022-11-04 12:49:09 +02:00
outisa 1dd41b0b9b Fix cypress tests 2022-11-03 14:42:35 +02:00
Laura Sinikallio 328437ced2 Remove 'only' from tests 2022-11-03 13:42:06 +02:00
Laura Sinikallio f79bd9f2f9 Add faculty graduation times tests 2022-11-03 13:40:01 +02:00
outisa 84bc0434fd Remove role if access no longer exists 2022-11-03 13:26:29 +02:00
outisa 96045d0e17 [Open Uni] Take course codes starting with AY into account 2022-11-03 13:06:56 +02:00
outisa 129c608108 [Navigation bar] Fix typo in role check 2022-11-02 15:45:23 +02:00
outisa d73aa95684 Add new iam to relevant iams 2022-11-02 09:22:45 +02:00
Laura Sinikallio fe9e9d2527 Fix updater looking at ALL term registrations of students-to-update instead of the individual student's registrations 2022-11-01 13:16:17 +02:00
outisa e5a52f3db6 Fix test 2022-11-01 12:37:47 +02:00
outisa 84bb527e5e Modify navigation bar visibility 2022-11-01 11:44:02 +02:00
outisa bd7bf5c712 Cypress please 2022-10-31 19:00:06 +02:00
outisa b6d3106ee9 Fix months calculation 2022-10-31 18:34:06 +02:00
outisa d7bc7d5d2e [faculty] Fix dynamic months limit for master 2022-10-31 18:30:04 +02:00
outisa 8eb3d58658 Change import for uuid 2022-10-31 16:27:58 +02:00
outisa 8c85467bcd [OpenUni] Add studyright filtering betterments 2022-10-31 16:18:02 +02:00
outisa f59f41cc3e Fix (tried at least) fetching courses when list is empty 2022-10-31 15:08:34 +02:00
outisa 678c90cbda [CSV] Change button's place and remove 'not passed' 2022-10-31 14:04:34 +02:00
outisa 7cee60596c Try to fix cypress tests 2022-10-28 18:20:57 +03:00
outisa 979d24d375 Fix some unique key errors 2022-10-28 14:43:24 +03:00
outisa cdf912db9b [Open uni] Add small modifications 2022-10-28 13:11:30 +03:00
outisa 51867d840b [Open uni] Create open uni logic 2022-10-27 22:22:18 +03:00
outisa 51137fa66f Add group for open uni people 2022-10-26 17:40:48 +03:00
outisa a394928e83 [Updater] Add secondary email to mapper to acually get it 2022-10-26 17:39:05 +03:00
Laura Sinikallio c7edc70ccb Replace to-be-deprecated commands 2022-10-25 17:30:36 +03:00
Laura Sinikallio 2aa2c9419c Update to-be-deprecated step 2022-10-25 17:08:28 +03:00
outisa 5b9728dc1d [Faculty] Fix broken link in students stats table 2022-10-21 15:02:20 +03:00
outisa 8c73a32631 Add secondary email to models 2022-10-20 22:00:47 +03:00
outisa 459c214164 [Updater] Add secondary email column for the student 2022-10-20 21:48:08 +03:00
Laura Sinikallio 96235ed242 Update info text 2022-10-20 14:57:28 +03:00
Laura Sinikallio 45ce3a70c9 Class statistics: hide buggy options, improve word choices 2022-10-20 14:42:52 +03:00
Laura Sinikallio c101efb961 Courses of class: fold/unfold all affects all tabs 2022-10-20 14:28:53 +03:00
Laura Sinikallio 0d47ad6cc7 Courses of class: keep module lists open when changing tabs 2022-10-20 13:56:08 +03:00
Luukkainen 9a338b79d5 Merge branch 'master' of github.com:UniversityOfHelsinkiCS/oodikone 2022-10-20 13:43:42 +03:00
Luukkainen 0087226de1 lintignore 2022-10-20 13:43:19 +03:00
Laura Sinikallio 8276359359 Remove duplicate pass/fail tab from courses of class 2022-10-20 12:47:44 +03:00
Laura Sinikallio 53214e822e Slight layout changes to class statistics view 2022-10-19 18:40:51 +03:00
Laura Sinikallio 20c24f2722 Update test 2022-10-19 18:30:08 +03:00
Laura Sinikallio 9b88f6c008 Update tests 2022-10-19 18:03:21 +03:00
Laura Sinikallio c1bab2fb63 Population statistics: move key info to header 2022-10-19 17:42:00 +03:00
outisa e6c23839e3 [Faculty] Change codes to progId 2022-10-18 20:02:42 +03:00
Laura Sinikallio 909448bad1 Add programme ids to before/after tables 2022-10-17 16:14:51 +03:00
Laura Sinikallio 565b6eb11c Restore faculty grad times redis 2022-10-17 15:48:20 +03:00
Laura Sinikallio bb69fcf908 Move studyprogramme id mapping to backend 2022-10-17 15:46:37 +03:00
Laura Sinikallio 7e66e7fafc Grad times: Move programme name mapping to backend 2022-10-17 14:37:22 +03:00
Laura Sinikallio 007644ddfc get programmes for faculty now contains 'letter ids' 2022-10-17 12:41:50 +03:00
Laura Sinikallio 153bacf3d0 Update test 2022-10-17 11:45:51 +03:00
outisa 3a5698011b [Population statistics] Fix course code visibility problem in excel file 2022-10-17 11:04:37 +03:00
Laura Sinikallio 95c37e5e8f Population statistics: switch to using 'class' instead of 'population' 2022-10-17 11:02:08 +03:00
Laura Sinikallio 633c08ee25 Add mention about filter name change 2022-10-12 18:29:41 +03:00
Laura Sinikallio 54e6f4fec9 Rename cherrypicker filter as student number filter 2022-10-12 15:28:54 +03:00
Laura Sinikallio ef2db12e54 Filter out duplicate course instances 2022-10-12 13:46:04 +03:00
Laura Sinikallio b4027560cc Use letter programme codes in faculty graduation times 2022-10-10 16:52:56 +03:00
Miko Keskimäki 6eb4e89199 Update minor package versions 2022-10-10 09:27:35 +03:00
outisa b30def95bc [faculty] Add dynamical change to credit limits 2022-10-07 20:52:12 +03:00
outisa 423c286071 [Cypress] Remove programme filter tests for population statistics view 2022-10-07 15:50:14 +03:00
outisa ddbbaab650 [Population Statistics] Remove Programme filter from the view 2022-10-07 15:26:00 +03:00
outisa 58827652f5 [Population Statistics] Add dynamical month change to Credits gained view 2022-10-07 15:16:59 +03:00
outisa 763f8765c9 [Faculty]Fix typos 2022-10-06 19:43:27 +03:00
outisa 75f83c70dc [Population statistics] Modify gained credits view 2022-10-06 19:40:24 +03:00
Laura Sinikallio d84aaab2af Add IDs for programmes with no jory-IAM 2022-10-06 17:32:34 +03:00
Laura Sinikallio 69a4eb113c Add programme 'letter id' to studyprogramme selector and overview title 2022-10-06 17:19:00 +03:00
Laura Sinikallio 8c5a898309 Switch grad times default view mode 2022-10-06 13:32:31 +03:00
Laura Sinikallio 667a0ef770 Add goal+1y line to grad time graph 2022-10-06 13:19:30 +03:00
160 changed files with 6498 additions and 4789 deletions
+3 -3
View File
@@ -21,7 +21,7 @@ jobs:
# This steps extracts release version from GITHUB_REF env var
# See: https://github.community/t/how-to-get-just-the-tag-name/16241/10
id: extract_version
run: echo ::set-output name=VERSION::"${GITHUB_REF#refs/tags/}"
run: echo "VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT
- name: 'Build and push'
uses: docker/build-push-action@v3
with:
@@ -50,13 +50,13 @@ jobs:
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: 'Get current time'
id: get_current_time
run: echo ::set-output name=NOW::"$(date --iso-8601=seconds)"
run: echo "NOW=$(date --iso-8601=seconds)" >> $GITHUB_OUTPUT
- name: 'Extract release version'
# Github context (e.g. github.ref) only has ref formatted as /refs/tags/vX.X.X
# This steps extracts release version from GITHUB_REF env var
# See: https://github.community/t/how-to-get-just-the-tag-name/16241/10
id: extract_version
run: echo ::set-output name=VERSION::"${GITHUB_REF#refs/tags/}"
run: echo "VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT
- name: 'Build and push'
uses: docker/build-push-action@v3
with:
+21 -1
View File
@@ -83,7 +83,7 @@ jobs:
- uses: docker/setup-buildx-action@v2
- name: 'Get current time'
id: get_current_time
run: echo ::set-output name=NOW::"$(date --iso-8601=seconds)"
run: echo "NOW=$(date --iso-8601=seconds)" >> $GITHUB_OUTPUT
- name: 'Build image and export it'
uses: docker/build-push-action@v3
with:
@@ -343,6 +343,24 @@ jobs:
password: ${{ secrets.TOSKAREGISTRY_PASSWORD }}
spec: cypress/e2e/Faculty_statistics.js
test_open_uni_search:
name: 'Test open uni search'
needs:
- build_backend
- build_frontend
- build_and_push_anon_sis_db
- build_and_push_anon_kone_db
- build_and_push_anon_user_db
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: 'Run tests'
uses: ./.github/actions/run-tests
with:
username: ${{ secrets.TOSKAREGISTRY_USERNAME }}
password: ${{ secrets.TOSKAREGISTRY_PASSWORD }}
spec: cypress/e2e/Openuni.js
# === Other tests ===
test_service_exit_statuses:
@@ -399,6 +417,7 @@ jobs:
- test_studyprogramme_overview
- test_teachers_and_users
- test_faculty_statistics
- test_open_uni_search
- test_service_exit_statuses
steps:
- uses: actions/checkout@v3
@@ -466,6 +485,7 @@ jobs:
- test_studyprogramme_overview
- test_teachers_and_users
- test_faculty_statistics
- test_open_uni_search
- test_service_exit_statuses
if: failure()
steps:
+8
View File
@@ -2,3 +2,11 @@
. "$(dirname "$0")/_/husky.sh"
npx lint-staged
if git grep -qE --cached "(it|describe)\.only" $(git diff --cached --name-only) ./cypress; then
echo "\e[0;31mDo not commit .only-tests."
exit 1
else
echo "No .only-tests found"
exit 0
fi
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 106 KiB

+5 -1
View File
@@ -50,7 +50,11 @@ What different CLI options do:
### Architecture
![Oodikone architecture](Oodikone.png)
![Oodikone architecture](./documentation/Oodikone.png)
### Documentation
Folder `documentation` currently contains some notes on sis-db schema and is meant as a place for storing useful information on Oodikone to make life easier for future developers. Feel free to add anything relevant!
### Basics
+165 -3
View File
@@ -1,11 +1,9 @@
/// <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.init('/faculties')
cy.get('[data-cy=select-faculty]').contains('td', 'H10')
cy.contains('td', 'H99').should('not.exist')
cy.contains('td', 'H90').click()
@@ -135,4 +133,168 @@ describe('Faculty overview', () => {
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-0"]').should('be.visible')
})
})
describe('Average graduation times', () => {
beforeEach(() => {
cy.init('/faculties')
cy.contains('td', 'H60').click()
cy.contains('Graduation times').click()
})
it('User can view graduation graphs', () => {
cy.get('[data-cy="Section-AverageGraduationTimes"]').should('be.visible')
cy.get('[data-cy="Section-bachelor"]').should('be.visible')
cy.get('[data-cy="Section-master"]').should('be.visible')
cy.get('[data-cy="Section-master"]').within(() => {
cy.contains('Graduation year').should('be.visible')
cy.contains('.message', "Click a bar to view that year's programme level breakdown").should('be.visible')
})
})
it('Graphs display data', () => {
cy.get('[data-cy="Section-bachelor"]').within(() => {
cy.get('div[class="faculty-breakdown-graph"]')
})
cy.get('[data-cy="GraduationTimeToggle"]').click()
cy.get('[data-cy="Section-bachelor"]').within(() => {
cy.get('div[class="faculty-graph"]')
cy.contains('1 graduated').should('have.length', 1)
cy.contains('1 graduated').trigger('mouseover')
cy.contains('1 students graduated in year 2019')
cy.contains('median study time: 40 months')
cy.contains('0 graduated on time')
cy.contains('1 graduated max year overtime')
cy.contains('1 graduated').click()
cy.contains('Year 2019 by graduation year')
cy.get('div[class="programmes-graph"]').should('be.visible')
cy.get('div[class="programmes-graph"]').within(() => {
cy.contains('EDUK')
cy.contains('1 graduated').trigger('mouseover')
cy.contains('Kasvatustieteiden kandiohjelma')
cy.contains('KH60_001')
})
})
cy.get('[data-cy="Section-master"]').within(() => {
cy.get('div[class="faculty-graph"]')
cy.contains('1 graduated').should('have.length', 1)
cy.contains('1 graduated').trigger('mouseover')
cy.contains('1 students graduated in year 2020')
cy.contains('median study time: 25 months')
cy.contains('0 graduated over year late')
cy.contains('1 graduated max year overtime')
cy.contains('1 graduated').click()
cy.contains('Year 2020 by graduation year')
cy.get('div[class="programmes-graph"]').should('be.visible')
cy.get('div[class="programmes-graph"]').within(() => {
cy.contains('EDUM')
cy.contains('1 graduated').trigger('mouseover')
cy.contains('Kasvatustieteiden maisteriohjelma')
cy.contains('MH60_001')
})
})
})
it('Graduation times grouping and time types can be toggled', () => {
cy.get('[data-cy="GraduationTimeToggle"]').click()
cy.get('[data-cy="GroupByToggle"]').click()
cy.get('[data-cy="Section-bachelor"]').should('not.exist')
cy.get('[data-cy="Section-master"]').should('be.visible')
cy.get('[data-cy="Section-master"]').within(() => {
cy.get('div[class="faculty-graph"]')
cy.contains('1 graduated (100 % of class)').should('have.length', 1)
cy.contains('1 graduated').trigger('mouseover')
cy.contains('From class of 2018, 1/undefined students have graduated')
})
cy.get('[data-cy="GraduationTimeToggle"]').click()
cy.get('[data-cy="Section-master"]').within(() => {
cy.contains('1').click()
cy.get('div[class="programmes-breakdown-graph"]').should('be.visible')
cy.get('div[class="programmes-breakdown-graph"]').within(() => {
cy.contains('Year 2018 by start year')
cy.contains('EDUM')
})
})
})
})
describe('Faculty Progress and student population -tab', () => {
beforeEach(() => {
cy.init('/faculties')
cy.contains('td', 'H50').click()
cy.contains('Programmes and student populations').click()
})
it('User can view studentdata table without %', () => {
cy.get('[data-cy="FacultyStudentStatsTable"]').should('be.visible')
cy.get('[data-cy="FacultyStudentStatsTable"]').should('not.contain', '0 %')
})
it('User can view studentdata table with %', () => {
cy.get('[data-cy="FacultyStudentStatsTable"]').should('be.visible')
cy.get('[data-cy="HidePercentagesToggle"]').click()
cy.get('[data-cy="FacultyStudentStatsTable"]').should('contain', '0 %')
})
it('Percentage Graphs exists', () => {
cy.get('[data-cy="Graph-FacultyBachelorsProgress"]').should('be.visible')
cy.get('[data-cy="Graph-FacultyBachelorMastersProgress"]').should('be.visible')
cy.get('[data-cy="Graph-FacultyMastersProgress"]').should('be.visible')
cy.get('[data-cy="Graph-FacultyBachelorsProgress"]').should('be.visible')
})
// Following two tests just test that nothing goes broken after toggling
// No data for testing that data actual change as supposed
it('Graduations can be excluded', () => {
cy.get('[data-cy="FacultyStudentStatsTable"]').should('be.visible')
cy.get('[data-cy="StudentToggle"]').click()
cy.get('[data-cy="FacultyStudentStatsTable"]').should('be.visible')
})
it('Transfers can be excluded', () => {
cy.get('[data-cy="FacultyStudentStatsTable"]').should('be.visible')
cy.get('[data-cy="GraduatedToggle"]').click()
cy.get('[data-cy="FacultyStudentStatsTable"]').should('be.visible')
})
it('Progress tables exists', () => {
cy.get('[data-cy="FacultyBachelorsProgressTable"]').should('be.visible')
cy.get('[data-cy="FacultyBachelorMasterProgressTable"]').should('be.visible')
cy.get('[data-cy="FacultyMastersProgressTable"]').should('be.visible')
cy.get('[data-cy="FacultyBachelorsProgressTable"]').should('be.visible')
})
it('Progress tables can be toggled', () => {
cy.get('[data-cy="Cell-FacultyBachelorsProgressTable-6"]').should('not.be.visible')
cy.get('[data-cy="Button-Show-FacultyBachelorsProgressTable-6"]').click()
cy.get('[data-cy="Cell-FacultyBachelorsProgressTable-0"]').should('not.exist')
cy.get('[data-cy="Cell-FacultyBachelorsProgressTable-1"]').should('not.exist')
cy.get('[data-cy="Cell-FacultyBachelorsProgressTable-2"]').should('not.exist')
cy.get('[data-cy="Cell-FacultyBachelorsProgressTable-3"]').should('not.exist')
cy.get('[data-cy="Cell-FacultyBachelorsProgressTable-4"]').should('not.exist')
cy.get('[data-cy="Cell-FacultyBachelorsProgressTable-6"]').should('be.visible')
cy.get('[data-cy="Button-Hide-FacultyBachelorsProgressTable-6"]').click()
cy.get('[data-cy="Cell-FacultyBachelorsProgressTable-6"]').should('not.be.visible')
})
it('Sudent statstable can be toggled', () => {
cy.get('[data-cy="FacultyStudentStatsTable"]').should('not.contain', 'TKT')
cy.get('[data-cy="Button-FacultyStudentStatsTable-3"]').click()
cy.get('[data-cy="FacultyStudentStatsTable"]').should('contain', 'TKT')
cy.get('[data-cy="Button-FacultyStudentStatsTable-3"]').click()
cy.get('[data-cy="FacultyStudentStatsTable"]').should('not.contain', 'TKT')
})
it('Progress infobox can be toggled', () => {
cy.get('[data-cy="InfoFacultyProgress-info-content"]').should('not.exist')
cy.get('[data-cy="InfoFacultyProgress-open-info"]').click()
cy.get('[data-cy="InfoFacultyProgress-info-content"]').should('be.visible')
cy.get('[data-cy="InfoFacultyProgress-close-info"]').click()
cy.get('[data-cy="InfoFacultyProgress-info-content"]').should('not.exist')
})
})
})
-15
View File
@@ -87,21 +87,6 @@ describe('Population Statistics', () => {
})
})
it('Programme defaults to "Active Study Right" mode', () => {
runTestStepWithPreAndPostParts('Programme', () => {
cy.cs('Programme-filter-mode-selector').children().eq(0).should('contain', 'Active Study Right')
})
})
it('Programme filter works', () => {
runTestStepWithPreAndPostParts('Programme', () => {
const card = cy.cs('Programme-filter-card')
const programmeDropdown = card.cs('Programme-filter-dropdown').selectFromDropdown(0)
checkFilteringResult(5)
programmeDropdown.get('i.delete').click()
})
})
// The test does not work since it tries to filter by "Ravitsemustiede"-studytrack when checking
// CS Bachelor students. Now that the irrelevant studytracks have been removed, the test fails.
// So fix this
+116
View File
@@ -0,0 +1,116 @@
/// <reference types="Cypress" />
const deleteAllSearches = () => {
cy.contains('Saved populations')
.parent()
.parent()
.parent()
.parent()
.get('.dropdown')
.then(d => {
const searchItems = d.find('div[role=option] > span[class=text]')
for (let i = 0; i < searchItems.length; i++) {
if (searchItems[i].textContent.includes('TEST-')) {
cy.get('[data-cy="history-search"]').children().eq(0).type(searchItems[i].textContent).type('{enter}')
cy.get('button').contains('Delete').click()
}
}
})
}
describe('Open uni population tests', () => {
describe('Open uni form can be used', () => {
// When openUniUser is created, change it here
beforeEach(() => {
cy.init('/openunipopulation', 'admin')
})
after(() => {
cy.init('/openunipopulation', 'admin')
cy.url().should('include', '/openunipopulation')
cy.contains('Open uni student population')
cy.get('[data-cy="open-uni-search-button"]').click()
deleteAllSearches()
})
it('Finds a proper population', () => {
cy.fixture('openUniPopulation').then(({ courseCodesSet1 }) => {
cy.get('[data-cy="open-uni-search-button"]').click()
cy.contains('Insert course code(s)').siblings().get('textarea').type(courseCodesSet1.join('\n'))
cy.get('[data-cy="begin-of-search"]').clear().type('01.01.2015')
cy.get('[data-cy="end-of-search"]').clear().type('01.01.2020')
cy.get('button').contains('Search population').click()
cy.contains('Beginning of the search for all fields:')
cy.contains('01.01.2015')
cy.contains('End of the search for enrollments:')
cy.contains('01.01.2020')
cy.url().should(
'include',
'/openunipopulation?courseCode=TKT10001&courseCode=TKT10002&enddate=01-01-2020&startdate=01-01-2015'
)
cy.get('[data-cy="open-uni-table-div"]').should('contain.text', courseCodesSet1[0])
cy.get('[data-cy="open-uni-table-div"]').should('contain.text', courseCodesSet1[1])
})
})
it('Textarea can handle different kinds of input', () => {
cy.get('[data-cy="open-uni-search-button"]').click()
cy.contains('Insert course code(s)')
.siblings()
.get('textarea')
.type('tkt10001 tkt10002,tkt10003\n TKT10004,,TKt10005\nTKT-1234')
cy.get('button').contains('Search population').click()
cy.get('[data-cy="open-uni-table-div"]').should('contain.text', 'TKT10001')
cy.get('[data-cy="open-uni-table-div"]').should('contain.text', 'TKT10002')
cy.get('[data-cy="open-uni-table-div"]').should('contain.text', 'TKT10003')
cy.get('[data-cy="open-uni-table-div"]').should('contain.text', 'TKT10004')
cy.get('[data-cy="open-uni-table-div"]').should('contain.text', 'TKT10005')
cy.get('[data-cy="open-uni-table-div"]').should('not.contain', 'TKT-1234')
})
it('Saves a custom population search', () => {
cy.fixture('openUniPopulation').then(({ courseCodesSet1 }) => {
cy.get('[data-cy="open-uni-search-button"]').click()
cy.get('[data-cy="search-name"]').type('TEST-Avoimen-populaatio')
cy.contains('Insert course code(s)').siblings().get('textarea').type(courseCodesSet1.join('\n'))
cy.get('[data-cy="save-search"]').click()
cy.get('[data-cy="history-search"]').children().eq(0).type('TEST-Avoimen-populaatio').type('{enter}')
cy.contains('TEST-Avoimen-populaatio')
cy.get('button').contains('Search population').click()
cy.get('[data-cy="open-uni-table-div"]').should('contain.text', courseCodesSet1[0])
cy.get('[data-cy="open-uni-table-div"]').should('contain.text', courseCodesSet1[1])
})
})
it('Updates a custom population search', () => {
cy.fixture('openUniPopulation').then(({ courseCodesSet1, courseCodesSet2 }) => {
cy.get('[data-cy="open-uni-search-button"]').click()
cy.get('[data-cy="search-name"]').type('TEST-Avoimen-populaatio')
cy.contains('Insert course code(s)').siblings().get('textarea').type(courseCodesSet1.join('\n'))
cy.get('[data-cy="save-search"]').click()
cy.get('button').contains('Search population').click()
cy.get('[data-cy="open-uni-table-div"]').should('contain.text', courseCodesSet1[0])
cy.get('[data-cy="open-uni-table-div"]').should('contain.text', courseCodesSet1[1])
cy.get('[data-cy="open-uni-search-button"]').click()
cy.get('[data-cy="history-search"]').children().eq(0).type('TEST-Avoimen-populaatio').type('{enter}')
cy.contains('Insert course code(s)').siblings().get('textarea').should('contain', courseCodesSet1.join(', '))
cy.contains('Insert course code(s)').siblings().get('textarea').type('\n')
cy.contains('Insert course code(s)').siblings().get('textarea').type(courseCodesSet2.join('\n'))
cy.get('[data-cy="save-search"]').click()
cy.get('button').contains('Search population').click()
cy.get('[data-cy="open-uni-table-div"]').should('contain.text', courseCodesSet1[0])
cy.get('[data-cy="open-uni-table-div"]').should('contain.text', courseCodesSet1[1])
cy.get('[data-cy="open-uni-table-div"]').should('contain.text', courseCodesSet2[0])
cy.get('[data-cy="open-uni-table-div"]').should('contain.text', courseCodesSet2[1])
cy.get('[data-cy="open-uni-table-div"]').should('contain.text', courseCodesSet2[2])
})
})
it('View does not crash with non-existing courses', () => {
cy.get('[data-cy="open-uni-search-button"]').click()
cy.contains('Insert course code(s)').siblings().get('textarea').type('cypress-fun')
cy.get('button').contains('Search population').click()
cy.get('[data-cy="open-uni-table-div"]').should('not.contain', 'cypress-fun')
})
})
})
+35 -35
View File
@@ -1,18 +1,8 @@
/// <reference types="Cypress" />
// Now "Class statistics" in UI
const moment = require('moment')
const _ = require('lodash')
// const setPopStatsUntil = (until, includeSettings = []) => {
// cy.contains('Advanced settings').siblings().get('[data-cy=advanced-toggle]').click()
// includeSettings.forEach(setting => {
// cy.contains('Advanced settings').parent().siblings().contains(setting).click()
// })
// cy.get('.adv-stats-until > .form-control').click().clear().type(until)
// cy.contains('Fetch population with new settings').click()
// cy.contains('Advanced settings')
// }
describe('Population Statistics tests', () => {
const pathToCSBach2017 =
'/populations?months=36&semesters=FALL&semesters=SPRING&studyRights={%22programme%22%3A%22KH50_005%22}&tag&year=2017'
@@ -23,9 +13,21 @@ describe('Population Statistics tests', () => {
cy.init('/populations')
})
it('Population statistics search infobox works', () => {
cy.get('[data-cy="PopulationSearch-info-content"]').should('not.exist')
cy.get('[data-cy="PopulationSearch-open-info"]').click()
cy.get('[data-cy="PopulationSearch-info-content"]').should('be.visible')
cy.get('[data-cy="PopulationSearch-info-content"]').should(
'contain',
'lukuvuosi, jolloin opiskelija on ilmoittautunut'
)
cy.get('[data-cy="PopulationSearch-close-info"]').click()
cy.get('[data-cy="PopulationSearch-info-content"]').should('not.exist')
})
it('Population statistics search form is usable', () => {
cy.contains('See population').should('be.disabled')
cy.contains('Search for population')
cy.contains('See class').should('be.disabled')
cy.contains('Search for class')
cy.contains('Class of')
.parent()
.within(() => {
@@ -47,7 +49,7 @@ describe('Population Statistics tests', () => {
.children()
.contains('Tietojenkäsittelytieteen maisteriohjelma')
.click()
cy.contains('See population').should('be.enabled')
cy.contains('See class').should('be.enabled')
})
it('Searching for population really shows population', () => {
@@ -57,27 +59,25 @@ describe('Population Statistics tests', () => {
.children()
.contains('Tietojenkäsittelytieteen kandiohjelma')
.click()
cy.contains('See population').click()
cy.contains('Population statistics')
cy.contains('See class').click()
cy.contains('class size')
cy.contains('Tietojenkäsittelytieteen kandiohjelma')
})
it('Population statistics is usable on general level', () => {
cy.visit(pathToCSBach2017)
cy.cs('filtered-students')
// This advanced settings does not work correctly
// see https://github.com/UniversityOfHelsinkiCS/oodikone/issues/3552
// setPopStatsUntil('toukokuu 2020')
cy.get('.card').within(() => {
cy.get('.header').within(() => {
cy.contains('Tietojenkäsittelytieteen kandiohjelma')
cy.contains('Sample size: 170 students')
cy.contains('class size 170 students')
})
cy.get('.card').within(() => {
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.cs('filtered-students')
cy.contains('Courses of population').click()
cy.contains('Courses of class').click()
cy.intercept('/api/v3/courseyearlystats**').as('coursePage')
// eslint-disable-next-line cypress/no-unnecessary-waiting
@@ -103,10 +103,9 @@ describe('Population Statistics tests', () => {
it.skip('Advanced settings work', () => {
cy.visit(pathToCSBach2017)
cy.get('[data-cy=advanced-toggle]').click()
cy.contains('Statistics until')
// only spring
cy.cs('toggle-fall').click()
cy.contains('Fetch population').click()
cy.contains('Fetch class').click()
// moar waiting hack
// eslint-disable-next-line cypress/no-unnecessary-waiting
@@ -119,14 +118,14 @@ describe('Population Statistics tests', () => {
cy.cs('toggle-fall').click()
cy.cs('toggle-spring').click()
cy.contains('Fetch population').click()
cy.contains('Fetch class').click()
cy.contains('Credit accumulation (for 170 students)')
// spring + fall
cy.get('[data-cy=advanced-toggle]').click()
cy.cs('toggle-spring').click()
cy.contains('Fetch population').click()
cy.contains('Fetch class').click()
cy.contains('Credit accumulation (for 170 students)')
})
@@ -135,10 +134,9 @@ describe('Population Statistics tests', () => {
cy.selectStudyProgramme('Tietojenkäsittelytieteen kandiohjelma')
cy.contains('Credit statistics').click()
cy.contains('Credits Gained').click()
cy.get("[data-cy='credits-gained-main-table']").should('contain', 'All students of the population')
const months = Math.ceil(moment.duration(moment().diff(moment('2017-08-1'))).asMonths())
cy.get("[data-cy='credits-gained-main-table']").should('contain', 'All students of the class')
const today = moment().endOf('month')
const months = Math.round(moment.duration(moment(today).diff(moment('2017-08-01'))).asMonths())
const limits = [1, ..._.range(1, 4).map(p => Math.ceil(months * ((p * 15) / 12))), null]
const ranges = _.range(1, limits.length).map(i => _.slice(limits, i - 1, i + 1))
@@ -219,17 +217,18 @@ describe('Population Statistics tests', () => {
})
})
/*
describe('when using IAM user', () => {
beforeEach(() => {
cy.init(pathToCSBach2017, 'onlyiamrights')
})
it('Population statistics is visible', () => {
cy.reload()
cy.contains('Tietojenkäsittelytieteen kandiohjelma')
cy.contains('class size 170 students')
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')
@@ -239,9 +238,10 @@ describe('Population Statistics tests', () => {
cy.contains('Credit statistics')
})
it('Students tab is not available', () => {
it.skip('Students tab is not available', () => {
cy.contains('Students (170)').should('not.exist')
// TODO: somehow check that obfuscation works
})
})
*/
})
+1 -1
View File
@@ -93,8 +93,8 @@ describe('Student Statistics tests', () => {
cy.get('.prompt').type(student.studentnumber)
cy.contains('td a', student.studentnumber).click()
cy.get('i.level.up.alternate.icon').eq(0).click()
cy.contains('Population statistics')
cy.contains('Tietojenkäsittelytieteen kandiohjelma')
cy.contains('class size')
})
})
+105 -65
View File
@@ -25,12 +25,14 @@ describe('Studyprogramme overview', () => {
describe('Basic information -view works for basic user', () => {
beforeEach(() => {
cy.init('/study-programme')
cy.viewport(1536, 960)
// eslint-disable-next-line cypress/no-unnecessary-waiting
cy.wait(10000)
cy.contains('a', 'Tietojenkäsittelytieteen kandiohjelma').click({ force: true })
// cy.viewport(1536, 960)
})
// If the backend breaks for one of the sections, the section header is not rendered and this will fail
it('Basic information -tab loads', () => {
cy.contains('a', 'Tietojenkäsittelytieteen kandiohjelma').click()
cy.get('[data-cy=Section-StudentsOfTheStudyprogramme]')
cy.get('[data-cy=Section-CreditsProducedByTheStudyprogramme]')
cy.get('[data-cy=Section-GraduatedAndThesisWritersOfTheProgramme]')
@@ -39,7 +41,6 @@ describe('Studyprogramme overview', () => {
})
it('Basic information contains correct students', () => {
cy.contains('a', 'Tietojenkäsittelytieteen kandiohjelma').click()
const years = getEmptyYears()
const tableContents = [
// [Year, Started, Graduated, Transferred Away, Transferred to]
@@ -55,23 +56,21 @@ describe('Studyprogramme overview', () => {
})
it('Basic information contains correct credits', () => {
cy.contains('a', 'Tietojenkäsittelytieteen kandiohjelma').click()
const years = getEmptyYears()
const tableContents = [
// [Year, Total, Major credits, Non-major credits, Non-degree credits, Transferred credits]
...years.map(year => [year, 0, 0, 0, 0, 0]),
[2021, 0, 0, 0, 0, 0],
[2020, 5860, 5591, 102, 0, 167],
[2019, 8696, 7737, 587, 23, 349],
[2018, 7818, 6584, 857, 105, 272],
[2017, 3085, 2492, 158, 47, 388],
[2019, 8696, 7737, 577, 33, 349],
[2018, 7818, 6584, 852, 110, 272],
[2017, 3085, 2492, 153, 52, 388],
]
cy.checkTableStats(tableContents, 'CreditsProducedByTheStudyprogramme')
})
it('Basic information contains correct thesis writers and graduates', () => {
cy.contains('a', 'Tietojenkäsittelytieteen kandiohjelma').click()
const years = getEmptyYears()
const tableContents = [
// [Year, Graduated, Wrote thesis]
@@ -87,7 +86,6 @@ describe('Studyprogramme overview', () => {
})
it.skip('Special studyrights can be excluded and basic data changes accordingly', () => {
cy.contains('a', 'Tietojenkäsittelytieteen maisteriohjelma').click()
cy.get('[data-cy=StudentToggle]').click()
const years = getEmptyYears()
const studentTableContents = [
@@ -129,39 +127,39 @@ describe('Studyprogramme overview', () => {
})
it('Year can be changed to academic year, and data changes accordingly', () => {
cy.contains('a', 'Tietojenkäsittelytieteen maisteriohjelma').click()
cy.get('[data-cy=YearToggle]').click()
// eslint-disable-next-line cypress/no-unnecessary-waiting
cy.get('[data-cy=YearToggle]').click({ force: true })
const isAcademicYear = true
const years = getEmptyYears(isAcademicYear)
const studentTableContents = [
// [Year, Started, Graduated, Transferred away, Transferred to]
...years.map(year => [year, 0, 0, 0, 0]),
['2021 - 2022', 0, 0, 0, 0],
['2020 - 2021', 5, 0, 0, 0],
['2019 - 2020', 27, 0, 0, 0],
['2018 - 2019', 4, 0, 0, 0],
['2017 - 2018', 0, 0, 0, 0],
['2020 - 2021', 12, 6, 3, 2],
['2019 - 2020', 86, 41, 1, 0],
['2018 - 2019', 162, 5, 0, 1],
['2017 - 2018', 171, 1, 0, 0],
]
// eslint-disable-next-line cypress/no-unnecessary-waiting
cy.wait(10000)
cy.checkTableStats(studentTableContents, 'StudentsOfTheStudyprogramme')
const creditTableContents = [
// [Year, Total, Major students credits, Non-major students credits, Non-degree credits, Transferred credits]
...years.map(year => [year, 0, 0, 0, 0]),
['2021 - 2022', 0, 0, 0, 0],
['2020 - 2021', 0, 0, 0, 0],
['2019 - 2020', 338.5, 177, 154, 0, 7.5],
['2018 - 2019', 173, 0, 173, 0, 0],
['2017 - 2018', 42, 0, 42, 0, 0],
...years.map(year => [year, 0, 0, 0, 0, 0]),
['2021 - 2022', 0, 0, 0, 0, 0],
['2020 - 2021', 566, 486, 20, 0, 60],
['2019 - 2020', 8595, 8271, 106, 0, 218],
['2018 - 2019', 9992, 8855, 781, 53, 303],
['2017 - 2018', 6055, 4792, 777, 142, 344],
]
cy.checkTableStats(creditTableContents, 'CreditsProducedByTheStudyprogramme')
//cy.wait(10000)
cy.get('[data-cy=YearToggle]').click()
})
it('Basic information graphs render', () => {
cy.contains('a', 'Tietojenkäsittelytieteen kandiohjelma').click()
cy.get('[data-cy=Graph-StudentsOfTheStudyprogramme')
.should('contain', 'Started')
.should('contain', 'Graduated')
@@ -171,8 +169,8 @@ describe('Studyprogramme overview', () => {
.should('contain', 'Major students credits')
.should('contain', 'Non-major students credits')
.should('contain', 'Transferred credits')
.should('contain', 857)
.should('contain', 587)
.should('contain', 852)
.should('contain', 577)
cy.get('[data-cy=Graph-GraduatedAndThesisWritersOfTheProgramme')
.should('contain', 'Graduated students')
@@ -183,10 +181,16 @@ describe('Studyprogramme overview', () => {
.should('contain', 25)
.should('contain', 35)
cy.get('[data-cy=Graph-2019-AverageGraduationTimes]')
.should('contain', '2019')
.should('contain', 'n = 17')
.should('contain', '23 kk')
cy.get('[data-cy=graduation-times-graph-breakdown]')
cy.get('[data-cy=GraduationTimeToggle]').click()
cy.get('[data-cy=graduation-times-graph]').within(() => {
cy.contains('Graduation year')
cy.contains('2019')
cy.contains('17 graduated').trigger('mouseover')
cy.contains('study time: 23 months')
cy.contains('17 graduated on time')
cy.contains('0 graduated max year overtime')
})
cy.get('[data-cy=Graph-ProgrammesBeforeOrAfter')
.should('contain', 'Tietojenkäsittelytieteen maisteriohjelma')
@@ -198,54 +202,79 @@ describe('Studyprogramme overview', () => {
})
})
describe('Graduation times of master programmes', () => {
it('are split into two graphs', () => {
cy.init('/study-programme', 'admin')
// eslint-disable-next-line cypress/no-unnecessary-waiting
cy.wait(5000)
cy.contains('a', 'Kasvatustieteiden maisteriohjelma').click({ force: true })
cy.get('[data-cy=graduation-times-graph-breakdownMaster]')
cy.get('[data-cy=graduation-times-graph-breakdownBachelor]')
cy.get('[data-cy=GraduationTimeToggle]').click()
cy.get('[data-cy=graduation-times-graphMaster]').within(() => {
cy.contains('Graduation year')
cy.contains('2020')
cy.contains('24 graduated').trigger('mouseover')
cy.contains('study time: 22 months')
cy.contains('20 graduated on time')
cy.contains('4 graduated max year overtime')
})
cy.get('[data-cy=graduation-times-graphBachelor]').within(() => {
cy.contains('Graduation year')
cy.contains('2020')
cy.contains('0 graduated').trigger('mouseover')
cy.contains('study time: 0 months')
cy.contains('0 graduated on time')
})
})
})
/* Studytrack overview -tests*/
describe('Studytrack overview works for basic user', () => {
beforeEach(() => {
cy.init('/study-programme')
// eslint-disable-next-line cypress/no-unnecessary-waiting
cy.wait(10000)
cy.contains('a', 'Tietojenkäsittelytieteen kandiohjelma').click()
cy.get('.attached').contains('Studytracks and class statistics').click()
})
// If the backend breaks for one of the sections, the section header is not rendered and this will fail
it('Studytracks and student populations -tab loads', () => {
cy.contains('a', 'Tietojenkäsittelytieteen kandiohjelma').click()
cy.get('.attached').contains('Studytracks and student populations').click()
it('Studytracks and class statistics -tab loads', () => {
cy.get('[data-cy=Section-StudytrackOverview]')
cy.get('[data-cy=Section-StudytrackProgress]')
cy.get('[data-cy=Section-AverageGraduationTimes]')
cy.get('[data-cy=Section-AverageGraduationTimesStudytracks]')
})
it('Students of the studyprogramme are shown correctly', () => {
cy.contains('a', 'Tietojenkäsittelytieteen kandiohjelma').click()
cy.get('.attached').contains('Studytracks and student populations').click()
const tableContents = [
// [Year, All, Started studying, Currently enrolled, Absent, Cancelled, Graduated, Men, Women, Finnish]
['2020 - 2021', 12, 10, 0, 0, 0, 0, 10, 2, 12],
['2019 - 2020', 86, 86, 0, 0, 1, 1, 65, 21, 86],
['2018 - 2019', 162, 161, 0, 0, 0, 11, 118, 44, 161],
['2017 - 2018', 171, 171, 0, 0, 4, 42, 135, 36, 166],
['Total', 431, 431, 0, 0, 5, 54, 328, 103, 425],
['Total', 431, 428, 0, 0, 5, 54, 328, 103, 425],
]
cy.checkTableStats(tableContents, 'StudytrackOverview')
})
it('Student progress data is shown correctly', () => {
cy.contains('a', 'Tietojenkäsittelytieteen kandiohjelma').click()
cy.get('.attached').contains('Studytracks and student populations').click()
const tableContents = [
['2020 - 2021', 12, 12, 0, 0, 0, 0, 0, 0, 0],
['2019 - 2020', 86, 11, 15, 35, 20, 5, 0, 0, 0],
['2018 - 2019', 162, 3, 9, 28, 50, 33, 25, 11, 3],
['2017 - 2018', 171, 7, 13, 22, 32, 18, 23, 32, 24],
['Total', 431, 16, 35, 65, 103, 75, 56, 50, 31],
['Total', 431, 33, 37, 85, 102, 56, 48, 43, 27],
]
cy.checkTableStats(tableContents, 'StudytrackProgress')
})
it('Studytrack overview graphs render', () => {
cy.contains('a', 'Tietojenkäsittelytieteen kandiohjelma').click()
cy.get('.attached').contains('Studytracks and student populations').click()
cy.get('[data-cy=Graph-StudytrackProgress]')
.should('contain', 'Less than 15 credits')
.should('contain', '30-59 credits')
@@ -254,10 +283,16 @@ describe('Studyprogramme overview', () => {
.should('contain', '5.6%') // The percentage for 15-29 credits in 2018-2019
.should('contain', '12.8%') // The percentage for less than 15 credits in 2019-2020
cy.get('[data-cy=Graph-2018-AverageGraduationTimes]')
.should('contain', '2018 - 2019')
.should('contain', 'n = 11 / 162')
.should('contain', '22 kk')
cy.get('[data-cy=graduation-times-graph-breakdown]')
cy.get('[data-cy=GraduationTimeToggle]').click()
cy.get('[data-cy=graduation-times-graph]').within(() => {
cy.contains('Start year')
cy.contains('2019 - 2020')
cy.contains('42 graduated').trigger('mouseover')
cy.contains('study time: 31.5 months')
cy.contains('39 graduated on time')
})
})
})
@@ -265,6 +300,8 @@ describe('Studyprogramme overview', () => {
describe('Programme courses works for basic user', () => {
beforeEach(() => {
cy.init('/study-programme')
// eslint-disable-next-line cypress/no-unnecessary-waiting
cy.wait(10000)
cy.contains('a', 'Tietojenkäsittelytieteen kandiohjelma').click()
cy.get('.attached').contains('Programme courses').click()
})
@@ -285,16 +322,21 @@ describe('Studyprogramme overview', () => {
})
it('calendar year -> academic year toggle works', () => {
// eslint-disable-next-line cypress/no-unnecessary-waiting
cy.wait(10000)
cy.get('[data-cy=fromYear]').click().contains('2018').click()
cy.get('[data-cy=toYear').click().contains('2019').click()
// eslint-disable-next-line cypress/no-unnecessary-waiting
cy.wait(20000)
cy.get('[data-cy=CoursesSortableTable]').within(() => {
cy.get('tr').eq(1).contains('684')
})
// eslint-disable-next-line cypress/no-unnecessary-waiting
cy.wait(20000)
cy.get('[data-cy=calendarAcademicYearToggle]').first().click()
// eslint-disable-next-line cypress/no-unnecessary-waiting
cy.wait(20000)
cy.get('[data-cy=CoursesSortableTable]').within(() => {
cy.get('tr').eq(1).contains('772')
})
@@ -336,12 +378,14 @@ describe('Studyprogramme overview', () => {
describe('Basic information -view works for basic user', () => {
beforeEach(() => {
cy.init('/study-programme')
// eslint-disable-next-line cypress/no-unnecessary-waiting
cy.wait(10000)
cy.contains('a', 'Tietojenkäsittelytieteen kandiohjelma').click()
cy.get('.attached').contains('Tags').click()
})
it('can create and delete tags for population', () => {
const name = `tag-${new Date().getTime()}`
cy.contains('a', 'Tietojenkäsittelytieteen kandiohjelma').click()
cy.get('.attached').contains('Tags').click()
cy.get('.tagNameSelectInput > .ui > input').type(name)
cy.get('.yearSelectInput').type('2018')
cy.contains('Create new tag').click()
@@ -352,8 +396,6 @@ describe('Studyprogramme overview', () => {
it('can create personal tags', () => {
const name = `tag-${new Date().getTime()}`
cy.contains('a', 'Tietojenkäsittelytieteen kandiohjelma').click()
cy.get('.attached').contains('Tags').click()
cy.get('.tagNameSelectInput > .ui > input').type(name)
cy.get('.yearSelectInput').type('2018')
@@ -367,9 +409,6 @@ describe('Studyprogramme overview', () => {
const name = `tag-${new Date().getTime()}`
const student = '010113437'
cy.contains('a', 'Tietojenkäsittelytieteen kandiohjelma').click()
cy.get('.attached').contains('Tags').click()
cy.get('.tagNameSelectInput > .ui > input').type(name)
cy.get('.yearSelectInput').type('2018')
cy.contains('Create new tag').click()
@@ -390,7 +429,8 @@ describe('Studyprogramme overview', () => {
cy.go('back')
cy.go('back')
// eslint-disable-next-line cypress/no-unnecessary-waiting
cy.wait(10000)
deleteTag(name)
cy.contains('Student statistics').click()
@@ -400,25 +440,25 @@ describe('Studyprogramme overview', () => {
})
})
/*
describe('IAM user', () => {
beforeEach(() => {
cy.init('/study-programme', 'onlyiamrights')
cy.contains('a', 'Tietojenkäsittelytieteen kandiohjelma').dblclick()
})
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('Studytracks and class statistics')
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]')
@@ -429,12 +469,12 @@ describe('Studyprogramme overview', () => {
})
it('can access studytracks', () => {
cy.contains('a', 'Tietojenkäsittelytieteen kandiohjelma').click()
cy.get('.attached').contains('Studytracks and student populations').click()
cy.get('.attached').contains('Studytracks and class statistics').click()
cy.get('[data-cy=Section-StudytrackOverview]')
cy.get('[data-cy=Section-StudytrackProgress]')
cy.get('[data-cy=Section-AverageGraduationTimes]')
cy.get('[data-cy=Section-AverageGraduationTimesStudytracks]')
})
})
*/
})
+4
View File
@@ -0,0 +1,4 @@
{
"courseCodesSet1": ["TKT10001", "TKT10002"],
"courseCodesSet2": ["TKT10003", "TKT10004", "TKT10005"]
}
+13 -1
View File
@@ -53,6 +53,18 @@ const onlyIamRightsUserHeaders = {
hypersonsisuid: 'hy-hlo-1111111',
}
// TODO add user into anon-DB (check the need for changing hypersonid)
// REMEMBER TO ADD ROLE openUniSearch
// const openUniUser = {
// uid: 'openuniuser',
// displayname: 'Open Uni User',
// 'shib-session-id': 'mock-cypress-session',
// hygroupcn: 'grp-oodikone-basics-users;hy-ypa-opa-dojo',
// edupersonaffiliation: 'member',
// mail: 'grp-toska+mockopenuniuser@helsinki.fi',
// hypersonsisuid: 'hy-hlo-9999999',
// }
const userHeaders = [
adminUserHeaders,
basicUserHeaders,
@@ -118,7 +130,7 @@ Cypress.Commands.add('selectStudyProgramme', name => {
cy.cs('navbar-studyProgramme').click()
cy.cs('navbar-class').click()
cy.cs('select-study-programme').click().children().contains(name).click()
cy.contains('See population').click()
cy.contains('See class').click()
})
Cypress.Commands.add('checkTableStats', (correctStats, tableName) => {
+24
View File
@@ -62,6 +62,30 @@ services:
POSTGRES_HOST_AUTH_METHOD: trust
image: registry.toska.cs.helsinki.fi/user-db
jami:
<<: *defaults
image: toska/jami
environment:
- PORT=3003
- NODE_ENV=production
- POSTGRES_HOST=jami-db
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=postgres
- POSTGRES_DATABASE=postgres
ports:
- 3003:3003
container_name: jami
jami-db:
<<: *defaults
image: postgres:13.2
environment:
- PGDATA=/data
- POSTGRES_PASSWORD=postgres
container_name: jami-db
ports:
- 5433:5432
nginx:
image: nginx:latest
container_name: nginx
+27
View File
@@ -168,6 +168,32 @@ services:
volumes:
- user-db-data:/data
jami:
<<: *defaults
image: toska/jami
environment:
- PORT=3003
- NODE_ENV=dev
- POSTGRES_HOST=jami-db
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=postgres
- POSTGRES_DATABASE=postgres
ports:
- 3003:3003
container_name: jami
jami-db:
<<: *defaults
image: postgres:13.2
environment:
- PGDATA=/data
- POSTGRES_PASSWORD=postgres
container_name: jami-db
ports:
- 5433:5432
volumes:
- jami-data:/data
volumes:
backend-nodemod:
frontend-nodemod:
@@ -178,3 +204,4 @@ volumes:
sis-updater-scheduler-nodemod:
sis-updater-worker-nodemod:
user-db-data:
jami-data:
Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 72 KiB

+48
View File
@@ -0,0 +1,48 @@
# Mystery numbers
Sis data uses some numeric classifiers that are not self-explanatory and (most) previously needed to be deciphered from existing code.
Some mystery numbers are gathered here for convenience. Feel free to add anything that could be useful. Remember to use the spelling used in code to enable project wide search.
## extentcode
(from **studyright**)
These are actually defined in db table studyright_extents. Tells studyright level or type, eg. bachelor's degree or non-degree studies.
## prioritycode
(from **studyright**)
1: Primary
2: Secondary
5: Rescinded (Seemingly not in use anymore)
6: Option
30: Graduated
## type
(from **element_details**)
20: A study programme
30: Module/Studytrack/kokonaisuus? Eg. Neurology
## enrollmenttype
(from **semester_enrollments**)
1: Present
2: Absent
3: Inactive (neither 1 or 2 has been declared)
## credittypecode
(from **credit**)
Defined in credit_types db table. Completed/Improved/Transferred/Failed
+265 -261
View File
File diff suppressed because it is too large Load Diff
+7 -7
View File
@@ -29,12 +29,12 @@
},
"homepage": "https://github.com/UniversityOfHelsinkiCS/oodikone#readme",
"devDependencies": {
"@babel/core": "^7.18.13",
"@babel/eslint-parser": "^7.17.0",
"@babel/core": "^7.19.3",
"@babel/eslint-parser": "^7.19.1",
"@babel/plugin-syntax-jsx": "^7.16.7",
"concurrently": "^7.3.0",
"cypress": "^10.6.0",
"eslint": "^8.23.0",
"concurrently": "^7.4.0",
"cypress": "^10.9.0",
"eslint": "^8.25.0",
"eslint-config-airbnb": "^19.0.4",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-babel": "^5.3.1",
@@ -43,13 +43,13 @@
"eslint-plugin-jsx-a11y": "^6.5.1",
"eslint-plugin-prefer-object-spread": "^1.2.1",
"eslint-plugin-prettier": "^4.2.1",
"eslint-plugin-react": "^7.31.1",
"eslint-plugin-react": "^7.31.9",
"eslint-plugin-react-hooks": "^4.3.0",
"husky": "^8.0.1",
"lint-staged": "^13.0.3",
"node-actionlint": "^1.2.2",
"prettier": "^2.5.1",
"stylelint": "^14.11.0",
"stylelint": "^14.13.0",
"stylelint-config-prettier": "^9.0.3",
"stylelint-config-standard": "^26.0.0",
"stylelint-prettier": "^2.0.0"
+28
View File
@@ -10,6 +10,7 @@
PROJECT_ROOT="$(git rev-parse --show-toplevel)"
DUMP_DIR="$PROJECT_ROOT/.databasedumps"
USER_DATA_FILE="$DUMP_DIR/hyuserdata"
DOCKER_COMPOSE=$PROJECT_ROOT/docker-compose.yml
## Following the naming convention in docker-compose, these are names for database
## services. The real data databases inside service will have names suffixed by "-real".
@@ -100,6 +101,28 @@ reset_databases() {
successmsg "Database setup finished"
}
reset_jami_data() {
local database="jami-db"
local pannu_url="svm-85.cs.helsinki.fi:/home/toska_user/most_recent_backup_store/jami.sql.gz"
local dump_destination="$DUMP_DIR/$database.sqz"
infomsg "Downloading Jami data"
scp -r -o ProxyCommand="ssh -l $username -W %h:%p melkki.cs.helsinki.fi" "$username@$pannu_url" "$dump_destination"
infomsg "Removing database and related volume"
docker volume rm oodikone_jami-data || warningmsg "This is okay, continuing"
docker-compose -f "$DOCKER_COMPOSE" down "$database" || warningmsg "This is okay, continuing"
infomsg "Starting postgres in the background"
docker-compose -f "$DOCKER_COMPOSE" up -d "$database"
retry docker-compose -f "$DOCKER_COMPOSE" exec "$database" pg_isready --dbname="postgres"
infomsg "Populating Jami"
docker exec -i "$database" /bin/bash -c "gunzip | psql -U postgres" < "$dump_destination"
docker-compose stop "$database"
msg ""
}
reset_all_real_data() {
infomsg "Downloading real data dumps, asking for pannu password when needed"
for i in ${!DATABASES[*]}; do
@@ -108,6 +131,7 @@ reset_all_real_data() {
download_real_dump "$database" "$url"
done
reset_databases "${DATABASES[@]}"
reset_jami_data
}
reset_single_database() {
@@ -119,6 +143,7 @@ reset_single_database() {
"sis-db"
"sis-importer-db"
"user-db"
"jami-db"
"Return to main menu."
)
@@ -144,6 +169,9 @@ reset_single_database() {
local database=$USER_DB_NAME
local url=$USER_DB_REAL_DUMP_URL
break 2;;
"jami-db")
reset_jami_data
break 2;;
"Return to main menu.")
break 2;;
*) msg "${RED}Invalid option:${NOFORMAT} $REPLY
-435
View File
@@ -1,435 +0,0 @@
const { facultyMap } = require('./data')
const joryMap = {
'hy-ttdk-tuk-jory': '100-K001',
'hy-ttdk-tum-jory': '100-M001',
'hy-oiktdk-on-jory': '200-K001',
'hy-oiktdk-otm-jory': '200-M001',
'hy-oiktdk-ibl-jory': '200-M002',
'hy-oiktdk-ggl-jory': '200-M003',
'hy-ltdk-psyk-jory': ['300-K001', '300-M004'],
'hy-ltdk-logo-jory': ['300-K002', '300-M005'],
'hy-ltdk-ll-jory': '300-M001',
'hy-ltdk-tmed-jory': '300-M002',
'hy-ltdk-hll-jory': '300-M003',
'hy-humtdk-filk-jory': '400-K001',
'hy-humtdk-ttk-jory': '400-K002',
'hy-humtdk-kik-jory': '400-K003',
'hy-humtdk-kok-jory': '400-K004',
'hy-humtdk-kuka-jory': '400-K005',
'hy-humtdk-hisk-jory': '400-K006',
'hy-humtdk-ttm-jory': '400-M001',
'hy-humtdk-kim-jory': '400-M002',
'hy-humtdk-eng-jory': '400-M003',
'hy-humtdk-rus-jory': '400-M004',
'hy-humtdk-lingdig-jory': '400-M005',
'hy-humtdk-tra-jory': '400-M006',
'hy-humtdk-suku-jory': '400-M007',
'hy-humtdk-nor-jory': '400-M008',
'hy-humtdk-kir-jory': '400-M009',
'hy-humtdk-kuma-jory': '400-M010',
'hy-humtdk-ice-jory': '400-M011',
'hy-humtdk-alku-jory': '400-M012',
'hy-humtdk-spt-jory': '400-M014',
'hy-humtdk-hism-jory': '400-M015',
'hy-mltdk-mat-jory': '500-K001',
'hy-mltdk-fys-jory': '500-K002',
'hy-mltdk-kemk-jory': '500-K003',
'hy-mltdk-mfkk-jory': '500-K004',
'hy-mltdk-tkt-jory': '500-K005',
'hy-mltdk-geok-jory': '500-K006',
'hy-mltdk-maa-jory': '500-K007',
'hy-mltdk-bsc-jory': '500-K008',
'hy-mltdk-mast-jory': '500-M001',
'hy-mltdk-lsi-jory': '500-M002',
'hy-mltdk-tcm-jory': '500-M003',
'hy-mltdk-paras-jory': '500-M004',
'hy-mltdk-matres-jory': '500-M005',
'hy-mltdk-atm-jory': '500-M006',
'hy-mltdk-kem-jory': '500-M007',
'hy-mltdk-mfkm-jory': '500-M008',
'hy-mltdk-csm-jory': '500-M009',
'hy-mltdk-data-jory': '500-M010',
'hy-mltdk-geom-jory': '500-M011',
'hy-mltdk-geog-jory': '500-M012',
'hy-mltdk-usp-jory': '500-M013',
'hy-ftdk-farm-jory': '550-K001',
'hy-ftdk-prov-jory': '550-M001',
'hy-ftdk-mpharm-jory': '550-M002',
'hy-bytdk-bio-jory': '570-K001',
'hy-bytdk-mole-jory': '570-K002',
'hy-bytdk-env-jory': '570-K003',
'hy-bytdk-eeb-jory': '570-M001',
'hy-bytdk-ips-jory': '570-M002',
'hy-bytdk-gmb-jory': '570-M003',
'hy-bytdk-neuro-jory': '570-M004',
'hy-bytdk-ecgs-jory': '570-M005',
'hy-ktdk-eduk-jory': '600-K001',
'hy-ktdk-edum-jory': '600-M001',
'hy-ktdk-ce-jory': '600-M002',
'hy-valttdk-pvk-jory': '700-K001',
'hy-valttdk-yk-jory': '700-K002',
'hy-valttdk-sosk-jory': '700-K003',
'hy-valttdk-ecok-jory': '700-K004',
'hy-valttdk-film-jory': '700-M001',
'hy-valttdk-pvm-jory': '700-M002',
'hy-valttdk-gpc-jory': '700-M003',
'hy-valttdk-ym-jory': '700-M004',
'hy-valttdk-cos-jory': '700-M005',
'hy-valttdk-ens-jory': '700-M006',
'hy-valttdk-msv-jory': '700-M007',
'hy-valttdk-sosm-jory': '700-M008',
'hy-valttdk-econ-jory': '700-M009',
'hy-valttdk-sote-jory': '700-M011',
'hy-sskh-ksv-jory': '740-K001',
'hy-mmtdk-maatk-jory': '800-K001',
'hy-mmtdk-metsak-jory': '800-K002',
'hy-mmtdk-etk-jory': '800-K003',
'hy-mmtdk-yet-jory': '800-K004',
'hy-mmtdk-agri-jory': '800-M001',
'hy-mmtdk-agere-jory': '800-M002',
'hy-mmtdk-for-jory': '800-M003',
'hy-mmtdk-food-jory': '800-M004',
'hy-mmtdk-hnfb-jory': '800-M005',
'hy-mmtdk-ekm-jory': '800-M006',
'hy-mmtdk-mmb-jory': '800-M007',
'hy-eltdk-elk-jory': '900-K001',
'hy-eltdk-ell-jory': '900-M001',
'hy-dp-dptheol-jory': 'T920101',
'hy-dp-dplaw-jory': 'T920102',
'hy-dp-philartsoc-jory': 'T920103',
'hy-dp-dphistcult-jory': 'T920104',
'hy-dp-dplang-jory': 'T920105',
'hy-dp-sky-jory': 'T920106',
'hy-dp-dpsocs-jory': 'T920107',
'hy-dp-pyam-jory': 'T920108',
'hy-dp-dpe-jory': 'T920109',
'hy-dp-seduce-jory': 'T920110',
'hy-dp-clic-jory': 'T920111',
'hy-dp-dpbm-jory': 'T921101',
'hy-dp-klto-jory': 'T921102',
'hy-dp-docpop-jory': 'T921103',
'hy-dp-findos-jory': 'T921104',
'hy-dp-dpdr-jory': 'T921105',
'hy-dp-ils-jory': 'T921106',
'hy-dp-bandm-jory': 'T921107',
'hy-dp-cvm-jory': 'T921108',
'hy-dp-dphub-jory': 'T921109',
'hy-dp-luova-jory': 'T922101',
'hy-dp-dpps-jory': 'T922102',
'hy-dp-denvi-jory': 'T922103',
'hy-dp-agforee-jory': 'T922104',
'hy-dp-mbdp-jory': 'T922105',
'hy-dp-foodhealth-jory': 'T922106',
'hy-dp-papu-jory': 'T923101',
'hy-dp-geodoc-jory': 'T923102',
'hy-dp-atm-dp-jory': 'T923103',
'hy-dp-chems-jory': 'T923104',
'hy-dp-domast-jory': 'T923105',
'hy-dp-matrena-jory': 'T923106',
'hy-dp-docs-jory': 'T923107',
}
const kojoMap = {
'hy-ttdk-tuk-jory': 'hy-ttdk-kandi-kojot',
'hy-ttdk-tum-jory': 'hy-ttdk-maisteri-kojot',
'hy-oiktdk-on-jory': 'hy-oiktdk-kandi-kojot',
'hy-oiktdk-otm-jory': 'hy-oiktdk-maisteri-kojot',
'hy-oiktdk-ibl-jory': 'hy-oiktdk-maisteri-kojot',
'hy-oiktdk-ggl-jory': 'hy-oiktdk-maisteri-kojot',
'hy-ltdk-psyk-jory': 'hy-ltdk-maisteri-kojot',
'hy-ltdk-logo-jory': 'hy-ltdk-maisteri-kojot',
'hy-ltdk-ll-jory': 'hy-ltdk-maisteri-kojot',
'hy-ltdk-tmed-jory': 'hy-ltdk-maisteri-kojot',
'hy-ltdk-hll-jory': 'hy-ltdk-maisteri-kojot',
'hy-humtdk-filk-jory': 'hy-humtdk-kandi-kojot',
'hy-humtdk-ttk-jory': 'hy-humtdk-kandi-kojot',
'hy-humtdk-kik-jory': 'hy-humtdk-kandi-kojot',
'hy-humtdk-kok-jory': 'hy-humtdk-kandi-kojot',
'hy-humtdk-kuka-jory': 'hy-humtdk-kandi-kojot',
'hy-humtdk-hisk-jory': 'hy-humtdk-kandi-kojot',
'hy-humtdk-ttm-jory': 'hy-humtdk-maisteri-kojot',
'hy-humtdk-kim-jory': 'hy-humtdk-maisteri-kojot',
'hy-humtdk-eng-jory': 'hy-humtdk-maisteri-kojot',
'hy-humtdk-rus-jory': 'hy-humtdk-maisteri-kojot',
'hy-humtdk-lingdig-jory': 'hy-humtdk-maisteri-kojot',
'hy-humtdk-tra-jory': 'hy-humtdk-maisteri-kojot',
'hy-humtdk-suku-jory': 'hy-humtdk-maisteri-kojot',
'hy-humtdk-nor-jory': 'hy-humtdk-maisteri-kojot',
'hy-humtdk-kir-jory': 'hy-humtdk-maisteri-kojot',
'hy-humtdk-kuma-jory': 'hy-humtdk-maisteri-kojot',
'hy-humtdk-ice-jory': 'hy-humtdk-maisteri-kojot',
'hy-humtdk-alku-jory': 'hy-humtdk-maisteri-kojot',
'hy-humtdk-spt-jory': 'hy-humtdk-maisteri-kojot',
'hy-humtdk-hism-jory': 'hy-humtdk-maisteri-kojot',
'hy-mltdk-mat-jory': 'hy-mltdk-kandi-kojot',
'hy-mltdk-fys-jory': 'hy-mltdk-kandi-kojot',
'hy-mltdk-kemk-jory': 'hy-mltdk-kandi-kojot',
'hy-mltdk-mfkk-jory': 'hy-mltdk-kandi-kojot',
'hy-mltdk-tkt-jory': 'hy-mltdk-kandi-kojot',
'hy-mltdk-geok-jory': 'hy-mltdk-kandi-kojot',
'hy-mltdk-maa-jory': 'hy-mltdk-kandi-kojot',
'hy-mltdk-bsc-jory': 'hy-mltdk-kandi-kojot',
'hy-mltdk-mast-jory': 'hy-mltdk-maisteri-kojot',
'hy-mltdk-lsi-jory': 'hy-mltdk-maisteri-kojot',
'hy-mltdk-tcm-jory': 'hy-mltdk-maisteri-kojot',
'hy-mltdk-paras-jory': 'hy-mltdk-maisteri-kojot',
'hy-mltdk-matres-jory': 'hy-mltdk-maisteri-kojot',
'hy-mltdk-atm-jory': 'hy-mltdk-maisteri-kojot',
'hy-mltdk-kem-jory': 'hy-mltdk-maisteri-kojot',
'hy-mltdk-mfkm-jory': 'hy-mltdk-maisteri-kojot',
'hy-mltdk-csm-jory': 'hy-mltdk-maisteri-kojot',
'hy-mltdk-data-jory': 'hy-mltdk-maisteri-kojot',
'hy-mltdk-geom-jory': 'hy-mltdk-maisteri-kojot',
'hy-mltdk-geog-jory': 'hy-mltdk-maisteri-kojot',
'hy-mltdk-usp-jory': 'hy-mltdk-maisteri-kojot',
'hy-ftdk-farm-jory': 'hy-ftdk-kandi-kojot',
'hy-ftdk-prov-jory': 'hy-ftdk-maisteri-kojot',
'hy-ftdk-mpharm-jory': 'hy-ftdk-maisteri-kojot',
'hy-bytdk-bio-jory': 'hy-bytdk-kandi-kojot',
'hy-bytdk-mole-jory': 'hy-bytdk-kandi-kojot',
'hy-bytdk-env-jory': 'hy-bytdk-kandi-kojot',
'hy-bytdk-eeb-jory': 'hy-bytdk-maisteri-kojot',
'hy-bytdk-ips-jory': 'hy-bytdk-maisteri-kojot',
'hy-bytdk-gmb-jory': 'hy-bytdk-maisteri-kojot',
'hy-bytdk-neuro-jory': 'hy-bytdk-maisteri-kojot',
'hy-bytdk-ecgs-jory': 'hy-bytdk-maisteri-kojot',
'hy-ktdk-eduk-jory': 'hy-ktdk-kandi-kojot',
'hy-ktdk-edum-jory': 'hy-ktdk-maisteri-kojot',
'hy-ktdk-ce-jory': 'hy-ktdk-maisteri-kojot',
'hy-valttdk-pvk-jory': 'hy-valttdk-kandi-kojot',
'hy-valttdk-yk-jory': 'hy-valttdk-kandi-kojot',
'hy-valttdk-sosk-jory': 'hy-valttdk-kandi-kojot',
'hy-valttdk-ecok-jory': 'hy-valttdk-kandi-kojot',
'hy-valttdk-film-jory': 'hy-valttdk-maisteri-kojot',
'hy-valttdk-pvm-jory': 'hy-valttdk-maisteri-kojot',
'hy-valttdk-gpc-jory': 'hy-valttdk-maisteri-kojot',
'hy-valttdk-ym-jory': 'hy-valttdk-maisteri-kojot',
'hy-valttdk-cos-jory': 'hy-valttdk-maisteri-kojot',
'hy-valttdk-ens-jory': 'hy-valttdk-maisteri-kojot',
'hy-valttdk-msv-jory': 'hy-valttdk-maisteri-kojot',
'hy-valttdk-sosm-jory': 'hy-valttdk-maisteri-kojot',
'hy-valttdk-econ-jory': 'hy-valttdk-maisteri-kojot',
'hy-valttdk-sote-jory': 'hy-valttdk-maisteri-kojot',
'hy-sskh-ksv-jory': 'hy-sskh-kandi-kojot',
'hy-mmtdk-maatk-jory': 'hy-mmtdk-kandi-kojot',
'hy-mmtdk-metsak-jory': 'hy-mmtdk-kandi-kojot',
'hy-mmtdk-etk-jory': 'hy-mmtdk-kandi-kojot',
'hy-mmtdk-yet-jory': 'hy-mmtdk-kandi-kojot',
'hy-mmtdk-agri-jory': 'hy-mmtdk-maisteri-kojot',
'hy-mmtdk-agere-jory': 'hy-mmtdk-maisteri-kojot',
'hy-mmtdk-for-jory': 'hy-mmtdk-maisteri-kojot',
'hy-mmtdk-food-jory': 'hy-mmtdk-maisteri-kojot',
'hy-mmtdk-hnfb-jory': 'hy-mmtdk-maisteri-kojot',
'hy-mmtdk-ekm-jory': 'hy-mmtdk-maisteri-kojot',
'hy-mmtdk-mmb-jory': 'hy-mmtdk-maisteri-kojot',
'hy-eltdk-elk-jory': 'hy-eltdk-kandi-kojot',
'hy-eltdk-ell-jory': 'hy-eltdk-maisteri-kojot',
'hy-dp-dptheol-jory': 'hy-tohtoriohjelma-johtajat',
'hy-dp-dplaw-jory': 'hy-tohtoriohjelma-johtajat',
'hy-dp-philartsoc-jory': 'hy-tohtoriohjelma-johtajat',
'hy-dp-dphistcult-jory': 'hy-tohtoriohjelma-johtajat',
'hy-dp-dplang-jory': 'hy-tohtoriohjelma-johtajat',
'hy-dp-sky-jory': 'hy-tohtoriohjelma-johtajat',
'hy-dp-dpsocs-jory': 'hy-tohtoriohjelma-johtajat',
'hy-dp-pyam-jory': 'hy-tohtoriohjelma-johtajat',
'hy-dp-dpe-jory': 'hy-tohtoriohjelma-johtajat',
'hy-dp-seduce-jory': 'hy-tohtoriohjelma-johtajat',
'hy-dp-clic-jory': 'hy-tohtoriohjelma-johtajat',
'hy-dp-dpbm-jory': 'hy-tohtoriohjelma-johtajat',
'hy-dp-klto-jory': 'hy-tohtoriohjelma-johtajat',
'hy-dp-docpop-jory': 'hy-tohtoriohjelma-johtajat',
'hy-dp-findos-jory': 'hy-tohtoriohjelma-johtajat',
'hy-dp-dpdr-jory': 'hy-tohtoriohjelma-johtajat',
'hy-dp-ils-jory': 'hy-tohtoriohjelma-johtajat',
'hy-dp-bandm-jory': 'hy-tohtoriohjelma-johtajat',
'hy-dp-cvm-jory': 'hy-tohtoriohjelma-johtajat',
'hy-dp-dphub-jory': 'hy-tohtoriohjelma-johtajat',
'hy-dp-luova-jory': 'hy-tohtoriohjelma-johtajat',
'hy-dp-dpps-jory': 'hy-tohtoriohjelma-johtajat',
'hy-dp-denvi-jory': 'hy-tohtoriohjelma-johtajat',
'hy-dp-agforee-jory': 'hy-tohtoriohjelma-johtajat',
'hy-dp-mbdp-jory': 'hy-tohtoriohjelma-johtajat',
'hy-dp-foodhealth-jory': 'hy-tohtoriohjelma-johtajat',
'hy-dp-papu-jory': 'hy-tohtoriohjelma-johtajat',
'hy-dp-geodoc-jory': 'hy-tohtoriohjelma-johtajat',
'hy-dp-atm-dp-jory': 'hy-tohtoriohjelma-johtajat',
'hy-dp-chems-jory': 'hy-tohtoriohjelma-johtajat',
'hy-dp-domast-jory': 'hy-tohtoriohjelma-johtajat',
'hy-dp-matrena-jory': 'hy-tohtoriohjelma-johtajat',
'hy-dp-docs-jory': 'hy-tohtoriohjelma-johtajat',
}
/**
* Maps kosu IAM to the name of TDKs, defined in data.js facultyMap
*/
const kosuFacultyMap = {
'hy-ypa-opa-kosu-kumpula': 'matemaattis-luonnontieteellinen',
'hy-ypa-opa-kosu-meilahti': ['lääketieteellinen'],
'hy-ypa-opa-kosu-kruununhaka': [
'oikeustieteellinen',
'valtiotieteellinen',
'kasvatustieteellinen',
'humanistinen',
'teologinen',
'svenska',
],
'hy-ypa-opa-kosu-metsatalo': [
'oikeustieteellinen',
'valtiotieteellinen',
'kasvatustieteellinen',
'humanistinen',
'teologinen',
'svenska',
],
'hy-ypa-opa-kosu-porthania': [
'oikeustieteellinen',
'valtiotieteellinen',
'kasvatustieteellinen',
'humanistinen',
'teologinen',
'svenska',
],
'hy-ypa-opa-kosu-siltavuori': [
'oikeustieteellinen',
'valtiotieteellinen',
'kasvatustieteellinen',
'humanistinen',
'teologinen',
'svenska',
],
'hy-ypa-opa-kosu-viikki': [
'bio- ja ympäristötieteellinen',
'farmasia',
'maatalous -metsätieteellinen',
'eläinlääketieteellinen',
],
}
const doctoralIams = ['hy-tohtorikoulutus-johtoryhma', 'hy-tine']
const doctoralSchoolMap = {
'hy-tutkijakoulut-hymy-jory': [
'T920101',
'T920102',
'T920103',
'T920104',
'T920105',
'T920106',
'T920107',
'T920108',
'T920109',
'T920110',
'T920111',
],
'hy-tutkijakoulut-dshealth-jory': [
'T921101',
'T921102',
'T921103',
'T921104',
'T921105',
'T921106',
'T921107',
'T921108',
'T921109',
],
'hy-tutkijakoulut-yeb-jory': ['T922101', 'T922102', 'T922103', 'T922104', 'T922105', 'T922106'],
'hy-tutkijakoulut-donasci-jory': ['T923101', 'T923102', 'T923103', 'T923104', 'T923105', 'T923106', 'T923107'],
}
const opetusVaradekaani = 'hy-varadekaanit-opetus'
const dekaaniFacultyMap = {
'hy-ttdk-dekanaatti': 'teologinen',
'hy-oiktdk-dekanaatti': 'oikeustieteellinen',
'hy-ltdk-dekanaatti': 'lääketieteellinen',
'hy-humtdk-dekanaatti': 'humanistinen',
'hy-mltdk-dekanaatti': 'matemaattis-luonnontieteellinen',
'hy-ftdk-dekanaatti': 'farmasia',
'hy-bytdk-dekanaatti': 'bio- ja ympäristötieteellinen',
'hy-ktdk-dekanaatti': 'kasvatustieteellinen',
'hy-valttdk-dekanaatti': 'valtiotieteellinen',
'hy-sskh-rehtoraatti': 'svenska',
'hy-mmtdk-dekanaatti': 'maatalous -metsätieteellinen',
'hy-eltdk-dekanaatti': 'eläinlääketieteellinen',
}
const oodikoneIams = [
'grp-oodikone-staging-users',
'grp-oodikone-basic-staging-users',
'grp-oodikone-users',
'grp-oodikone-basic-users',
]
const universityWideGroups = ['hy-ypa-opa-opintoasiainpaallikot', 'hy-rehtoraatti']
const superAdminGroups = ['grp-toska']
const adminGroups = ['grp-ospa']
const employeeGroups = ['hy-employees']
const isSuperAdminIam = iam => superAdminGroups.includes(iam)
const isAdminIam = iam => adminGroups.includes(iam)
const isUniversityWideIam = iam => universityWideGroups.includes(iam)
const isDoctoralIam = iam => doctoralIams.includes(iam)
const isEmployeeIam = iam => employeeGroups.includes(iam)
const iamToDoctoralSchool = iam => doctoralSchoolMap[iam]
const getStudyLeaderGroup = iam => kojoMap[iam]
const iamToOrganisationCode = iam => {
const organisationCodes = joryMap[iam]
if (Array.isArray(organisationCodes)) {
return organisationCodes
}
return [organisationCodes]
}
const kosuIamToFaculties = iam => {
const faculties = kosuFacultyMap[iam]
if (!Array.isArray(faculties) || !faculties.length) return []
return faculties.map(f => facultyMap[f])
}
const dekaaniIamToFaculty = iam => {
const faculty = dekaaniFacultyMap[iam]
if (!faculty) return null
return facultyMap[faculty]
}
const relevantIAMs = []
.concat(Object.keys(joryMap))
.concat(Object.keys(kojoMap))
.concat(Object.values(kojoMap))
.concat(Object.keys(kosuFacultyMap))
.concat(Object.keys(dekaaniFacultyMap))
.concat(opetusVaradekaani)
.concat(doctoralIams)
.concat(Object.keys(doctoralSchoolMap))
.concat(universityWideGroups)
.concat(superAdminGroups)
.concat(adminGroups)
.concat(employeeGroups)
.concat(oodikoneIams)
module.exports = {
isSuperAdminIam,
isAdminIam,
isUniversityWideIam,
isDoctoralIam,
isEmployeeIam,
iamToDoctoralSchool,
iamToOrganisationCode,
kosuIamToFaculties,
dekaaniIamToFaculty,
opetusVaradekaani,
getStudyLeaderGroup,
relevantIAMs,
}
-19
View File
@@ -1,19 +0,0 @@
const mapToDegreeCode = organisationCode => {
if (!organisationCode) return ''
if (organisationCode.length < 7) return ''
const doctoral = organisationCode[0] === 'T'
if (doctoral) {
return organisationCode
}
const [start, end] = organisationCode.split('-')
if (end && end.length < 3) return ''
if (start.length < 2) return ''
const masters = end[0] === 'M'
const code = `${masters ? 'M' : 'K'}H${start.substr(0, 2)}_${end.substr(-3)}`
return code
}
module.exports = {
mapToDegreeCode,
}
File diff suppressed because it is too large Load Diff
+139
View File
@@ -0,0 +1,139 @@
// Map programme codes to "letter indentifier"
const codes = {
KH10_001: 'tuk',
MH10_001: 'tum',
KH20_001: 'on',
MH20_001: 'otm',
MH20_002: 'ibl',
MH20_003: 'ggl',
KH30_001: 'psyk',
MH30_004: 'psym',
KH30_002: 'logk',
MH30_005: 'logm',
MH30_001: 'll',
MH30_002: 'tmed',
MH30_003: 'hll',
KH40_001: 'filk',
KH40_002: 'ttk',
KH40_003: 'kik',
KH40_004: 'kok',
KH40_005: 'kuka',
KH40_006: 'hisk',
MH40_001: 'ttm',
MH40_002: 'kim',
MH40_003: 'eng',
MH40_004: 'rus',
MH40_005: 'lingdig',
MH40_006: 'tra',
MH40_007: 'suku',
MH40_008: 'nor',
MH40_009: 'kir',
MH40_010: 'kuma',
MH40_011: 'ice',
MH40_012: 'alku',
MH40_014: 'spt',
MH40_015: 'hism',
KH50_001: 'mat',
KH50_002: 'fys',
KH50_003: 'kemk',
KH50_004: 'mfkk',
KH50_005: 'tkt',
KH50_006: 'geok',
KH50_007: 'maa',
KH50_008: 'bsc',
MH50_001: 'mast',
MH50_002: 'lsi',
MH50_003: 'tcm',
MH50_004: 'paras',
MH50_005: 'matres',
MH50_006: 'atm',
MH50_007: 'kem',
MH50_008: 'mfkm',
MH50_009: 'csm',
MH50_010: 'data',
MH50_011: 'geom',
MH50_012: 'geog',
MH50_013: 'usp',
KH55_001: 'farm',
MH55_001: 'prov',
MH55_002: 'mpharm',
KH57_001: 'bio',
KH57_002: 'mole',
KH57_003: 'env',
MH57_001: 'eeb',
MH57_002: 'ips',
MH57_003: 'gmb',
MH57_004: 'neuro',
MH57_005: 'ecgs',
KH60_001: 'eduk',
MH60_001: 'edum',
MH60_002: 'ce',
KH70_001: 'pvk',
KH70_002: 'yk',
KH70_003: 'sosk',
KH70_004: 'ecok',
MH70_001: 'film',
MH70_002: 'pvm',
MH70_003: 'gpc',
MH70_004: 'ym',
MH70_005: 'cos',
MH70_006: 'ens',
MH70_007: 'msv',
MH70_008: 'sosm',
MH70_009: 'econ',
MH70_011: 'sote',
KH74_001: 'ksv',
KH80_001: 'maatk',
KH80_002: 'metsak',
KH80_003: 'etk',
KH80_004: 'yet',
MH80_001: 'agri',
MH80_002: 'agere',
MH80_003: 'for',
MH80_004: 'food',
MH80_005: 'hnfb',
MH80_006: 'ekm',
MH80_007: 'mmb',
KH90_001: 'elk',
MH90_001: 'ell',
T920101: 'dptheol',
T920102: 'dplaw',
T920111: 'philartsoc',
T920103: 'dphistcult',
T920104: 'dplang',
T920105: 'sky',
T920106: 'dpsocs',
T920107: 'pyam',
T920108: 'dpe',
T920109: 'seduce',
T920110: 'clic',
T921101: 'dpbm',
T921102: 'klto',
T921103: 'docpop',
T921104: 'findos',
T921105: 'dpdr',
T921106: 'ils',
T921107: 'bandm',
T921108: 'cvm',
T921109: 'dphub',
T922101: 'luova',
T922102: 'dpps',
T922103: 'denvi',
T922104: 'agforee',
T922105: 'mbdp',
T922106: 'foodhealth',
T923101: 'papu',
T923102: 'geodoc',
T923103: 'atm-dp',
T923104: 'chems',
T923105: 'domast',
T923106: 'matrena',
T923107: 'docs',
// manually added programs with no IAM, either abolished, or special co-operation programmes
MH50_014: 'ech',
MH40_013: 'mkk',
MH70_010: 'imes',
}
module.exports = { codes }
+58 -53
View File
@@ -14,7 +14,7 @@
"bluebird": "^3.7.2",
"cors": "^2.8.5",
"cron": "^1.8.2",
"express": "^4.17.3",
"express": "^4.18.2",
"express-async-errors": "^3.1.1",
"lodash": "^4.17.21",
"lru-cache": "^5.1.1",
@@ -23,11 +23,11 @@
"morgan": "^1.10.0",
"pg": "^8.8.0",
"redis": "^3.1.2",
"sequelize": "^6.21.4",
"sequelize": "^6.24.0",
"umzug": "^2.3.0",
"unfuck-utf8-headers-middleware": "^1.0.1",
"uuid": "^3.4.0",
"winston": "^3.6.0"
"winston": "^3.8.2"
},
"devDependencies": {
"nodemon": "^1.19.4"
@@ -495,9 +495,9 @@
"integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg=="
},
"node_modules/body-parser": {
"version": "1.20.0",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.0.tgz",
"integrity": "sha512-DfJ+q6EPcGKZD1QWUjSpqp+Q7bDQTsQIF4zfUAtZ6qk+H/3/QRhg9CEp39ss+/T2vw0+HaidC0ecJj/DRLIaKg==",
"version": "1.20.1",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz",
"integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==",
"dependencies": {
"bytes": "3.1.2",
"content-type": "~1.0.4",
@@ -507,7 +507,7 @@
"http-errors": "2.0.0",
"iconv-lite": "0.4.24",
"on-finished": "2.4.1",
"qs": "6.10.3",
"qs": "6.11.0",
"raw-body": "2.5.1",
"type-is": "~1.6.18",
"unpipe": "1.0.0"
@@ -1178,13 +1178,13 @@
}
},
"node_modules/express": {
"version": "4.18.1",
"resolved": "https://registry.npmjs.org/express/-/express-4.18.1.tgz",
"integrity": "sha512-zZBcOX9TfehHQhtupq57OF8lFZ3UZi08Y97dwFCkD8p9d/d2Y3M+ykKcwaMDEL+4qyUolgBDX6AblpR3fL212Q==",
"version": "4.18.2",
"resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz",
"integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==",
"dependencies": {
"accepts": "~1.3.8",
"array-flatten": "1.1.1",
"body-parser": "1.20.0",
"body-parser": "1.20.1",
"content-disposition": "0.5.4",
"content-type": "~1.0.4",
"cookie": "0.5.0",
@@ -1203,7 +1203,7 @@
"parseurl": "~1.3.3",
"path-to-regexp": "0.1.7",
"proxy-addr": "~2.0.7",
"qs": "6.10.3",
"qs": "6.11.0",
"range-parser": "~1.2.1",
"safe-buffer": "5.2.1",
"send": "0.18.0",
@@ -1407,9 +1407,9 @@
"integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A=="
},
"node_modules/get-intrinsic": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.2.tgz",
"integrity": "sha512-Jfm3OyCxHh9DJyc28qGk+JmfkpO41A4XkneDSujN9MDXrm4oDKdHvndhZ2dN94+ERNfkYJWDclW6k2L/ZGHjXA==",
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.3.tgz",
"integrity": "sha512-QJVz1Tj7MS099PevUG5jvnt9tSkXN8K14dxQlikJuPt4uD9hHAHjLyLBiLR5zELelBdD9QNRAXZzsJx0WaDL9A==",
"dependencies": {
"function-bind": "^1.1.1",
"has": "^1.0.3",
@@ -2785,9 +2785,9 @@
"dev": true
},
"node_modules/qs": {
"version": "6.10.3",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.10.3.tgz",
"integrity": "sha512-wr7M2E0OFRfIfJZjKGieI8lBKb7fRCH4Fv5KNPEs7gJ8jadvotdsS08PzOKR7opXhZ/Xkjtt3WF9g38drmyRqQ==",
"version": "6.11.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz",
"integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==",
"dependencies": {
"side-channel": "^1.0.4"
},
@@ -3013,9 +3013,9 @@
}
},
"node_modules/retry-as-promised": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/retry-as-promised/-/retry-as-promised-5.0.0.tgz",
"integrity": "sha512-6S+5LvtTl2ggBumk04hBo/4Uf6fRJUwIgunGZ7CYEBCeufGFW1Pu6ucUf/UskHeWOIsUcLOGLFXPig5tR5V1nA=="
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/retry-as-promised/-/retry-as-promised-6.1.0.tgz",
"integrity": "sha512-Hj/jY+wFC+SB9SDlIIFWiGOHnNG0swYbGYsOj2BJ8u2HKUaobNKab0OIC0zOLYzDy0mb7A4xA5BMo4LMz5YtEA=="
},
"node_modules/safe-buffer": {
"version": "5.2.1",
@@ -3108,9 +3108,9 @@
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
},
"node_modules/sequelize": {
"version": "6.21.4",
"resolved": "https://registry.npmjs.org/sequelize/-/sequelize-6.21.4.tgz",
"integrity": "sha512-5A0+giRhGHerTDRMsZ54TYRB8oQPWxeVscbc4USG9wRtw2Eqik0Vk0p2EVDrhoq7tmNBh2nHpd9YMfvGdwPEJw==",
"version": "6.24.0",
"resolved": "https://registry.npmjs.org/sequelize/-/sequelize-6.24.0.tgz",
"integrity": "sha512-mPo7Q7gWkrsstjR2aw8ahkrj8RUJCozz3kedAz2B5ZIdLoQEH1z2tvaeJONI8R6RqeuyRQosx9Yn3s9yQyo6lQ==",
"funding": [
{
"type": "opencollective",
@@ -3127,7 +3127,7 @@
"moment": "^2.29.1",
"moment-timezone": "^0.5.34",
"pg-connection-string": "^2.5.0",
"retry-as-promised": "^5.0.0",
"retry-as-promised": "^6.1.0",
"semver": "^7.3.5",
"sequelize-pool": "^7.1.0",
"toposort-class": "^1.0.1",
@@ -3148,6 +3148,9 @@
"mysql2": {
"optional": true
},
"oracledb": {
"optional": true
},
"pg": {
"optional": true
},
@@ -4035,10 +4038,11 @@
}
},
"node_modules/winston": {
"version": "3.8.1",
"resolved": "https://registry.npmjs.org/winston/-/winston-3.8.1.tgz",
"integrity": "sha512-r+6YAiCR4uI3N8eQNOg8k3P3PqwAm20cLKlzVD9E66Ch39+LZC+VH1UKf9JemQj2B3QoUHfKD7Poewn0Pr3Y1w==",
"version": "3.8.2",
"resolved": "https://registry.npmjs.org/winston/-/winston-3.8.2.tgz",
"integrity": "sha512-MsE1gRx1m5jdTTO9Ld/vND4krP2To+lgDoMEHGGa4HIlAUyXJtfc7CxQcGXVyz2IBpw5hbFkj2b/AtUdQwyRew==",
"dependencies": {
"@colors/colors": "1.5.0",
"@dabh/diagnostics": "^2.0.2",
"async": "^3.2.3",
"is-stream": "^2.0.0",
@@ -4526,9 +4530,9 @@
"integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg=="
},
"body-parser": {
"version": "1.20.0",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.0.tgz",
"integrity": "sha512-DfJ+q6EPcGKZD1QWUjSpqp+Q7bDQTsQIF4zfUAtZ6qk+H/3/QRhg9CEp39ss+/T2vw0+HaidC0ecJj/DRLIaKg==",
"version": "1.20.1",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz",
"integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==",
"requires": {
"bytes": "3.1.2",
"content-type": "~1.0.4",
@@ -4538,7 +4542,7 @@
"http-errors": "2.0.0",
"iconv-lite": "0.4.24",
"on-finished": "2.4.1",
"qs": "6.10.3",
"qs": "6.11.0",
"raw-body": "2.5.1",
"type-is": "~1.6.18",
"unpipe": "1.0.0"
@@ -5085,13 +5089,13 @@
}
},
"express": {
"version": "4.18.1",
"resolved": "https://registry.npmjs.org/express/-/express-4.18.1.tgz",
"integrity": "sha512-zZBcOX9TfehHQhtupq57OF8lFZ3UZi08Y97dwFCkD8p9d/d2Y3M+ykKcwaMDEL+4qyUolgBDX6AblpR3fL212Q==",
"version": "4.18.2",
"resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz",
"integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==",
"requires": {
"accepts": "~1.3.8",
"array-flatten": "1.1.1",
"body-parser": "1.20.0",
"body-parser": "1.20.1",
"content-disposition": "0.5.4",
"content-type": "~1.0.4",
"cookie": "0.5.0",
@@ -5110,7 +5114,7 @@
"parseurl": "~1.3.3",
"path-to-regexp": "0.1.7",
"proxy-addr": "~2.0.7",
"qs": "6.10.3",
"qs": "6.11.0",
"range-parser": "~1.2.1",
"safe-buffer": "5.2.1",
"send": "0.18.0",
@@ -5261,9 +5265,9 @@
"integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A=="
},
"get-intrinsic": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.2.tgz",
"integrity": "sha512-Jfm3OyCxHh9DJyc28qGk+JmfkpO41A4XkneDSujN9MDXrm4oDKdHvndhZ2dN94+ERNfkYJWDclW6k2L/ZGHjXA==",
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.3.tgz",
"integrity": "sha512-QJVz1Tj7MS099PevUG5jvnt9tSkXN8K14dxQlikJuPt4uD9hHAHjLyLBiLR5zELelBdD9QNRAXZzsJx0WaDL9A==",
"requires": {
"function-bind": "^1.1.1",
"has": "^1.0.3",
@@ -6330,9 +6334,9 @@
"dev": true
},
"qs": {
"version": "6.10.3",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.10.3.tgz",
"integrity": "sha512-wr7M2E0OFRfIfJZjKGieI8lBKb7fRCH4Fv5KNPEs7gJ8jadvotdsS08PzOKR7opXhZ/Xkjtt3WF9g38drmyRqQ==",
"version": "6.11.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz",
"integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==",
"requires": {
"side-channel": "^1.0.4"
}
@@ -6509,9 +6513,9 @@
"dev": true
},
"retry-as-promised": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/retry-as-promised/-/retry-as-promised-5.0.0.tgz",
"integrity": "sha512-6S+5LvtTl2ggBumk04hBo/4Uf6fRJUwIgunGZ7CYEBCeufGFW1Pu6ucUf/UskHeWOIsUcLOGLFXPig5tR5V1nA=="
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/retry-as-promised/-/retry-as-promised-6.1.0.tgz",
"integrity": "sha512-Hj/jY+wFC+SB9SDlIIFWiGOHnNG0swYbGYsOj2BJ8u2HKUaobNKab0OIC0zOLYzDy0mb7A4xA5BMo4LMz5YtEA=="
},
"safe-buffer": {
"version": "5.2.1",
@@ -6580,9 +6584,9 @@
}
},
"sequelize": {
"version": "6.21.4",
"resolved": "https://registry.npmjs.org/sequelize/-/sequelize-6.21.4.tgz",
"integrity": "sha512-5A0+giRhGHerTDRMsZ54TYRB8oQPWxeVscbc4USG9wRtw2Eqik0Vk0p2EVDrhoq7tmNBh2nHpd9YMfvGdwPEJw==",
"version": "6.24.0",
"resolved": "https://registry.npmjs.org/sequelize/-/sequelize-6.24.0.tgz",
"integrity": "sha512-mPo7Q7gWkrsstjR2aw8ahkrj8RUJCozz3kedAz2B5ZIdLoQEH1z2tvaeJONI8R6RqeuyRQosx9Yn3s9yQyo6lQ==",
"requires": {
"@types/debug": "^4.1.7",
"@types/validator": "^13.7.1",
@@ -6593,7 +6597,7 @@
"moment": "^2.29.1",
"moment-timezone": "^0.5.34",
"pg-connection-string": "^2.5.0",
"retry-as-promised": "^5.0.0",
"retry-as-promised": "^6.1.0",
"semver": "^7.3.5",
"sequelize-pool": "^7.1.0",
"toposort-class": "^1.0.1",
@@ -7283,10 +7287,11 @@
}
},
"winston": {
"version": "3.8.1",
"resolved": "https://registry.npmjs.org/winston/-/winston-3.8.1.tgz",
"integrity": "sha512-r+6YAiCR4uI3N8eQNOg8k3P3PqwAm20cLKlzVD9E66Ch39+LZC+VH1UKf9JemQj2B3QoUHfKD7Poewn0Pr3Y1w==",
"version": "3.8.2",
"resolved": "https://registry.npmjs.org/winston/-/winston-3.8.2.tgz",
"integrity": "sha512-MsE1gRx1m5jdTTO9Ld/vND4krP2To+lgDoMEHGGa4HIlAUyXJtfc7CxQcGXVyz2IBpw5hbFkj2b/AtUdQwyRew==",
"requires": {
"@colors/colors": "1.5.0",
"@dabh/diagnostics": "^2.0.2",
"async": "^3.2.3",
"is-stream": "^2.0.0",
+3 -3
View File
@@ -15,7 +15,7 @@
"bluebird": "^3.7.2",
"cors": "^2.8.5",
"cron": "^1.8.2",
"express": "^4.17.3",
"express": "^4.18.2",
"express-async-errors": "^3.1.1",
"lodash": "^4.17.21",
"lru-cache": "^5.1.1",
@@ -24,11 +24,11 @@
"morgan": "^1.10.0",
"pg": "^8.8.0",
"redis": "^3.1.2",
"sequelize": "^6.21.4",
"sequelize": "^6.24.0",
"umzug": "^2.3.0",
"unfuck-utf8-headers-middleware": "^1.0.1",
"uuid": "^3.4.0",
"winston": "^3.6.0"
"winston": "^3.8.2"
},
"devDependencies": {
"nodemon": "^1.19.4"
+4
View File
@@ -16,6 +16,9 @@ const requiredGroup = isStaging
// Pate
const pateToken = process.env.PATE_API_TOKEN || ''
// Jami
const jamiUrl = isDev || runningInCI ? 'http://jami:3003/' : 'https://importer.cs.helsinki.fi/api/auth'
// Importer client
const importerToken = process.env.IMPORTER_DB_API_TOKEN || ''
@@ -79,6 +82,7 @@ module.exports = {
sentryEnvironment,
isProduction,
pateToken,
jamiUrl,
baseUrl,
frontUrl,
backendPort,
@@ -0,0 +1,26 @@
const { BIGINT, STRING, DATE, ARRAY } = require('sequelize')
module.exports = {
up: async queryInterface => {
await queryInterface.createTable('open_uni_population_searches', {
id: {
type: BIGINT,
primaryKey: true,
autoIncrement: true,
},
user_id: {
type: BIGINT,
},
name: {
type: STRING,
allowNull: false,
},
course_codes: {
type: ARRAY(STRING),
},
created_at: DATE,
updated_at: DATE,
})
},
down: () => {},
}
@@ -0,0 +1,33 @@
const { INTEGER, STRING, ARRAY, DATE } = require('sequelize')
module.exports = {
up: async queryInterface => {
await queryInterface.createTable('progress_criteria', {
code: {
type: STRING,
primaryKey: true,
},
courses_year_one: {
type: ARRAY(STRING),
},
courses_year_two: {
type: ARRAY(STRING),
},
courses_year_three: {
type: ARRAY(STRING),
},
credits_year_one: {
type: INTEGER,
},
credits_year_two: {
type: INTEGER,
},
credits_year_three: {
type: INTEGER,
},
created_at: DATE,
updated_at: DATE,
})
},
down: () => {},
}
@@ -0,0 +1,17 @@
module.exports = {
up: queryInterface => {
return queryInterface.bulkInsert('access_groups', [
{
group_code: 'openUniSearch',
group_info: 'grants access to custom open uni course searches',
},
])
},
down: queryInterface => {
return queryInterface.bulkDelete('access_groups', [
{
group_code: 'openUniSearch',
},
])
},
}
+8 -1
View File
@@ -38,9 +38,16 @@ const refreshFaculties = async () => {
const refreshNewOverviews = async () => {
logger.info('Refreshing studyprogramme and studytrack overview statistics for all programmes')
// Filters out old programmes and special ones like Fitech studies. Filters also out the programmes starting with
// 2_ or ending with _2. Those programmes are mapped to correct programme (studentProgrammeModuleFixer.js)
const codes = (await getAllProgrammes())
.map(p => p.code)
.filter(code => code.includes('KH') || code.includes('MH') || /^(T)[0-9]{6}$/.test(code))
.filter(
code =>
(code.includes('KH') && !code.startsWith('2_KH') && !code.endsWith('_2')) ||
(code.includes('MH') && !code.startsWith('2_MH') && !code.endsWith('_2')) ||
/^(T)[0-9]{6}$/.test(code)
)
let ready = 0
for (const code of codes) {
try {
@@ -3,8 +3,8 @@ const { ApplicationError } = require('../util/customErrors')
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 logger = require('../util/logger')
const parseIamGroups = iamGroups => iamGroups?.split(';').filter(Boolean) ?? []
@@ -27,11 +27,12 @@ const currentUserMiddleware = async (req, _res, next) => {
throw new ApplicationError('Not enough data in request headers', 403, { logoutUrl })
}
const iamGroups = parseIamGroups(hygroupcn).filter(iam => relevantIAMs.includes(iam))
const iamRights = Object.keys(await getOrganizationAccess({ iamGroups }))
const iamGroups = parseIamGroups(hygroupcn)
const { access = {}, specialGroup = {} } = await getOrganizationAccess({ iamGroups })
const iamRights = Object.keys(access)
if (!hasRequiredIamGroup(iamGroups, iamRights)) {
logger.error({ message: 'User does not have required iam group', meta: { iamGroups, iamRights } })
throw new ApplicationError('User does not have required iam group', 403, { logoutUrl })
}
@@ -40,6 +41,8 @@ const currentUserMiddleware = async (req, _res, next) => {
name,
email,
iamGroups,
iamRights,
specialGroup,
sisId,
})
@@ -1,3 +1,4 @@
const { STRING } = require('sequelize')
const Sequelize = require('sequelize')
const { sequelizeKone } = require('../database/connection')
@@ -121,6 +122,57 @@ const CustomPopulationSearch = sequelizeKone.define('custom_population_searches'
},
})
const OpenUniPopulationSearch = sequelizeKone.define(
'open_uni_population_searches',
{
id: {
primaryKey: true,
type: Sequelize.BIGINT,
autoIncrement: true,
},
userId: {
type: Sequelize.BIGINT,
},
name: {
type: Sequelize.STRING,
allowNull: false,
},
courseCodes: {
type: Sequelize.ARRAY(Sequelize.STRING),
},
},
{ underscored: true, timestamps: true }
)
const StudyProgrammeCriteria = sequelizeKone.define(
'progress_criteria',
{
code: {
primaryKey: true,
type: STRING,
},
coursesYearOne: {
type: Sequelize.ARRAY(Sequelize.STRING),
},
coursesYearTwo: {
type: Sequelize.ARRAY(Sequelize.STRING),
},
coursesYearThree: {
type: Sequelize.ARRAY(Sequelize.STRING),
},
creditsYearOne: {
type: Sequelize.INTEGER,
},
creditsYearTwo: {
type: Sequelize.INTEGER,
},
creditsYearThree: {
type: Sequelize.INTEGER,
},
},
{ underscored: true, timestamps: true }
)
const StudyGuidanceGroupTag = sequelizeKone.define(
'study_guidance_group_tags',
{
@@ -162,7 +214,9 @@ module.exports = {
ThesisTypeEnums,
MandatoryCourseLabels,
CustomPopulationSearch,
OpenUniPopulationSearch,
sequelizeKone,
ExcludedCourse,
StudyGuidanceGroupTag,
StudyProgrammeCriteria,
}
+5 -2
View File
@@ -1,4 +1,4 @@
const { Model, STRING, DATE, INTEGER } = require('sequelize')
const { Model, STRING, DATE, INTEGER, BOOLEAN } = require('sequelize')
const { dbConnections } = require('../database/connection')
class Student extends Model {}
@@ -16,15 +16,18 @@ Student.init(
creditcount: { type: INTEGER },
dateofuniversityenrollment: { type: DATE },
email: { type: STRING },
secondary_email: { type: STRING },
national_student_number: { type: STRING },
phone_number: { type: STRING },
country_fi: { type: STRING },
country_sv: { type: STRING },
country_en: { type: STRING },
home_country_fi: { type: STRING },
home_country_sv: { type: STRING },
home_country_en: { type: STRING },
gender_code: { type: INTEGER },
gender_code: { type: STRING },
sis_person_id: { type: STRING },
dissemination_info_allowed: { type: BOOLEAN },
createdAt: {
type: DATE,
},
+4
View File
@@ -30,6 +30,8 @@ const trends = require('./routes/trends')
const programmeModules = require('./routes/programmeModules')
const studyGuidanceGroups = require('./routes/studyGuidanceGroups')
const studyProgramme = require('./routes/studyProgramme')
const customOpenUniSearch = require('./routes/customOpenUniSearch')
const studyProgrammeCriteria = require('./routes/studyProgrammeCriteria')
const initializeSentry = require('./util/sentry')
const errorMiddleware = require('./middleware/errorMiddleware')
@@ -57,6 +59,8 @@ module.exports = (app, url) => {
app.use(url, tags)
app.use(url, programmeModules)
app.use(url, studyProgramme)
app.use(`${url}/programmecriteria`, studyProgrammeCriteria)
app.use(`${url}/openunisearch`, auth.roles(['openUniSearch']), customOpenUniSearch)
app.use(`${url}/faculties`, auth.roles(['facultyStatistics']), faculties)
app.use(`${url}/updater`, auth.roles(['admin']), updater)
app.use(`${url}/teachers`, auth.roles(['teachers']), teachers)
+6
View File
@@ -1,6 +1,7 @@
const router = require('express').Router()
const crypto = require('crypto')
const Course = require('../services/courses')
const { faculties } = require('../services/organisations')
const { validateParamLength } = require('../util')
router.get('/v2/coursesmulti', async (req, res) => {
@@ -43,4 +44,9 @@ router.get('/v3/courseyearlystats', async (req, res) => {
}
})
router.get('/faculties-only', async (_req, res) => {
const facultyList = await faculties()
res.json(facultyList)
})
module.exports = router
@@ -0,0 +1,74 @@
const router = require('express').Router()
const moment = require('moment-timezone')
const { getCustomOpenUniCourses } = require('../services/openUni/openUniStats')
const {
getOpenUniSearches,
createNewSearch,
deleteSearch,
updateSearch,
} = require('../services/openUni/openUniManageSearches')
router.get('/', async (req, res) => {
const courseCodes = JSON.parse(req.query?.courselist) || []
const startdate = req.query?.startdate ? req.query?.startdate : moment('01-08-2017 00:00:00', 'DD-MM-YYYY')
const enddate = req.query?.enddate ? req.query?.enddate : moment().endOf('day')
if (!Array.isArray(courseCodes)) return res.status(400).json({ error: 'Courses must be of type array' })
const customOpenUniSearches = await getCustomOpenUniCourses(courseCodes, startdate, enddate)
return res.json(customOpenUniSearches)
})
router.get('/searches', async (req, res) => {
const userId = req.user.id
const foundSearches = await getOpenUniSearches(userId)
return res.json(foundSearches)
})
router.post('/searches', async (req, res) => {
const courseCodes = req.body?.courselist || []
const userId = req.user.id
const name = req.body?.name
if (!name) return res.status(400).json({ error: 'Name missing' })
if (courseCodes && !Array.isArray(courseCodes))
return res.status(400).json({ error: 'Course codes must be type of array' })
const createdSearch = await createNewSearch(userId, name, courseCodes)
if (!createdSearch) return res.status(400).json({ error: '' })
return res.status(201).json({
id: createdSearch.id,
userId: createdSearch.userId,
name: createdSearch.name,
courseList: createdSearch.courseCodes,
updatedAt: createdSearch.updatedAt,
})
})
router.put('/searches/:id', async (req, res) => {
const id = req.params?.id
const courseCodes = req.body?.courselist || []
const userId = req.user.id
if (!id || !userId) return res.status(422).end()
const updatedSearch = await updateSearch(userId, id, courseCodes)
if (!updatedSearch) return res.status(404).json({ error: `Open uni search could not be found` })
return res.json({
id: updatedSearch.id,
userId: updatedSearch.userId,
name: updatedSearch.name,
courseList: updatedSearch.courseCodes,
updatedAt: updatedSearch.updatedAt,
})
})
router.delete('/searches/:id', async (req, res) => {
const id = req.params?.id
const userId = req.user.id
if (!id || !userId) return res.status(422).end()
const deletedSearch = await deleteSearch(userId, id)
if (!deletedSearch) return res.status(404).json({ error: `Open uni search could not be found` })
return res.json(id)
})
module.exports = router
+25 -10
View File
@@ -53,10 +53,8 @@ router.get('/:id/basicstats', async (req, res) => {
const specialGroups = req.query?.special_groups
if (!code) return res.status(422).end()
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()
@@ -90,14 +88,16 @@ router.get('/:id/creditstats', async (req, res) => {
const programmeFilter = req.query?.programme_filter
if (!code) return res.status(422).end()
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)
// list of all programmes is also needed in classification of credits
const allProgrammes = programmeFilter === 'ALL_PROGRAMMES' ? programmes : await getProgrammes(code, 'ALL_PROGRAMMES')
let updatedStats = await combineFacultyCredits(code, programmes.data, allProgrammes.data, yearType, specialGroups)
if (updatedStats) {
updatedStats = await setCreditStats(updatedStats, yearType, programmeFilter, specialGroups)
}
@@ -114,7 +114,6 @@ router.get('/:id/thesisstats', async (req, res) => {
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)
@@ -145,13 +144,25 @@ router.get('/:id/progressstats', async (req, res) => {
const programmeFilter = req.query.programme_filter
const specialGroups = req.query?.special_groups
const graduated = req.query?.graduated
const programmes = await getProgrammes(code, programmeFilter)
if (!programmes) return res.status(422).end()
//all programmes are required for correct sorting of transfers
let allProgrammeCodes = []
const allProgs = await getProgrammes(code, 'ALL_PROGRAMMES')
allProgs?.data.forEach(prog => allProgrammeCodes.push(prog.code))
if (!code) return res.status(422).end()
const data = await getFacultyProgressStats(code, specialGroups, graduated)
if (data) return res.json(data)
const programmes = await getProgrammes(code, programmeFilter)
if (!programmes) return res.status(422).end()
let updateStats = await combineFacultyStudentProgress(code, programmes.data, specialGroups, graduated)
let updateStats = await combineFacultyStudentProgress(
code,
programmes.data,
allProgrammeCodes,
specialGroups,
graduated
)
if (updateStats) {
updateStats = await setFacultyProgressStats(updateStats, specialGroups, graduated)
}
@@ -165,12 +176,16 @@ router.get('/:id/studentstats', async (req, res) => {
const graduated = req.query?.graduated
if (!code) return res.status(422).end()
const data = await getFacultyStudentStats(code, specialGroups, graduated)
if (data) return res.json(data)
const programmes = await getProgrammes(code, programmeFilter)
if (!programmes) return res.status(422).end()
let updateStats = await combineFacultyStudents(code, programmes.data, specialGroups, graduated)
//all programmes are required for correct sorting of transfers
let allProgrammeCodes = []
const allProgs = await getProgrammes(code, 'ALL_PROGRAMMES')
allProgs?.data.forEach(prog => allProgrammeCodes.push(prog.code))
let updateStats = await combineFacultyStudents(code, programmes.data, allProgrammeCodes, specialGroups, graduated)
if (updateStats) {
updateStats = await setFacultyStudentStats(updateStats, specialGroups, graduated)
}
@@ -10,6 +10,7 @@ const CourseService = require('../services/courses')
const StatMergeService = require('../services/statMerger')
const { mapToProviders } = require('../util/utils')
const { encrypt, decrypt } = require('../services/encrypt')
const { mapCodesToIds } = require('../services/studyprogrammeHelpers')
const { ApplicationError } = require('../util/customErrors')
@@ -268,6 +269,7 @@ router.get('/v3/populationstatistics', async (req, res) => {
...student,
firstnames: '',
lastname: '',
phone_number: '',
started: null,
iv,
studentNumber: encryptedData,
@@ -380,6 +382,7 @@ router.get('/v3/populationstatisticsbycourse', async (req, res) => {
studentNumber: crypto.createHash('md5').update(`${studentNumber}${randomHash}`).digest('hex'),
firstnames: '',
lastname: '',
phone_number: '',
name: '',
email: '',
started: null,
@@ -429,9 +432,11 @@ router.get('/v3/populationstatistics/studyprogrammes', async (req, res) => {
} = req
if (roles?.includes('admin')) {
const studyrights = await StudyrightService.getAssociations()
mapCodesToIds(studyrights.programmes)
res.json(studyrights)
} else {
const studyrights = await StudyrightService.getFilteredAssociations(rights.concat(iamRights))
mapCodesToIds(studyrights.programmes)
res.json(studyrights)
}
})
@@ -0,0 +1,30 @@
const {
getCriteria,
saveYearlyCourseCriteria,
saveYearlyCreditCriteria,
} = require('../services/studyProgrammeCriteria')
const router = require('express').Router()
router.get('/', async (req, res) => {
const studyProgramme = req.query?.programmecode
if (!studyProgramme) return res.status(422).end()
const studyProgrammeCriteria = await getCriteria(studyProgramme)
return res.json(studyProgrammeCriteria)
})
router.post('/courses', async (req, res) => {
const { code, courses, year } = req.body
if (!code || !courses || !year) return res.status(400).end()
const studyProgrammeCriteria = await saveYearlyCourseCriteria(code, courses, year)
return res.json(studyProgrammeCriteria)
})
router.post('/credits', async (req, res) => {
const { code, credits } = req.body
if (!code || !credits) return res.status(400).end()
const studyProgrammeCriteria = await saveYearlyCreditCriteria(code, credits)
return res.json(studyProgrammeCriteria)
})
module.exports = router
@@ -4,10 +4,9 @@ const {
ElementDetail,
Organization,
ProgrammeModule,
Semester,
SemesterEnrollment,
Studyright,
StudyrightElement,
SemesterEnrollment,
Student,
Transfer,
Credit,
@@ -19,23 +18,54 @@ const {
formatFacultyTransfer,
formatFacultyThesisWriter,
formatOrganization,
formatAbsence,
isNewProgramme,
mapCodesToIds,
facultyProgrammeStudents,
} = 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 awayTransfers = await transferredAway(programmeCodes, allProgrammeCodes, since)
const toTransfers = await transferredTo(programmeCodes, allProgrammeCodes, since)
return [...toTransfers, ...awayTransfers]
}
const getTransferredInside = async (programmeCodes, allProgrammeCodes, since) => {
const transfersInside = (await transferredInsideFaculty(programmeCodes, allProgrammeCodes, since)).map(
t => t.studyrightid
)
return transfersInside
return await transferredInsideFaculty(programmeCodes, allProgrammeCodes, since)
}
const getTransfers = async (programmeCode, allProgrammeCodes, since, end) => {
return await transferredFaculty(programmeCode, allProgrammeCodes, since, end)
}
const transferredFaculty = async (programme, allProgrammeCodes, since, end) =>
(
await Transfer.findAll({
where: {
transferdate: {
[Op.between]: [since, end],
},
[Op.or]: [
{
sourcecode: allProgrammeCodes,
targetcode: programme,
},
{
sourcecode: programme,
targetcode: {
[Op.notIn]: allProgrammeCodes,
},
},
{
sourcecode: {
[Op.notIn]: allProgrammeCodes,
},
targetcode: programme,
},
],
},
})
).map(formatFacultyTransfer)
const startedStudyrights = async (faculty, since, studyRightWhere) =>
(
await Studyright.findAll({
@@ -124,15 +154,58 @@ const studyrightsByRightStartYear = async (faculty, since, graduated = 1) =>
})
).map(facultyFormatStudyright)
const bachelorStudyright = async id => {
return await Studyright.findOne({
attributes: ['studystartdate'],
where: {
studyrightid: id,
extentcode: 1,
},
})
}
const getStudyRightsByExtent = async (faculty, elementStart, studyrightStart, code, extents, graduated) =>
(
await Studyright.findAll({
include: [
{
model: Student,
attributes: ['studentnumber'],
required: true,
},
{
model: StudyrightElement,
required: true,
where: {
code: code,
...elementStart,
},
include: {
model: ElementDetail,
required: true,
where: {
type: 20,
},
},
},
],
where: {
faculty_code: faculty,
extentcode: {
[Op.in]: extents,
},
graduated: {
[Op.in]: graduated,
},
studystartdate: { [Op.not]: null },
...studyrightStart,
student_studentnumber: { [Op.not]: null },
},
})
).map(facultyFormatStudyright)
const getStudentsByStudentnumbers = async studentnumbers =>
(
await Student.findAll({
where: {
studentnumber: studentnumbers,
},
include: {
model: SemesterEnrollment,
attributes: ['semestercode', 'enrollmenttype'],
},
})
).map(facultyProgrammeStudents)
const hasMasterRight = async id => {
return await Studyright.findOne({
@@ -255,29 +328,6 @@ const thesisWriters = async (provider, since, thesisTypes, students) =>
})
).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) => {
@@ -326,6 +376,8 @@ const findFacultyProgrammeCodes = async (faculty, programmeFilter) => {
allProgrammes = allProgrammes.filter(prog => isNewProgramme(prog.code))
}
mapCodesToIds(allProgrammes)
return allProgrammes
}
@@ -333,7 +385,6 @@ module.exports = {
startedStudyrights,
graduatedStudyrights,
studyrightsByRightStartYear,
bachelorStudyright,
hasMasterRight,
transferredInsideFaculty,
transferredAway,
@@ -342,7 +393,9 @@ module.exports = {
thesisWriters,
findFacultyProgrammeCodes,
facultyOgranizationId,
statutoryAbsences,
getTransferredToAndAway,
getTransferredInside,
getStudyRightsByExtent,
getStudentsByStudentnumbers,
getTransfers,
}
@@ -1,4 +1,5 @@
const { indexOf } = require('lodash')
const moment = require('moment')
const {
startedStudyrights,
graduatedStudyrights,
@@ -7,9 +8,11 @@ const {
transferredTo,
getTransferredToAndAway,
getTransferredInside,
studyrightsByRightStartYear,
} = require('./faculty')
const { getStatsBasis, getYearsArray, defineYear } = require('../studyprogrammeHelpers')
const { findRightProgramme, isNewProgramme, checkTransfers, getExtentFilter } = require('./facultyHelpers')
const { codes } = require('../../../config/programmeCodes')
const filterDuplicateStudyrights = studyrights => {
// bachelor+master students have two studyrights (separated by two last digits in studyrightid)
@@ -40,19 +43,18 @@ const addProgramme = async (
facultyProgrammes,
includeAllSpecials
) => {
if (!(programme in stats)) {
stats[programme] = {}
let { progId, name, code } = facultyProgrammes.filter(prog => prog.code === programme)[0]
if (!(progId in stats)) {
stats[progId] = {}
if (includeAllSpecials) {
Object.keys(tableStats).forEach(year => (stats[programme][year] = [0, 0, 0]))
Object.keys(tableStats).forEach(year => (stats[progId][year] = [0, 0, 0]))
} else {
Object.keys(tableStats).forEach(year => (stats[programme][year] = [0]))
Object.keys(tableStats).forEach(year => (stats[progId][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
stats[progId][transferYear][index] += 1
if (!(progId in allBasics.programmeNames)) {
allBasics.programmeNames[progId] = { ...name, code: code }
}
}
@@ -76,6 +78,7 @@ const getFacultyStarters = async (
const studyrightWhere = getExtentFilter(includeAllSpecials)
const studyrights = await startedStudyrights(faculty, since, studyrightWhere)
let filteredStudyrights = filterDuplicateStudyrights(studyrights)
const keys = Object.keys(codes)
const programmeTableStats = {}
if (!includeAllSpecials) {
filteredStudyrights = filteredStudyrights.filter(
@@ -85,6 +88,11 @@ const getFacultyStarters = async (
filteredStudyrights.forEach(({ studystartdate, studyrightElements }) => {
const { programme, programmeName } = findRightProgramme(studyrightElements, 'started')
let programmeId = programme
if (keys.includes(programme)) {
programmeId = codes[programme].toUpperCase()
}
if (programmeFilter === 'ALL_PROGRAMMES' || isNewProgramme(programme)) {
const startYear = defineYear(studystartdate, isAcademicYear)
@@ -92,18 +100,18 @@ const getFacultyStarters = async (
startedGraphStats[indexOf(yearsArray, startYear)] += 1
startedTableStats[startYear] += 1
if (!(programme in programmeTableStats)) {
programmeTableStats[programme] = { ...tableStats }
if (!(programmeId in programmeTableStats)) {
programmeTableStats[programmeId] = { ...tableStats }
}
programmeTableStats[programme][startYear] += 1
programmeTableStats[programmeId][startYear] += 1
if (!(programme in allBasics.programmeNames)) {
allBasics.programmeNames[programme] = programmeName
if (!(programmeId in allBasics.programmeNames)) {
allBasics.programmeNames[programmeId] = { ...programmeName, code: programme }
}
}
})
allBasics.studentInfo.graphStats.push({ name: 'Started studying', data: startedGraphStats })
allBasics.studentInfo.graphStats.push({ name: 'Started studying (new in faculty)', data: startedGraphStats })
programmeData['started'] = programmeTableStats
Object.keys(startedTableStats).forEach(year => {
@@ -129,7 +137,7 @@ const getFacultyGraduates = async (
) => {
const graduatedGraphStats = [[...graphStats], [...graphStats], [...graphStats], [...graphStats], [...graphStats]]
const graduatedTableStats = {}
const keys = Object.keys(codes)
let studyrightWhere = getExtentFilter(includeAllSpecials)
let graduatedRights = await graduatedStudyrights(faculty, since, studyrightWhere)
const programmeTableStats = {}
@@ -140,40 +148,45 @@ const getFacultyGraduates = async (
s => !checkTransfers(s, insideTransfersStudyrights, transfersToOrAwayStudyrights)
)
}
graduatedRights.forEach(({ enddate, extentcode, studyrightElements }) => {
const { programme, programmeName } = findRightProgramme(studyrightElements, 'graduated')
let programmeId = programme
if (keys.includes(programme)) {
programmeId = codes[programme].toUpperCase()
}
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]))
if (!(programmeId in programmeTableStats)) {
programmeTableStats[programmeId] = {}
Object.keys(tableStats).forEach(year => (programmeTableStats[programmeId][year] = [0, 0, 0, 0, 0]))
}
programmeTableStats[programme][endYear][0] += 1
programmeTableStats[programmeId][endYear][0] += 1
if (extentcode === 1) {
graduatedGraphStats[1][indexOf(yearsArray, endYear)] += 1
graduatedTableStats[endYear][1] += 1
programmeTableStats[programme][endYear][1] += 1
programmeTableStats[programmeId][endYear][1] += 1
} else if (extentcode === 2) {
graduatedGraphStats[2][indexOf(yearsArray, endYear)] += 1
graduatedTableStats[endYear][2] += 1
programmeTableStats[programme][endYear][2] += 1
programmeTableStats[programmeId][endYear][2] += 1
} else if (extentcode === 4) {
graduatedGraphStats[3][indexOf(yearsArray, endYear)] += 1
graduatedTableStats[endYear][3] += 1
programmeTableStats[programme][endYear][3] += 1
programmeTableStats[programmeId][endYear][3] += 1
} else {
graduatedGraphStats[4][indexOf(yearsArray, endYear)] += 1
graduatedTableStats[endYear][4] += 1
programmeTableStats[programme][endYear][4] += 1
programmeTableStats[programmeId][endYear][4] += 1
}
if (!(programme in allBasics.programmeNames)) {
allBasics.programmeNames[programme] = programmeName
if (!(programmeId in allBasics.programmeNames)) {
allBasics.programmeNames[programmeId] = { code: programme, ...programmeName }
}
}
})
@@ -192,7 +205,18 @@ const getFacultyGraduates = async (
countsGraduations[year] = graduatedTableStats[year]
})
}
// If includeAllSpecial false, insidetransfers should contain only students started after 1.8.2017.
const getInsideTransfers = async (programmeCodes, allProgrammeCodes, since, includeAllSpecials, faculty) => {
if (includeAllSpecials) {
return await transferredInsideFaculty(programmeCodes, allProgrammeCodes, since)
}
const insiders = await transferredInsideFaculty(programmeCodes, allProgrammeCodes, since)
const studyrights = (await studyrightsByRightStartYear(faculty, moment('2017-08-01', 'YYYY-MM-DD'))).map(
sr => sr.studyrightid
)
const filteredTransfers = insiders.filter(tr => studyrights.includes(tr.studyrightid))
return filteredTransfers
}
const getFacultyTransfers = async (
programmes,
programmeCodes,
@@ -205,10 +229,16 @@ const getFacultyTransfers = async (
allBasics,
counts,
programmeData,
includeAllSpecials
includeAllSpecials,
faculty
) => {
const insideTransfers = await transferredInsideFaculty(programmeCodes, allProgrammeCodes, since)
// Filter still away those that have started studying before 1.8.2017, when include all specials false.
const insideTransfers = await getInsideTransfers(
programmeCodes,
allProgrammeCodes,
since,
includeAllSpecials,
faculty
)
let transferGraphStats = []
let transferTableStats = {}
if (includeAllSpecials) {
@@ -224,17 +254,19 @@ const getFacultyTransfers = async (
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 (isAcademicYear || !(transferYear > moment().year())) {
transferTableStats[transferYear][0] += 1
await addProgramme(
programmeTableStats,
targetcode,
tableStats,
transferYear,
0,
allBasics,
programmes,
includeAllSpecials
)
}
}
if (includeAllSpecials) {
const awayTransfers = await transferredAway(programmeCodes, allProgrammeCodes, since)
@@ -242,34 +274,38 @@ const getFacultyTransfers = async (
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
)
if (isAcademicYear || !(transferYear > moment().year())) {
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
)
if (isAcademicYear || !(transferYear > moment().year())) {
transferGraphStats[2][indexOf(yearsArray, transferYear)] += 1
transferTableStats[transferYear][2] += 1
await addProgramme(
programmeTableStats,
targetcode,
tableStats,
transferYear,
2,
allBasics,
programmes,
includeAllSpecials
)
}
}
}
@@ -367,7 +403,8 @@ const combineFacultyBasics = async (faculty, programmes, yearType, allProgrammeC
allBasics,
counts,
programmeData,
includeAllSpecials
includeAllSpecials,
faculty
)
// combine tableStats from all categories
@@ -413,7 +450,11 @@ const combineFacultyBasics = async (faculty, programmes, yearType, allProgrammeC
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])
if (includeAllSpecials) {
studentInfo[code][year] = studentInfo[code][year].concat([0, 0, 0])
} else {
studentInfo[code][year] = studentInfo[code][year].concat([0])
}
}
}
})
@@ -95,6 +95,7 @@ const getFacultyRegularCreditStats = async ({
nonDegree.tableStats[attainmentYear] += credits || 0
}
})
if (
majors.graphStats.every(year => year === 0) &&
facultyNonMajors.graphStats.every(year => year === 0) &&
@@ -191,7 +192,7 @@ const getProgrammeCredits = async (code, yearType, specialGroups, facultyProgram
return updatedStats
}
const combineFacultyCredits = async (faculty, programmes, yearType, specialGroups) => {
const combineFacultyCredits = async (faculty, programmes, allProgrammes, yearType, specialGroups) => {
let counts = {}
let years = []
const titles =
@@ -216,12 +217,13 @@ const combineFacultyCredits = async (faculty, programmes, yearType, specialGroup
titles,
}
const progCodes = programmes.map(p => p.code)
const progCodes = allProgrammes.map(p => p.code)
for (const prog of programmes) {
if (prog.code === 'MH70_008_2') continue
const data = await getProgrammeCredits(prog.code, yearType, specialGroups, progCodes)
if (data) {
allCredits.programmeNames[prog.code] = prog.name
allCredits.programmeTableStats[prog.code] = data.tableStats
allCredits.programmeNames[prog.progId] = { code: prog.code, ...prog.name }
allCredits.programmeTableStats[prog.progId] = data.tableStats
if (!allCredits.lastUpdated || new Date(data.lastUpdated) > new Date(allCredits.lastUpdated))
allCredits.lastUpdated = data.lastUpdated
@@ -237,6 +239,7 @@ const combineFacultyCredits = async (faculty, programmes, yearType, specialGroup
})
}
}
// save table stats and graph stats
years.forEach(year => {
allCredits.tableStats.push([year, ...counts[year]])
@@ -1,17 +1,39 @@
const moment = require('moment')
const {
graduatedStudyrights,
studyrightsByRightStartYear,
bachelorStudyright,
hasMasterRight,
statutoryAbsences,
} = require('./faculty')
const { graduatedStudyrights, studyrightsByRightStartYear, hasMasterRight } = require('./faculty')
const { findRightProgramme, isNewProgramme } = require('./facultyHelpers')
const { getYearsArray, getYearsObject, getMean, getMedian, defineYear } = require('../studyprogrammeHelpers')
const { getYearsArray, getYearsObject, getMedian, defineYear } = require('../studyprogrammeHelpers')
const { codes } = require('../../../config/programmeCodes')
const { bachelorStudyright, countTimeCategories, getStatutoryAbsences } = require('../graduationHelpers')
const sortProgrammes = data => {
const check = name => {
if (isNaN(name[0])) return -1
return 1
}
data.sort((a, b) => {
if (check(a.name) === check(b.name)) return a.name.localeCompare(b.name)
return check(a.name) - check(b.name)
})
}
const getSortedProgIds = progs => {
let programmes = []
for (const prog of progs) {
if (Object.keys(codes).includes(prog)) {
const name = prog === 'T923103' ? codes[prog].toUpperCase() + '-DP' : codes[prog].toUpperCase()
programmes = [...programmes, { name: name, code: prog }]
} else {
programmes = [...programmes, { name: prog, code: prog }]
}
}
sortProgrammes(programmes)
return programmes
}
const findBachelorStartdate = async id => {
const studyright = await bachelorStudyright(id)
if (studyright) return studyright.studystartdate
if (studyright) return studyright.startdate
return null
}
@@ -22,35 +44,6 @@ const getProgrammeObjectBasis = (years, levels, emptyObject = true) => {
)
}
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 sortProgrammes = codes => {
const check = code => {
if (isNaN(code[0])) return -1
return 1
}
codes.sort((a, b) => {
if (check(a) === check(b)) return a.localeCompare(b)
return check(a) - check(b)
})
}
const hasMS = async (programme, elements, studyrightid) => {
// Only bachelor studyright: socCom, kasvatustiede - varhaiskasvatus track, farmasia - applied only to Bc = farmaseutti,
if (['KH74_001', '620050-ba', '620030-ba', '520091-ba'].includes(programme)) {
@@ -188,11 +181,9 @@ const count = async (faculty, since, years, yearsList, levels, programmeFilter,
let graduationTimes = {}
let programmes = getProgrammeObjectBasis(yearsList, levels)
let data = {
means: {},
medians: {},
programmes: {
medians: getProgrammeObjectBasis(yearsList, levels),
means: getProgrammeObjectBasis(yearsList, levels),
},
}
@@ -200,7 +191,6 @@ const count = async (faculty, since, years, yearsList, levels, programmeFilter,
graduationAmounts[level] = getYearsObject({ years: yearsList })
graduationTimes[level] = getYearsObject({ years: yearsList, emptyArrays: true })
data.medians[level] = []
data.means[level] = []
})
let studyrights = null
@@ -243,47 +233,45 @@ const count = async (faculty, since, years, yearsList, levels, programmeFilter,
years.forEach(year => {
levels.forEach(level => {
const median = getMedian(graduationTimes[level][year])
const mean = getMean(graduationTimes[level][year])
const statistics = { onTime: 0, yearOver: 0, wayOver: 0 }
// Programme level breakdown
data.programmes.medians[level][year] = { programmes: [], data: [] }
data.programmes.means[level][year] = { programmes: [], data: [] }
const programmeCodes = Object.keys(programmes[level][year])
sortProgrammes(programmeCodes)
const programmeIds = getSortedProgIds(programmeCodes)
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)
if (programmeIds.length > 0) {
for (const prog of programmeIds) {
const progMedian = getMedian(programmes[level][year][prog.code].graduationTimes)
let goal = goals[level]
if (faculty === 'H30' && Object.keys(goals.exceptions).includes(prog)) {
goal += goals.exceptions[prog]
if (faculty === 'H30' && Object.keys(goals.exceptions).includes(prog.code)) {
goal += goals.exceptions[prog.code]
}
const progStatistics = countTimeCategories(programmes[level][year][prog].graduationTimes, goal)
const progStatistics = countTimeCategories(programmes[level][year][prog.code].graduationTimes, goal)
Object.keys(progStatistics).forEach(key => (statistics[key] += progStatistics[key]))
data.programmes.medians[level][year].programmes = [...data.programmes.medians[level][year].programmes, prog]
data.programmes.medians[level][year].programmes = [
...data.programmes.medians[level][year].programmes,
prog.code,
]
data.programmes.medians[level][year].data = [
...data.programmes.medians[level][year].data,
{
y: progMedian,
amount: programmes[level][year][prog].graduationAmounts,
amount: programmes[level][year][prog.code].graduationAmounts,
statistics: progStatistics,
name: prog.name,
code: prog.code,
},
]
data.programmes.means[level][year].programmes = [...data.programmes.means[level][year].programmes, prog]
data.programmes.means[level][year].data = [
...data.programmes.means[level][year].data,
{ y: progMean, amount: programmes[level][year][prog].graduationAmounts, statistics: progStatistics },
]
}
}
data.medians[level] = [...data.medians[level], { y: median, amount: graduationAmounts[level][year], statistics }]
data.means[level] = [...data.means[level], { y: mean, amount: graduationAmounts[level][year], statistics }]
data.medians[level] = [
...data.medians[level],
{ y: median, amount: graduationAmounts[level][year], statistics, name: year },
]
})
})
@@ -350,7 +338,7 @@ const countGraduationTimes = async (faculty, programmeFilter) => {
classSizes['programmes'] = {}
await getClassSizes(faculty, since, classSizes, programmeFilter, years)
return { id: faculty, years, goals, byGradYear, byStartYear, programmeNames, classSizes }
return { id: faculty, goals, byGradYear, byStartYear, programmeNames, classSizes }
}
module.exports = { countGraduationTimes }
@@ -1,5 +1,6 @@
const Sequelize = require('sequelize')
const { Op } = Sequelize
const { codes } = require('../../../config/programmeCodes')
const findRightProgramme = (studyrightElements, mode) => {
let programme = ''
@@ -55,14 +56,23 @@ const facultyFormatStudyright = studyright => {
studyrightElements: studyright_elements,
}
}
const facultyProgrammeStudents = student => {
const { studentnumber, home_country_en, gender_code, semester_enrollments } = student
return {
studentnumber,
home_country_en,
gender_code,
semesters: semester_enrollments.map(s => s.dataValues),
}
}
const formatFacultyTransfer = transfer => {
const { sourcecode, targetcode, transferdate, studyrightid } = transfer
const { sourcecode, targetcode, transferdate, studyrightid, studentnumber } = transfer
return {
sourcecode,
targetcode,
transferdate,
studyrightid,
studentnumber,
}
}
@@ -87,11 +97,6 @@ const formatOrganization = 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 => {
@@ -104,10 +109,11 @@ const isNewProgramme = code => {
}
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 allTransfers = [
...insideTransfersStudyrights.map(sr => sr.studentnumber),
...transfersToOrAwayStudyrights.map(sr => sr.studentnumber),
]
return allTransfers.includes(s.studentnumber)
}
const getExtentFilter = includeAllSpecials => {
@@ -123,6 +129,21 @@ const getExtentFilter = includeAllSpecials => {
return studyrightWhere
}
const mapCodesToIds = data => {
// Add programme id e.g. TKT
const keys = Object.keys(codes)
for (const prog of data) {
if (keys.includes(prog.code)) {
prog.progId = codes[prog.code].toUpperCase()
if (prog.code === 'T923103') {
prog.progId = prog.progId + '-DP'
}
} else {
prog.progId = prog.code
}
}
}
module.exports = {
findRightProgramme,
facultyFormatStudyright,
@@ -130,8 +151,9 @@ module.exports = {
formatFacultyTransfer,
formatFacultyThesisWriter,
formatOrganization,
formatAbsence,
isNewProgramme,
checkTransfers,
getExtentFilter,
mapCodesToIds,
facultyProgrammeStudents,
}
@@ -1,4 +1,5 @@
const { indexOf } = require('lodash')
const { Op } = require('sequelize')
const moment = require('moment')
const { getAcademicYearDates } = require('../../util/semester')
const {
@@ -7,191 +8,314 @@ const {
getMasterCreditGraphStats,
getDoctoralCreditGraphStats,
getYearsArray,
getCorrectStudentnumbers,
tableTitles,
getOnlyMasterCreditGraphStats,
getOnlyMasterThresholds,
getBcMsThresholds,
getVetenaryCreditGraphStats,
} = require('../studyprogrammeHelpers')
const { studytrackStudents, allStudyrights } = require('../studyprogramme')
const { studytrackStudents } = require('../studyprogramme')
const { getStudyRightsByExtent, getTransferredInside, getTransferredToAndAway } = require('./faculty')
const { checkTransfers } = require('./facultyHelpers')
const getStudentData = (startDate, students, thresholdKeys, thresholdAmounts) => {
const getStudentData = (startDate, students, thresholdKeys, thresholdAmounts, limits = [], limitKeys = [], prog) => {
let data = {}
let programmeData = {}
thresholdKeys.forEach(t => (data[t] = 0))
limitKeys.forEach(t => (programmeData[t] = 0))
students.forEach(({ credits }) => {
const creditcount = credits
.filter(credit => moment(credit.attainment_date).isAfter(startDate))
.reduce((prev, curr) => (prev += curr.credits), 0)
.filter(credit => moment(credit.attainment_date).isSameOrAfter(startDate))
.reduce((prev, curr) => prev + curr.credits, 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
if (thresholdKeys.length === 8) {
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[5] && creditcount < thresholdAmounts[6] ? 1 : 0
data[thresholdKeys[7]] += creditcount >= thresholdAmounts[6] ? 1 : 0
} else if (thresholdKeys.length === 7) {
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[5] ? 1 : 0
} else {
} else if (thresholdKeys.length === 6) {
data[thresholdKeys[4]] += creditcount >= thresholdAmounts[3] && creditcount < thresholdAmounts[4] ? 1 : 0
data[thresholdKeys[5]] += creditcount >= thresholdAmounts[4] ? 1 : 0
} else {
data[thresholdKeys[4]] += creditcount >= thresholdAmounts[3] ? 1 : 0
}
if (['KH', 'MH'].includes(prog)) {
programmeData[limitKeys[0]] += creditcount <= limits[5][0] ? 1 : 0
programmeData[limitKeys[1]] += limits[4][0] <= creditcount && limits[4][1] > creditcount ? 1 : 0
programmeData[limitKeys[2]] += limits[3][0] <= creditcount && limits[3][1] > creditcount ? 1 : 0
programmeData[limitKeys[3]] += limits[2][0] <= creditcount && limits[2][1] > creditcount ? 1 : 0
programmeData[limitKeys[4]] += limits[1][0] <= creditcount && limits[1][1] > creditcount ? 1 : 0
programmeData[limitKeys[5]] += creditcount >= limits[0][0] ? 1 : 0
}
})
return data
return { data, progData: programmeData }
}
const combineFacultyStudentProgress = async (faculty, programmes, specialGroups, graduated) => {
const createLimits = (months, creditsToAdd) => {
return [
[Math.ceil(months * (60 / 12)) + creditsToAdd, null],
[Math.ceil(months * (45 / 12)) + creditsToAdd, Math.ceil(months * (60 / 12)) + creditsToAdd],
[Math.ceil(months * (30 / 12)) + creditsToAdd, Math.ceil(months * (45 / 12)) + creditsToAdd],
[Math.ceil(months * (15 / 12)) + creditsToAdd, Math.ceil(months * (30 / 12)) + creditsToAdd],
[creditsToAdd + 1, Math.ceil(months * (15 / 12)) + creditsToAdd],
[creditsToAdd, null],
]
}
const createYearlyTitles = (start, limitList) => {
return [
`${start} Credits`,
`${limitList[4][0]} ≤ Credits < ${limitList[4][1]}`,
`${limitList[3][0]} ≤ Credits < ${limitList[3][1]}`,
`${limitList[2][0]} ≤ Credits < ${limitList[2][1]}`,
`${limitList[1][0]} ≤ Credits < ${limitList[1][1]}`,
`${limitList[0][0]} ≤ Credits`,
]
}
const filterOutTransfers = async (studyrights, programmes, allProgrammeCodes, since) => {
const transferredInAndOut = await getTransferredToAndAway(programmes, allProgrammeCodes, since)
const insideTransfers = await getTransferredInside(programmes, allProgrammeCodes, since)
const filteredStudyrights = studyrights.filter(sr => !checkTransfers(sr, insideTransfers, transferredInAndOut))
return filteredStudyrights
}
const combineFacultyStudentProgress = async (faculty, programmes, allProgrammeCodes, specialGroups, graduated) => {
const since = new Date('2017-08-01')
const isAcademicYear = true
const includeYearsCombined = true
const includeAllSpecials = specialGroups === 'SPECIAL_INCLUDED'
const includeGraduated = graduated === 'GRADUATED_INCLUDED'
const yearsArray = getYearsArray(since.getFullYear(), isAcademicYear, includeYearsCombined)
let allBachelorGraphStats = getBachelorCreditGraphStats(yearsArray)
let allBsMsGraphstats = getMasterCreditGraphStats(yearsArray)
let allMastersGraphStats = getOnlyMasterCreditGraphStats(yearsArray)
let allDoctoralGraphStats = getDoctoralCreditGraphStats(yearsArray)
let bachelorsProgrammeStats = {}
let bcMsProgrammeStats = {}
let mastersProgrammeStats = {}
let doctoralProgrammeStats = {}
let bachelorsTableStats = new Array(yearsArray.length)
let bachelorMastersTableStats = new Array(yearsArray.length)
let mastersTableStats = new Array(yearsArray.length)
let doctoralTableStats = new Array(yearsArray.length)
const graduationStatus = [0]
if (includeGraduated) {
graduationStatus.push(1)
}
const yearsArray = getYearsArray(since.getFullYear(), true, true)
const titlesVet = [
'',
'All',
'< 210 credits',
'210-239 credits',
'240-269 credits',
'270-299 credits',
'300-329 credits',
'330-359 credits',
' 360 ≤ credits ',
]
const progressStats = {
id: faculty,
years: yearsArray,
bachelorsTableStats: new Array(yearsArray.length),
bcMsTableStats: new Array(yearsArray.length),
mastersTableStats: new Array(yearsArray.length),
doctoralTableStats: new Array(yearsArray.length),
bachelorsGraphStats: getBachelorCreditGraphStats(yearsArray),
bcMsGraphStats: faculty === 'H90' ? getVetenaryCreditGraphStats(yearsArray) : getMasterCreditGraphStats(yearsArray),
mastersGraphStats: getOnlyMasterCreditGraphStats(yearsArray),
doctoralGraphStats: getDoctoralCreditGraphStats(yearsArray),
bachelorTitles: tableTitles.creditProgress.bachelor,
bcMsTitles: faculty === 'H90' ? titlesVet : tableTitles.creditProgress.master,
mastersTitles: tableTitles.creditProgress.masterOnly,
doctoralTitles: tableTitles.creditProgress.doctoral,
yearlyBachelorTitles: [],
yearlyBcMsTitles: [],
yearlyMasterTitles: [],
programmeNames: {},
bachelorsProgStats: {},
bcMsProgStats: {},
mastersProgStats: {},
doctoralProgStats: {},
}
const reversedYears = [...yearsArray].reverse()
reversedYears.forEach(year => {
bachelorsTableStats[indexOf(reversedYears, year)] = [year, 0, 0, 0, 0, 0, 0, 0, 0, 0]
bachelorMastersTableStats[indexOf(reversedYears, year)] = [year, 0, 0, 0, 0, 0, 0, 0, 0]
mastersTableStats[indexOf(reversedYears, year)] = [year, 0, 0, 0, 0, 0, 0, 0]
doctoralTableStats[indexOf(reversedYears, year)] = [year, 0, 0, 0, 0, 0, 0, 0, 0]
progressStats.bachelorsTableStats[indexOf(reversedYears, year)] = [year, 0, 0, 0, 0, 0, 0, 0, 0, 0]
progressStats.bcMsTableStats[indexOf(reversedYears, year)] = [year, 0, 0, 0, 0, 0, 0, 0, 0]
progressStats.mastersTableStats[indexOf(reversedYears, year)] = [year, 0, 0, 0, 0, 0, 0, 0]
progressStats.doctoralTableStats[indexOf(reversedYears, year)] = [year, 0, 0, 0, 0, 0, 0]
})
const limitKeys = ['zero', 't5', 't4', 't3', 't2', 't1']
let bachelorlimits = []
let masterlimits = []
let bcmslimits = []
const bachelorProgress = {}
const masterProgress = {}
const bcmsProgress = {}
const doctoralProgress = {}
for (const year of reversedYears) {
if (year === 'Total') continue
const { startDate, endDate } = getAcademicYearDates(year, since)
const lastDayOfMonth = moment().endOf('month')
let months = Math.round(moment.duration(moment(lastDayOfMonth).diff(moment(startDate))).asMonths())
if (months >= 36) {
bachelorlimits = createLimits(36, 0)
} else {
bachelorlimits = createLimits(months, 0)
}
if (months >= 24) {
masterlimits = createLimits(24, 0)
} else {
masterlimits = createLimits(months, 0)
}
if (months >= 72 && faculty === 'H90') {
bcmslimits = createLimits(72, 0)
} else if (months >= 60) {
bcmslimits = createLimits(60, 0)
} else {
bcmslimits = createLimits(months, 0)
}
progressStats.yearlyBachelorTitles = [...progressStats.yearlyBachelorTitles, createYearlyTitles(0, bachelorlimits)]
progressStats.yearlyMasterTitles = [...progressStats.yearlyMasterTitles, createYearlyTitles(0, masterlimits)]
progressStats.yearlyBcMsTitles = [...progressStats.yearlyBcMsTitles, createYearlyTitles(0, bcmslimits)]
const programmeCodes = programmes.map(prog => prog.code)
for (const programme of programmes) {
let { creditThresholdKeys, creditThresholdAmounts } = getCreditThresholds(programme.code)
if (!creditThresholdKeys) return
for (const year of reversedYears) {
const { startDate, endDate } = getAcademicYearDates(year, since)
const studentnumbers = await getCorrectStudentnumbers({
codes: [programme.code],
startDate,
endDate,
includeAllSpecials,
includeGraduated,
})
const all = await allStudyrights(programme.code, studentnumbers)
const students = await studytrackStudents(studentnumbers)
if (programme.code.includes('KH')) {
const studentData = getStudentData(startDate, students, creditThresholdKeys, creditThresholdAmounts)
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')) {
const all_BcMs = all.filter(sr => sr.studyrightid.slice(-2) === '-2')
const all_Ms = all.filter(sr => sr.studyrightid.slice(-2) !== '-2')
const all_MsStudentnumbers = all_Ms.map(sr => sr.studentnumber)
const all_BcMsStudentnumbers = all_BcMs.map(sr => sr.studentnumber)
mastersTableStats[indexOf(reversedYears, year)][1] += all_Ms.length || 0
bachelorMastersTableStats[indexOf(reversedYears, year)][1] += all_BcMs.length || 0
const msStudents = students.filter(student => all_MsStudentnumbers.includes(student.studentnumber))
const bcMsStudents = students.filter(student => all_BcMsStudentnumbers.includes(student.studentnumber))
const bcMsStudentdata = getStudentData(startDate, bcMsStudents, creditThresholdKeys, creditThresholdAmounts)
const { msOnlyCreditThresholdKeys, msOnlyCreditThresholdAmount } = getOnlyMasterThresholds()
const msStudentdata = getStudentData(
for (const { progId, code, name } of programmes) {
let extentcodes = [1, 2, 3, 4]
if (includeAllSpecials) {
extentcodes = [...extentcodes, ...[7, 9, 34, 22, 99, 14, 13]]
}
const startDateWhere = {
startdate: {
[Op.and]: {
[Op.gte]: startDate,
[Op.lte]: endDate,
},
},
}
let studyrights = await getStudyRightsByExtent(faculty, {}, startDateWhere, code, extentcodes, graduationStatus)
let { creditThresholdKeys, creditThresholdAmounts } = getCreditThresholds(code)
if (!creditThresholdKeys) continue
if (!(progId in progressStats.programmeNames)) {
progressStats.programmeNames[progId] = { code: code, ...name }
}
if (!includeAllSpecials) {
studyrights = await filterOutTransfers(studyrights, programmeCodes, allProgrammeCodes, since)
}
if (code.includes('KH')) {
const allBachelors = studyrights.filter(sr => sr.extentcode === 1).map(sr => sr.studentnumber)
const students = await studytrackStudents(allBachelors)
const { data: studentData, progData } = getStudentData(
startDate,
msStudents,
msOnlyCreditThresholdKeys,
msOnlyCreditThresholdAmount
students,
creditThresholdKeys,
creditThresholdAmounts,
bachelorlimits,
limitKeys,
'KH'
)
if (!(programme.code in mastersProgrammeStats)) {
mastersProgrammeStats[programme.code] = new Array(reversedYears.length - 1)
if (!(progId in bachelorProgress)) {
bachelorProgress[progId] = new Array(reversedYears.length - 1)
}
if (!(programme.code in bcMsProgrammeStats)) {
bcMsProgrammeStats[programme.code] = new Array(reversedYears.length - 1)
progressStats.bachelorsTableStats[indexOf(reversedYears, year)][1] += students.length || 0
progressStats.bachelorsTableStats[indexOf(reversedYears, 'Total')][1] += students.length || 0
bachelorProgress[progId][indexOf(reversedYears, year)] = limitKeys.map(key => progData[key])
for (const key of Object.keys(studentData)) {
progressStats.bachelorsTableStats[indexOf(reversedYears, year)][indexOf(creditThresholdKeys, key) + 2] +=
studentData[key] || 0
progressStats.bachelorsGraphStats[key].data[indexOf(yearsArray, year)] += studentData[key] || 0
progressStats.bachelorsTableStats[indexOf(reversedYears, 'Total')][indexOf(creditThresholdKeys, key) + 2] +=
studentData[key] || 0
progressStats.bachelorsGraphStats[key].data[indexOf(yearsArray, 'Total')] += studentData[key] || 0
}
if (year !== 'Total') {
mastersProgrammeStats[programme.code][indexOf(reversedYears, year)] = msOnlyCreditThresholdKeys.map(
key => msStudentdata[key]
)
bcMsProgrammeStats[programme.code][indexOf(reversedYears, year)] = creditThresholdKeys.map(
key => bcMsStudentdata[key]
)
} else if (code.includes('MH')) {
const allMsStudents = studyrights
.filter(sr => sr.extentcode === 2 && sr.studyrightid.slice(-2) !== '-2')
.map(sr => sr.studentnumber)
const allBcMsStudents = studyrights
.filter(sr => sr.extentcode === 2 && sr.studyrightid.slice(-2) === '-2')
.map(sr => sr.studentnumber)
const bcMsStudents = await studytrackStudents(allBcMsStudents)
const msStudents = await studytrackStudents(allMsStudents)
progressStats.mastersTableStats[indexOf(reversedYears, year)][1] += msStudents.length || 0
progressStats.mastersTableStats[indexOf(reversedYears, 'Total')][1] += msStudents.length || 0
progressStats.bcMsTableStats[indexOf(reversedYears, year)][1] += bcMsStudents.length || 0
progressStats.bcMsTableStats[indexOf(reversedYears, 'Total')][1] += bcMsStudents.length || 0
const { creditThresholdKeysBcMs, creditThresholdAmountsBcMs } = getBcMsThresholds()
const { data: bcMsStudentdata, progData: bcMsProgdata } = getStudentData(
startDate,
bcMsStudents,
creditThresholdKeysBcMs,
creditThresholdAmountsBcMs,
bcmslimits,
limitKeys,
'MH'
)
const { data: msStudentdata, progData: msProgdata } = getStudentData(
startDate,
msStudents,
creditThresholdKeys,
creditThresholdAmounts,
masterlimits,
limitKeys,
'MH'
)
if (!(progId in masterProgress)) {
masterProgress[progId] = new Array(reversedYears.length - 1)
bcmsProgress[progId] = new Array(reversedYears.length - 1)
}
masterProgress[progId][indexOf(reversedYears, year)] = limitKeys.map(key => msProgdata[key])
bcmsProgress[progId][indexOf(reversedYears, year)] = limitKeys.map(key => bcMsProgdata[key])
for (const key of Object.keys(msStudentdata)) {
mastersTableStats[indexOf(reversedYears, year)][indexOf(msOnlyCreditThresholdKeys, key) + 2] +=
progressStats.mastersTableStats[indexOf(reversedYears, year)][indexOf(creditThresholdKeys, key) + 2] +=
msStudentdata[key] || 0
allMastersGraphStats[key].data[indexOf(yearsArray, year)] += msStudentdata[key] || 0
progressStats.mastersTableStats[indexOf(reversedYears, 'Total')][indexOf(creditThresholdKeys, key) + 2] +=
msStudentdata[key] || 0
progressStats.mastersGraphStats[key].data[indexOf(yearsArray, year)] += msStudentdata[key] || 0
progressStats.mastersGraphStats[key].data[indexOf(yearsArray, 'Total')] += msStudentdata[key] || 0
}
for (const key of Object.keys(bcMsStudentdata)) {
bachelorMastersTableStats[indexOf(reversedYears, year)][indexOf(creditThresholdKeys, key) + 2] +=
progressStats.bcMsTableStats[indexOf(reversedYears, year)][indexOf(creditThresholdKeysBcMs, key) + 2] +=
bcMsStudentdata[key] || 0
allBsMsGraphstats[key].data[indexOf(yearsArray, year)] += bcMsStudentdata[key] || 0
progressStats.bcMsTableStats[indexOf(reversedYears, 'Total')][indexOf(creditThresholdKeysBcMs, key) + 2] +=
bcMsStudentdata[key] || 0
progressStats.bcMsGraphStats[key].data[indexOf(yearsArray, year)] += bcMsStudentdata[key] || 0
progressStats.bcMsGraphStats[key].data[indexOf(yearsArray, 'Total')] += bcMsStudentdata[key] || 0
}
} else {
const studentData = getStudentData(startDate, students, creditThresholdKeys, creditThresholdAmounts)
doctoralTableStats[indexOf(reversedYears, year)][1] += all.length || 0
if (!(programme.code in doctoralProgrammeStats)) {
doctoralProgrammeStats[programme.code] = new Array(reversedYears.length - 1)
const all = studyrights.filter(sr => sr.extentcode === 4 || sr.extentcode === 3).map(sr => sr.studentnumber)
const doctoralStudents = await studytrackStudents(all)
const { data } = getStudentData(startDate, doctoralStudents, creditThresholdKeys, creditThresholdAmounts, 'T')
progressStats.doctoralTableStats[indexOf(reversedYears, year)][1] += doctoralStudents.length || 0
progressStats.doctoralTableStats[indexOf(reversedYears, 'Total')][1] += doctoralStudents.length || 0
if (!(progId in doctoralProgress)) {
doctoralProgress[progId] = 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
doctoralProgress[progId][indexOf(reversedYears, year)] = creditThresholdKeys.map(key => data[key])
for (const key of Object.keys(data)) {
progressStats.doctoralTableStats[indexOf(reversedYears, year)][indexOf(creditThresholdKeys, key) + 2] +=
data[key] || 0
progressStats.doctoralTableStats[indexOf(reversedYears, 'Total')][indexOf(creditThresholdKeys, key) + 2] +=
data[key] || 0
progressStats.doctoralGraphStats[key].data[indexOf(yearsArray, year)] += data[key] || 0
progressStats.doctoralGraphStats[key].data[indexOf(yearsArray, 'Total')] += data[key] || 0
}
}
}
}
const facultyProgressStats = {
id: faculty,
years: yearsArray,
bachelorsTableStats: bachelorsTableStats,
bcMsTableStats: bachelorMastersTableStats,
mastersTableStats: mastersTableStats,
doctoralTableStats: doctoralTableStats,
bachelorsGraphStats: allBachelorGraphStats,
bcMsGraphStats: allBsMsGraphstats,
mastersGraphStats: allMastersGraphStats,
doctoralGraphStats: allDoctoralGraphStats,
bachelorTitles: tableTitles.creditProgress.bachelor,
bcMsTitles: tableTitles.creditProgress.master,
mastersTitles: tableTitles.creditProgress.masterOnly,
doctoralTitles: tableTitles.creditProgress.doctoral,
programmeNames: programmes.reduce((obj, dataItem) => ({ ...obj, [dataItem.code]: dataItem.name }), {}),
bachelorsProgrammeStats: bachelorsProgrammeStats,
bcMsProgrammeStats: bcMsProgrammeStats,
mastersProgrammeStats: mastersProgrammeStats,
doctoralProgrammeStats: doctoralProgrammeStats,
}
return facultyProgressStats
progressStats.bachelorsProgStats = Object.keys(bachelorProgress)
.filter(prog => !bachelorProgress[prog].every(year => year.every(value => value === 0)))
.reduce((result, prog) => ({ ...result, [prog]: bachelorProgress[prog] }), {})
progressStats.mastersProgStats = Object.keys(masterProgress)
.filter(prog => !masterProgress[prog].every(year => year.every(value => value === 0)))
.reduce((result, prog) => ({ ...result, [prog]: masterProgress[prog] }), {})
progressStats.bcMsProgStats = Object.keys(bcmsProgress)
.filter(prog => !bcmsProgress[prog].every(year => year.every(value => value === 0)))
.reduce((result, prog) => ({ ...result, [prog]: bcmsProgress[prog] }), {})
progressStats.doctoralProgStats = Object.keys(doctoralProgress)
.filter(prog => !doctoralProgress[prog].every(year => year.every(value => value === 0)))
.reduce((result, prog) => ({ ...result, [prog]: doctoralProgress[prog] }), {})
return progressStats
}
module.exports = { combineFacultyStudentProgress }
@@ -1,39 +1,13 @@
const {
getStartDate,
getYearsArray,
getPercentage,
getYearsObject,
getCorrectStudentnumbers,
tableTitles,
} = require('../studyprogrammeHelpers')
const {
studytrackStudents,
allStudyrights,
startedStudyrights,
inactiveStudyrights,
enrolledStudents,
absentStudents,
graduatedStudyRights,
} = require('../studyprogramme')
const moment = require('moment')
const { Op } = require('sequelize')
const { getStartDate, getYearsArray, getPercentage, getYearsObject, tableTitles } = require('../studyprogrammeHelpers')
const { getAcademicYearDates } = require('../../util/semester')
const getStudentData = students => {
let data = { female: 0, male: 0, finnish: 0 }
students.forEach(({ gender_code, home_country_en }) => {
data.male += gender_code === '1' ? 1 : 0
data.female += gender_code === '2' ? 1 : 0
data.finnish += home_country_en === 'Finland' ? 1 : 0
})
return data
}
// Goes through the faculty and its study programmes for the year
const getFacultyDataForYear = async ({ programmes, since, settings, year, years, programmeTableStats }) => {
const { includeAllSpecials, includeGraduated } = settings
const { startDate, endDate } = getAcademicYearDates(year, since)
// Totals for the year into table stats
const totals = {
const { getStudyRightsByExtent, getStudentsByStudentnumbers, getTransfers } = require('./faculty')
const { getCurrentSemester } = require('../semesters')
const { checkTransfers } = require('./facultyHelpers')
const { inactiveStudyrights, absentStudents } = require('../studyprogramme')
const emptyTotals = () => {
return {
total: 0,
allStarted: 0,
allEnrolled: 0,
@@ -43,113 +17,261 @@ const getFacultyDataForYear = async ({ programmes, since, settings, year, years,
allFinnish: 0,
allFemale: 0,
allMale: 0,
allOtherUnknown: 0,
allOtherCountries: 0,
}
}
const toltalsCalculation = totals => {
return [
totals.total,
totals.allStarted,
getPercentage(totals.allStarted, totals.total),
totals.allEnrolled,
getPercentage(totals.allEnrolled, totals.total),
totals.allAbsent,
getPercentage(totals.allAbsent, totals.total),
totals.allInactive,
getPercentage(totals.allInactive, totals.total),
totals.allGraduated,
getPercentage(totals.allGraduated, totals.total),
totals.allMale,
getPercentage(totals.allMale, totals.total),
totals.allFemale,
getPercentage(totals.allFemale, totals.total),
totals.allOtherUnknown,
getPercentage(totals.allOtherUnknown, totals.total),
totals.allFinnish,
getPercentage(totals.allFinnish, totals.total),
totals.allOtherCountries,
getPercentage(totals.allOtherCountries, totals.total),
]
}
const addTotals = (facultyTableStats, year, totals) => {
facultyTableStats[year] = [year, ...toltalsCalculation(totals)]
}
const addTotalsProgramme = (programmeTableStats, progId, year, totals) => {
programmeTableStats[progId][year] = toltalsCalculation(totals)
}
for (const programme of programmes) {
const studentnumbers = await getCorrectStudentnumbers({
codes: [programme.code],
startDate,
endDate,
includeAllSpecials,
includeGraduated,
})
const getStudentData = (students, facultyExtra, year, code) => {
let data = { female: 0, male: 0, finnish: 0, otherCountries: 0, otherUnknown: 0 }
students.forEach(({ gender_code, home_country_en }) => {
data.male += gender_code === '1' ? 1 : 0
data.female += gender_code === '2' ? 1 : 0
data.otherUnknown += ['0', '3'].includes(gender_code) ? 1 : 0
facultyExtra[year][code].otherG += gender_code === '3' ? 1 : 0
facultyExtra[year][code].unknownG += gender_code === '0' ? 1 : 0
facultyExtra['Total'][code].otherG += gender_code === '3' ? 1 : 0
facultyExtra['Total'][code].unknownG += gender_code === '0' ? 1 : 0
data.finnish += home_country_en === 'Finland' ? 1 : 0
data.otherCountries += home_country_en !== 'Finland' ? 1 : 0
if (!Object.keys(facultyExtra[year][code].countries).includes(home_country_en))
facultyExtra[year][code].countries[home_country_en] = 0
if (!Object.keys(facultyExtra['Total'][code].countries).includes(home_country_en))
facultyExtra['Total'][code].countries[home_country_en] = 0
facultyExtra[year][code].countries[home_country_en] += 1
facultyExtra['Total'][code].countries[home_country_en] += 1
})
return data
}
// Goes through the faculty and its study programmes for the year
const getFacultyDataForYear = async ({
faculty,
programmes,
allProgrammeCodes,
since,
settings,
year,
years,
programmeTableStats,
facultyExtra,
allTotals,
progTotals,
programmeNames,
}) => {
const { includeAllSpecials, includeGraduated } = settings
const { startDate, endDate } = getAcademicYearDates(year, since)
// Totals for the year into table stats
let extents = [1, 2, 3, 4]
let graduated = [0]
if (includeAllSpecials) {
extents = [...extents, ...[7, 9, 34, 22, 99, 14, 13]]
}
if (includeGraduated) {
graduated = [...graduated, 1]
}
const totals = emptyTotals()
for (const { progId, code, name } of programmes) {
let elementStart = {}
let studyrightStart = {}
const start = moment(`${year.slice(0, 4)}-08-01`, 'YYYY-MM-DD')
const end = moment(`${year.slice(-4)}-07-31`, 'YYYY-MM-DD').endOf('day')
const allTransfers = await getTransfers(code, allProgrammeCodes, start, end)
if (!Object.keys(facultyExtra[year]).includes(code))
facultyExtra[year][code] = { unknownG: 0, otherG: 0, countries: {} }
if (!Object.keys(facultyExtra['Total']).includes(code))
facultyExtra['Total'][code] = { unknownG: 0, otherG: 0, countries: {} }
const startDateWhere = {
startdate: {
[Op.and]: {
[Op.gte]: startDate,
[Op.lte]: endDate,
},
},
}
if (code.includes('MH')) {
elementStart = startDateWhere
} else {
studyrightStart = startDateWhere
}
const allStudyrights = await getStudyRightsByExtent(
faculty,
elementStart,
studyrightStart,
code,
extents,
graduated
)
let studyrights = allStudyrights
let allStudents = []
studyrights = allStudyrights.filter(s => !checkTransfers(s, allTransfers, allTransfers))
if (includeAllSpecials) {
allStudents = [...new Set([...allTransfers.map(sr => sr.studentnumber).filter(smbr => smbr !== null)])]
}
// Get all the studyrights and students for the calculations
let all = await allStudyrights(programme.code, studentnumbers)
const students = await studytrackStudents(studentnumbers)
const studentData = getStudentData(students)
const started = await startedStudyrights(programme.code, startDate, studentnumbers)
const enrolled = await enrolledStudents(programme.code, startDate, studentnumbers)
const absent = await absentStudents(programme.code, studentnumbers)
const inactive = await inactiveStudyrights(programme.code, studentnumbers)
const graduated = await graduatedStudyRights(programme.code, startDate, studentnumbers)
const currentSemester = await getCurrentSemester()
const studentNbrs = studyrights.map(sr => sr.studentnumber)
allStudents = [...studentNbrs, ...allStudents]
const students = await getStudentsByStudentnumbers(allStudents)
const studentData = getStudentData(students, facultyExtra, year, code)
const started = await getStudentsByStudentnumbers(studentNbrs)
const inactive = await inactiveStudyrights(code, studentNbrs)
const graduatedStudents = studyrights.filter(sr => sr.graduated === 1)
const absent = await absentStudents(code, studentNbrs)
const enrolled = students.filter(
s =>
s.semesters.filter(semester => semester.semestercode === currentSemester.semestercode)[0]?.enrollmenttype === 1
)
// Count stats for the programme
if (!(programme.code in programmeTableStats)) {
programmeTableStats[programme.code] = years.reduce((resultObj, yearKey) => {
if (students.length === 0) continue
if (!(progId in programmeTableStats)) {
programmeTableStats[progId] = years.reduce((resultObj, yearKey) => {
return { ...resultObj, [yearKey]: [] }
}, {})
progTotals[progId] = emptyTotals()
}
if (!(progId in programmeNames)) {
programmeNames[progId] = { code: code, ...name }
progTotals[progId] = emptyTotals()
}
programmeTableStats[programme.code][year] = [
all.length,
started.length,
getPercentage(started.length, all.length),
enrolled.length,
getPercentage(enrolled.length, all.length),
absent.length,
getPercentage(absent.length, all.length),
inactive.length,
getPercentage(inactive.length, all.length),
graduated.length,
getPercentage(graduated.length, all.length),
studentData.male,
getPercentage(studentData.male, all.length),
studentData.female,
getPercentage(studentData.female, all.length),
studentData.finnish,
getPercentage(studentData.finnish, all.length),
]
const yearTotals = {
total: students.length,
allStarted: started.length,
allEnrolled: enrolled.length,
allAbsent: absent.length,
allInactive: inactive.length,
allGraduated: graduatedStudents.length,
allMale: studentData.male,
allFemale: studentData.female,
allOtherUnknown: studentData.otherUnknown,
allFinnish: studentData.finnish,
allOtherCountries: studentData.otherCountries,
}
addTotalsProgramme(programmeTableStats, progId, year, yearTotals)
totals.total += all.length
totals.total += students.length
totals.allStarted += started.length
totals.allEnrolled += enrolled.length
totals.allAbsent += absent.length
totals.allInactive += inactive.length
totals.allGraduated += graduated.length
totals.allGraduated += graduatedStudents.length
totals.allFemale += studentData.female
totals.allMale += studentData.male
totals.allOtherUnknown += studentData.otherUnknown
totals.allFinnish += studentData.finnish
}
totals.allOtherCountries += studentData.otherCountries
allTotals.total += students.length
allTotals.allStarted += started.length
allTotals.allEnrolled += enrolled.length
allTotals.allAbsent += absent.length
allTotals.allInactive += inactive.length
allTotals.allGraduated += graduatedStudents.length
allTotals.allFemale += studentData.female
allTotals.allMale += studentData.male
allTotals.allOtherUnknown += studentData.otherUnknown
allTotals.allFinnish += studentData.finnish
allTotals.allOtherCountries += studentData.otherCountries
progTotals[progId].total += students.length
progTotals[progId].allStarted += started.length
progTotals[progId].allEnrolled += enrolled.length
progTotals[progId].allAbsent += absent.length
progTotals[progId].allInactive += inactive.length
progTotals[progId].allGraduated += graduatedStudents.length
progTotals[progId].allFemale += studentData.female
progTotals[progId].allMale += studentData.male
progTotals[progId].allOtherUnknown += studentData.otherUnknown
progTotals[progId].allFinnish += studentData.finnish
progTotals[progId].allOtherCountries += studentData.otherCountries
}
return totals
}
// Combines all the data for faculty students table
const combineFacultyStudents = async (code, programmes, specialGroups, graduated) => {
const combineFacultyStudents = async (code, programmes, allProgrammeCodes, specialGroups, graduated) => {
// Only academic years are considered
const isAcademicYear = true
const includeAllSpecials = specialGroups === 'SPECIAL_INCLUDED'
const includeGraduated = graduated === 'GRADUATED_INCLUDED'
const includeYearsCombined = true
const since = getStartDate(code, isAcademicYear)
const years = getYearsArray(since.getFullYear(), isAcademicYear, includeYearsCombined)
const since = getStartDate(code, true)
const years = getYearsArray(since.getFullYear(), true, true)
const settings = { includeAllSpecials, includeGraduated }
let facultyTableStats = getYearsObject({ years, emptyArrays: true })
let programmeTableStats = {}
const facultyTableStats = getYearsObject({ years, emptyArrays: true })
const programmeTableStats = {}
const facultyExtra = years.reduce((acc, year) => ({ ...acc, [year]: {} }), {})
const progTotals = {}
const allTotals = emptyTotals()
const programmeNames = {}
for (const year of years.reverse()) {
const queryParams = { programmes, since, settings, year, years, programmeTableStats }
const totals = await getFacultyDataForYear(queryParams)
facultyTableStats[year] = [
if (year === 'Total') continue
const queryParams = {
faculty: code,
programmes,
allProgrammeCodes,
since,
settings,
year,
totals.total,
totals.allStarted,
getPercentage(totals.allStarted, totals.total),
totals.allEnrolled,
getPercentage(totals.allEnrolled, totals.total),
totals.allAbsent,
getPercentage(totals.allAbsent, totals.total),
totals.allInactive,
getPercentage(totals.allInactive, totals.total),
totals.allGraduated,
getPercentage(totals.allGraduated, totals.total),
totals.allMale,
getPercentage(totals.allMale, totals.total),
totals.allFemale,
getPercentage(totals.allFemale, totals.total),
totals.allFinnish,
getPercentage(totals.allFinnish, totals.total),
]
years,
programmeTableStats,
facultyExtra,
allTotals,
progTotals,
programmeNames,
}
const totals = await getFacultyDataForYear(queryParams)
if (totals.total !== 0) {
addTotals(facultyTableStats, year, totals)
}
}
addTotals(facultyTableStats, 'Total', allTotals)
Object.keys(progTotals).forEach(programme =>
addTotalsProgramme(programmeTableStats, programme, 'Total', progTotals[programme])
)
const studentsData = {
id: code,
years: years,
facultyTableStats: facultyTableStats,
facultyTableStatsExtra: facultyExtra,
programmeStats: programmeTableStats,
titles: tableTitles['studytracks'],
programmeNames: programmes.reduce((obj, dataItem) => ({ ...obj, [dataItem.code]: dataItem.name }), {}),
titles: tableTitles['programmes'],
programmeNames: programmeNames,
}
return studentsData
}
@@ -11,21 +11,15 @@ const {
} = 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',
]
const thesisTypes = ['urn:code:course-unit-type:bachelors-thesis', 'urn:code:course-unit-type:masters-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) {
for (const { progId, code, name } of facultyProgrammes) {
if (code === 'MH70_008_2' || code.startsWith('LIS') || code.startsWith('T')) continue
const provider = mapToProviders([code])[0]
const students = await getCorrectStudentnumbers({
codes: [code],
@@ -33,42 +27,35 @@ const getFacultyThesisWriters = async ({ since, years, isAcademicYear, facultyPr
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] = {}
if (!(progId in programmeCounts)) {
programmeCounts[progId] = {}
Object.keys(bachelors.tableStats).forEach(year => (programmeCounts[code][year] = [0, 0, 0, 0, 0]))
programmeNames[code] = name
Object.keys(bachelors.tableStats).forEach(year => (programmeCounts[progId][year] = [0, 0, 0]))
programmeNames[progId] = { ...name, code: code }
}
programmeCounts[code][thesisYear][0] += 1
programmeCounts[progId][thesisYear][0] += 1
if (courseUnitType === thesisTypes[0]) {
bachelors.graphStats[indexOf(years, thesisYear)] += 1
bachelors.tableStats[thesisYear] += 1
programmeCounts[code][thesisYear][1] += 1
programmeCounts[progId][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
programmeCounts[progId][thesisYear][2] += 1
}
})
}
return { bachelors, masters, doctors, licentiates, programmeCounts, programmeNames }
return { bachelors, masters, programmeCounts, programmeNames }
}
const getFacultyThesisWritersForStudyTrack = async (
const getFacultyThesisWritersForProgrammes = async (
allThesisWriters,
facultyProgrammes,
isAcademicYear,
@@ -77,33 +64,27 @@ const getFacultyThesisWritersForStudyTrack = async (
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
)
const { bachelors, masters, 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],
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 => {
programmes.forEach(programmeId => {
reversedYears.forEach(year => {
if (programmeCounts && programmeCode in programmeCounts) {
if (!(programmeCode in allThesisWriters.programmeTableStats)) {
allThesisWriters.programmeTableStats[programmeCode] = []
if (programmeCounts && programmeId in programmeCounts) {
if (!(programmeId in allThesisWriters.programmeTableStats)) {
allThesisWriters.programmeTableStats[programmeId] = []
}
allThesisWriters.programmeTableStats[programmeCode].push([year, ...programmeCounts[programmeCode][year]])
allThesisWriters.programmeTableStats[programmeId].push([year, ...programmeCounts[programmeId][year]])
}
})
})
@@ -118,7 +99,7 @@ const combineFacultyThesisWriters = async (faculty, facultyProgrammes, yearType,
tableStats: [],
graphStats: [],
programmeTableStats: {},
titles: ['', 'All', 'Bachelors', 'Masters', 'Doctors', 'Licentiates'],
titles: ['', 'All', 'Bachelors', 'Masters'],
programmeNames: {},
status: 'Done',
lastUpdated: '',
@@ -126,7 +107,7 @@ const combineFacultyThesisWriters = async (faculty, facultyProgrammes, yearType,
const isAcademicYear = yearType === 'ACADEMIC_YEAR'
const includeAllSpecials = specialGroups === 'SPECIAL_INCLUDED'
await getFacultyThesisWritersForStudyTrack(allThesisWriters, facultyProgrammes, isAcademicYear, includeAllSpecials)
await getFacultyThesisWritersForProgrammes(allThesisWriters, facultyProgrammes, isAcademicYear, includeAllSpecials)
return allThesisWriters
}
@@ -104,6 +104,7 @@ const updateFacultyOverview = async (faculty, statsType) => {
const updatedCredits = await combineFacultyCredits(
faculty,
programmeFilter === 'ALL_PROGRAMMES' ? allProgrammes.data : newProgrammes.data,
allProgrammes.data,
yearType,
specialGroups
)
@@ -154,9 +155,14 @@ const updateFacultyProgressOverview = async faculty => {
const options = [specialGraduated, specialNotGraduated, notSpecialGraduated, notSpecialNotGraduated]
let newProgrammes = []
let allProgrammes = []
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)
}
@@ -164,20 +170,22 @@ const updateFacultyProgressOverview = async faculty => {
for (const option of options) {
const { specialGroups, graduated } = option
try {
const updateFacultyProgressStats = await combineFacultyStudentProgress(
faculty,
newProgrammes.data,
specialGroups,
graduated
)
await setFacultyProgressStats(updateFacultyProgressStats, specialGroups, graduated)
const updateFacultyStudentStats = await combineFacultyStudents(
faculty,
newProgrammes.data,
allProgrammeCodes,
specialGroups,
graduated
)
await setFacultyStudentStats(updateFacultyStudentStats, specialGroups, graduated)
const updateFacultyProgressStats = await combineFacultyStudentProgress(
faculty,
newProgrammes.data,
allProgrammeCodes,
specialGroups,
graduated
)
await setFacultyProgressStats(updateFacultyProgressStats, specialGroups, graduated)
} catch (e) {
logger.error(e)
}
@@ -0,0 +1,73 @@
const Sequelize = require('sequelize')
const { Op } = Sequelize
const moment = require('moment')
const { Semester, SemesterEnrollment, Studyright } = require('../models')
// categorize graduation times
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
}
// discount staturory absences from graduation times
const formatAbsence = absence => {
const { semestercode, semester } = absence
return { semestercode, start: semester.startdate, end: semester.enddate }
}
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)
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
}
// count bachelor time in Bc+Ms studyrights to master graduation times
const bachelorStudyright = async id => {
return await Studyright.findOne({
attributes: ['startdate'],
where: {
studyrightid: id,
extentcode: 1,
},
})
}
module.exports = {
countTimeCategories,
getStatutoryAbsences,
bachelorStudyright,
}
@@ -0,0 +1,47 @@
const mapOpenCredits = credit => {
const { course_code, attainment_date, student_studentnumber, grade } = credit
return {
course_code,
attainment_date,
student_studentnumber,
grade,
}
}
const mapStundentInfo = student => {
const { email, secondary_email, studentnumber, dissemination_info_allowed } = student
return {
studentnumber,
email,
secondary_email,
dissemination_info_allowed,
}
}
const mapOpenEnrollments = enrollment => {
const { studentnumber, course_code, state, enrollment_date_time } = enrollment
return {
enrollmentStudentnumber: studentnumber,
course_code,
state,
enrollment_date_time,
}
}
const mapStudyRights = studyright => {
const { enddate, startdate, student } = studyright
return {
startdate,
enddate,
studyrightStudentnumber: student.studentnumber,
}
}
const mapCourseInfo = course => {
const { code, name } = course
return {
label: code,
name,
}
}
module.exports = { mapOpenCredits, mapOpenEnrollments, mapStundentInfo, mapStudyRights, mapCourseInfo }
@@ -0,0 +1,65 @@
const logger = require('../../util/logger')
const {
getOpenUniSearchesByUser,
createOpenUniPopulationSearch,
updateOpenUniPopulationSearch,
deleteOpenUniSearch,
} = require('./openUniSearches')
const getOpenUniSearches = async userId => {
const savedSearches = await getOpenUniSearchesByUser(userId)
if (savedSearches === undefined) return []
const foundSearches = savedSearches.reduce(
(res, search) => [
...res,
{
id: search.id,
userId: search.userId,
name: search.name,
courseList: search.courseCodes,
updatedAt: search.updatedAt,
},
],
[]
)
return foundSearches
}
const createNewSearch = async (userId, name, courseCodes) => {
const savedSearches = await getOpenUniSearchesByUser(userId)
if (savedSearches !== undefined) {
const searchNames = savedSearches.map(search => search.name)
if (searchNames.includes(name)) return null
}
try {
const created = await createOpenUniPopulationSearch(userId, name, courseCodes)
if (!created) return null
return created
} catch (e) {
logger.error(`Couldn't create new open uni search instance: ${e}`)
return null
}
}
const deleteSearch = async (userId, id) => {
try {
const deleted = await deleteOpenUniSearch(userId, id)
if (!deleted) return null
return deleted
} catch (e) {
logger.error(`Couldn't delete open uni search instance: ${e}`)
return null
}
}
const updateSearch = async (userId, id, courseCodes) => {
try {
const updated = await updateOpenUniPopulationSearch(userId, id, courseCodes)
if (!updated) return null
return updated
} catch (e) {
logger.error(`Couldn't update open uni search instance: ${e}`)
}
}
module.exports = { getOpenUniSearches, createNewSearch, deleteSearch, updateSearch }
@@ -0,0 +1,134 @@
const { Credit, Enrollment, Studyright, Student, Course } = require('../../models')
const { OpenUniPopulationSearch } = require('../../models/models_kone')
const {
mapOpenCredits,
mapOpenEnrollments,
mapStundentInfo,
mapStudyRights,
mapCourseInfo,
} = require('./openUniHelpers')
const { Op } = require('sequelize')
const getCredits = async (courseCodes, startdate) =>
(
await Credit.findAll({
attributes: ['course_code', 'student_studentnumber', 'grade', 'attainment_date'],
where: {
course_code: {
[Op.in]: courseCodes,
},
is_open: true,
attainment_date: {
[Op.gte]: startdate,
},
},
})
).map(mapOpenCredits)
const getStudentInfo = async students =>
(
await Student.findAll({
where: {
studentnumber: {
[Op.in]: students,
},
},
})
).map(mapStundentInfo)
const getEnrollments = async (courseCodes, startdate, enddate) =>
(
await Enrollment.findAll({
where: {
course_code: {
[Op.in]: courseCodes,
},
is_open: true,
enrollment_date_time: {
[Op.and]: {
[Op.lte]: enddate,
[Op.gte]: startdate,
},
},
},
})
).map(mapOpenEnrollments)
const getCourseNames = async courseCodes =>
(
await Course.findAll({
attributes: ['code', 'name'],
where: {
code: {
[Op.in]: courseCodes,
},
},
})
).map(mapCourseInfo)
const getStudyRights = async students =>
(
await Studyright.findAll({
include: [
{
model: Student,
attributes: ['studentnumber'],
required: true,
},
],
where: {
student_studentnumber: {
[Op.in]: students.length > 0 ? students : { [Op.not]: null },
},
},
})
).map(mapStudyRights)
const getOpenUniSearchesByUser = async userId => {
return await OpenUniPopulationSearch.findAll({
where: {
userId,
},
})
}
const createOpenUniPopulationSearch = async (userId, name, courseCodes) => {
return await OpenUniPopulationSearch.create({
userId,
name,
courseCodes,
})
}
const updateOpenUniPopulationSearch = async (userId, id, courseCodes) => {
const searchToUpdate = await OpenUniPopulationSearch.findOne({
where: {
userId: userId,
id: id,
},
})
if (!searchToUpdate) return null
return await searchToUpdate.update({ courseCodes })
}
const deleteOpenUniSearch = async (userId, id) => {
return await OpenUniPopulationSearch.destroy({
where: {
userId: userId,
id: id,
},
})
}
module.exports = {
getCredits,
getEnrollments,
getStudyRights,
getStudentInfo,
getCourseNames,
getOpenUniSearchesByUser,
createOpenUniPopulationSearch,
updateOpenUniPopulationSearch,
deleteOpenUniSearch,
}
@@ -0,0 +1,108 @@
const moment = require('moment')
const { getCredits, getStudyRights, getEnrollments, getStudentInfo, getCourseNames } = require('./openUniSearches')
const uniq = objects => [...new Set(objects)]
const calulateTotalsForStudent = async (studentStats, studentnumber) => {
Object.keys(studentStats[studentnumber].courseInfo).forEach(course => {
if (studentStats[studentnumber].courseInfo[course].status.passed) {
studentStats[studentnumber].totals.passed += 1
} else if (studentStats[studentnumber].courseInfo[course].status.failed) {
studentStats[studentnumber].totals.failed += 1
} else if (studentStats[studentnumber].courseInfo[course].status.unfinished) {
studentStats[studentnumber].totals.unfinished += 1
}
})
}
const getCustomOpenUniCourses = async (courseCodes, startdate, enddate) => {
const ayCourseCodes = courseCodes.map(courseCode => 'AY' + courseCode)
const allCourseCodes = courseCodes.concat(ayCourseCodes)
const allCredits = await getCredits(allCourseCodes, startdate)
const allEnrollments = await getEnrollments(allCourseCodes, startdate, enddate)
const students = uniq(allEnrollments.map(enrollment => enrollment.enrollmentStudentnumber))
const courses = await getCourseNames(courseCodes)
const passedGrades = ['1', '2', '3', '4', '5', 'Hyv.', 'hyv.', 'HT', 'TT']
const failedGrades = ['Hyl.', 'HYL', '0']
const allStudyrights = await getStudyRights(students)
const studentInfo = await getStudentInfo(students)
// Filter out current studyrights:
// Case 1: Both startdate and enddate are outside of the given interval
// Case 2: Startdate is inside of the given interval and enddate is outside
// Case 3: Startdate is before the interval start and the enddate is within the interval
const studentsWithCurrentStudyRight = allStudyrights
.filter(
right =>
moment(right.startdate).isBetween(startdate, moment()) ||
(moment(right.startdate).isSameOrBefore(startdate) && moment(right.enddate).isSameOrAfter(moment())) ||
moment(right.enddate).isSameOrBefore(moment())
)
.map(right => right.studyrightStudentnumber)
const uniqueStudentsWithCurrentStudyRight = uniq(studentsWithCurrentStudyRight)
const studentStats = {}
const getEmptyCourseInfo = () => {
return {
status: {
passed: null,
failed: null,
unfinished: null,
},
}
}
for (const { studentnumber, email, secondary_email, dissemination_info_allowed } of studentInfo) {
// Check if the student has existing studyright: if yes, then stop here
if (!uniqueStudentsWithCurrentStudyRight.includes(studentnumber)) {
if (!(studentnumber in studentStats)) {
studentStats[studentnumber] = {
courseInfo: courseCodes.reduce((acc, code) => ({ ...acc, [code]: getEmptyCourseInfo() }), {}),
email: email,
secondaryEmail: secondary_email,
disseminationInfoAllowed: dissemination_info_allowed,
totals: { passed: 0, failed: 0, unfinished: 0 },
}
}
for (const { course_code, attainment_date, student_studentnumber, grade } of allCredits) {
if (student_studentnumber === studentnumber) {
const courseCode = course_code.replace('AY', '')
if (
passedGrades.includes(grade) &&
(!studentStats[studentnumber].courseInfo[courseCode].status.passed ||
moment(studentStats[studentnumber].courseInfo[courseCode].status.passed).isBefore(attainment_date, 'day'))
) {
studentStats[studentnumber].courseInfo[courseCode].status.passed = attainment_date
} else if (
failedGrades.includes(grade) &&
!studentStats[studentnumber].courseInfo[courseCode].status.passed &&
(!studentStats[studentnumber].courseInfo[courseCode].status.failed ||
moment(studentStats[studentnumber].courseInfo[courseCode].status.failed).isBefore(attainment_date, 'day'))
) {
studentStats[studentnumber].courseInfo[courseCode].status.failed = attainment_date
}
}
}
for (const { enrollmentStudentnumber, course_code, enrollment_date_time } of allEnrollments) {
if (enrollmentStudentnumber === studentnumber) {
const courseCode = course_code.replace('AY', '')
if (
!studentStats[studentnumber].courseInfo[courseCode].status.passed &&
!studentStats[studentnumber].courseInfo[courseCode].status.failed &&
(!studentStats[studentnumber].courseInfo[courseCode].status.unfinished ||
moment(studentStats[studentnumber].courseInfo[courseCode].status.unfinished).isBefore(
enrollment_date_time,
'day'
))
) {
studentStats[studentnumber].courseInfo[courseCode].status.unfinished = enrollment_date_time
}
}
}
await calulateTotalsForStudent(studentStats, studentnumber)
}
}
const openUniStats = { students: studentStats, courses: courses }
return openUniStats
}
module.exports = { getCustomOpenUniCourses }
+4 -10
View File
@@ -48,6 +48,7 @@ const formatStudentForPopulationStatistics = (
creditcount,
abbreviatedname,
email,
phone_number,
studyrights,
studyplans,
semester_enrollments,
@@ -66,7 +67,6 @@ const formatStudentForPopulationStatistics = (
enrollments,
credits,
startDate,
endDate,
startDateMoment,
endDateMoment
) => {
@@ -104,6 +104,7 @@ const formatStudentForPopulationStatistics = (
gender_code,
gender: { gender_en, gender_fi, gender_sv },
email,
phoneNumber: phone_number,
semesterenrollments: semester_enrollments || [],
updatedAt: updatedAt || createdAt,
tags: tags || [],
@@ -232,6 +233,7 @@ const getStudentsIncludeCoursesBetween = async (studentnumbers, startDate, endDa
'creditcount',
'abbreviatedname',
'email',
'phone_number',
'updatedAt',
'gender_code',
'birthdate',
@@ -782,15 +784,7 @@ const formatStudentsForApi = async (
if (student.studentnumber in optionData) student.option = optionData[student.studentnumber]
else student.option = null
stats.students.push(
formatStudentForPopulationStatistics(
student,
enrollments,
credits,
startDate,
endDate,
startDateMoment,
endDateMoment
)
formatStudentForPopulationStatistics(student, enrollments, credits, startDate, startDateMoment, endDateMoment)
)
return stats
},
@@ -38,8 +38,14 @@ const byProgrammeCode = async code => {
},
})
// filter out possible duplicates
const filtered = result.filter(
(course, index, array) =>
array.findIndex(c => c.id == course.id && c.code == course.code && c.label_code == course.label_code) == index
)
// this labels the modules to match the old system in frontend
const labeled = result.map(module => {
const labeled = filtered.map(module => {
const label = {
id: module.label_name.fi,
label: `${module.label_code}\n${module.label_name.fi}`,
@@ -0,0 +1,122 @@
const { StudyProgrammeCriteria } = require('../models/models_kone')
const logger = require('../util/logger')
const getCriteriaByStudyProgramme = code =>
StudyProgrammeCriteria.findOne({
where: {
code: code,
},
})
const formattedData = updatedData => {
const criteria = {
courses: {
yearOne: updatedData.coursesYearOne,
yearTwo: updatedData.coursesYearTwo,
yearThree: updatedData.coursesYearThree,
},
credits: {
yearOne: updatedData.creditsYearOne,
yearTwo: updatedData.creditsYearTwo,
yearThree: updatedData.creditsYearThree,
},
}
return criteria
}
const createCriteria = async (studyProgramme, coYear1, coYear2, coYear3, crYear1, crYear2, crYear3) => {
const newProgrammeCriteria = {
code: studyProgramme,
coursesYearOne: coYear1,
coursesYearTwo: coYear2,
coursesYearThree: coYear3,
creditsYearOne: crYear1,
creditsYearTwo: crYear2,
creditsYearThree: crYear3,
}
try {
const createdData = await StudyProgrammeCriteria.create(newProgrammeCriteria)
const criteria = formattedData(createdData)
return criteria
} catch (error) {
logger.error(`Create criteria failed: ${error}`)
}
}
const saveYearlyCreditCriteria = async (studyProgramme, credits) => {
const studyProgrammeToUpdate = await getCriteriaByStudyProgramme(studyProgramme)
if (!studyProgrammeToUpdate) {
return await createCriteria(
studyProgramme,
[],
[],
[],
parseInt(credits.year1),
parseInt(credits.year2),
parseInt(credits.year3)
)
}
const yearlyCredits = {
creditsYearOne: parseInt(credits.year1),
creditsYearTwo: parseInt(credits.year2),
creditsYearThree: parseInt(credits.year3),
}
try {
const updatedData = await studyProgrammeToUpdate.update({ ...yearlyCredits })
const criteria = formattedData(updatedData)
return criteria
} catch (error) {
logger.error(`Update yearly credit criteria failed: ${error}`)
}
}
const saveYearlyCourseCriteria = async (studyProgramme, courses, year) => {
const studyProgrammeToUpdate = await getCriteriaByStudyProgramme(studyProgramme)
if (!studyProgrammeToUpdate) {
const courseObj = { year1: [], year2: [], year3: [] }
if (year === 1) {
courseObj.year1 = courses
} else if (year === 2) {
courseObj.year2 = courses
} else {
courseObj.year3 = courses
}
return await createCriteria(studyProgramme, courseObj.year1, courseObj.year2, courseObj.year3, 0, 0, 0)
}
try {
let updatedData = {}
if (year === 1) updatedData = await studyProgrammeToUpdate.update({ coursesYearOne: courses })
if (year === 2) updatedData = await studyProgrammeToUpdate.update({ coursesYearTwo: courses })
if (year === 3) updatedData = await studyProgrammeToUpdate.update({ coursesYearThree: courses })
const criteria = formattedData(updatedData)
return criteria
} catch (error) {
logger.error(`Update yearly credit criteria failed: ${error}`)
}
}
const getCriteria = async studyProgramme => {
const studyProgrammeCriteria = await getCriteriaByStudyProgramme(studyProgramme)
const criteria = {
courses: {
yearOne: studyProgrammeCriteria ? studyProgrammeCriteria.coursesYearOne : [],
yearTwo: studyProgrammeCriteria ? studyProgrammeCriteria.coursesYearTwo : [],
yearThree: studyProgrammeCriteria ? studyProgrammeCriteria.coursesYearThree : [],
},
credits: {
yearOne: studyProgrammeCriteria ? studyProgrammeCriteria.creditsYearOne : 0,
yearTwo: studyProgrammeCriteria ? studyProgrammeCriteria.creditsYearTwo : 0,
yearThree: studyProgrammeCriteria ? studyProgrammeCriteria.creditsYearThree : 0,
},
}
return criteria
}
module.exports = {
getCriteria,
saveYearlyCourseCriteria,
saveYearlyCreditCriteria,
}
@@ -36,9 +36,7 @@ const studytrackStudents = async studentnumbers =>
separate: true,
attributes: ['credits', 'attainment_date'],
where: {
isStudyModule: {
[Op.eq]: null,
},
[Op.or]: [{ isStudyModule: null }, { isStudyModule: false }],
credittypecode: {
[Op.in]: [4, 9],
},
@@ -9,11 +9,12 @@ const {
getYearsObject,
getYearsArray,
getMedian,
getMean,
getStartDate,
getThesisType,
alltimeStartDate,
alltimeEndDate,
getId,
getGoal,
} = require('./studyprogrammeHelpers')
const {
graduatedStudyRights,
@@ -23,8 +24,30 @@ const {
allStudyrights,
allTransfers,
} = require('./studyprogramme')
const { countTimeCategories, bachelorStudyright, getStatutoryAbsences } = require('./graduationHelpers')
const { getAllProgrammes } = require('./studyrights')
const checkStartdate = async (id, startdate) => {
if (id.slice(-2) === '-1') {
return startdate
}
const studyright = await bachelorStudyright(id.replace(/-2$/, '-1'))
if (studyright) return studyright.startdate
return null
}
const addGraduation = async (studyright, isAcademicYear, amounts, times) => {
const startdate = await checkStartdate(studyright.studyrightid, studyright.startdate)
const graduationYear = defineYear(studyright.enddate, isAcademicYear)
const totalTimeToGraduation = moment(studyright.enddate).diff(moment(startdate), 'months')
const statutoryAbsences = await getStatutoryAbsences(studyright.studentnumber, startdate, studyright.enddate)
const timeToGraduation = totalTimeToGraduation - statutoryAbsences
amounts[graduationYear] += 1
times[graduationYear] = [...times[graduationYear], timeToGraduation]
}
const getGraduatedStats = async ({ studyprogramme, since, years, isAcademicYear, includeAllSpecials }) => {
const { graphStats, tableStats } = getStatsBasis(years)
@@ -77,6 +100,10 @@ const getThesisStats = async ({ studyprogramme, since, years, isAcademicYear, in
const getGraduationTimeStats = async ({ studyprogramme, since, years, isAcademicYear, includeAllSpecials }) => {
let graduationAmounts = getYearsObject({ years })
let graduationTimes = getYearsObject({ years, emptyArrays: true })
// for bc+ms combo
const doCombo = studyprogramme.startsWith('MH') && !['MH30_001', 'MH30_003'].includes(studyprogramme)
let graduationAmountsCombo = getYearsObject({ years })
let graduationTimesCombo = getYearsObject({ years, emptyArrays: true })
const studentnumbers = await getCorrectStudentnumbers({
codes: [studyprogramme],
@@ -87,32 +114,35 @@ const getGraduationTimeStats = async ({ studyprogramme, since, years, isAcademic
const studyrights = await graduatedStudyRights(studyprogramme, since, studentnumbers)
studyrights.forEach(({ enddate, studystartdate }) => {
const graduationYear = defineYear(enddate, isAcademicYear)
const timeToGraduation = moment(enddate).diff(moment(studystartdate), 'months')
graduationAmounts[graduationYear] += 1
graduationTimes[graduationYear] = [...graduationTimes[graduationYear], timeToGraduation]
})
// The maximum amount of months in the graph depends on the studyprogramme intended graduation time
const comparisonValue = studyprogramme.includes('KH') ? 72 : 48
// for masters, separate bc+ms combo from just ms students (except some medical progs)
for (const right of studyrights) {
if (doCombo && right.studyrightid.slice(-2) === '-2') {
await addGraduation(right, isAcademicYear, graduationAmountsCombo, graduationTimesCombo)
} else {
await addGraduation(right, isAcademicYear, graduationAmounts, graduationTimes)
}
}
const medians = getYearsObject({ years, emptyArrays: true })
const means = getYearsObject({ years, emptyArrays: true })
const goal = getGoal(studyprogramme)
const times = { medians: [], goal }
const comboTimes = { medians: [], goal: goal + 36 }
// HighCharts graph require the data to have this format (ie. actual value, "empty value")
years.forEach(year => {
const rev = [...years].reverse()
for (const year of rev) {
const median = getMedian(graduationTimes[year])
const mean = getMean(graduationTimes[year])
medians[year] = [
['', median],
['', comparisonValue - median],
]
means[year] = [
['', mean],
['', comparisonValue - mean],
]
})
return { medians, means, graduationAmounts }
const statistics = countTimeCategories(graduationTimes[year], goal)
times.medians = [...times.medians, { y: median, amount: graduationAmounts[year], name: year, statistics }]
if (doCombo) {
const median = getMedian(graduationTimesCombo[year])
const statistics = countTimeCategories(graduationTimesCombo[year], goal + 36)
comboTimes.medians = [
...comboTimes.medians,
{ y: median, amount: graduationAmountsCombo[year], name: year, statistics },
]
}
}
return { times, doCombo, comboTimes }
}
// Creates (studyrightid, transfer)-map out of programmes transfers
@@ -126,7 +156,7 @@ const getTransferStudyrightMap = async (studyprogramme, since) => {
const formatStats = (stats, years) => {
const tableStats = Object.values(stats)
.filter(p => years.map(year => p[year]).find(started => started !== 0)) // Filter out programmes with no-one started between the selected years
.map(p => [p.code, p.name['fi'], ...years.map(year => p[year])])
.map(p => [p.code, getId(p.code), p.name['fi'], ...years.map(year => p[year])])
const graphStats = Object.values(stats)
.filter(p => years.map(year => p[year]).find(started => started !== 0)) // Filter out programmes with no-one started between the selected years
@@ -223,33 +253,49 @@ const getGraduationStatsForStudytrack = async ({ studyprogramme, settings }) =>
const thesis = await getThesisStats(queryParameters)
const graduated = await getGraduatedStats(queryParameters)
const graduationTimeStats = await getGraduationTimeStats(queryParameters)
const programmesBeforeOrAfter = await getProgrammesBeforeOrAfter(studyprogramme, queryParameters)
const reversedYears = getYearsArray(since.getFullYear(), isAcademicYear).reverse()
const titles = ['', 'Graduated', 'Wrote thesis']
const programmesBeforeOrAfterTitles = ['Code', 'Programme', ...years]
const tableStats = reversedYears.map(year => [year, graduated.tableStats[year], thesis.tableStats[year]])
const titles =
studyprogramme.includes('LIS') || studyprogramme.includes('T')
? ['', 'Graduated']
: ['', 'Graduated', 'Wrote thesis']
const programmesBeforeOrAfterTitles = ['Code', 'Id', 'Programme', ...years]
const tableStats =
studyprogramme.includes('LIS') || studyprogramme.includes('T')
? reversedYears.map(year => [year, graduated.tableStats[year]])
: reversedYears.map(year => [year, graduated.tableStats[year], thesis.tableStats[year]])
return {
id: studyprogramme,
years,
tableStats,
titles,
graphStats: [
{
name: 'Graduated students',
data: graduated.graphStats,
},
{
name: 'Wrote thesis',
data: thesis.graphStats,
},
],
graduationMedianTime: graduationTimeStats.medians,
graduationMeanTime: graduationTimeStats.means,
graduationAmounts: graduationTimeStats.graduationAmounts,
graphStats:
studyprogramme.includes('LIS') || studyprogramme.includes('T')
? [
{
name: 'Graduated students',
data: graduated.graphStats,
},
]
: [
{
name: 'Graduated students',
data: graduated.graphStats,
},
{
name: 'Wrote thesis',
data: thesis.graphStats,
},
],
graduationTimes: graduationTimeStats.times,
doCombo: graduationTimeStats.doCombo,
comboTimes: graduationTimeStats.comboTimes,
programmesBeforeOrAfterTableStats: programmesBeforeOrAfter?.tableStats,
programmesBeforeOrAfterGraphStats: programmesBeforeOrAfter?.graphStats,
programmesBeforeOrAfterTitles,
@@ -1,5 +1,5 @@
const { mean } = require('lodash')
const { studentnumbersWithAllStudyrightElements } = require('./populations')
const { codes } = require('../../config/programmeCodes')
// Helper functions
const getCorrectStudentnumbers = async ({ codes, startDate, endDate, includeAllSpecials, includeGraduated = true }) => {
@@ -44,6 +44,7 @@ const resolveStudyRightCode = studyright_elements => {
const formatStudyright = studyright => {
const {
studyrightid,
startdate,
studystartdate,
enddate,
givendate,
@@ -57,6 +58,7 @@ const formatStudyright = studyright => {
return {
studyrightid,
startdate,
studystartdate: defineStartDate(studyright_elements, studystartdate),
enddate,
givendate,
@@ -169,11 +171,6 @@ const getMedian = values => {
return (values[half - 1] + values[half]) / 2.0
}
const getMean = values => {
if (values.length === 0) return 0
return Math.round(mean(values))
}
const defineYear = (date, isAcademicYear) => {
if (!date) return ''
const year = date.getFullYear()
@@ -288,6 +285,36 @@ const getMasterCreditGraphStats = years => ({
data: getEmptyArray(years.length),
},
})
const getVetenaryCreditGraphStats = years => ({
lte210: {
name: 'Less than 210 credits',
data: getEmptyArray(years.length),
},
lte240: {
name: '210-239 credits',
data: getEmptyArray(years.length),
},
lte270: {
name: '240-269 credits',
data: getEmptyArray(years.length),
},
lte300: {
name: '270-299 credits',
data: getEmptyArray(years.length),
},
lte330: {
name: '300-329 credits',
data: getEmptyArray(years.length),
},
lte360: {
name: '330-359 credits',
data: getEmptyArray(years.length),
},
mte360: {
name: '360 or more credits',
data: getEmptyArray(years.length),
},
})
const getOnlyMasterCreditGraphStats = years => ({
lte15: {
@@ -317,60 +344,60 @@ const getOnlyMasterCreditGraphStats = years => ({
})
const getDoctoralCreditGraphStats = years => ({
lte50: {
name: 'Less than 50 credits',
lte10: {
name: 'Less than 10 credits',
data: getEmptyArray(years.length),
},
lte100: {
name: '50-99 credits',
lte20: {
name: '10-19 credits',
data: getEmptyArray(years.length),
},
lte150: {
name: '100-149 credits',
lte30: {
name: '20-29 credits',
data: getEmptyArray(years.length),
},
lte200: {
name: '150-199 credits',
lte40: {
name: '30-39 credits',
data: getEmptyArray(years.length),
},
lte250: {
name: '200-249 credits',
data: getEmptyArray(years.length),
},
lte300: {
name: '250-299 credits',
data: getEmptyArray(years.length),
},
mte300: {
name: '300 or more credits',
mte40: {
name: '40 or more credits',
data: getEmptyArray(years.length),
},
})
const getCreditGraphStats = (studyprogramme, years) => {
const getCreditGraphStats = (studyprogramme, years, studyprog = false) => {
if (studyprogramme.includes('KH')) return getBachelorCreditGraphStats(years)
if (studyprogramme.includes('MH')) return getMasterCreditGraphStats(years)
if (studyprogramme === 'MH90_001')
return studyprog ? getBachelorCreditGraphStats(years) : getVetenaryCreditGraphStats(years)
if (studyprogramme.includes('MH')) return getOnlyMasterCreditGraphStats(years)
return getDoctoralCreditGraphStats(years)
}
const bachelorCreditThresholds = ['lte15', 'lte30', 'lte60', 'lte90', 'lte120', 'lte150', 'lte180', 'mte180']
const masterCreditThresholds = ['lte200', 'lte220', 'lte240', 'lte260', 'lte280', 'lte300', 'mte300']
const onlyMasterCreditThresholds = ['lte15', 'lte30', 'lte60', 'lte90', 'lte120', 'mte120']
const doctoralCreditThresholds = ['lte50', 'lte100', 'lte150', 'lte200', 'lte250', 'lte300', 'mte300']
const doctoralCreditThresholds = ['lte10', 'lte20', 'lte30', 'lte40', 'mte40']
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 getOnlyMasterThresholds = () => {
return { msOnlyCreditThresholdKeys: onlyMasterCreditThresholds, msOnlyCreditThresholdAmount: onlyMasterCreditAmounts }
const doctoralCreditAmounts = [10, 20, 30, 40, 40]
const vetenaryCreditThresholds = ['lte210', 'lte240', 'lte270', 'lte300', 'lte330', 'lte360', 'mte360']
const vetenaryCreditAmounts = [210, 240, 270, 300, 330, 360, 360]
const getBcMsThresholds = () => {
return { creditThresholdKeysBcMs: masterCreditThresholds, creditThresholdAmountsBcMs: masterCreditAmounts }
}
const getCreditThresholds = studyprogramme => {
const getCreditThresholds = (studyprogramme, studyProg = false) => {
if (studyprogramme === 'MH90_001') {
return studyProg
? { creditThresholdKeys: bachelorCreditThresholds, creditThresholdAmounts: bachelorCreditAmounts }
: { creditThresholdKeys: vetenaryCreditThresholds, creditThresholdAmounts: vetenaryCreditAmounts }
}
if (studyprogramme.includes('KH')) {
return { creditThresholdKeys: bachelorCreditThresholds, creditThresholdAmounts: bachelorCreditAmounts }
}
if (studyprogramme.includes('MH')) {
return { creditThresholdKeys: masterCreditThresholds, creditThresholdAmounts: masterCreditAmounts }
return { creditThresholdKeys: onlyMasterCreditThresholds, creditThresholdAmounts: onlyMasterCreditAmounts }
}
return { creditThresholdKeys: doctoralCreditThresholds, creditThresholdAmounts: doctoralCreditAmounts }
}
@@ -402,7 +429,7 @@ const tableTitles = {
'90-119 credits',
'120-149 credits',
'150-179 credits',
'>= 180 credits',
'180 ≤ credits',
],
master: [
'',
@@ -413,7 +440,7 @@ const tableTitles = {
'240-259 credits',
'260-279 credits',
'280-299 credits',
'>= 300 credits',
'300 ≤ credits',
],
masterOnly: [
'',
@@ -423,19 +450,9 @@ const tableTitles = {
'30-59 credits',
'60-89 credits',
'90-119 credits',
'>= 120 credits',
],
doctoral: [
'',
'All',
'< 50 credits',
'50-99 credits',
'100-149 credits',
'150-199 credits',
'200-249 credits',
'250-299 credits',
'>= 300 credits',
'120 ≤ credits',
],
doctoral: ['', 'All', '< 10 credits', '10-19 credits', '20-29 credits', '30-39 credits', '40 ≤ credits'],
},
studytracks: [
'',
@@ -447,16 +464,71 @@ const tableTitles = {
'Graduated',
'Men',
'Women',
'Finnish',
'Finland',
],
programmes: [
'',
'All',
'Started studying',
'Currently enrolled',
'Absent',
'Inactive',
'Graduated',
'Men',
'Women',
'Other/Unknown',
'Finland',
'Other',
],
}
const getCreditProgressTableTitles = studyprogramme => {
if (studyprogramme.includes('KH')) return tableTitles.creditProgress.bachelor
if (studyprogramme.includes('MH')) return tableTitles.creditProgress.master
if (studyprogramme.includes('MH')) return tableTitles.creditProgress.masterOnly
return tableTitles.creditProgress.doctoral
}
const mapCodesToIds = data => {
// Add programme id e.g. TKT
const keys = Object.keys(codes)
const progs = Object.keys(data)
for (const prog of progs) {
if (keys.includes(prog)) {
data[prog].progId = codes[prog].toUpperCase()
}
}
}
const getId = code => {
if (Object.keys(codes).includes(code)) {
return codes[code].toUpperCase()
}
return ''
}
const getGoal = programme => {
if (programme.startsWith('KH') || programme.endsWith('-ba')) {
return 36
}
if (programme.startsWith('MH') || programme.endsWith('-ma')) {
if (['MH30_004', '420420-ma'].includes(programme)) {
return 24 + 6
}
if (['MH30_001', 'MH30_003', '320011-ma', '320001-ma', '320002-ma'].includes(programme)) {
return 36 + 24 + 12 // medical, no separate bachelor
}
return 24
}
if (programme.includes('T')) {
return 48
}
if (programme.startsWith('LI')) {
return 78
}
return 48 //unknown, likely old doctor or licentiate
}
module.exports = {
getCorrectStudentnumbers,
formatStudyright,
@@ -469,7 +541,6 @@ module.exports = {
isMajorStudentCredit,
isSpecialGroupCredit,
getMedian,
getMean,
defineYear,
getStartDate,
alltimeStartDate,
@@ -478,12 +549,16 @@ module.exports = {
getPercentage,
getBachelorCreditGraphStats,
getMasterCreditGraphStats,
getVetenaryCreditGraphStats,
getDoctoralCreditGraphStats,
getOnlyMasterCreditGraphStats,
getCreditGraphStats,
getCreditThresholds,
tableTitles,
getCreditProgressTableTitles,
getOnlyMasterThresholds,
getBcMsThresholds,
isNonMajorCredit,
mapCodesToIds,
getId,
getGoal,
}
+155 -123
View File
@@ -4,7 +4,6 @@ const moment = require('moment')
const { getAssociations } = require('./studyrights')
const {
getMedian,
getMean,
getStartDate,
getYearsArray,
getPercentage,
@@ -14,6 +13,7 @@ const {
getCreditGraphStats,
tableTitles,
getCreditProgressTableTitles,
getGoal,
} = require('./studyprogrammeHelpers')
const {
studytrackStudents,
@@ -25,11 +25,11 @@ const {
graduatedStudyRights,
} = require('./studyprogramme')
const { getAcademicYearDates } = require('../util/semester')
const { countTimeCategories } = require('./graduationHelpers')
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, credits }) => {
const creditcount = credits
.filter(credit => moment(credit.attainment_date).isAfter(startDate))
@@ -42,49 +42,67 @@ const getStudentData = (startDate, students, thresholdKeys, thresholdAmounts) =>
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
// bachelor programme has more columns than master and doctoral programmes
if (thresholdKeys.length > 7) {
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[5] && creditcount < thresholdAmounts[6] ? 1 : 0
data[thresholdKeys[7]] += creditcount >= thresholdAmounts[6] ? 1 : 0
} else {
} else if (thresholdKeys.length > 6) {
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[5] ? 1 : 0
} else if (thresholdKeys.length > 5) {
data[thresholdKeys[4]] += creditcount >= thresholdAmounts[3] && creditcount < thresholdAmounts[4] ? 1 : 0
data[thresholdKeys[5]] += creditcount >= thresholdAmounts[4] ? 1 : 0
} else {
data[thresholdKeys[4]] += creditcount >= thresholdAmounts[3] ? 1 : 0
}
})
return data
}
const getGraduationTimeStats = async ({
year,
graduated,
studyprogramme,
track,
graduationMeanTime,
graduationMedianTime,
graduationAmounts,
}) => {
const getGraduationTimeStats = async ({ year, graduated, track, graduationAmounts, graduationTimes, classSize }) => {
if (year === 'Total') {
return
}
// Count how long each student took to graduate
let graduationTimes = []
graduated.forEach(({ enddate, studystartdate }) => {
const timeToGraduation = moment(enddate).diff(moment(studystartdate), 'months')
let times = []
graduated.forEach(({ enddate, startdate }) => {
const timeToGraduation = moment(enddate).diff(moment(startdate), 'months')
graduationAmounts[track][year] += 1
graduationTimes = [...graduationTimes, timeToGraduation]
times = [...times, timeToGraduation]
})
// The maximum amount of months in the graph depends on the studyprogramme intended graduation time
const comparisonValue = studyprogramme.includes('KH') ? 72 : 48
const median = getMedian(times)
const statistics = countTimeCategories(times, graduationTimes.goal)
// HighCharts graph requires the data to have this format (ie. actual value, "empty value")
const median = getMedian(graduationTimes)
const mean = getMean(graduationTimes)
graduationMedianTime[track][year] = [
['', median],
['', comparisonValue - median],
graduationTimes[track].medians = [
...graduationTimes[track].medians,
{ y: median, amount: graduationAmounts[track][year], name: year, statistics, classSize },
]
graduationMeanTime[track][year] = [
['', mean],
['', comparisonValue - mean],
}
const getStats = (all, started, enrolled, absent, inactive, graduated, studentData) => {
return [
all.length,
getPercentage(all.length, all.length),
started.length,
getPercentage(started.length, all.length),
enrolled.length,
getPercentage(enrolled.length, all.length),
absent.length,
getPercentage(absent.length, all.length),
inactive.length,
getPercentage(inactive.length, all.length),
graduated.length,
getPercentage(graduated.length, all.length),
studentData.male,
getPercentage(studentData.male, all.length),
studentData.female,
getPercentage(studentData.female, all.length),
studentData.finnish,
getPercentage(studentData.finnish, all.length),
]
}
@@ -109,17 +127,16 @@ const getStudytrackDataForTheYear = async ({
mainStatsByTrack,
creditGraphStats,
creditTableStats,
graduationMeanTime,
graduationMedianTime,
graduationAmounts,
graduationTimes,
totalAmounts,
emptyTracks,
totals,
} = data
await Promise.all(
studytracks.map(async track => {
const codes = studyprogramme === track ? [studyprogramme] : [studyprogramme, track]
const studentnumbers = await getCorrectStudentnumbers({
codes,
startDate,
@@ -127,16 +144,42 @@ const getStudytrackDataForTheYear = async ({
includeAllSpecials,
includeGraduated,
})
// Get all the studyrights and students for the calculations
const all = await allStudyrights(track, studentnumbers)
const students = await studytrackStudents(studentnumbers)
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)
const inactive = await inactiveStudyrights(track, studentnumbers)
const graduated = await graduatedStudyRights(track, startDate, studentnumbers)
let all = []
let studentData = {}
let started = []
let enrolled = []
let absent = []
let inactive = []
let graduated = []
// Get all the studyrights and students for the calculations
if (year !== 'Total') {
all = await allStudyrights(track, studentnumbers)
studentData = getStudentData(startDate, students, creditThresholdKeys, creditThresholdAmounts)
started = await startedStudyrights(track, startDate, studentnumbers)
enrolled = await enrolledStudents(track, startDate, studentnumbers)
absent = await absentStudents(track, studentnumbers)
inactive = await inactiveStudyrights(track, studentnumbers)
graduated = await graduatedStudyRights(track, startDate, studentnumbers)
totals[track].all = [...totals[track].all, ...all]
totals[track].studentData.male += studentData.male
totals[track].studentData.female += studentData.female
totals[track].studentData.finnish += studentData.finnish
totals[track].started = [...totals[track].started, ...started]
totals[track].enrolled = [...totals[track].enrolled, ...enrolled]
totals[track].absent = [...totals[track].absent, ...absent]
totals[track].inactive = [...totals[track].inactive, ...inactive]
totals[track].graduated = [...totals[track].graduated, ...graduated]
} else {
all = totals[track].all
studentData = totals[track].studentData
started = totals[track].started
enrolled = totals[track].enrolled
absent = totals[track].absent
inactive = totals[track].inactive
graduated = totals[track].graduated
}
// If the track has no stats for that year, it should be removed from the table and dropdown options
if (all.length === 0) {
@@ -144,82 +187,63 @@ const getStudytrackDataForTheYear = async ({
return
}
// Count stats for creditgraph for the year per track
creditThresholdKeys.forEach(
threshold => (creditGraphStats[track][threshold].data[indexOf(years, year)] = studentData[threshold])
)
// Count stats for the credit progress table for the year per track
creditTableStats[track] = [
...creditTableStats[track],
[year, all.length, ...creditThresholdKeys.map(threshold => studentData[threshold])],
]
if (year !== 'Total') {
// Count stats for creditgraph for the year per track
creditThresholdKeys.forEach(
threshold => (creditGraphStats[track][threshold].data[indexOf(years, year)] += studentData[threshold])
)
creditThresholdKeys.forEach(
threshold => (creditGraphStats[track][threshold].data[indexOf(years, 'Total')] += studentData[threshold])
)
// Count stats for the credit progress table for the year per track
creditTableStats[track] = [
[year, all.length, ...creditThresholdKeys.map(threshold => studentData[threshold])],
...creditTableStats[track],
]
} else {
const totalCredits = creditTableStats[track].reduce(
(a, b) => a.map((colVal, idx) => colVal + (b[idx] || 0)),
Array(creditThresholdKeys.length + 2).fill(0)
)
creditTableStats[track] = [[year, ...totalCredits.slice(1)], ...creditTableStats[track]]
}
// Count stats for the main studytrack table grouped by tracks
mainStatsByTrack[track] = [
[year, ...getStats(all, started, enrolled, absent, inactive, graduated, studentData)],
...mainStatsByTrack[track],
[
year,
all.length,
getPercentage(all.length, all.length),
started.length,
getPercentage(started.length, all.length),
enrolled.length,
getPercentage(enrolled.length, all.length),
absent.length,
getPercentage(absent.length, all.length),
inactive.length,
getPercentage(inactive.length, all.length),
graduated.length,
getPercentage(graduated.length, all.length),
studentData.male,
getPercentage(studentData.male, all.length),
studentData.female,
getPercentage(studentData.female, all.length),
studentData.finnish,
getPercentage(studentData.finnish, all.length),
],
]
// Count stats for the main studytrack table grouped by year
mainStatsByYear[year] = [
...mainStatsByYear[year],
[
studytrackNames[track]?.name['fi'] ? `${studytrackNames[track]?.name['fi']}, ${track}` : year,
all.length,
getPercentage(all.length, all.length),
started.length,
getPercentage(started.length, all.length),
enrolled.length,
getPercentage(enrolled.length, all.length),
absent.length,
getPercentage(absent.length, all.length),
inactive.length,
getPercentage(inactive.length, all.length),
graduated.length,
getPercentage(graduated.length, all.length),
studentData.male,
getPercentage(studentData.male, all.length),
studentData.female,
getPercentage(studentData.female, all.length),
studentData.finnish,
getPercentage(studentData.finnish, all.length),
...getStats(all, started, enrolled, absent, inactive, graduated, studentData),
],
...mainStatsByYear[year],
]
// Count stats for the graduation time charts grouped by year
totalAmounts[track][year] = await all.length
if (!(track in graduationTimes)) {
graduationTimes[track] = { medians: [] }
}
totalAmounts[track][year] = all.length
await getGraduationTimeStats({
year,
graduated,
studyprogramme,
track,
graduationMedianTime,
graduationMeanTime,
graduationAmounts,
graduationTimes,
classSize: totalAmounts[track][year],
})
})
)
Object.keys(graduationTimes).forEach(track => {
if (track !== 'goal') {
const sortedMedians = graduationTimes[track].medians.sort((a, b) => b.name.localeCompare(a.name))
graduationTimes[track].medians = sortedMedians
}
})
}
// Defines the studytrack names for the studytrack selector
@@ -241,20 +265,26 @@ const getEmptyStatsObjects = (years, studytracks, studyprogramme) => {
const mainStatsByTrack = {}
const creditGraphStats = {}
const creditTableStats = {}
const graduationMedianTime = {}
const graduationMeanTime = {}
const graduationAmounts = {}
const graduationTimes = { goal: getGoal(studyprogramme) }
const totalAmounts = {}
const emptyTracks = new Map()
const totals = {}
studytracks.forEach(async track => {
mainStatsByTrack[track] = []
creditGraphStats[track] = getCreditGraphStats(studyprogramme, years)
creditGraphStats[track] = getCreditGraphStats(studyprogramme, years, true)
creditTableStats[track] = []
graduationMedianTime[track] = getYearsObject({ years, emptyArrays: true })
graduationMeanTime[track] = getYearsObject({ years, emptyArrays: true })
graduationAmounts[track] = getYearsObject({ years })
totalAmounts[track] = getYearsObject({ years })
totals[track] = {
all: [],
started: [],
enrolled: [],
absent: [],
inactive: [],
graduated: [],
studentData: { male: 0, female: 0, finnish: 0 },
}
})
return {
@@ -262,11 +292,11 @@ const getEmptyStatsObjects = (years, studytracks, studyprogramme) => {
mainStatsByTrack,
creditGraphStats,
creditTableStats,
graduationMedianTime,
graduationMeanTime,
graduationAmounts,
graduationTimes,
totalAmounts,
emptyTracks,
totals,
}
}
@@ -285,35 +315,37 @@ const getStudytrackStatsForStudyprogramme = async ({ studyprogramme, settings })
const studytrackNames = associations.studyTracks
const data = getEmptyStatsObjects(years, studytracks, studyprogramme)
const { creditThresholdKeys, creditThresholdAmounts } = getCreditThresholds(studyprogramme)
const { creditThresholdKeys, creditThresholdAmounts } = getCreditThresholds(studyprogramme, true)
await Promise.all(
years.map(async year => {
return await getStudytrackDataForTheYear({
studyprogramme,
since,
settings,
studytracks,
studytrackNames,
year,
years,
data,
creditThresholdKeys,
creditThresholdAmounts,
})
const yearsReversed = [...years].reverse()
for (const year of yearsReversed) {
await getStudytrackDataForTheYear({
studyprogramme,
since,
settings,
studytracks,
studytrackNames,
year,
years,
data,
creditThresholdKeys,
creditThresholdAmounts,
})
)
}
const studytrackOptions = getStudytrackOptions(studyprogramme, studytrackNames, studytracks, data.emptyTracks, years)
return {
id: studyprogramme,
years: getYearsArray(since.getFullYear(), isAcademicYear, includeYearsCombined),
years: years,
...data,
studytrackOptions,
includeGraduated: settings.graduated,
populationTitles: tableTitles['studytracks'],
creditTableTitles: getCreditProgressTableTitles(studyprogramme),
creditTableTitles:
studyprogramme === 'MH90_001'
? tableTitles['creditProgress']['bachelor']
: getCreditProgressTableTitles(studyprogramme),
}
}
+45 -24
View File
@@ -8,11 +8,10 @@ const { checkStudyGuidanceGroupsAccess, getAllStudentsUserHasInGroups } = requir
const _ = require('lodash')
const { User, UserElementDetails, AccessGroup, UserFaculties, sequelizeUser } = require('../models/models_user')
const { getOrganizationAccess } = require('../util/organizationAccess')
const { getUserIams, getAllUserAccess } = require('../util/jami')
const courseStatisticsGroup = 'grp-oodikone-basic-users'
const facultyStatisticsGroup = 'grp-oodikone-users'
const hyOneGroup = 'hy-one'
const toskaGroup = 'grp-toska'
// Max 25 users can be stored in the cache
const userDataCache = new LRU({
@@ -169,32 +168,34 @@ const getAccessGroups = async () => await AccessGroup.findAll()
const enrichProgrammesFromFaculties = faculties =>
facultiesAndProgrammesForTrends.filter(f => faculties.includes(f.faculty_code)).map(f => f.programme_code)
const findAll = async () =>
(
await User.findAll({
include: userIncludes,
})
).map(user => ({
const findAll = async () => {
const allUsers = await User.findAll({
include: userIncludes,
})
const userAccess = await getAllUserAccess()
const formattedUsers = allUsers.map(user => ({
...user.get(),
is_enabled: true,
iam_groups: userAccess.find(({ id }) => id === user.sisu_person_id)?.iamGroups || [],
elementdetails: _.uniqBy([
...user.programme.map(p => p.elementDetailCode),
...enrichProgrammesFromFaculties(user.faculty.map(f => f.faculty_code)),
]),
}))
const formatUser = async userFromDb => {
return formattedUsers
}
const formatUser = async (userFromDb, extraRights) => {
const [accessGroups, programmes, faculties] = [
['accessgroup', 'group_code'],
['programme', 'elementDetailCode'],
['faculty', 'faculty_code'],
].map(([field, code]) => userFromDb[field].map(entry => entry[code]))
const rights = _.uniqBy([...programmes, ...enrichProgrammesFromFaculties(faculties)])
const iamRights = Object.keys(await getOrganizationAccess(userFromDb)).filter(
programmeCode => !rights.includes(programmeCode)
)
const rights = _.uniqBy([...programmes, ...enrichProgrammesFromFaculties(faculties)]).concat(extraRights)
const {
dataValues: {
@@ -223,7 +224,6 @@ const formatUser = async userFromDb => {
email,
is_enabled: true, // this is probably not needed: is here mainly because old userservice created users for every logged in user, even if they hadn't correct iamgroups
rights,
iamRights,
roles: accessGroups,
studentsUserCanAccess, // studentnumbers used in various parts of backend. For admin this is usually empty, since no programmes / faculties are set.
isAdmin: accessGroups.includes('admin'),
@@ -234,15 +234,28 @@ const getMockedUser = async ({ userToMock, mockedBy }) => {
if (userDataCache.has(userToMock)) {
return userDataCache.get(userToMock)
}
const userFromDb = await byUsername(userToMock)
userFromDb.iamGroups = await getUserIams(userFromDb.sisu_person_id)
const { access = {}, specialGroup = {} } = await getOrganizationAccess(userFromDb)
const iamRights = Object.keys(access)
const formattedUser = await formatUser(userFromDb, specialGroup.kosu ? iamRights : [])
const toReturn = {
...(await formatUser(await byUsername(userToMock))),
...formattedUser,
iamRights,
specialGroup,
mockedBy,
}
userDataCache.set(userToMock, toReturn)
return toReturn
}
const getUser = async ({ username, name, email, iamGroups, sisId }) => {
const getUser = async ({ username, name, email, iamGroups, iamRights, specialGroup, sisId }) => {
if (userDataCache.has(username)) {
return userDataCache.get(username)
}
@@ -256,23 +269,30 @@ const getUser = async ({ username, name, email, iamGroups, sisId }) => {
email,
language,
sisu_person_id: sisId,
iam_groups: iamGroups,
last_login: new Date(),
})
const { kosu, jory, hyOne, superAdmin, openUni } = specialGroup
const userFromDb = await byUsername(username)
const formattedUser = await formatUser(userFromDb)
const formattedUser = await formatUser(userFromDb, kosu ? iamRights : [])
const { roles: currentAccessGroups } = formattedUser
// Modify accessgroups based on current groups and iamGroups
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) {
if (jory || iamGroups.includes(facultyStatisticsGroup)) newAccessGroups.push('facultyStatistics')
if (hyOne || currentAccessGroups.includes('teachers')) newAccessGroups.push('teachers')
if (superAdmin || currentAccessGroups.includes('admin')) newAccessGroups.push('admin')
if (openUni) newAccessGroups.push('openUniSearch')
// In the difference method "the order and references of result values are determined by the first array." https://lodash.com/docs/4.17.15#difference
// Both directions needs to be checked in order to update roles when the access should no longer exists.
if (
_.difference(newAccessGroups, currentAccessGroups).length > 0 ||
_.difference(currentAccessGroups, newAccessGroups).length > 0
) {
userFromDb.setAccessgroup(
(
await AccessGroup.findAll({
@@ -290,6 +310,7 @@ const getUser = async ({ username, name, email, iamGroups, sisId }) => {
const toReturn = {
...formattedUser,
iamRights,
roles: newAccessGroups,
}
userDataCache.set(username, toReturn)
-239
View File
@@ -1,239 +0,0 @@
const {
isSuperAdminIam,
isAdminIam,
isUniversityWideIam,
isDoctoralIam,
getStudyLeaderGroup,
iamToOrganisationCode,
isEmployeeIam,
iamToDoctoralSchool,
kosuIamToFaculties,
dekaaniIamToFaculty,
opetusVaradekaani,
} = require('../../config/IAMConfig')
const { data } = require('../../config/data')
const { mapToDegreeCode } = require('../../config/common')
/**
* Return given access to all programmes where predicate is true
* (all if no predicate defined)
* @param {object} accessLevel
* @param {(program: object) => boolean} where
* @returns {object} access to programmes
*/
const getAllProgrammeAccess = (accessLevel, where) => {
const access = {}
data.forEach(faculty => {
faculty.programmes.forEach(program => {
if (where?.(program) === false) return
access[program.key] = { ...accessLevel }
})
})
return access
}
/**
* NOT USED
* Grant super-admin rights if the user has correct iams (eg. grp-toska)
* @param {string[]} hyGroups
* @returns superAdmin special group
*/
const getSuperAdmin = hyGroups => {
const isToska = hyGroups.some(isSuperAdminIam)
if (isToska) {
return { specialGroup: { superAdmin: true } }
}
return {}
}
/**
* NOT USED
* Grant admin rights if the user has correct iams (eg. grp-ospa)
* @param {string[]} hyGroups
* @returns admin special group
*/
const getAdmin = hyGroups => {
const isOspa = hyGroups.some(isAdminIam)
if (isOspa) {
return { specialGroup: { admin: true } }
}
return {}
}
/**
* Grant reading rights to all programmes if user has uni wide IAM (eg. hy-rehtoraatti)
* @param {string[]} hyGroups
* @returns read access to ALL programmes
*/
const getUniversityReadingRights = hyGroups => {
const hasUniversityReadingRights = hyGroups.some(isUniversityWideIam)
if (!hasUniversityReadingRights) {
return {}
}
const access = getAllProgrammeAccess({ read: true })
const specialGroup = { allProgrammes: true }
return { access, specialGroup }
}
/**
* Grant reading rights to programmes of faculties if user is kosu or dekaanaatti of some faculties
* @param {string[]} hyGroups
*/
const getFacultyReadingRights = hyGroups => {
// faculty codes from kosu iam
const facultyCodes = hyGroups
.flatMap(kosuIamToFaculties)
// faculty codes from dekanaatti iam
.concat(hyGroups.map(dekaaniIamToFaculty))
.filter(Boolean)
const access = {}
facultyCodes.forEach(fc => {
const faculty = data.find(faculty => faculty.code === fc)
const programmeCodes = faculty.programmes.map(p => p.key)
programmeCodes.forEach(code => {
access[code] = { read: true }
})
})
return { access, specialGroup: {} }
}
/**
* Grant admin rights to faculty programmes if user is opetusvaradekaani of that faculty
* @param {string[]} hyGroups
*/
const getFacultyAdminRights = hyGroups => {
if (!hyGroups.includes(opetusVaradekaani)) return {}
const facultyCodes = hyGroups.map(dekaaniIamToFaculty).filter(Boolean)
const access = {}
facultyCodes.forEach(fc => {
const faculty = data.find(faculty => faculty.code === fc)
const programmeCodes = faculty.programmes.map(p => p.key)
programmeCodes.forEach(code => {
access[code] = { read: true, write: true, admin: true }
})
})
return { access, specialGroup: {} }
}
/**
* Grant reading rights to all doctoral programmes if the user belongs to doctoral IAM
* @param {string[]} hyGroups
* @returns read access to ALL doctoral programs
*/
const getDoctoralAccess = hyGroups => {
const hasDoctoralReadingRights = hyGroups.some(isDoctoralIam)
if (!hasDoctoralReadingRights) return {}
const access = getAllProgrammeAccess({ read: true }, program => program.level === 'doctoral')
const specialGroup = { doctoral: true }
return { access, specialGroup }
}
/**
* Grants reading rights to all doctoral programmes that belong to user's
* doctoral school IAMs
* @param {string[]} hyGroups
* @returns read access to doctoral programs
*/
const getDoctoralSchoolAccess = hyGroups => {
const doctoralProgrammeCodes = hyGroups.flatMap(iamToDoctoralSchool)
const access = {}
doctoralProgrammeCodes.forEach(code => {
if (!code) return
access[code] = { read: true }
})
return { access }
}
/**
* Grant admin access if the user belongs to studyprogramme's manager group and is a study program leader
* @param {string[]} hyGroups
*/
const getProgrammeAdminAccess = hyGroups => {
const orgCodes = hyGroups
.filter(iam => hyGroups.includes(getStudyLeaderGroup(iam)))
.map(iam => iamToOrganisationCode(iam))
.filter(Boolean)
const degreeCodes = orgCodes.flatMap(codes => codes.map(mapToDegreeCode))
if (!degreeCodes?.length > 0) {
return {}
}
const access = {}
degreeCodes.forEach(code => {
access[code] = { read: true, write: true, admin: true }
})
return { access }
}
/**
* Grant write and read access if the user belongs to employees group and studyprogramme's manager group
* @param {string[]} hyGroups
*/
const getProgrammeWriteAccess = hyGroups => {
if (!hyGroups.some(isEmployeeIam)) return {}
const orgCodes = hyGroups.map(iam => iamToOrganisationCode(iam)).filter(Boolean)
const degreeCodes = orgCodes.flatMap(codes => codes.map(mapToDegreeCode))
const access = {}
degreeCodes.forEach(code => {
if (!code) return
access[code] = { read: true, write: true }
})
return { access }
}
/**
* Grant read access if the user belongs to studyprogramme's manager group
* @param {string[]} hyGroups
*/
const getProgrammeReadAccess = hyGroups => {
const orgCodes = hyGroups.map(iam => iamToOrganisationCode(iam)).filter(Boolean)
const degreeCodes = orgCodes.flatMap(codes => codes.map(mapToDegreeCode))
const access = {}
degreeCodes.forEach(code => {
if (!code) return
access[code] = { read: true }
})
return { access }
}
/**
* Gets access rights and special groups,
* based on IAM-groups in IAM header string
* @param {string} hyGroupsHeader
*/
const getIAMRights = hyGroups => {
let access = {}
let specialGroup = {}
;[
getUniversityReadingRights,
getFacultyReadingRights,
getDoctoralAccess,
getDoctoralSchoolAccess,
getProgrammeReadAccess,
getProgrammeWriteAccess,
getProgrammeAdminAccess,
getFacultyAdminRights,
getAdmin,
getSuperAdmin,
]
.map(f => f(hyGroups))
.forEach(({ access: newAccess, specialGroup: newSpecialGroup }) => {
access = { ...access, ...newAccess }
specialGroup = { ...specialGroup, ...newSpecialGroup }
})
return { access, specialGroup }
}
module.exports = {
getIAMRights,
}
@@ -0,0 +1,190 @@
/* eslint-disable */
const { Credit, Enrollment, Studyright } = require('../models')
const moment = require('moment')
const { Op } = require('sequelize')
const hasValidStudyright = (studyrights, date) => {
const within = (right, date) => {
const first = moment(right.startdate)
const last = moment(right.enddate)
return first.isSameOrBefore(date) && last.isSameOrAfter(date)
}
return studyrights.find(r => within(r, date))
}
const studyrightsOf = async student => {
const rights = await Studyright.findAll({
where: {
student_studentnumber: student,
},
})
return rights
}
const uniq = objects => [...new Set(objects)]
const courseInfo = async course_code => {
const credits = await Credit.findAll({
where: {
course_code,
is_open: true,
grade: {
[Op.in]: ['1', '2', '3', '4', '5', 'Hyv.'],
},
attainment_date: {
[Op.gte]: moment('8-1-2020', 'MM-DD-YYYY').toDate(),
},
},
})
const enrollments = await Enrollment.findAll({
where: {
course_code,
is_open: true,
enrollment_date_time: {
[Op.lte]: moment('8-1-2022', 'MM-DD-YYYY').toDate(),
[Op.gte]: moment('8-1-2020', 'MM-DD-YYYY').toDate(),
},
},
})
let avoin_credited = []
for (let credit of credits) {
const { attainment_date, student_studentnumber: studentnumber } = credit
const rights = await studyrightsOf(studentnumber)
if (!hasValidStudyright(rights, attainment_date)) {
avoin_credited.push(studentnumber)
}
}
let avoin_enrolled = []
for (let enrollment of enrollments) {
const { enrollment_date_time, studentnumber } = enrollment
const rights = await studyrightsOf(studentnumber)
if (!hasValidStudyright(rights, enrollment_date_time)) {
avoin_enrolled.push(studentnumber)
}
}
avoin_credited = uniq(avoin_credited)
avoin_enrolled = uniq(avoin_enrolled)
return {
credits,
enrollments,
avoin_credited,
avoin_enrolled,
}
}
const unifiedCourseInfo = async course_code => {
const a = await courseInfo(course_code)
const b = await courseInfo('AY' + course_code)
avoin_credited = uniq(a.avoin_credited.concat(b.avoin_credited))
avoin_enrolled = uniq(a.avoin_enrolled.concat(b.avoin_enrolled))
return {
credits: uniq(a.credits.concat(b.credits)),
enrollments: uniq(a.enrollments.concat(b.enrollments)),
avoin_credited,
avoin_enrolled,
only_enrolled: avoin_enrolled.filter(e => !avoin_credited.includes(e)),
}
}
const statsOf = async courses => {
console.log(Object.keys(courses).join('\n'))
console.log()
let students = []
for (let course_code of Object.keys(courses)) {
const { credits, enrollments, avoin_enrolled, avoin_credited, only_enrolled } = await unifiedCourseInfo(course_code)
courses[course_code] = { credits, enrollments, avoin_enrolled, avoin_credited, only_enrolled }
students = uniq(students.concat(avoin_enrolled).concat(avoin_credited))
}
const rows = []
for (let s of students) {
let string = s + ' '
let score = 0
for (let course_code of Object.keys(courses)) {
const { enrollments, avoin_credited, only_enrolled } = courses[course_code]
let course_info = '-'
if (avoin_credited.includes(s)) {
course_info = 'X'
score += 10
}
if (only_enrolled.includes(s)) {
const studentsEnrolments = enrollments.filter(e => e.studentnumber === s)
course_info = '' + studentsEnrolments.length
score += 1
}
string += course_info + ' '
}
const rights = await studyrightsOf(s)
const stRight = hasValidStudyright(rights, new Date('2022-8-1')) ? 'yes' : 'no'
string += stRight
rows.push({ string, score })
}
const byScore = (a, b) => b.score - a.score
console.log(
rows
.sort(byScore)
.map(r => r.string)
.join('\n')
)
}
const main = async () => {
const courses_psyk_a = {
'PSYK-111': null,
'PSYK-121': null,
'PSYK-131': null,
'PSYK-141': null,
'PSYK-181': null,
}
const courses_psyk_p = {
'PSYK-251': null,
'PSYK-221': null,
'PSYK-232': null,
'PSYK-226': null,
'PSYK-241': null,
'PSYK-212': null,
}
const courses_hist = {
'HISK-110': null,
'HISK-120': null,
'HISK-131A': null,
'HISK-131': null,
'HISK-141': null,
'HISK-142': null,
}
await statsOf(courses_psyk_a)
console.log()
console.log()
await statsOf(courses_psyk_p)
console.log()
console.log()
await statsOf(courses_hist)
process.exit()
}
main()
+79
View File
@@ -0,0 +1,79 @@
const axios = require('axios')
const Sentry = require('@sentry/node')
const { importerToken, jamiUrl } = require('../conf-backend')
const logger = require('./logger')
const jamiClient = axios.create({
baseURL: jamiUrl,
params: {
token: importerToken,
},
})
const getUserIamAccess = async (user, attempt = 1) => {
if (user.iamGroups.length === 0) return {}
const { id, iamGroups } = user
try {
const { data: iamAccess } = await jamiClient.post('/', {
userId: id,
iamGroups,
})
const { specialGroup } = iamAccess
delete iamAccess.specialGroup
return { iamAccess, specialGroup }
} catch (error) {
if (attempt > 3) {
logger.error('[Jami] error: ', error)
Sentry.captureException(error)
return {}
}
return getUserIamAccess(user, attempt + 1)
}
}
const getUserIams = async userId => {
try {
const { data } = await jamiClient.get(`/${userId}`)
return data.iamGroups
} catch (error) {
if (error.response.status !== 404) {
logger.error('[Jami] error: ', error)
Sentry.captureException(error)
}
return []
}
}
const getAllUserAccess = async () => {
const { data } = await jamiClient.get('/all-access')
return data
}
const testJami = async () => {
try {
await jamiClient.get('/ping', { timeout: 4000 })
logger.info('JAMI connected')
} catch (error) {
logger.error(error)
logger.warn('JAMI not responding :(')
logger.info('Are you sure you are using the latest JAMI image?')
}
}
testJami()
module.exports = {
getUserIamAccess,
getUserIams,
getAllUserAccess,
}
+15 -80
View File
@@ -1,92 +1,27 @@
/* eslint-disable no-unused-vars */
const _ = require('lodash')
const Organization = require('../models/organization')
const { getIAMRights } = require('./IAMRights')
const isNumber = value => !Number.isNaN(parseInt(value, 10))
const { getUserIamAccess } = require('./jami')
const normalizeOrganizationCode = r => {
if (!r.includes('_')) {
return r
const getAccessFromIAMs = async user => {
if (user.iamGroups.length === 0) return {}
const { iamAccess, specialGroup } = await getUserIamAccess(user)
const access = {}
if (_.isObject(iamAccess)) {
Object.keys(iamAccess).forEach(code => {
access[code] = iamAccess[code]
})
}
const [left, right] = r.split('_')
const prefix = [...left].filter(isNumber).join('')
const suffix = `${left[0]}${right}`
const providercode = `${prefix}0-${suffix}`
return providercode
}
const RELEVANT_ORGANIZATION_CODES = [
'H906', // Kielikeskus
'H930', // Avoin yliopisto
]
const ORGANIZATION_ACCESS_BY_IAM_GROUP = {
'grp-kielikeskus-esihenkilot': {
// Kielikeskus
H906: {
read: true,
write: true,
admin: true,
},
},
'grp-avoin-johto': {
// Avoin yliopisto
H930: {
read: true,
},
},
}
const getOrganizationAccessFromIamGroups = user => {
const access = {}
const iamAccess = (user.iamGroups ?? []).reduce(
(access, group) => _.merge(access, ORGANIZATION_ACCESS_BY_IAM_GROUP[group] ?? {}),
{}
)
Object.keys(iamAccess).forEach(code => {
access[code] = iamAccess[code]
})
return access
}
const organizationIsRelevant = organization => {
const { code } = organization
return code.includes('-') || RELEVANT_ORGANIZATION_CODES.includes(code)
}
const getAccessToAll = async accessLevel => {
const access = {}
const allOrganizations = await Organization.findAll({ attributes: ['code'] })
allOrganizations.filter(organizationIsRelevant).forEach(({ code }) => {
access[code] = accessLevel
})
return access
}
const getLomakeAccess = async user => {
if (!user.iamGroups) user.iamGroups = user.iam_groups
const access = {}
const { access: iamAccess } = getIAMRights(user.iamGroups)
if (!_.isObject(iamAccess)) return access
Object.keys(iamAccess).forEach(code => {
access[code] = iamAccess[code]
})
return access
return { access, specialGroup }
}
const getOrganizationAccess = async user => {
const access = {
...(await getLomakeAccess(user)),
...getOrganizationAccessFromIamGroups(user),
}
if (!user.iamGroups) user.iamGroups = user.iam_groups
const access = await getAccessFromIAMs(user)
return access
}
+14 -14
View File
@@ -6,7 +6,7 @@
"": {
"name": "frontend",
"dependencies": {
"@reduxjs/toolkit": "^1.8.5",
"@reduxjs/toolkit": "^1.8.6",
"@sentry/browser": "^6.18.2",
"@sentry/tracing": "^6.18.2",
"axios": "^0.21.4",
@@ -36,7 +36,7 @@
"react-scripts": "4.0.3",
"react-scroll-to-component": "^1.0.2",
"scheduler": "^0.23.0",
"semantic-ui-css": "^2.4.1",
"semantic-ui-css": "^2.5.0",
"semantic-ui-react": "^2.1.2",
"unfuck-spa-shibboleth-session": "^3.1.4",
"use-context-selector": "^1.3.9",
@@ -2841,9 +2841,9 @@
}
},
"node_modules/@reduxjs/toolkit": {
"version": "1.8.5",
"resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-1.8.5.tgz",
"integrity": "sha512-f4D5EXO7A7Xq35T0zRbWq5kJQyXzzscnHKmjnu2+37B3rwHU6mX9PYlbfXdnxcY6P/7zfmjhgan0Z+yuOfeBmA==",
"version": "1.8.6",
"resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-1.8.6.tgz",
"integrity": "sha512-4Ia/Loc6WLmdSOzi7k5ff7dLK8CgG2b8aqpLsCAJhazAzGdp//YBUSaj0ceW6a3kDBDNRrq5CRwyCS0wBiL1ig==",
"dependencies": {
"immer": "^9.0.7",
"redux": "^4.1.2",
@@ -17914,9 +17914,9 @@
}
},
"node_modules/semantic-ui-css": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/semantic-ui-css/-/semantic-ui-css-2.4.1.tgz",
"integrity": "sha512-Pkp0p9oWOxlH0kODx7qFpIRYpK1T4WJOO4lNnpNPOoWKCrYsfHqYSKgk5fHfQtnWnsAKy7nLJMW02bgDWWFZFg==",
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/semantic-ui-css/-/semantic-ui-css-2.5.0.tgz",
"integrity": "sha512-jIWn3WXXE2uSaWCcB+gVJVRG3masIKtTMNEP2X8Aw909H2rHpXGneYOxzO3hT8TpyvB5/dEEo9mBFCitGwoj1A==",
"dependencies": {
"jquery": "x.*"
}
@@ -24283,9 +24283,9 @@
"integrity": "sha512-9X2obfABZuDVLCgPK9aX0a/x4jaOEweTTWE2+9sr0Qqqevj2Uv5XorvusThmc9XGYpS9yI+fhh8RTafBtGposw=="
},
"@reduxjs/toolkit": {
"version": "1.8.5",
"resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-1.8.5.tgz",
"integrity": "sha512-f4D5EXO7A7Xq35T0zRbWq5kJQyXzzscnHKmjnu2+37B3rwHU6mX9PYlbfXdnxcY6P/7zfmjhgan0Z+yuOfeBmA==",
"version": "1.8.6",
"resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-1.8.6.tgz",
"integrity": "sha512-4Ia/Loc6WLmdSOzi7k5ff7dLK8CgG2b8aqpLsCAJhazAzGdp//YBUSaj0ceW6a3kDBDNRrq5CRwyCS0wBiL1ig==",
"requires": {
"immer": "^9.0.7",
"redux": "^4.1.2",
@@ -35833,9 +35833,9 @@
}
},
"semantic-ui-css": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/semantic-ui-css/-/semantic-ui-css-2.4.1.tgz",
"integrity": "sha512-Pkp0p9oWOxlH0kODx7qFpIRYpK1T4WJOO4lNnpNPOoWKCrYsfHqYSKgk5fHfQtnWnsAKy7nLJMW02bgDWWFZFg==",
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/semantic-ui-css/-/semantic-ui-css-2.5.0.tgz",
"integrity": "sha512-jIWn3WXXE2uSaWCcB+gVJVRG3masIKtTMNEP2X8Aw909H2rHpXGneYOxzO3hT8TpyvB5/dEEo9mBFCitGwoj1A==",
"requires": {
"jquery": "x.*"
}
+2 -2
View File
@@ -6,7 +6,7 @@
"build": "DISABLE_ESLINT_PLUGIN=true react-scripts build"
},
"dependencies": {
"@reduxjs/toolkit": "^1.8.5",
"@reduxjs/toolkit": "^1.8.6",
"@sentry/browser": "^6.18.2",
"@sentry/tracing": "^6.18.2",
"axios": "^0.21.4",
@@ -36,7 +36,7 @@
"react-scripts": "4.0.3",
"react-scroll-to-component": "^1.0.2",
"scheduler": "^0.23.0",
"semantic-ui-css": "^2.4.1",
"semantic-ui-css": "^2.5.0",
"semantic-ui-react": "^2.1.2",
"unfuck-spa-shibboleth-session": "^3.1.4",
"use-context-selector": "^1.3.9",
@@ -34,15 +34,16 @@ export default {
**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.`,
ThesisWritersOfTheFaculty: `Sisältää kyseisenä vuonna kandidaatintutkielman tai pro gradu -tutkielman 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.`,
GroupByToggle: `Graduation year: Valmistuneet opiskelijat on ryhmitelty opintojen valmistumisvuoden perusteella.\n
Group by: Starting year: Valmistuneet opiskelijat on jaoteltu opinto-oikeuden alkuvuoden mukaan.
`,
AverageGraduationTimes: `
Opiskelijoiden keskimääräiset valmistumisajat tutkintotasoittain.
@@ -52,25 +53,29 @@ export default {
- 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ä.
Valmistumisajoista on **vähennetty lakisääteiset poissaolot**.
Palkissa oleva lukumäärä kertoo **montako opiskelijaa** kuhunkin valmistuneiden ryhmään kuuluu.
Tarkastellessa valmistumisaikoja aloitusvuosittain palkissa näkyy myös **kuinka suuri osa** kyseisenä vuonna tiedekuntaan/opinto-ohjelmaan opinto-oikeuden saaneista on valmistunut.
Palkissa oleva lukumäärä kertoo **montako opiskelijaa** kuhunkin valmistuneiden ryhmään kuuluu.
Vihreä **vaakapalkki** kuvaa tavoiteajassa (tai alle) valmistuneista, keltainen palkki korkeintaan vuodella tavoiteajan ylittäneitä
ja punainen yli vuodella tavoiteajan ylittäneitä.
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.
**Breakdown**: Näyttää, kuinka moni opiskelija valmistui ajallaan, korkeintaan vuoden myöhässä tavoiteajasta tai tätä myöhemmin.
**Median time**: Näyttää kyseisenä vuonna/lukuvuonna valmistuneiden opiskelijoiden opintojen keston **mediaanin** kuukausina.
**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.
**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.
**Mean time**: Näyttää kyseisenä vuonna valmistuneiden opiskelijoiden opintojen keston **keskiarvon** kuukausina.`,
Mediaaninäkymässä vihreä **katkoviiva** kuvastaa tavoiteaikaa. Keltainen katkoviiva on tavoiteaika + yksi vuosi.\n
Tarkastellessa valmistumisaikoja aloitusvuosittain palkissa näkyy myös **kuinka suuri osa** kyseisenä vuonna tiedekuntaan/opinto-ohjelmaan opinto-oikeuden saaneista on valmistunut.
`,
// Faculty Student population
GraduatedToggle: `
@@ -82,33 +87,26 @@ export default {
Kuvaa tiedekunnan ohjelmissa opiskelevien opiskelijoden tilastoja. Esimerkiksi 2020-2021 aloittaneiden opintopistejakaumaa.
HUOM!
**All** Sisältää kaikki opiskelijat, joiden opinto-oikeudella on aloituspäivä kyseisenä vuonna, riippumatta siitä, onko opiskelija aloittanut opiskelemaan ko. vuonna.
**Currently enrolled** Sisältää kaikki tällä hetkellä läsnäolevat olevat opiskelija.
**Absent** Sisältää kaikki lakisääteisistä syistä poissaolevat opiskelijat.
**Started studying** Sisältää kaikki opiskelijat, jotka ovat aloittaneet opiskelemaan koulutusohjelmassa kyseisenä lukuvuonna.
**Inactive** Sisältää kaikki opinto-oikeudet, jotka ovat alkaneet kyseisenä vuonna, mutta jotka ovat sittemmin vanhentuneet ilman, että opiskelija on valmistunut ohjelmasta. Sisältää myös opinto-oikeudet,
- **All** Sisältää opiskelijat, jotka siirtyneet ohjelmaan tai siitä ulos sekä aloittaneet ohjelmassa
- **Currently enrolled** Sisältää kaikki tällä hetkellä läsnäolevat olevat opiskelija.
- **Absent** Sisältää kaikki lakisääteisistä syistä poissaolevat opiskelijat.
- **Started studying** Sisältää kaikki opiskelijat, jotka ovat aloittaneet opiskelemaan koulutusohjelmassa kyseisenä lukuvuonna.
- **Inactive** Sisältää kaikki opinto-oikeudet, jotka ovat alkaneet kyseisenä vuonna, mutta jotka ovat sittemmin vanhentuneet ilman, että opiskelija on valmistunut ohjelmasta. Sisältää myös opinto-oikeudet,
joissa opiskelija on laiminlyönyt ilmoittautumisen tälle lukukaudelle.
Mukana ovat oletusarvoisesti myös vaihto-opiskelijat, ohjelmaan ja siitä pois siirtyneet opiskelijat sekä erillisopinto-oikeudella opiskelevat.
Taulukko näyttää myös näistä opiskelijoista miesten, naisten ja suomalaisten osuudet sekä valmistuneiden määrän.
Yläosan valikosta on mahdollista valita tarkasteluun yhden opinto-ohjelman opiskelijat tietylle vuodelle. Sekä opiskelijoiden yleistiedot että edistyminen opintopisteittäin
kuvaavat tällöin kyseisen opintosuunnan opiskelijoita.`,
Taulukko näyttää myös näistä opiskelijoista miesten, naisten ja suomalaisten osuudet sekä valmistuneiden määrän. `,
StudentProgress: `
Kuvaa tiedekuntaan kuuluvien eri ohjemien opinto-oikeudella varustettujen opiskelijoiden etenemistä opintopisteissä mitattuna. Esimerkiksi 2021-2022 aloittaneiden opintopistejakaumaa.
- Bachelor: Kandidaatin opinto-oikeudella varustettujen opiskelijoiden eteneminen
- Bachelor + Master: Kandi+maisteriopinto-oikeudella opiskelleiden ja siitä maisteriksi ohjelmiin siirtyneiden etenenminen
- Master: Pelkällä maisterintutkinnon opinto-oikeudella varustettujen opiskelleiden eteneminen
- Doctor: Tohtorin ja lisensiaatin opinto-oikeudella varustettujen opiskelijoiden eteneminen
- Bachelor + Master: Kandi+maisteriopinto-oikeudella opiskelleiden ja siitä maisteriksi ohjelmiin siirtyneiden etenenminen. Aloitusvuodeksi on merkitty kandiohjelman aloitusvuosi.
Opinto-ohjelmaksi on merkitty maisteriohjelma, johon opiskelija on siirtynyt kandiksi valmistumisen jälkeen.
- Master: Pelkällä maisterintutkinnon opinto-oikeudella varustettujen opiskelijoiden eteneminen
- Doctor: Tohtorin opinto-oikeudella varustettujen opiskelijoiden eteneminen
HUOM! Mukana ovat myös muuhun kuin valittuun koulutusohjelmaan tai opintosuuntaan tehdyt opintopisteet.
HUOM! Mukana ovat myös muuhun kuin valittuun koulutusohjelmaan tai opintosuuntaan tehdyt opintopisteet. Opintopisteet on laskettu opinto-oikeuden alkamisesta, eikä tällä hetkellä ota huomioon mahdollisia hyväksilukuja.
Mukana ovat oletusarvoisesti myös vaihto-opiskelijat, erillisopinto-oikeudella opiskelevat ja ohjelmaan siirtyneet.
Mikäli yläosan valikosta valittuna on jokin tietty opintosuunta, myös nämä graafit ja taulukot kuvaavat kyseisen opintosuunnan opiskelijoita.
**Total** Kuvaa opiskelijoinden opintopisteiden määrien kertymää alkaen 1.8.2017. Luvut ovat isompia kuin vuosittain lasketut, sillä nyt kaikki 1.8.2017 jälkeen
suoritetut opintopisteet lasketaan mukaan, vaikka ne olisi suoritettu ennen varsinaista opinto-oikeuden alkua.
`,
}
@@ -1,8 +1,8 @@
export default {
enrollmentStatus: {
label: 'Filter students present or absent',
label: 'Filter students present, absent or passive',
short:
'Suodata opiskelijoita sen mukaan ovatko he ilmoittautuneet läsnä- tai poissaoleviksi kyseiselle lukukaudelle',
'Suodata opiskelijoita sen mukaan ovatko he joko ilmoittautuneet läsnä- tai poissaoleviksi tai jääneet passiivisiksi kyseiselle lukukaudelle. Huom. osa passiivisista opiskelijoista on jo valmistunut ko. ohjelmasta.',
},
studyrightStatus: {
label: 'Rajaa opiskelijoita opinto-oikeuden statuksen mukaan',
@@ -18,9 +18,9 @@ export default {
label: null,
short: 'Rajaa kurssisuorituksia tietyn ajanjakson mukaan. Tämä rajaus ei muuta opiskelijoiden määrää',
},
cherrypicker: {
studentNumber: {
label: null,
short:
'Rajaa yksittäisiä opiskelijoita pois tilastoista (blocklist) tai näytä vain määritellyt opiskelijat (allowlist). Vain täsmälliset opiskelijanumerot toimivat.',
'(ent. cherry-picker) Rajaa yksittäisiä opiskelijoita pois tilastoista (blocklist) tai näytä vain määritellyt opiskelijat (allowlist). Vain täsmälliset opiskelijanumerot toimivat.',
},
}
@@ -1,7 +1,8 @@
export default {
Advanced: `**Semesters:** Jos haluaa tarkastella vain syyslukukaudella tai kevätlukukaudella ilmoittautuneita, haluttu ajanjakso valitaan tästä.
Advanced: `**Semesters:** Jos haluaa tarkastella vain syyslukukaudella tai kevätlukukaudella aloittaneita, haluttu ajanjakso valitaan tästä.
**Include:** Koulutusohjelmapopulaatiosta on oletusarvoisesti suodatettu pois vaihto-opiskelijat ja erillisopinto-oikeudella opiskelevat. Näitä ryhmiä voi sisällyttää mukaan lisäämällä ruksin haluamaansa kohtaan.`,
**Include:** Koulutusohjelmapopulaatiosta on oletusarvoisesti suodatettu pois ohjelmasta poissiirtyneet opiskelijat. Heidät voi sisällyttää mukaan lisäämällä ruksin ja noutamalla tulokset uudestaan (Fetch class with new settings).`,
// Koulutusohjelmapopulaatiosta on oletusarvoisesti suodatettu pois vaihto-opiskelijat ja erillisopinto-oikeudella opiskelevat. Näitä ryhmiä voi sisällyttää mukaan lisäämällä ruksin haluamaansa kohtaan.
QueryCard: `**Valitun koulutusohjelmavuosikurssin perustiedot.**
**Sample size** luku sisältää läsnä- ja poissaolevaksi ilmoittautuneet opiskelijat, muttei "excludes"-ryhmiä, jotka voi määritellä "advanced settings" -kohdasta.
@@ -48,7 +49,7 @@ export default {
**Cumulative**-valinnan ollessa käytössä kunkin opiskelijan kaikki aiemmat opintopisteet lasketaan mukaan opintopistemäärään. Muutoin vain pylvästä vastaavalla aikavälillä kirjatut opintopisteet otetaan huomioon.
`,
CoursesOfPopulation: `**Courses of population**
CoursesOfPopulation: `**Courses of class**
Tämä taulukko sisältää näkyvissä olevien opiskelijoiden kaikki kurssisuoritukset.
@@ -65,8 +66,8 @@ export default {
**Taulukot**
- **pass/fail** - oletusnäkymä, joka sisältää kurssisuoritukset, hylätyt sekä yritykset. Taulukon tiedot voi järjestää sarakkeita (n, after retry, percentage, many times, per student, passed ja attempted) klikkaamalla.
- **pass/fail w/ enrollments** - näkymä, joka sisältää suoritustiedot ilmoittautumisten kanssa. Huom.! ilmoittautumistiedot ovat saatavilla vain Sisun käyttöönoton jälkeiseltä ajalta.
- **pass/fail** - näkymä sisältää kurssisuoritukset, hylätyt sekä yritykset sekä ilmoittautumistiedot. Huom.! ilmoittautumistiedot ovat saatavilla vain Sisun käyttöönoton jälkeiseltä ajalta.
- **grades** - arvosanajakauma.
- **when passed** - kurssisuorituksen ajankohta. Huom.! *Passed*-sarakkeen kokonaissumma ei välttämättä täsmää puolivuosittaisten lukumäärien summan kanssa johtuen tuplakirjauksista Oodissa.
`,
@@ -82,31 +83,64 @@ export default {
Tägien ja opintosuuntien valinnat ovat siirtyneet tästä omiksi filtereikseen.
Voit tehdä valinnat siirryttyäsi tarkastelemaan ohjelman tilastoja.
`,
Students: `**Students**
StudentsGuidanceGroups: `**Students**
Taulukko näyttää oletusarvoisesti vain opiskelijanumerot, mutta harmaata liukukytkintä klikkaamalla saa näkyviin opiskelijoiden nimet.
Klikkaamalla opiskelijanumeron vieressä olevaa sinistä henkilösymbolia, siirryt kyseisen opiskelijan "student statistic" -näkymään.
Klikkaamalla "Sisu"-nuolta, siirryt tarkastelemaan opiskelijan tietoja Sisu-järjestelmässä.
**General**
Klikkaamalla opiskelijanumeron vieressä olevaa sinistä nuolta, siirtyy "student statistic" näkymään valitun opiskelijan osalta.
Valikoituja selvennyksiä:
- **Credits**
- **All** listaa opiskelijan suoritetut opintopisteet valitun tarkastelujakson ajalta.
- **Since ... ** listaa opiskelijan suoritetut opintopisteet määritellystä ajankohdasta alkaen
- **Start of studyright**: Opiskelijan valittuun ohjelmaan liittyvän opinto-oikeuden alkupäivä
- **Started in programme**: Opiskelijan valitussa ohjelmassa aloituspäivämäärä. Kandi+maisteriopiskelijoille kandiksi valmistumispäivämäärä +1.
- **credits since start of studyright:** listaa opiskelijan suoritetut opintopisteet valitun tarkastelujakson / populaation ajalta.
- **all credits:** listaa opiskelijan kaikki opintopisteet (myös aiemmat suoritukset, eli ne jotka tehty ennen valittuun tarkastelujaksoon ja populaatioon kuulumista)
- **transferred from:** opiskelijan vanha koulutusohjelma, josta opiskelija on siirtynyt uuteen.
- **studytracks:** opintosuunta
`,
StudentsClass: `**Students**
Taulukko näyttää oletusarvoisesti vain opiskelijanumerot, mutta harmaata liukukytkintä klikkaamalla saa näkyviin opiskelijoiden nimet.
**Mandatory Courses**
Klikkaamalla opiskelijanumeron vieressä olevaa sinistä henkilösymbolia, siirryt kyseisen opiskelijan "student statistic" -näkymään.
Klikkaamalla "Sisu"-nuolta, siirryt tarkastelemaan opiskelijan tietoja Sisu-järjestelmässä.
**General**
Valikoituja selvennyksiä:
- **Credits**
- **All** listaa opiskelijan suoritetut opintopisteet valitun tarkastelujakson ajalta
- **HOPS** listaa opiskelijan kaikki HOPSiin sijoitut opintopisteet (myös aiemmat suoritukset, eli ne jotka tehty ennen valittuun tarkastelujaksoon ja populaatioon kuulumista)
- **Since start in programme** listaa opiskelijan ohjelmassa aloittamisen jälkeen suoritut opintopisteet
- **Transferred from:** opiskelijan vanha koulutusohjelma, josta opiskelija on siirtynyt uuteen.
- **Start of studyright**: Opiskelijan valittuun ohjelmaan liittyvän opinto-oikeuden alkupäivä
- **Started in programme**: Opiskelijan ko. ohjelmassa aloituspäivämäärä. Kandi+maisteriopiskelijoille kandiksi valmistumispäivämäärä +1
- **Semesters**: Läsnäololukukausien lukumäärä
**Courses**
Tällä välilehdellä näkyy, mitä pakollisiksi määriteltyjä kursseja opiskelija on suorittanut.
Kurssit voi määrittää kohdassa "Study program" > "Overview" > haluttu koulutusohjelma. Scrollatessa kurssitaulukkoa alaspäin, kurssin nimen saa näkyviin viemällä kursorin halutun solun kohdalle.
**Tags**
Opiskelijoille voi luoda tag:eja ryhmittelyä varten kohdassa "Study Program" > "Overview" > haluttu koulutusohjelma.
**Download**
`,
StudentsCustom: `**Students**
Taulukko näyttää oletusarvoisesti vain opiskelijanumerot, mutta harmaata liukukytkintä klikkaamalla saa näkyviin opiskelijoiden nimet.
Klikkaamalla tätä harmaata kuvaketta, saa ladattua .xlsx tiedoston, joka sisältää kaikki "General" ja "Mandatory Courses" välilehtien tiedot.
Klikkaamalla opiskelijanumeron vieressä olevaa sinistä henkilösymbolia, siirryt kyseisen opiskelijan "student statistic" -näkymään.
Klikkaamalla "Sisu"-nuolta, siirryt tarkastelemaan opiskelijan tietoja Sisu-järjestelmässä.
**All credits** listaa opiskelijan kaikki opintopisteet (myös aiemmat suoritukset, eli ne jotka tehty ennen valittuun tarkastelujaksoon ja populaatioon kuulumista)
`,
Filters: {
Add: `Harmaata ”add” painiketta klikkaamalla aukeaa filters- eli suodatinvalikko,
@@ -41,11 +41,30 @@ export default {
**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.
`,
AverageGraduationTimes: `
**Median time**: Näyttää kyseisenä vuonna valmistuneiden opiskelijoiden opintojen keston **mediaanin** kuukausina. Luku sisältää mahdolliset poissaolokuukaudet.
Yksittäinen palkki kertoo, kuinka moni opiskelija on valmistunut kyseisenä vuonna/lukuvuonna.
**Mean time**: Näyttää kyseisenä vuonna valmistuneiden opiskelijoiden opintojen keston **keskiarvon** kuukausina. Luku sisältää mahdolliset poissaolokuukaudet.
Vihreä **vaakapalkki** kuvaa tavoiteajassa (tai alle) valmistuneita, keltainen palkki korkeintaan vuoden tavoiteajasta myöhässä valmistuneita
ja punainen yli vuoden tavoiteajan ylittäneitä.
**Breakdown**: Näyttää, kuinka moni kyseisenä kalenteri- tai lukuvuotena valmistuneista opiskelijoista valmistui ajallaan, korkeintaan vuoden myöhässä tavoiteajasta tai tätä myöhemmin.
**Median time**: Näyttää kyseisenä kalenteri- tai lukuvuotena valmistuneiden opiskelijoiden opintojen keston **mediaanin** kuukausina.
Vihreä **katkoviiva** kuvastaa tavoiteaikaa. Keltainen katkoviiva on tavoiteaika + yksi vuosi.\n
Valmistumisajoista on vähennetty lakisääteiset poissaolot.`,
AverageGraduationTimesStudytracks: `
Yksittäinen palkki kertoo, kuinka moni kyseisenä **lukuvuotena ALOITTANEISTA** opiskelijoista on valmistunut.
Vihreä **vaakapalkki** kuvaa tavoiteajassa (tai alle) valmistuneita, keltainen palkki korkeintaan vuoden tavoiteajasta myöhässä valmistuneita
ja punainen yli vuoden tavoiteajan ylittäneitä.
**Breakdown**: Näyttää, kuinka moni kyseisenä lukuvuotena aloittaneista, jo valmistuneista, opiskelijoista on valmistui ajallaan, korkeintaan vuoden myöhässä tavoiteajasta tai tätä myöhemmin.
**Median time**: Näyttää valmistuneiden opiskelijoiden opintojen keston **mediaanin** kuukausina sekä heidän prosentuaalisen osuutensa koko vuosikurssista.
Vihreä **katkoviiva** kuvastaa tavoiteaikaa. Keltainen katkoviiva on tavoiteaika + yksi vuosi.
n = **valmistuneet opiskelijat / kaikki ohjelmassa kyseisenä vuonna alkaneet opinto-oikeudet**. Kandiohjelmissa tavoiteaika on 36 kk, maisteriohjelmissa 24 kk`,
Valmistumisajoista on vähennetty lakisääteiset poissaolot.`,
ProgrammesBeforeOrAfter: `
**Mikäli valittuna on kandiohjelma:** Sisältää maisteriohjelmat, joissa tämän kandiohjelman opiskelijat ovat aloittaneet valmistuttuaan kandeiksi. Vuosijaottelu on tehty maisteriohjelmassa aloittamisen perusteella.
@@ -31,7 +31,7 @@ import {
startYearAtUniFilter,
programmeFilter,
studyTrackFilter,
cherrypickFilter,
studentNumberFilter,
} from '../FilterView/filters'
import FilterView from '../FilterView'
import useLanguage from '../LanguagePicker/useLanguage'
@@ -319,7 +319,7 @@ const CoursePopulation = ({
name="CoursePopulation"
filters={[
genderFilter,
cherrypickFilter,
studentNumberFilter,
ageFilter,
courseFilter({ courses: courseStatistics?.coursestatistics }),
creditsEarnedFilter,
@@ -107,11 +107,11 @@ export const getThesisGradeSpread = (series, isRelative) => {
3: [],
4: [],
5: [],
'Hyv.': [],
}
const newSeries = series.reduce(
(acc, cur, i) => {
const currentEntries = Object.entries(cur)
currentEntries.forEach(([k, v]) => {
const merged = k === 'LA' ? 'LUB' : k
acc[merged].push(v)
@@ -0,0 +1,230 @@
import moment from 'moment'
import React, { useEffect, useState } from 'react'
import Datetime from 'react-datetime'
import { Modal, Form, Button, TextArea } from 'semantic-ui-react'
import qs from 'query-string'
import SearchHistory from 'components/SearchHistory'
import {
useCreateOpenUniCourseSearchMutation,
useDeleteOpenUniCourseSearchMutation,
useUpdateOpenUniCourseSearchMutation,
} from 'redux/openUniPopulations'
const CustomOpenUniSearch = ({ setValues, savedSearches, location, history }) => {
const [modal, setModal] = useState(false)
const [input, setInput] = useState('')
const [searchList, setSearches] = useState(savedSearches)
const [name, setName] = useState('')
const [startdate, setStartdate] = useState(moment('01-08-2017 00:00:00', 'DD-MM-YYYY'))
const [enddate, setEnddate] = useState(moment().endOf('day'))
const [selectedSearchId, setSelectedSearchId] = useState('')
const [updateOpenUniCourseSearch, { isLoading: updateIsLoading, data: updatedData }] =
useUpdateOpenUniCourseSearchMutation()
const [createOpenUniCourseSearch, { isLoading: createIsLoading, data: createdData }] =
useCreateOpenUniCourseSearchMutation()
const [deleteOpenUniCourseSearch, { isLoading: deleteIsLoading, data: deletedData }] =
useDeleteOpenUniCourseSearchMutation()
useEffect(() => {
if (updatedData) {
const updatedSearches = searchList.map(s => (s.id === updatedData.id ? updatedData : s))
setSearches(updatedSearches)
}
}, [updatedData])
useEffect(() => {
if (createdData && !createdData.error) {
const newList = searchList.concat(createdData)
setSearches(newList)
}
}, [createdData])
useEffect(() => {
if (deletedData) {
const filteredsearches = searchList.filter(s => s.id !== deletedData)
setSearches(filteredsearches)
}
}, [deletedData])
const pushQueryToUrl = query => {
setImmediate(() => {
const searchString = qs.stringify(query)
history.push({ search: searchString })
})
}
const parseQueryFromUrl = () => {
const { courseCode, startdate, enddate } = qs.parse(location.search)
let courseCodes = courseCode
if (!Array.isArray(courseCode)) courseCodes = [courseCode]
const query = {
courseList: courseCodes,
startdate: moment(startdate, 'DD-MM-YYYY').toISOString(),
enddate: moment(enddate, 'DD-MM-YYYY').endOf('day').toISOString(),
}
return query
}
useEffect(() => {
setImmediate(() => {
if (!location.search) {
setValues({})
} else {
const query = parseQueryFromUrl()
setValues(query)
}
})
}, [location.search])
const clearForm = () => {
setInput('')
setName('')
setStartdate(moment('01-08-2017 00-00-00', 'DD-MM-YYYY'))
setEnddate(moment().endOf('day'))
setSelectedSearchId('')
}
const handleClose = () => {
setModal(false)
clearForm()
}
const onClicker = e => {
e.preventDefault()
const courseList = input.split(/[\s,]+/).map(code => code.trim().toUpperCase())
const query = {
courseCode: courseList,
startdate: moment(startdate).isValid()
? moment(startdate).format('DD-MM-YYYY')
: moment('01-08-2017', 'DD-MM-YYYY').format('DD-MM-YYYY'),
enddate: moment(enddate).isValid() ? moment(enddate).format('DD-MM-YYYY') : moment().format('DD-MM-YYYY'),
}
setValues({
courseList,
startdate: moment(startdate).isValid()
? moment(startdate).toISOString()
: moment('01-08-2017', 'DD-MM-YYYY').toISOString(),
enddate: moment(enddate).isValid() ? moment(enddate).toISOString() : moment().toISOString(),
})
pushQueryToUrl(query)
handleClose()
}
const onDelete = () => {
deleteOpenUniCourseSearch({ id: selectedSearchId })
clearForm()
}
const onSelectSearch = selectedId => {
if (!selectedId) {
clearForm()
return
}
const { id: selectedSearchId } = selectedId
const selectedSearch = searchList.find(search => search.id === selectedSearchId)
if (selectedSearch) {
setInput(selectedSearch.courseList.join(', '))
setName(selectedSearch.name)
setSelectedSearchId(selectedSearch.id)
}
}
const onSave = () => {
const courseList = input.split(/[\s,]+/).map(code => code.trim().toUpperCase())
if (selectedSearchId !== '') {
updateOpenUniCourseSearch({ id: selectedSearchId, courseList })
} else {
createOpenUniCourseSearch({ courseList, name })
}
}
return (
<Modal
trigger={
<Button size="small" color="blue" onClick={() => setModal(true)} data-cy="open-uni-search-button">
Fetch Open Uni Students
</Button>
}
open={modal}
onClose={handleClose}
size="small"
>
<Modal.Content>
<Form>
<h2> Fetch open uni course population</h2>
<Form.Field>
<Form.Field data-cy="search-name">
<em> Insert name for this population if you wish to save it </em>
<Form.Input
disabled={selectedSearchId !== ''}
value={name}
placeholder="name"
onChange={e => setName(e.target.value)}
/>
</Form.Field>
<em>Insert course code(s)</em>
<TextArea value={input} placeholder="TKT12345, PSYK-123" onChange={e => setInput(e.target.value)} />
</Form.Field>
<SearchHistory
header="Saved populations"
items={searchList?.map(s => ({
...s,
text: s.name,
timestamp: s.updatedAt,
params: { id: s.id },
}))}
updateItem={() => null}
handleSearch={onSelectSearch}
/>
<Form.Field data-cy="begin-of-search">
<em>Select beginning</em>
<Datetime
value={startdate}
onChange={value => setStartdate(value)}
timeFormat={false}
locale="fi"
isValidDate={date => date.isBefore(enddate)}
closeOnSelect
/>
</Form.Field>
<Form.Field data-cy="end-of-search">
<em>Select ending for enrollments:</em>
<br />
<em>Attainments are fetched until today.</em>
<Datetime
value={enddate}
onChange={value => setEnddate(value)}
timeFormat={false}
locale="fi"
isValidDate={date => date.isAfter(startdate)}
closeOnSelect
/>
</Form.Field>
</Form>
</Modal.Content>
<Modal.Actions>
<Button
data-cy="save-search"
disabled={!name || updateIsLoading || createIsLoading}
loading={updateIsLoading || createIsLoading}
floated="left"
icon="save"
onClick={onSave}
content="Save"
/>
<Button
disabled={!selectedSearchId || deleteIsLoading}
negative
floated="left"
icon="trash"
onClick={onDelete}
content="Delete"
/>
<Button onClick={handleClose}>Cancel</Button>
<Button positive onClick={e => onClicker(e)} data-cy="search-button">
Search population
</Button>
</Modal.Actions>
</Modal>
)
}
export default CustomOpenUniSearch
@@ -0,0 +1,292 @@
import React, { useEffect, useState } from 'react'
import moment from 'moment'
import { Loader, Icon } from 'semantic-ui-react'
import { useGetOpenUniCourseStudentsQuery } from 'redux/openUniPopulations'
import SortableTable from 'components/SortableTable'
const getTableData = studentsData => {
return Object.keys(studentsData).reduce(
(acc, student) => [
...acc,
{
studentnumber: student,
courseInfo: { ...studentsData[student].courseInfo },
email: studentsData[student].email,
secondaryEmail: studentsData[student].secondaryEmail,
disseminationInfoAllowed: studentsData[student].disseminationInfoAllowed,
totals: studentsData[student].totals,
},
],
[]
)
}
const getColumns = (labelsToCourses, language) => {
const style = {
verticalAlign: 'middle',
textAlign: 'center',
}
const studentNbrColumn = [
{
key: 'studentnumber-parent',
mergeHeader: true,
// merge: true,
title: 'Student Number',
children: [
{
key: 'studentnumber-child',
title: 'Student Number',
cellProps: { style },
getRowVal: s => s.studentnumber,
getRowContent: s => s.studentnumber,
child: true,
},
],
},
]
const statisticColumns = [
{
key: 'passed',
title: 'Passed',
cellProps: { style },
headerProps: { title: 'Passed' },
getRowVal: s => s.totals.passed,
getRowContent: s => s.totals.passed,
child: true,
},
{
key: 'failed',
title: 'Failed',
headerProps: { title: 'Failed' },
getRowVal: s => s.totals.failed,
getRowContent: s => s.totals.failed,
cellProps: { style },
child: true,
},
{
key: 'unfinished',
title: 'Unfinished',
headerProps: { title: 'Unfinished' },
getRowVal: s => s.totals.unfinished,
getRowContent: s => s.totals.unfinished,
cellProps: { style },
child: true,
},
]
const getDisseminationInfoAllowed = s => {
if (s.disseminationInfoAllowed === null) return 'unknown'
if (s.disseminationInfoAllowed) return 'yes'
return 'no'
}
const informationColumns = [
{
key: 'disseminationInfoAllowed',
title: 'Marketing Allowed',
getRowVal: s => getDisseminationInfoAllowed(s),
getRowContent: s => getDisseminationInfoAllowed(s),
headerProps: { title: 'Marketing Allowed' },
child: true,
childOf: 'Marketing Allowed',
cellProps: { style },
},
{
key: 'email-child',
title: 'Email',
getRowVal: s => (s.email ? s.email : ''),
getRowContent: s => (s.email ? s.email : ''),
headerProps: { title: 'Email' },
childOf: 'Email',
child: true,
},
{
key: 'secondary_email-child',
title: 'Secondary Email',
getRowVal: s => (s.secondaryEmail ? s.secondaryEmail : ''),
getRowContent: s => (s.secondaryEmail ? s.secondaryEmail : ''),
headerProps: { title: 'Secondary Email' },
childOf: 'Secondary Email',
child: true,
},
]
const findRowContent = (s, courseCode) => {
if (s.courseInfo[courseCode] === undefined) return null
if (s.courseInfo[courseCode].status.passed) return <Icon fitted name="check" color="green" />
if (s.courseInfo[courseCode].status.failed) return <Icon fitted name="times" color="red" />
if (s.courseInfo[courseCode].status.unfinished) return <Icon fitted name="minus" color="grey" />
return null
}
const findRowValue = (s, courseCode, hidden = false) => {
if (s.courseInfo[courseCode] === undefined) return ''
if (s.courseInfo[courseCode].status.passed && hidden)
return `Passed: ${moment(s.courseInfo[courseCode].status.passed).format('DD-MM-YYYY')}`
if (s.courseInfo[courseCode].status.failed && hidden)
return `Failed: ${moment(s.courseInfo[courseCode].status.failed).format('DD-MM-YYYY')}`
if (s.courseInfo[courseCode].status.unfinished && hidden)
return `Enrollment: ${moment(s.courseInfo[courseCode].status.unfinished).format('DD-MM-YYYY')}`
if (s.courseInfo[courseCode].status.passed) return 'Passed'
if (s.courseInfo[courseCode].status.failed) return 'Failed'
if (s.courseInfo[courseCode].status.unfinished) return 'Unfinished'
return ''
}
const findProp = (s, courseCode) => {
const propObj = {
title: '',
style,
}
if (s.courseInfo[courseCode] === undefined) return propObj
if (s.courseInfo[courseCode].status.passed)
return { ...propObj, title: `Passed: ${moment(s.courseInfo[courseCode].status.passed).format('DD-MM-YYYY')}` }
if (s.courseInfo[courseCode].status.failed)
return { ...propObj, title: `Failed: ${moment(s.courseInfo[courseCode].status.failed).format('DD-MM-YYYY')}` }
if (s.courseInfo[courseCode].status.passed)
return {
...propObj,
title: `Enrollment: ${moment(s.courseInfo[courseCode].status.unfinished).format('DD-MM-YYYY')}`,
}
return propObj
}
const columnsToShow = labelsToCourses.map(course => ({
key: `${course.label}-${course.name[language]}`,
title: (
<div style={{ maxWidth: '15em', whiteSpace: 'normal', overflow: 'hidden', width: 'max-content' }}>
<div>{course.label}</div>
</div>
),
textTitle: `${course.label}-${course.name[language]}`,
vertical: false,
forceToolsMode: 'dangling',
cellProps: s => findProp(s, course.label),
headerProps: { title: `${course.label}-${course.name[language]}` },
getRowVal: s => {
return findRowValue(s, course.label)
},
getRowContent: s => {
return findRowContent(s, course.label)
},
child: true,
childOf: course.label,
code: course.label,
}))
const columnsToHide = labelsToCourses.map(course => ({
key: `hidden-${course.label}-${course.name[language]}`,
export: true,
forceToolsMode: 'none',
textTitle: `Dates-${course.label}-${course.name[language]}`,
headerProps: {
title: `Dates-${course.label}-${course.name[language]}`,
style: {
display: 'none',
},
},
cellProps: {
style: {
display: 'none',
},
},
getRowVal: s => {
return findRowValue(s, course.label, true)
},
child: true,
code: `hidden ${course.label}`,
}))
const columns = []
columns.push(
{
key: 'general',
title: <b>Labels:</b>,
textTitle: null,
parent: true,
children: studentNbrColumn,
},
{
key: 'Courses',
title: <b>Courses:</b>,
textTitle: null,
parent: true,
children: columnsToShow,
},
{
key: 'statistics',
title: <b>Total of:</b>,
textTitle: null,
parent: true,
children: statisticColumns,
},
{
key: 'information',
title: <b>Information:</b>,
textTitle: null,
parent: true,
children: informationColumns,
}
)
if (labelsToCourses.length > 0) {
columns.push({
key: 'hiddencourses',
title: '',
mergeHeader: true,
textTitle: null,
parent: true,
children: columnsToHide,
})
}
return columns
}
const OpenUniPopulationResults = ({ fieldValues, language }) => {
const { courseList, startdate, enddate } = fieldValues
const [tableData, setData] = useState({ data: [], columns: [] })
const openUniStudentStats = useGetOpenUniCourseStudentsQuery({ courseList, startdate, enddate })
const isFetchingOrLoading = openUniStudentStats.isLoading || openUniStudentStats.isFetching
const isError = openUniStudentStats.isError || (openUniStudentStats.isSuccess && !openUniStudentStats.data)
useEffect(() => {
if (!isError && !isFetchingOrLoading) {
const unOrderedlabels = openUniStudentStats?.data.courses
const labelsToCourses = [...unOrderedlabels].sort((a, b) => a.label.localeCompare(b.label))
setImmediate(() => {
const data = getTableData(openUniStudentStats?.data.students)
const columns = getColumns(labelsToCourses, language)
setData({ data, columns })
})
}
}, [openUniStudentStats])
if (isError) return <h3>Something went wrong, please try refreshing the page.</h3>
if (isFetchingOrLoading) return <Loader active style={{ marginTop: '15em' }} />
return (
<div style={{ paddingBottom: '50px' }}>
<div style={{ maxHeight: '80vh', width: '100%' }} data-cy="open-uni-table-div">
{courseList.length > 0 && (
<SortableTable
title={`Open Uni Student Population (${Object.keys(openUniStudentStats?.data.students).length} students)`}
getRowKey={s => s.studentNumber}
tableProps={{
celled: true,
compact: 'very',
padded: false,
collapsing: true,
basic: true,
striped: true,
singleLine: true,
textAlign: 'center',
}}
columns={tableData.columns}
data={tableData.data}
/>
)}
</div>
</div>
)
}
export default OpenUniPopulationResults
@@ -0,0 +1,79 @@
import React, { useEffect, useState } from 'react'
import { withRouter, useLocation, useHistory } from 'react-router-dom'
import { Message, Icon, Loader } from 'semantic-ui-react'
import moment from 'moment'
import { useGetSavedSearchesQuery } from 'redux/openUniPopulations'
import OpenUniPopulationResults from './OpenUniPopulationResults'
import { useTitle } from '../../common/hooks'
import useLanguage from '../LanguagePicker/useLanguage'
import CustomOpenUniSearch from './CustomOpenUniSearch'
import TSA from '../../common/tsa'
const CustomOpenUniPopulation = () => {
useTitle('Custom open uni population')
const [fieldValues, setValues] = useState({})
const { language } = useLanguage()
const location = useLocation()
const history = useHistory()
const savedSearches = useGetSavedSearchesQuery()
const isFetchingOrLoading = savedSearches.isLoading || savedSearches.isFetching
const isError = savedSearches.isError || (savedSearches.isSuccess && !savedSearches.data)
useEffect(() => {
if (fieldValues && fieldValues.courseList?.length > 0) {
TSA.Influx.sendEvent({
group: 'Custom Open Uni Usage',
name: 'open uni populations',
label: 'openUniPopulations',
value: 1,
})
}
}, [fieldValues])
if (isError) return <h3>Something went wrong, please try refreshing the page.</h3>
if (isFetchingOrLoading) return <Loader active style={{ marginTop: '15em' }} />
return (
<div className="segmentContainer">
<Message style={{ maxWidth: '800px', fontSize: '16px' }}>
<Message.Header>Open uni student population</Message.Header>
<p>
Here you can create custom population using a list of courses. Clicking the blue custom population button will
open a modal where you can enter a list of courses. <br />
<Icon fitted name="check" color="green" />: Student passed the course. <br />
<Icon fitted name="times" color="red" />: Student failed the course. <br />
<Icon fitted name="minus" color="grey" />: Student has enrolled, but has not received any grade from the
course. <br />
<b>Empty cell</b>: Student has no enrollments for the course. <br />
</p>
</Message>
<CustomOpenUniSearch
setValues={setValues}
savedSearches={savedSearches.data}
location={location}
history={history}
/>
<div style={{ paddingTop: '25px', paddingBottom: '10px', fontSize: '20px' }}>
{fieldValues && fieldValues.courseList?.length > 0 && (
<p>
<span style={{ color: '#484848' }}>Beginning of the search for all fields:</span>
<span style={{ paddingLeft: '10px', fontWeight: '600' }}>
{moment(fieldValues.startdate).format('DD.MM.YYYY')}
</span>
<br />
<span style={{ color: '#484848' }}>End of the search for enrollments:</span>
<span style={{ paddingLeft: '30px', fontWeight: '600' }}>
{moment(fieldValues.enddate).format('DD.MM.YYYY')}
</span>
</p>
)}
</div>
<div style={{ paddingTop: '25px' }}>
{fieldValues && fieldValues.courseList?.length > 0 && (
<OpenUniPopulationResults fieldValues={fieldValues} language={language} />
)}
</div>
</div>
)
}
export default withRouter(CustomOpenUniPopulation)
@@ -87,7 +87,7 @@ const CustomPopulationSearch = ({
const onClicker = e => {
e.preventDefault()
const studentnumbers = input.match(/[0-9]+/g)
const studentnumbers = input.match(/[0-9]+/g).map(sNumber => (sNumber.length === 8 ? `0${sNumber}` : sNumber))
getCustomPopulationDispatch({ studentnumberlist: studentnumbers, onProgress })
getCustomPopulationCoursesByStudentnumbers({ studentnumberlist: studentnumbers })
selectCustomPopulationSearchDispatch(selectedSearchId || null)
@@ -128,6 +128,59 @@ const Overview = ({
transferShortTitles = ['Code', 'Started', 'Graduated']
}
const options = {
KH: 'Bachelors',
MH: 'Masters',
T: 'Doctors and Licentiates',
LIS: 'Doctors and Licentiates',
OTHER: 'Other',
}
const getChartPlotLinePlaces = programmeKeys => {
if (programmeKeys.length === 0) return []
let key = programmeKeys[0][1].slice(0, 2)
if (!['KH', 'MH', 'T', 'LIS'].includes(key)) {
key = 'OTHER'
}
const plotLinePlaces = [[0, options[key]]]
for (let i = 0; i < programmeKeys.length - 1; i++) {
if (
(programmeKeys[i][1].startsWith('KH') && programmeKeys[i + 1][1].startsWith('MH')) ||
(programmeKeys[i][1].startsWith('MH') && programmeKeys[i + 1][1].startsWith('KH')) ||
(programmeKeys[i][1].startsWith('MH') && programmeKeys[i + 1][1].startsWith('T')) ||
((programmeKeys[i][1].startsWith('T') || programmeKeys[i][1].startsWith('LIS')) &&
(programmeKeys[i + 1][1].startsWith('KH') || programmeKeys[i + 1][1].startsWith('MH'))) ||
((programmeKeys[i][1].startsWith('T') ||
programmeKeys[i][1].startsWith('LIS') ||
programmeKeys[i][1].startsWith('KH') ||
programmeKeys[i][1].startsWith('MH')) &&
programmeKeys[i + 1][1].startsWith('K-'))
) {
let key = programmeKeys[i + 1][1].slice(0, 2)
if (!['KH', 'MH'].includes(key)) {
const keyT = programmeKeys[i + 1][1].slice(0, 1)
const keyLis = programmeKeys[i + 1][1].slice(0, 3)
if (keyT === 'T') {
key = keyT
} else if (keyLis === 'LIS') {
key = keyLis
} else {
key = 'OTHER'
}
}
if (
!programmeKeys[i + 1][1].includes(faculty.code) &&
(programmeKeys[i + 1][1].startsWith('MH') || programmeKeys[i + 1][1].startsWith('KH'))
) {
plotLinePlaces.push([i + 1, `${options[key]} secondary`])
} else {
plotLinePlaces.push([i + 1, options[key]])
}
}
}
return plotLinePlaces
}
return (
<div className="faculty-overview">
<div className="toggle-container">
@@ -195,7 +248,22 @@ const Overview = ({
dataStats={basics?.data?.studentInfo.tableStats}
dataProgrammeStats={basics?.data?.studentInfo.programmeTableStats}
programmeNames={basics?.data?.programmeNames}
sortedKeys={sortProgrammeKeys(Object.keys(basics?.data?.studentInfo.programmeTableStats))}
sortedKeys={sortProgrammeKeys(
Object.keys(basics?.data?.studentInfo.programmeTableStats).map(obj => [
obj,
basics?.data?.programmeNames[obj].code,
]),
faculty.code
).map(listObj => listObj[0])}
plotLinePlaces={getChartPlotLinePlaces(
sortProgrammeKeys(
Object.keys(basics?.data?.studentInfo.programmeTableStats).map(obj => [
obj,
basics?.data?.programmeNames[obj].code,
]),
faculty.code
)
)}
titles={basics?.data?.studentInfo.titles}
sliceStart={1}
language={language}
@@ -231,7 +299,22 @@ const Overview = ({
dataStats={credits?.data?.tableStats}
dataProgrammeStats={credits?.data?.programmeTableStats}
programmeNames={credits?.data?.programmeNames}
sortedKeys={sortProgrammeKeys(Object.keys(credits?.data?.programmeTableStats))}
sortedKeys={sortProgrammeKeys(
Object.keys(credits?.data.programmeTableStats).map(obj => [
obj,
credits?.data?.programmeNames[obj].code,
]),
faculty.code
).map(listObj => listObj[0])}
plotLinePlaces={getChartPlotLinePlaces(
sortProgrammeKeys(
Object.keys(credits?.data?.programmeTableStats).map(obj => [
obj,
credits?.data?.programmeNames[obj].code,
]),
faculty.code
)
)}
titles={credits?.data?.titles}
extraHeight="EXTRA HEIGHT"
sliceStart={2}
@@ -266,7 +349,22 @@ const Overview = ({
dataStats={basics?.data?.graduationInfo.tableStats}
dataProgrammeStats={basics?.data?.graduationInfo.programmeTableStats}
programmeNames={basics?.data?.programmeNames}
sortedKeys={sortProgrammeKeys(Object.keys(basics?.data?.graduationInfo.programmeTableStats))}
sortedKeys={sortProgrammeKeys(
Object.keys(basics?.data?.graduationInfo.programmeTableStats).map(obj => [
obj,
basics?.data?.programmeNames[obj].code,
]),
faculty.code
).map(listObj => listObj[0])}
plotLinePlaces={getChartPlotLinePlaces(
sortProgrammeKeys(
Object.keys(basics?.data?.graduationInfo.programmeTableStats).map(obj => [
obj,
basics?.data?.programmeNames[obj].code,
]),
faculty.code
)
)}
titles={basics?.data?.graduationInfo.titles}
sliceStart={2}
language={language}
@@ -299,7 +397,22 @@ const Overview = ({
dataStats={thesisWriters?.data.tableStats}
dataProgrammeStats={thesisWriters?.data.programmeTableStats}
programmeNames={thesisWriters?.data.programmeNames}
sortedKeys={sortProgrammeKeys(Object.keys(thesisWriters?.data.programmeTableStats))}
sortedKeys={sortProgrammeKeys(
Object.keys(thesisWriters?.data.programmeTableStats).map(obj => [
obj,
thesisWriters?.data?.programmeNames[obj].code,
]),
faculty.code
).map(listObj => listObj[0])}
plotLinePlaces={getChartPlotLinePlaces(
sortProgrammeKeys(
Object.keys(thesisWriters?.data?.programmeTableStats).map(obj => [
obj,
thesisWriters?.data?.programmeNames[obj].code,
]),
faculty.code
)
)}
titles={thesisWriters?.data?.titles}
sliceStart={2}
extraHeight="EXTRA HEIGHT"
@@ -12,16 +12,17 @@ const FacultyProgressTable = ({
programmeNames,
language,
cypress,
progressTitles,
needsExtra = 'NO EXTRA',
}) => {
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>
@@ -35,43 +36,47 @@ const FacultyProgressTable = ({
</Table.Header>
<Table.Body>
{data.map((yearArray, yearIndex) => (
<React.Fragment key={`random-fragment-key-${Math.random()}`}>
{yearIndex < lenOfYearArray ? (
<BasicRow
icon={visible[yearIndex] ? 'angle down' : 'angle 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)}
{data?.map(
(yearArray, yearIndex) =>
yearArray?.slice(1).some(value => value !== 0) && (
<React.Fragment key={`random-fragment-key-${Math.random()}`}>
{yearIndex < lenOfYearArray ? (
<BasicRow
icon={visible[yearIndex] ? 'angle down' : 'angle right'}
yearArray={yearArray}
cypress={visible[yearIndex] ? `Hide-${cypress}` : `Show-${cypress}`}
yearIndex={yearIndex}
toggleVisibility={() => toggleVisibility(yearIndex)}
/>
</Table.Cell>
</Table.Row>
) : (
<></>
)}
</React.Fragment>
))}
) : (
<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={progressTitles ? progressTitles[yearIndex] : titles?.slice(2)}
needsExtra={needsExtra === 'EXTRA HEIGHT'}
/>
</Table.Cell>
</Table.Row>
) : (
<></>
)}
</React.Fragment>
)
)}
</Table.Body>
</Table>
</div>
@@ -1,12 +1,38 @@
/* eslint-disable react/no-array-index-key */
import PopulationLink from 'components/StudyProgramme/StudytrackOverview/PopulationLink'
import React, { useState } from 'react'
import { Table, Button, Icon, Label } from 'semantic-ui-react'
import { Table, Button, Icon, Label, Popup } from 'semantic-ui-react'
import Toggle from '../../StudyProgramme/Toggle'
const getStyle = idx => {
if ([4, 12].includes(idx)) return { backgroundColor: '#f9f9f9', borderLeftWidth: 'thick' }
if ([1, 4, 5, 8, 9, 12, 13, 16, 17, 20, 21].includes(idx)) return { backgroundColor: '#f9f9f9' }
if (idx === 18) return { borderLeftWidth: 'thick' }
return {}
}
const backgroundColors = { KH: '#ffffff', MH: '#f2f6f7', T: '#e7edee' }
const backgroundColorsDarks = { KH: '#f9f9f9', MH: '#eeeeee', T: '#dddddd' }
const getTitlePopup = idx => {
if (idx === 0) return <p>All</p>
if ([1, 2].includes(idx)) return <p>Started studying</p>
if ([3, 4].includes(idx)) return <p>Currently enrolled</p>
if ([5, 6].includes(idx)) return <p>Absent</p>
if ([7, 8].includes(idx)) return <p>Inactive</p>
if ([9, 10].includes(idx)) return <p>Graduated</p>
if ([11, 12].includes(idx)) return <p>Men</p>
if ([13, 14].includes(idx)) return <p>Women</p>
if ([15, 16].includes(idx)) return <p>Other/Unknown</p>
if ([17, 18].includes(idx)) return <p>Finnish</p>
return <p>Other</p>
}
const FacultyStudentDataTable = ({
tableStats,
programmeStats,
programmeNames,
tableLinePlaces,
titles,
years,
sortedKeys,
@@ -16,35 +42,90 @@ const FacultyStudentDataTable = ({
}) => {
const [yearsVisible, setVisible] = useState(new Array(years.length).fill(false))
const [showPercentages, setShowPercentages] = useState(false)
const calendarYears = years.reduce((all, year) => {
if (year === 'Total') return all
return all.concat(Number(year.slice(0, 4)))
}, [])
const toggleVisibility = yearIndex => {
const arrayToModify = [...yearsVisible]
arrayToModify[yearIndex] = !yearsVisible[yearIndex]
setVisible(arrayToModify)
}
const getRowStyle = (idx, dark = false) => {
if (tableLinePlaces.length >= 3 && tableLinePlaces[2][0] <= idx)
return dark
? { backgroundColor: backgroundColorsDarks[tableLinePlaces[2][1]] }
: { backgroundColor: backgroundColors[tableLinePlaces[2][1]] }
if (tableLinePlaces.length >= 3 && tableLinePlaces[2][0] > idx && tableLinePlaces[1][0] <= idx)
return dark
? { backgroundColor: backgroundColorsDarks[tableLinePlaces[1][1]] }
: { backgroundColor: backgroundColors[tableLinePlaces[1][1]] }
if (tableLinePlaces.length >= 2 && tableLinePlaces[1][0] > idx && tableLinePlaces[0][0] <= idx)
return dark
? { backgroundColor: backgroundColorsDarks[tableLinePlaces[0][1]] }
: { backgroundColor: backgroundColors[tableLinePlaces[0][1]] }
return dark
? { backgroundColor: backgroundColorsDarks[tableLinePlaces[0][1]] }
: { backgroundColor: backgroundColors[tableLinePlaces[0][1]] }
}
const calendarYears = years.reduce((all, year) => {
if (year === 'Total') return all
return all.concat(Number(year.slice(0, 4)))
}, [])
const getRows = (idx, programme, year) => {
return programmeStats[programme][year].map((value, valIdx) => {
if (!showPercentages && typeof value === 'string' && (value.includes('%') || value.includes('NA'))) return null
return (
<Popup
content={getTitlePopup(valIdx)}
trigger={
<Table.Cell
key={`${year}-${programme}-color-${valIdx}`}
style={
getStyle(valIdx + 1)?.backgroundColor
? { borderLeftWidth: getStyle(valIdx + 1).borderLeftWidth, ...getRowStyle(idx, true) }
: { ...getStyle(valIdx + 1), ...getRowStyle(idx) }
}
>
{value}
</Table.Cell>
}
/>
)
})
}
return (
<div className="datatable">
<Toggle
cypress="HidePercentagesToggle"
firstLabel="Hide percentages"
secondLabel="Show percentages"
value={showPercentages}
setValue={setShowPercentages}
/>
<Table data-cy="Table-StudytrackOverview" celled>
<Table data-cy={cypress} celled structured>
<Table.Header>
<Table.Row>
<Table.Row key="FirstHeader">
<Table.HeaderCell colSpan={!showPercentages ? 3 : 4} />
<Table.HeaderCell colSpan={!showPercentages ? 4 : 8} style={{ borderLeftWidth: 'thick' }}>
Status
</Table.HeaderCell>
<Table.HeaderCell colSpan={!showPercentages ? 3 : 6} style={{ borderLeftWidth: 'thick' }}>
Gender
</Table.HeaderCell>
<Table.HeaderCell colSpan={!showPercentages ? 2 : 4} style={{ borderLeftWidth: 'thick' }}>
Country
</Table.HeaderCell>
</Table.Row>
<Table.Row key="secondHeader">
{titles.map((title, index) => (
<Table.HeaderCell
key={title}
colSpan={index === 0 || index === 1 || !showPercentages ? 1 : 2}
style={
[3, 7, 10].includes(index) ? { fontWeight: 'bold', borderLeftWidth: 'thick' } : { fontWeight: 'bold' }
}
textAlign="left"
style={{ fontWeight: 'bold' }}
>
{title}
</Table.HeaderCell>
@@ -56,12 +137,13 @@ const FacultyStudentDataTable = ({
{years.map((year, yearIndex) => {
return (
<>
<Table.Row key={`${year}-faculty-row-${Math.random()}`} className="header-row">
<Table.Row key={`${year}-faculty-row}`} className="header-row">
{tableStats[year].map((value, valueIdx) => {
if (valueIdx === 0)
return (
<Table.Cell key={`${year}-faculty-cell-${Math.random()}`}>
<Table.Cell key={`${year}-faculty-cell}`}>
<Button
key={`${year}-studentsTableButton}`}
as="div"
onClick={() => toggleVisibility(yearIndex)}
labelPosition="right"
@@ -77,54 +159,52 @@ const FacultyStudentDataTable = ({
</Button>
</Table.Cell>
)
if (!showPercentages && typeof value === 'string' && value.includes('%')) return null
if ([1, 4, 5, 8, 9, 12, 13, 16, 17].includes(valueIdx))
return (
<Table.Cell style={{ backgroundColor: '#f9f9f9' }} key={`${year}-cell-color-${Math.random()}`}>
{value}
</Table.Cell>
)
return <Table.Cell key={`${year}$-${value}-cell-${Math.random()}`}>{value}</Table.Cell>
if (!showPercentages && typeof value === 'string' && (value.includes('%') || value.includes('NA')))
return null
return (
<Table.Cell
style={getStyle(valueIdx)}
key={`${year}$-cell-colorless-${valueIdx + Math.random()}`}
>
{value}
</Table.Cell>
)
})}
</Table.Row>
{yearsVisible[yearIndex] &&
sortedKeys.map(programme => {
return (
<Table.Row className="regular-row" key={`${year}-regular-row-${Math.random()}`}>
<Table.Cell textAlign="left" style={{ paddingLeft: '50px' }} key={`${year}-${programme}`}>
<b>
{programme}
<br />
{programmeNames[programme][language]
? programmeNames[programme][language]
: programmeNames[programme].fi}
</b>
{(requiredRights.rights?.includes(programme) ||
sortedKeys.map((programme, idx) => {
return programmeStats[programme][year].length === 0 ? null : (
<Table.Row className="regular-row" key={`${year}-regular-row-${programme}`}>
<Table.Cell
textAlign="left"
key={`${year}-${programme}`}
style={{ paddingLeft: '50px', ...getRowStyle(idx) }}
>
<Popup
content={
programmeNames[programme][language] ? (
<p>
{programmeNames[programme].code} - {programmeNames[programme][language]}
</p>
) : (
<p>
{programmeNames[programme].code} - {programmeNames[programme].fi}
</p>
)
}
trigger={<b>{programme}</b>}
/>
{(requiredRights.rights?.includes(programmeNames[programme].code) ||
requiredRights.isAdmin ||
requiredRights.IAMrights?.includes(programme)) && (
<PopulationLink studyprogramme={programme} year={year} years={calendarYears} />
requiredRights.IAMrights?.includes(programmeNames[programme].code)) && (
<PopulationLink
studyprogramme={programmeNames[programme].code}
year={year}
years={calendarYears}
/>
)}
</Table.Cell>
{programmeStats[programme][year].map((value, valIdx) => {
if (
!showPercentages &&
typeof value === 'string' &&
(value.includes('%') || value.includes('NA'))
)
return null
if ([0, 3, 4, 7, 8, 11, 12, 15, 16].includes(valIdx))
return (
<Table.Cell
key={`${year}-${programme}-color-${Math.random()}`}
style={{ backgroundColor: '#f9f9f9' }}
>
{value}
</Table.Cell>
)
return (
<Table.Cell key={`${year}-${programme}-colorless-${Math.random()}`}>{value}</Table.Cell>
)
})}
{getRows(idx, programme, year)}
</Table.Row>
)
})}
@@ -4,11 +4,23 @@ import Highcharts from 'highcharts'
import ReactHighcharts from 'react-highcharts'
const getColors = len => {
if (len < 6) return ['#fba678', '#dbda7d', '#9ec27c', '#60a866', '#008c59']
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 }) => {
// Point width is 24 px different multipliers adjusts the height.
const getFlexHeight = (len, needsExtra) => {
if (len > 7 && needsExtra) return `${len * 24 * 1.5}px`
if (len > 5 && !needsExtra) return `${len * 24 * 1.5}px`
if (needsExtra && len <= 2) return `${len * 24 * 6}px`
if (needsExtra && len <= 4) return `${len * 24 * 3}px`
if (needsExtra) return `${len * 24 * 2}px`
if (len < 2) return `${len * 24 * 5}px`
if (len < 3) return `${len * 24 * 4}px`
if (len <= 4) return `${len * 24 * 3}px`
return `${len * 24}px`
}
const ProgrammeProgressChart = ({ data, labels, longLabels, names, language, needsExtra }) => {
if (!data || data.length === 0)
return (
<>
@@ -29,6 +41,7 @@ const ProgrammeProgressChart = ({ data, labels, longLabels, names, language }) =
chart: {
type: 'bar',
marginTop: 60,
height: getFlexHeight(labels.length, needsExtra),
},
credits: {
text: 'oodikone | TOSKA',
@@ -70,7 +83,7 @@ const ProgrammeProgressChart = ({ data, labels, longLabels, names, language }) =
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 />`
} </b><br /><p>${this.x} - ${longLabels[this.x]?.code}</p><br />`
this.points.forEach(point => {
tooltipString += `<span style="color:${point.color}">●</span> <b>${point.series.name}: ${point.y}</b>
</span><br />`
@@ -81,15 +94,19 @@ const ProgrammeProgressChart = ({ data, labels, longLabels, names, language }) =
plotOptions: {
series: {
stacking: 'percent',
pointWidth: 24,
pointWidth: 20,
dataLabels: {
enabled: true,
format: '{point.percentage:.1f}%',
fontSize: '24px',
color: '#000000',
align: 'left',
style: {
textOutline: 'none',
},
filter: {
property: 'y',
property: 'percentage',
operator: '>',
value: 2,
value: 3,
},
},
},
@@ -1,5 +1,5 @@
import React from 'react'
import { Divider, Loader } from 'semantic-ui-react'
import { Divider, Loader, Message } from 'semantic-ui-react'
import { useGetFacultyProgressStatsQuery, useGetFacultyStudentStatsQuery } from 'redux/facultyStats'
import FacultyProgressTable from './FacultyProgressTable'
import FacultyBarChart from './FacultyBarChart'
@@ -10,17 +10,19 @@ import '../faculty.css'
import FacultyStudentDataTable from './FacultyStudentDataTable'
import sortProgrammeKeys from '../facultyHelpers'
const getDivider = (title, toolTipText, content) => (
const getDivider = (title, toolTipText, content, cypress) => (
<>
<div className="divider">
<Divider data-cy={`Section-${toolTipText}`} horizontal>
{title}
</Divider>
</div>
{content === 'no-infobox' ? null : <InfoBox content={content} />}
{content === 'no-infobox' ? null : <InfoBox content={content} cypress={cypress} />}
</>
)
const getKey = (programmeKeys, idx) => {
return programmeKeys[idx][1].startsWith('T') ? 'T' : programmeKeys[idx][1].slice(0, 2)
}
const FacultyProgrammeOverview = ({
faculty,
language,
@@ -57,6 +59,22 @@ const FacultyProgrammeOverview = ({
(studentStats.isSuccess && !studentStats.data)
if (isError) return <h3>Something went wrong, please try refreshing the page.</h3>
const getTableLinePlaces = programmeKeys => {
if (programmeKeys.length === 0) return []
const key = getKey(programmeKeys, 0)
const plotLinePlaces = [[0, key]]
for (let i = 0; i < programmeKeys.length - 1; i++) {
if (
(programmeKeys[i][1].startsWith('KH') && programmeKeys[i + 1][1].startsWith('MH')) ||
(programmeKeys[i][1].startsWith('MH') && programmeKeys[i + 1][1].startsWith('T'))
) {
const key = getKey(programmeKeys, i + 1)
plotLinePlaces.push([i + 1, key])
}
}
return plotLinePlaces
}
return (
<div className="faculty-overview">
<div className="toggle-container">
@@ -86,7 +104,8 @@ const FacultyProgrammeOverview = ({
{getDivider(
'Students of the faculty By Starting year',
'StudentsOfTheFacultyByStartingYear',
toolTips.StudentsStatsOfTheFaculty
toolTips.StudentsStatsOfTheFaculty,
'InfoFacultyStudentTable'
)}
<div>
<FacultyStudentDataTable
@@ -95,7 +114,22 @@ const FacultyProgrammeOverview = ({
programmeNames={studentStats?.data.programmeNames}
titles={studentStats?.data.titles}
years={studentStats?.data.years}
sortedKeys={sortProgrammeKeys(Object.keys(studentStats?.data.programmeStats))}
sortedKeys={sortProgrammeKeys(
Object.keys(studentStats?.data.programmeStats).map(obj => [
obj,
studentStats?.data?.programmeNames[obj]?.code,
]),
faculty.code
).map(listObj => listObj[0])}
tableLinePlaces={getTableLinePlaces(
sortProgrammeKeys(
Object.keys(studentStats?.data?.programmeStats).map(obj => [
obj,
studentStats?.data?.programmeNames[obj].code,
]),
faculty.code
)
)}
language={language}
cypress="FacultyStudentStatsTable"
requiredRights={requiredRights}
@@ -109,7 +143,8 @@ const FacultyProgrammeOverview = ({
{getDivider(
'Progress of students of the faculty ',
'BachelorStudentsOfTheFacultyByStartingYear',
toolTips.StudentProgress
toolTips.StudentProgress,
'InfoFacultyProgress'
)}
{getDivider('Bachelor', 'BachelorStudentsOfTheFacultyByStartingYear', 'no-infobox')}
<div className="section-container">
@@ -126,17 +161,29 @@ const FacultyProgrammeOverview = ({
<div className="table-container">
<FacultyProgressTable
data={progressStats?.data.bachelorsTableStats}
programmeStats={progressStats?.data.bachelorsProgrammeStats}
programmeStats={progressStats?.data.bachelorsProgStats}
titles={progressStats?.data.bachelorTitles}
sortedKeys={sortProgrammeKeys(Object.keys(progressStats?.data.bachelorsProgrammeStats))}
sortedKeys={sortProgrammeKeys(
Object.keys(progressStats?.data.bachelorsProgStats).map(obj => [
obj,
progressStats?.data?.programmeNames[obj].code,
]),
faculty.code
).map(listObj => listObj[0])}
progressYearsVisible={Array(progressStats?.data.years.slice(1).length).fill(false)}
programmeNames={progressStats?.data.programmeNames}
language={language}
cypress="FacultyBachelorsProgressTable"
progressTitles={progressStats?.data.yearlyBachelorTitles}
/>
</div>
</div>
{getDivider('Bachelor + Master', 'ProgressOfBachelorMaster', 'no-infobox')}
<Message data-cy="FacultyProgrammesShownInfo">
The starting year is the studyright start in the bachelor programme. The credits are comouted by the
start date of the bachelor programme and at the moment, they do not include any transferred credits.
Thus, in these statistics some students have less credits than in reality.
</Message>
<div className="section-container">
<div className="graph-container">
<FacultyBarChart
@@ -151,42 +198,62 @@ const FacultyProgrammeOverview = ({
<div className="table-container">
<FacultyProgressTable
data={progressStats?.data.bcMsTableStats}
programmeStats={progressStats?.data.bcMsProgrammeStats}
programmeStats={progressStats?.data.bcMsProgStats}
titles={progressStats?.data.bcMsTitles}
sortedKeys={sortProgrammeKeys(Object.keys(progressStats?.data.bcMsProgrammeStats))}
sortedKeys={sortProgrammeKeys(
Object.keys(progressStats?.data.bcMsProgStats).map(obj => [
obj,
progressStats?.data?.programmeNames[obj].code,
]),
faculty.code
).map(listObj => listObj[0])}
progressYearsVisible={Array(progressStats?.data.years.slice(1).length).fill(false)}
programmeNames={progressStats?.data.programmeNames}
language={language}
cypress="FacultyBachelorMasterProgressTable"
progressTitles={progressStats?.data.yearlyBcMsTitles}
needsExtra={faculty.code === 'H60' ? 'NO EXTRA' : 'EXTRA HEIGHT'}
/>
</div>
</div>
{getDivider('Master', 'MasterStudentsOfTheFacultyByStartingYear', 'no-infobox')}
<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('Doctor and licentiates', 'DoctoralStudentsOfTheFacultyByStartingYear', 'no-infobox')}
{!(faculty.code === 'H90') && (
<>
{getDivider('Master', 'MasterStudentsOfTheFacultyByStartingYear', 'no-infobox')}
<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.mastersProgStats}
titles={progressStats?.data.mastersTitles}
sortedKeys={sortProgrammeKeys(
Object.keys(progressStats?.data.mastersProgStats).map(obj => [
obj,
progressStats?.data?.programmeNames[obj].code,
]),
faculty.code
).map(listObj => listObj[0])}
progressYearsVisible={Array(progressStats?.data.years.slice(1).length).fill(false)}
programmeNames={progressStats?.data.programmeNames}
language={language}
cypress="FacultyMastersProgressTable"
progressTitles={progressStats?.data.yearlyMasterTitles}
needsExtra={faculty.code === 'H60' ? 'NO EXTRA' : 'EXTRA HEIGHT'}
/>
</div>
</div>
</>
)}
{getDivider('Doctor', 'DoctoralStudentsOfTheFacultyByStartingYear', 'no-infobox')}
<div className="section-container">
<div className="graph-container">
<FacultyBarChart
@@ -201,13 +268,19 @@ const FacultyProgrammeOverview = ({
<div className="table-container">
<FacultyProgressTable
data={progressStats?.data.doctoralTableStats}
programmeStats={progressStats?.data.doctoralProgrammeStats}
programmeStats={progressStats?.data.doctoralProgStats}
titles={progressStats?.data.doctoralTitles}
sortedKeys={sortProgrammeKeys(Object.keys(progressStats?.data.doctoralProgrammeStats))}
sortedKeys={sortProgrammeKeys(
Object.keys(progressStats?.data.doctoralProgStats).map(obj => [
obj,
progressStats?.data?.programmeNames[obj].code,
]),
faculty.code
).map(listObj => listObj[0])}
progressYearsVisible={Array(progressStats?.data.years.slice(1).length).fill(false)}
programmeNames={progressStats?.data.programmeNames}
language={language}
cypress="FacultyBachelorsProgressTable"
cypress="FacultyDoctoralProgressTable"
/>
</div>
</div>
@@ -1,23 +1,24 @@
/* 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 CollapsedStackedBar = ({
data,
labels,
longLabels,
names,
plotLinePlaces,
language,
differenceData,
extraHeight,
}) => {
const transpose = matrix => {
return matrix.reduce((prev, next) => next.map((_item, i) => (prev[i] || []).concat(next[i])), [])
}
if (names[0] === 'Started studying') names[0] += ' (new in faculty)'
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()
@@ -40,12 +41,37 @@ const CollapsedStackedBar = ({ data, labels, longLabels, names, language, differ
return change
}
const chartPlotLinePlaces = plotLinePlaces
? plotLinePlaces.map(val => ({
color: '#90A959',
width: 1,
value: val[0] - 0.5,
dashStyle: 'solid',
label: {
text: val[1],
style: {
color: 'black',
fontWeight: 'bold',
fontSize: 14,
position: 'absolute',
},
align: 'right',
x: 0,
y: 5,
},
}))
: []
// 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 getFlexHeight = (len, needsExtra) => {
if (len > 7 && needsExtra) return `${len * 24 * 1.5}px`
if (len > 5 && !needsExtra) return `${len * 24 * 1.5}px`
if (needsExtra && len <= 2) return `${len * 24 * 6}px`
if (needsExtra && len <= 4) return `${len * 24 * 3}px`
if (needsExtra) return `${len * 24 * 2}px`
if (len <= 2) return `${len * 24 * 5}px`
if (len <= 4) return `${len * 24 * 3}px`
return `${len * 24}px`
}
const getColor = change => {
@@ -59,7 +85,7 @@ const CollapsedStackedBar = ({ data, labels, longLabels, names, language, differ
chart: {
type: 'bar',
marginTop: 60,
height: getFlexHeight(labels.length, needsExtra, manyProgrammes, labels),
height: getFlexHeight(labels.length, needsExtra),
},
credits: {
text: 'oodikone | TOSKA',
@@ -76,6 +102,7 @@ const CollapsedStackedBar = ({ data, labels, longLabels, names, language, differ
title: {
text: '',
},
plotLines: chartPlotLinePlaces,
fontSize: '24px',
},
yAxis: {
@@ -86,7 +113,7 @@ const CollapsedStackedBar = ({ data, labels, longLabels, names, language, differ
stackLabels: {
enabled: true,
style: {
color: (Highcharts.defaultOptions.title.style && Highcharts.defaultOptions.title.style.color) || 'gray',
color: 'gray',
},
fontSize: '24px',
},
@@ -99,7 +126,7 @@ const CollapsedStackedBar = ({ data, labels, longLabels, names, language, differ
verticalAlign: 'top',
y: -10,
floating: true,
backgroundColor: Highcharts.defaultOptions.legend.backgroundColor || 'white',
backgroundColor: 'white',
borderColor: '#CCC',
borderWidth: 1,
shadow: false,
@@ -111,7 +138,7 @@ const CollapsedStackedBar = ({ data, labels, longLabels, names, language, differ
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 />`
}</b><br /><p>${this.x} - ${longLabels[this.x]?.code}</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>
@@ -126,9 +153,10 @@ const CollapsedStackedBar = ({ data, labels, longLabels, names, language, differ
plotOptions: {
series: {
stacking: 'normal',
pointWidth: 24,
pointWidth: 20,
dataLabels: {
enabled: true,
align: 'left',
formatter() {
if (Number.isInteger(this.y)) return `${this.y}`
return `${this.y.toFixed(1)}`
@@ -9,6 +9,7 @@ const InteractiveDataTable = ({
dataProgrammeStats,
programmeNames,
sortedKeys,
plotLinePlaces,
titles,
language,
sliceStart,
@@ -28,14 +29,32 @@ const InteractiveDataTable = ({
const keys = {}
if (!(columnIndex === 0)) {
const numbersOfYears = dataStats.length
const data = Object.entries(dataProgrammeStats)
const data = sortedKeys?.map(programme => [programme, dataProgrammeStats[programme]])
const groupIndices = plotLinePlaces.length > 0 ? plotLinePlaces.map(val => val[0]) : []
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])
let yearlySortedKeys = []
if (groupIndices.length > 0) {
for (let i = 0; i < groupIndices.length; i++) {
const ending = i === groupIndices.length - 1 ? data.length : groupIndices[i + 1]
yearlySortedKeys = [
...yearlySortedKeys,
...data
.slice(groupIndices[i], ending)
.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]),
]
}
} else {
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
}
}
@@ -132,6 +151,7 @@ const InteractiveDataTable = ({
)
}
labels={sortbyColumn === 0 ? sortedKeys : keyOrder[yearIndex]}
plotLinePlaces={plotLinePlaces}
differenceData={Object.keys(differenceToPrevYears)?.reduce(
(yearlyObject, programme) => ({
...yearlyObject,
@@ -0,0 +1,122 @@
/* eslint-disable react/no-this-in-sfc */
import React from 'react'
import ReactHighcharts from 'react-highcharts'
const BarChart = ({ data, handleClick, facultyGraph = true, year = null, label, programmeNames, language = null }) => {
const statData = [
{ name: 'On time', color: '#90A959', data: [] },
{ name: 'Max. year overtime', color: '#FEE191', data: [] },
{ name: 'Overtime', color: '#FB6962', data: [] },
]
let categories = []
const codeMap = {}
// eslint-disable-next-line no-restricted-syntax
for (const item of data) {
statData[0].data = [...statData[0].data, item.statistics.onTime]
statData[1].data = [...statData[1].data, item.statistics.yearOver]
statData[2].data = [...statData[2].data, item.statistics.wayOver]
categories = [...categories, item.name]
if (!facultyGraph) {
codeMap[item.name || item.code] = item.code
}
}
const getHeight = () => {
if (data.length === 1) {
return 250
}
const t = data.length > 8 ? 80 : 110
return data.length * t + 100
}
const getTooltipText = (programmeId, seriesName, amount) => {
if (!facultyGraph) {
const code = codeMap[programmeId]
return `<b>${
programmeNames[code]?.[language] ? programmeNames[code]?.[language] : programmeNames[code]?.fi
}</b><br />${code}<br /><b>${seriesName}</b>: ${amount}`
}
return `<b>${seriesName}</b>: ${amount}`
}
const config = {
chart: {
type: 'bar',
height: getHeight(),
margin: [70, 0],
},
title: { text: ' ' },
series: statData,
tooltip: {
backgroundColor: 'white',
fontSize: '25px',
// outside: true,
// eslint-disable-next-line
formatter: function() {
return getTooltipText(this.x, this.series.name, this.y)
},
},
xAxis: {
type: 'category',
categories,
title: {
text: facultyGraph ? label : 'Programme',
align: 'high',
offset: 0,
rotation: 0,
y: -10,
},
},
yAxis: {
min: 0,
// max: maxValue,
title: { text: 'Number of students' },
labels: {
overflow: 'justify',
},
allowDecimals: false,
showFirstLabel: false,
},
credits: {
text: 'oodikone | TOSKA',
},
plotOptions: {
bar: {
dataLabels: {
enabled: true,
align: 'right',
color: '#424949',
style: {
textOutline: 'none',
},
formatter() {
// eslint-disable-next-line react/no-this-in-sfc
return this.y !== 0 ? this.y : ''
},
},
},
series: {
pointWidth: data.length > 8 ? 16 : 20,
groupPadding: 0.15,
point: {
events: {
click(e) {
handleClick(e, facultyGraph, categories[this.x])
},
},
},
},
},
}
if (!facultyGraph) config.title.text = `Year ${year} by ${label.toLowerCase()}`
return (
<div className={`${facultyGraph ? 'faculty' : 'programmes'}-breakdown-graph`}>
<ReactHighcharts config={config} />
</div>
)
}
export default BarChart
@@ -1,19 +1,119 @@
import React, { useState } from 'react'
import { Divider, Message } from 'semantic-ui-react'
import BarChart from './BarChart'
import MedianBarChart from './MedianBarChart'
import BreakdownBarChart from './BreakdownBarChart'
import useLanguage from '../../LanguagePicker/useLanguage'
import '../faculty.css'
const GraduationTimes = ({
title,
years,
const MedianDisplay = ({
handleClick,
data,
level,
goal,
label,
levelProgrammeData,
programmeNames,
showMeanTime,
classSizes,
groupBy,
goalExceptions,
year,
language,
programmeData,
}) => {
return (
<div>
{level === 'bcMsCombo' && groupBy === 'byStartYear' && (
<div className="graduations-message">
<Message compact>
Programme class sizes for recent years are not reliable as students might still lack relevant master studies
data in Sisu
</Message>
</div>
)}
{goalExceptions.needed && ['master', 'bcMsCombo'].includes(level) && (
<div className="graduations-message">
<Message compact>
<b>Different goal times</b> have been taken into account in all numbers and programme level bar coloring,
but the faculty level bar color is based on the typical goal time of {goal} months
</Message>
</div>
)}
<div className="graduations-chart-container">
<MedianBarChart
data={data}
goal={goal}
handleClick={handleClick}
label={label}
programmeNames={programmeNames}
classSizes={classSizes?.[level]}
/>
{!programmeData ? (
<div className="graduations-message">
<Message compact>Click a bar to view that year's programme level breakdown</Message>
</div>
) : (
<MedianBarChart
data={levelProgrammeData[year]?.data}
goal={goal}
facultyGraph={false}
handleClick={handleClick}
year={year}
label={label}
programmeNames={programmeNames}
language={language}
classSizes={classSizes?.programmes}
level={level}
goalExceptions={goalExceptions}
/>
)}
</div>
</div>
)
}
const BreakdownDislay = ({
handleClick,
data,
label,
levelProgrammeData,
programmeNames,
year,
language,
programmeData,
}) => {
return (
<div>
<div className="graduations-chart-container">
<BreakdownBarChart data={data} handleClick={handleClick} label={label} />
{!programmeData ? (
<div className="graduations-message">
<Message compact>Click a bar to view that year's programme level breakdown</Message>
</div>
) : (
<BreakdownBarChart
data={levelProgrammeData[year]?.data}
handleClick={handleClick}
facultyGraph={false}
year={year}
label={label}
programmeNames={programmeNames}
language={language}
/>
)}
</div>
</div>
)
}
const GraduationTimes = ({
title,
data,
level,
goal,
label,
levelProgrammeData,
programmeNames,
showMedian,
classSizes,
groupBy,
goalExceptions,
@@ -23,9 +123,9 @@ const GraduationTimes = ({
const { language } = useLanguage()
if (!data.some(a => a.amount > 0)) return null
const handleClick = (e, isFacultyGraph) => {
const handleClick = (e, isFacultyGraph, seriesCategory = null) => {
if (isFacultyGraph) {
setYear(e.point.category)
setYear(seriesCategory || e.point.name)
setProgrammeData(true)
} else {
setProgrammeData(false)
@@ -34,62 +134,38 @@ const GraduationTimes = ({
}
return (
<>
<Divider data-cy={`Section-${level}`} horizontal>
{title}
</Divider>
<div>
{level === 'bcMsCombo' && groupBy === 'byStartYear' && (
<div className="graduations-message">
<Message compact>
Programme class sizes for recent years are not reliable as students might still lack relevant master
studies data in Sisu
</Message>
</div>
)}
{goalExceptions.needed && ['master', 'bcMsCombo'].includes(level) && (
<div className="graduations-message">
<Message compact>
<b>Different goal times</b> have been taken into account in all numbers and programme level bar coloring,
but the faculty level bar color is based on the typical goal time of {goal} months
</Message>
</div>
)}
<div className="graduations-chart-container">
<BarChart
categories={years}
data={data}
goal={goal}
handleClick={handleClick}
label={label}
programmeNames={programmeNames}
showMeanTime={showMeanTime}
classSizes={classSizes?.[level]}
/>
{!programmeData ? (
<div className="graduations-message">
<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}
label={label}
programmeNames={programmeNames}
language={language}
showMeanTime={showMeanTime}
classSizes={classSizes?.programmes}
level={level}
goalExceptions={goalExceptions}
/>
)}
</div>
</div>
</>
<div className={`graduation-times-${level}`} data-cy={`Section-${level}`}>
<Divider horizontal>{title}</Divider>
{!showMedian ? (
<BreakdownDislay
handleClick={handleClick}
data={data}
level={level}
label={label}
levelProgrammeData={levelProgrammeData}
programmeNames={programmeNames}
year={year}
language={language}
programmeData={programmeData}
/>
) : (
<MedianDisplay
handleClick={handleClick}
data={data}
level={level}
goal={goal}
label={label}
levelProgrammeData={levelProgrammeData}
programmeNames={programmeNames}
classSizes={classSizes}
groupBy={groupBy}
goalExceptions={goalExceptions}
year={year}
language={language}
programmeData={programmeData}
/>
)}
</div>
)
}
@@ -4,7 +4,6 @@ import ReactHighcharts from 'react-highcharts'
const BarChart = ({
data,
categories,
goal,
handleClick,
facultyGraph = true,
@@ -12,7 +11,6 @@ const BarChart = ({
label,
programmeNames,
language = null,
showMeanTime,
classSizes,
level,
goalExceptions,
@@ -22,8 +20,8 @@ const BarChart = ({
// change colors for longer medicine goal times
modData = JSON.parse(JSON.stringify(data))
for (let i = 0; i < modData.length; i++) {
if (Object.keys(goalExceptions).includes(categories[i])) {
const realGoal = goal + goalExceptions[categories[i]]
if (Object.keys(goalExceptions).includes(modData[i].code)) {
const realGoal = goal + goalExceptions[modData[i].code]
if (modData[i].y <= realGoal) {
modData[i].color = '#90A959'
} else if (modData[i].y <= realGoal + 12) {
@@ -61,20 +59,16 @@ const BarChart = ({
}
const getHeight = () => {
const t = categories.length > 8 ? 35 : 55
return categories.length * t + 100
const t = data.length > 8 ? 35 : 55
return data.length * t + 100
}
const getTooltipText = (category, amount, y, statistics, realGoal) => {
const getTooltipText = (name, code, amount, y, statistics, realGoal) => {
const sortingText =
label === 'Start year'
? `<b>From class of ${facultyGraph ? category : year}, ${amount}/${getClassSize(
category
)} students have graduated</b>`
: `<b>${amount} students graduated in year ${facultyGraph ? category : year}</b>`
const timeText = `<br /><p>${sortingText}, <br /><b>${
showMeanTime ? 'mean' : 'median'
} study time: ${y} months</p></b>`
? `<b>From class of ${facultyGraph ? name : year}, ${amount}/${getClassSize(code)} students have graduated</b>`
: `<b>${amount} students graduated in year ${facultyGraph ? name : year}</b>`
const timeText = `<br /><p>${sortingText}, <br /><b> median study time: ${y} months</p></b>`
const statisticsText = `<br /><p>${statistics.onTime} graduated on time</p><br />
<p>${statistics.yearOver} graduated max year overtime</p>
<br /><p>${statistics.wayOver} graduated over year late</p>`
@@ -82,8 +76,8 @@ const BarChart = ({
if (!facultyGraph) {
const goalText = realGoal ? `<br /><p><b>** Exceptional goal time: ${realGoal} months **</b></p>` : ''
return `<b>${
programmeNames[category]?.[language] ? programmeNames[category]?.[language] : programmeNames[category]?.fi
}</b><br />${category}${timeText}${statisticsText}${goalText}`
programmeNames[code]?.[language] ? programmeNames[code]?.[language] : programmeNames[code]?.fi
}</b><br />${code}${timeText}${statisticsText}${goalText}`
}
return `${timeText}${statisticsText}`
}
@@ -99,9 +93,17 @@ const BarChart = ({
tooltip: {
backgroundColor: 'white',
fontSize: '25px',
// outside: true,
// eslint-disable-next-line
formatter: function() {
return getTooltipText(this.x, this.point.amount, this.y, this.point.statistics, this.point?.realGoal)
return getTooltipText(
this.point.name,
this.point.code,
this.point.amount,
this.y,
this.point.statistics,
this.point?.realGoal
)
},
},
plotOptions: {
@@ -126,7 +128,7 @@ const BarChart = ({
},
// eslint-disable-next-line
formatter: function () {
return getDataLabel(this.point.amount, this.x)
return getDataLabel(this.point.amount, this.point.code ? this.point.code : this.point.name)
},
},
],
@@ -154,7 +156,7 @@ const BarChart = ({
},
],
xAxis: {
categories,
type: 'category',
title: {
text: facultyGraph ? label : 'Programme',
align: 'high',
@@ -179,6 +181,12 @@ const BarChart = ({
value: goal,
dashStyle: 'shortDash',
},
{
color: '#FEE191',
width: 2,
value: goal + 12,
dashStyle: 'shortDash',
},
],
},
credits: {
@@ -189,7 +197,7 @@ const BarChart = ({
if (!facultyGraph) config.title.text = `Year ${year} by ${label.toLowerCase()}`
return (
<div>
<div className={`${facultyGraph ? 'faculty' : 'programmes'}-graph`}>
<ReactHighcharts config={config} />
</div>
)
@@ -10,23 +10,20 @@ import '../faculty.css'
const TimesAndPathsView = ({ faculty, studyProgrammes, setStudyProgrammes }) => {
const toolTips = InfotoolTips.Faculty
const [showMeanTime, setShowMeanTime] = useState(false)
const [groupByGradYear, setGroupByGradYear] = useState(false)
const [showMedian, setShowMedian] = useState(false)
const [groupByStartYear, setGroupByStartYear] = 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 groupBy = groupByStartYear ? 'byStartYear' : 'byGradYear'
const label = groupByStartYear ? 'Start year' : 'Graduation year'
const data = graduationStats?.data?.[groupBy].medians
const goals = graduationStats?.data?.goals
const goalExceptions = { ...goals?.exceptions, needed: faculty?.code === 'H30' }
const programmeData = showMeanTime
? graduationStats?.data?.[groupBy].programmes.means
: graduationStats?.data?.[groupBy].programmes.medians
const programmeData = graduationStats?.data?.[groupBy].programmes.medians
const programmeNames = graduationStats?.data?.programmeNames
const classSizes = graduationStats?.data?.classSizes
const commonProps = { years, label, programmeNames, showMeanTime, classSizes, goalExceptions }
const commonProps = { label, programmeNames, showMedian, classSizes, goalExceptions }
const isFetchingOrLoading = graduationStats.isLoading || graduationStats.isFetching
@@ -67,18 +64,18 @@ const TimesAndPathsView = ({ faculty, studyProgrammes, setStudyProgrammes }) =>
{getDivider('Average graduation times', 'AverageGraduationTimes')}
<div className="toggle-container">
<Toggle
cypress="GroupByToggle"
firstLabel="Group by: Starting year"
secondLabel="Graduation year"
value={groupByGradYear}
setValue={setGroupByGradYear}
cypress="GraduationTimeToggle"
firstLabel="Breakdown"
secondLabel="Median times"
value={showMedian}
setValue={setShowMedian}
/>
<Toggle
cypress="GraduationTimeToggle"
firstLabel="Median time"
secondLabel="Mean time"
value={showMeanTime}
setValue={setShowMeanTime}
cypress="GroupByToggle"
firstLabel="Group by: Graduation year"
secondLabel="Starting year"
value={groupByStartYear}
setValue={setGroupByStartYear}
/>
</div>
<div>
@@ -1,5 +1,7 @@
/*
Order of the programme keys: KH -> MH -> T -> FI -> K- -> Numbers containing letters at end -> Y- -> Numbers
Order of the programme keys (such as TKT, PSYK) is chosen by "old" code:
KH -> MH -> T -> FI -> K- -> Numbers containing letters at end -> Y- -> Number
Take
*/
const regexValuesAll = [
/^KH/,
@@ -25,13 +27,23 @@ const testKey = value => {
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 sortProgrammeKeys = (programmeKeys, faculty) => {
try {
return programmeKeys.sort((a, b) => {
if (a[1].includes(faculty) && !b[1].includes(faculty)) return -1
if (!a[1].includes(faculty) && b[1].includes(faculty)) return 1
if (a[1].startsWith('T') && !b[1].includes(faculty) && !b[1].includes('T')) return -1
if (!a[1].includes(faculty) && b[1].startsWith('T') && !a[1].includes('T')) return 1
if (a[1].startsWith('LIS') && !b[1].includes(faculty) && !b[1].includes('LIS')) return -1
if (!a[1].includes(faculty) && b[1].startsWith('LIS') && !a[1].includes('LIS')) return 1
if (testKey(a[1]) - testKey(b[1]) === 0) {
return a[0].localeCompare(b[0])
}
return testKey(a[1]) - testKey(b[1])
})
} catch (e) {
return programmeKeys
}
}
export default sortProgrammeKeys
@@ -23,7 +23,7 @@ const Header = ({ children, actions, contextItems }) => {
/>
}
on="hover"
position="bottom"
position="bottom left"
>
Toggle Fullscreen
</Popup>
@@ -32,7 +32,9 @@ const Header = ({ children, actions, contextItems }) => {
<Dropdown position="bottom center" direction="left" icon="ellipsis vertical">
<Dropdown.Menu>
{contextItems.map(({ label, onClick }) => (
<Dropdown.Item onClick={onClick}>{label}</Dropdown.Item>
<Dropdown.Item key={label} onClick={onClick}>
{label}
</Dropdown.Item>
))}
</Dropdown.Menu>
</Dropdown>
@@ -8,10 +8,10 @@ const filterTexts = {
[FilterType.ALL]: { label: 'All' },
[FilterType.PASSED]: { label: 'Passed' },
[FilterType.PASSED_AFTER_FAILURE]: { label: 'Passed After Failure' },
[FilterType.FAILED]: { label: 'Failed' },
[FilterType.FAILED_MANY_TIMES]: { label: 'Failed Multiple Times' },
// [FilterType.FAILED]: { label: 'Failed' },
// [FilterType.FAILED_MANY_TIMES]: { label: 'Failed Multiple Times' },
// [FilterType.NOT_PARTICIPATED]: { label: 'Not Participated' },
[FilterType.DID_NOT_PASS]: { label: "Didn't Pass" },
// [FilterType.DID_NOT_PASS]: { label: "Didn't Pass" },
}
// a bandaid solution to prevent oodikone crashing
const translate = {
@@ -25,6 +25,8 @@ const translate = {
}
const CourseCard = ({ course, filterType, onChange }) => {
// Object.entries(filterTexts).map(([type, { label, info }]) => console.log(type, label, translate[type]))
// console.log('course', course)
const { language } = useLanguage()
const name = 'courseFilter'
@@ -37,7 +39,7 @@ const CourseCard = ({ course, filterType, onChange }) => {
return (
<>
<Label style={{ marginTop: '0.5rem' }}>
{getTextIn(course.course.name, language)}
{getTextIn(course?.course?.name, language)}
<Dropdown
text={filterTexts[filterType].label}
@@ -60,7 +62,7 @@ const CourseCard = ({ course, filterType, onChange }) => {
/>
)
}
if (Object.keys(course?.students[translate[type]]).length === 0)
if (course?.students[translate[type]] && 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} />
})}
@@ -15,7 +15,6 @@ const CourseFilterCard = ({ courseStats, options, onOptionsChange }) => {
const { courseFilters } = options
const { language } = useLanguage()
const name = 'course-filter-card'
// Wrestle course stats into something semantic-ui eats without throwing up.
const makeLabel = cs => `${cs.course.code} - ${getTextIn(cs.course.name, language)}`
const dropdownOptions = Object.values(courseStats)
@@ -81,9 +80,9 @@ const filterFunctions = {
[FilterType.ALL]: createFilterFunc('all'),
[FilterType.PASSED]: createFilterFunc('passed'),
[FilterType.PASSED_AFTER_FAILURE]: createFilterFunc('retryPassed'),
[FilterType.FAILED]: createFilterFunc('failed'),
[FilterType.FAILED_MANY_TIMES]: createFilterFunc('failedMany'),
[FilterType.DID_NOT_PASS]: createFilterFunc('didNotPass'),
// [FilterType.FAILED]: createFilterFunc('failed'),
// [FilterType.FAILED_MANY_TIMES]: createFilterFunc('failedMany'),
// [FilterType.DID_NOT_PASS]: createFilterFunc('didNotPass'),
// [FilterType.NOT_PARTICIPATED]: createFilterFunc('all', true),
// [FilterType.DID_NOT_PASS]: ({ studentNumber }, { students }) =>
// !Object.keys(students.all).includes(studentNumber) || Object.keys(students.failed).includes(studentNumber),
@@ -103,9 +102,7 @@ const filter = createFilter({
filter(student, { courseFilters }, { precomputed: courseMap }) {
return Object.entries(courseFilters).reduce((result, [code, filterType]) => {
const stats = courseMap[code]
if (!stats) return false
return result && filterFunctions[filterType](student, stats)
}, true)
},
@@ -8,6 +8,7 @@ import createFilter from './createFilter'
const STATUS_OPTIONS = [
{ key: 'enrl-status-present', text: 'Present', value: 1 },
{ key: 'enrl-status-absent', text: 'Absent', value: 2 },
{ key: 'enrl-status-inactive', text: 'Passive', value: 3 },
]
const EnrollmentStatusFilterCard = ({ options, onOptionsChange, allSemesters, language, semesterCodes }) => {
@@ -13,7 +13,7 @@ import date from './date'
import enrollmentStatus from './enrollmentStatus'
import studyTrack from './studyTrack'
import studyrightStatus from './studyrightStatus'
import cherrypick from './cherrypick'
import studentNumber from './studentNumber'
import hops from './hops'
export const genderFilter = gender
@@ -31,5 +31,5 @@ export const creditDateFilter = date
export const enrollmentStatusFilter = enrollmentStatus
export const studyTrackFilter = studyTrack
export const studyrightStatusFilter = studyrightStatus
export const cherrypickFilter = cherrypick
export const studentNumberFilter = studentNumber
export const hopsFilter = hops
@@ -115,7 +115,7 @@ const EditableList = ({ value, onChange, renderLabel }) => {
)
}
const CherryPickFilterCard = ({ options, onOptionsChange, withoutSelf }) => {
const StudentNumberFilterCard = ({ options, onOptionsChange, withoutSelf }) => {
const students = withoutSelf()
const swapLists = () => {
@@ -177,16 +177,16 @@ const CherryPickFilterCard = ({ options, onOptionsChange, withoutSelf }) => {
}
export default createFilter({
key: 'CherryPick',
key: 'studentNumber',
title: 'Cherry-picker',
title: 'Student Number',
defaultOptions: {
allowlist: [],
blocklist: [],
},
info: filterInfo.cherrypicker,
info: filterInfo.studentNumber,
isActive: ({ allowlist, blocklist }) => allowlist.length > 0 || blocklist.length > 0,
@@ -198,7 +198,7 @@ export default createFilter({
)
},
render: props => <CherryPickFilterCard {...props} />,
render: props => <StudentNumberFilterCard {...props} />,
actions: {
addToAllowlist: (options, students) => {
@@ -49,7 +49,7 @@ const StudyrightStatusFilterCard = ({ options, onOptionsChange }) => {
export default createFilter({
key: 'studyrightStatusFilter',
title: 'Studyright status',
title: 'Studyright Status',
info: filterInfo.studyrightStatus,
@@ -13,7 +13,7 @@ const InfoBox = ({ content, cypress }) => {
<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>
<ReactMarkdown>{content}</ReactMarkdown>
</Message.Content>
</div>
<Button onClick={toggleOpen} className="ok-infobox-close" data-cy={`${cypress}-close-info`}>
@@ -30,9 +30,12 @@ const allNavigationItems = {
reqRights: ['studyGuidanceGroups'],
},
customPopulations: {
path: '/custompopulation',
key: 'customPopulation',
label: 'Custom populations',
items: [
{ path: '/custompopulation', key: 'customSearch', label: 'Search by Studentnumbers' },
{ path: '/openunipopulation', key: 'openUniSearch', label: 'Fetch Open Uni Students by Courses' },
],
},
updater: { path: '/updater', key: 'updater', label: 'Updater', reqRights: ['admin'] },
feedback: { path: '/feedback', key: 'feedback', label: 'Give feedback' },
@@ -50,8 +53,11 @@ const NavigationBar = () => {
if (key === 'populations') {
if (!isAdmin && rights.length === 0 && iamRights.length === 0) return
}
if (key === 'students' || key === 'customPopulations') {
if (key === 'students') {
if (!checkUserAccess(['admin', 'studyGuidanceGroups'], roles) && rights.length === 0) return
}
if (key === 'customPopulations') {
if (!checkUserAccess(['admin', 'studyGuidanceGroups', 'openUniSearch'], roles) && rights.length === 0) return
} else if (key === 'courseStatistics') {
if (!checkUserAccess(['courseStatistics', 'admin'], roles) && rights.length === 0) return
} else if (key === 'faculty') {
@@ -75,23 +81,33 @@ const NavigationBar = () => {
</span>
</Menu.Item>
)
const showSearch = item => {
if (item.key === 'class' || item.key === 'overview') return true
if (checkUserAccess(['openUniSearch', 'admin'], roles) && item.key === 'openUniSearch') return true
if ((checkUserAccess(['studyGuidanceGroups', 'admin'], roles) || rights.length > 0) && item.key === 'customSearch')
return true
return false
}
const renderNavigationRoutes = () =>
Object.values(visibleNavigationItems).map(({ items, path, key, label, tag }) =>
items ? (
<Menu.Item as={Dropdown} key={`menu-item-drop-${key}`} tabIndex="-1" text={label} data-cy={`navbar-${key}`}>
<Dropdown.Menu>
{items.map(i => (
<Dropdown.Item
as={NavLink}
key={`menu-item-${i.path}`}
to={i.path}
tabIndex="-1"
data-cy={`navbar-${i.key}`}
>
{i.label}
</Dropdown.Item>
))}
{items.map(
i =>
showSearch(i) && (
<Dropdown.Item
as={NavLink}
key={`menu-item-${i.path}`}
to={i.path}
tabIndex="-1"
data-cy={`navbar-${i.key}`}
>
{i.label}
</Dropdown.Item>
)
)}
</Dropdown.Menu>
</Menu.Item>
) : (

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