Compare commits

...
414 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
outisa 8e97596fc0 [faculty] Fix population link visibility 2022-10-04 13:52:53 +03:00
outisa 3c7259f1cb [Facultt] Modify info text 2022-10-04 12:01:05 +03:00
outisa 17759294fb [Faculty] Add progress tab for faculty public view 2022-10-04 11:05:00 +03:00
outisa d4925669e8 Hide population link for user with no rights 2022-10-03 20:16:07 +03:00
Laura Sinikallio 971e7a3d1b Merge branch 'master' of github.com:UniversityOfHelsinkiCS/oodikone 2022-10-03 15:38:55 +03:00
Laura Sinikallio 86418abd05 Include graduation times to cronjob 2022-10-03 15:24:22 +03:00
outisa 9331609afd Fix another test 2022-09-30 17:31:27 +03:00
outisa 9093d701e4 Fix cypress test 2022-09-30 17:07:55 +03:00
outisa 3ad51b7408 Merge branch 'master' of github.com:UniversityOfHelsinkiCS/oodikone 2022-09-30 16:45:39 +03:00
outisa ebc0cb487a [Faculty] Add progress tab to redis 2022-09-30 16:45:08 +03:00
outisa 2771eeb3d1 [Faculty] Update infoboxes betterment 2022-09-30 16:38:06 +03:00
outisa 4ca31490ae [Faculty] Update infoboxes 2022-09-30 16:36:24 +03:00
Laura Sinikallio 94ab15bd96 Graduation times to Redis 2022-09-30 14:01:09 +03:00
Laura Sinikallio 686cc31e49 Fix test 2022-09-30 10:08:20 +03:00
Laura Sinikallio 6e1976d1e2 Faculty graduations time tab visible for non-admins 2022-09-30 09:55:27 +03:00
outisa 0408a42153 Fix cypress tests 2022-09-29 22:45:21 +03:00
outisa ce2e90c476 [Faculty] Make students stats table more readable 2022-09-29 22:25:00 +03:00
outisa 79eec6e595 Hide % as default in students stats tables 2022-09-29 22:15:47 +03:00
outisa c6812ecc99 [Faculty-Progress tab] Add toggles and hide % as default 2022-09-29 21:45:26 +03:00
Laura Sinikallio 208cf732ac Handle exceptional goal times in faculty 2022-09-29 19:47:49 +03:00
Laura Sinikallio cbd46126cd Set exception goal times for veterinary 2022-09-29 12:46:08 +03:00
Laura Sinikallio ee040806ae Add old programmes to graduation 'only bachelor' filtering, bar text edits 2022-09-29 10:59:48 +03:00
Laura Sinikallio 13d16c3f14 Edit graduations bar text 2022-09-29 10:36:56 +03:00
Laura Sinikallio 29cd40bfbe Clean up inline styling 2022-09-29 09:03:43 +03:00
outisa 27249697bd Fix exporting excel bug? 2022-09-28 21:39:38 +03:00
Laura Sinikallio 3ae80fc123 Gradutations times: form bc + ms combo's class size from bachelors 2022-09-27 16:37:41 +03:00
Laura Sinikallio 7f5a66cc63 Smite a backend monster 2022-09-27 13:37:40 +03:00
Laura Sinikallio a9710ca5ec Merge branch 'master' of github.com:UniversityOfHelsinkiCS/oodikone 2022-09-27 12:53:51 +03:00
Laura Sinikallio ec0239f985 Grad times: sort new programmes before old, tweak chart sizing 2022-09-27 12:53:20 +03:00
outisa a4ceae10e5 [Faculty - pogress tab] Separate masters/bachelor-masters 2022-09-27 12:22:05 +03:00
Laura Sinikallio 0409df0ee4 Graduations chart height set dynamically 2022-09-27 12:05:05 +03:00
outisa b3cc01e9e0 Merge branch 'master' of github.com:UniversityOfHelsinkiCS/oodikone 2022-09-23 15:17:38 +03:00
outisa e7c3c4657d [Faculty] Add improvements to programmes and student poulations -tag 2022-09-23 15:16:58 +03:00
Laura Sinikallio 530254cfd5 Cleaner bar label font, add info 2022-09-23 13:06:54 +03:00
Laura Sinikallio c5ad9309cd Show percentage of graduated, refactor bar label formatting 2022-09-23 12:41:15 +03:00
outisa beaca19d07 [Faculty] Add data fetching for students of the faculty view 2022-09-22 20:10:06 +03:00
outisa 16bfbb4b62 Fix cypress test to correspond changes 2022-09-22 13:40:19 +03:00
outisa 3af18f2408 Merge branch 'master' of github.com:UniversityOfHelsinkiCS/oodikone 2022-09-22 13:22:51 +03:00
outisa 9d08501242 [Faculty/Study Programme] Fix upper credit limits 2022-09-22 13:06:11 +03:00
Laura Sinikallio d8b8e0ef89 Graduation times - show class size 2022-09-21 17:02:30 +03:00
Laura Sinikallio 1626f76e3a Open programme charts update on toggling 2022-09-20 18:28:10 +03:00
Laura Sinikallio d597b9e67f Show how many graduated on time for programmes 2022-09-20 17:58:51 +03:00
Laura Sinikallio e1dbcd0e7e Show how many graduated on time for faculty 2022-09-20 17:33:51 +03:00
outisa 70b33a3ae4 Merge branch 'master' of github.com:UniversityOfHelsinkiCS/oodikone 2022-09-20 16:26:56 +03:00
outisa 4834515b4c Fix studyprogramme update problem 2022-09-20 16:26:07 +03:00
Laura Sinikallio fa0bf87307 Graduation times: show programme names, improve tooltip 2022-09-20 16:22:11 +03:00
outisa 4b9fe9d6a0 Fix cypress test values 2022-09-20 13:59:36 +03:00
outisa a674c4257d Faculty/Study programme unify student progress views 2022-09-20 13:06:05 +03:00
outisa 992d1c13dc [Faculty] Fix faculty progress chart bug for missing data 2022-09-20 09:14:07 +03:00
outisa 7024345088 [Faculty] First betterments for the math 2022-09-19 23:43:58 +03:00
outisa 9560f84395 [Faculty] Add faculty progress for students 2022-09-19 22:53:31 +03:00
Laura Sinikallio 9f9ab3ce09 Faculty - Show programme level graduation times 2022-09-19 19:11:17 +03:00
Laura Sinikallio 4ac29fcfe8 Add faculty grad times info text 2022-09-19 15:17:25 +03:00
Laura Sinikallio 174a3069fe Ordered faculty and studyprogramme lists 2022-09-19 14:31:04 +03:00
Miko Keskimäki fc6b77e32b Log when using iam rights to access view for Grafana 2022-09-16 09:55:21 +03:00
Laura Sinikallio 6cfb89336f Show graduation times by start year 2022-09-15 19:53:40 +03:00
Laura Sinikallio 635409442c Remove old graduation time display code 2022-09-15 11:28:39 +03:00
outisa 2ffd2db995 Merge branch 'master' of github.com:UniversityOfHelsinkiCS/oodikone 2022-09-15 11:08:11 +03:00
outisa 431c2292cb Fix faculty access 2022-09-15 11:02:52 +03:00
Laura Sinikallio 1a4a1a1f1c Redo faculty average graduation times display 2022-09-14 15:30:43 +03:00
outisa fe1b5ac4c6 Merge branch 'master' of github.com:UniversityOfHelsinkiCS/oodikone 2022-09-13 16:08:07 +03:00
outisa 0e9253f698 Update Faculty updater 2022-09-13 16:07:47 +03:00
Miko Keskimäki 11f9b935b4 Log iam rights status via accessLogger for Grafana 2022-09-13 12:53:50 +03:00
outisa a16896e74d [Faculty] Update updater 2022-09-12 13:33:47 +03:00
outisa 814599d6ea [Faculty] Small refactoring and adjusting numbers 2022-09-10 12:18:01 +03:00
outisa 8f2dcac0f9 Fix bug for updating statistics 2022-09-09 18:24:29 +03:00
outisa 240659afc9 [Faculty] Add include/exclude studywork toggle 2022-09-09 15:12:22 +03:00
outisa b9059e2a52 Merge branch 'master' of github.com:UniversityOfHelsinkiCS/oodikone 2022-09-09 15:09:22 +03:00
outisa a1688f7cdd [Faculty] Add infotool text 2022-09-09 14:19:05 +03:00
Laura Sinikallio 2c378bfca2 Patch breaking population statistics course filters ('didn't participate' disabled for now) 2022-09-09 11:54:39 +03:00
outisa aec495b161 [Navigation] Fix issue for faculty tab showing wrongly 2022-09-06 09:58:59 +03:00
outisa 486bc1a194 Merge branch 'master' of github.com:UniversityOfHelsinkiCS/oodikone 2022-09-02 14:19:55 +03:00
outisa 18bcb91d09 Add one more test for faculty view 2022-09-02 14:19:19 +03:00
outisa 5c4d73f555 Add new tests for faculty 2022-09-02 13:21:50 +03:00
Laura Sinikallio 27e8450789 Small clean-ups 2022-09-01 14:40:35 +03:00
Laura Sinikallio 56733f2445 Subtract statutory absences from faculty graduation times 2022-09-01 14:34:41 +03:00
Laura Sinikallio 089b00fcd6 Show graduation times only for levels with data 2022-09-01 11:52:05 +03:00
Laura Sinikallio a709fe70ba Merge branch 'master' of github.com:UniversityOfHelsinkiCS/oodikone 2022-08-31 19:23:14 +03:00
Laura Sinikallio f26e4b4859 Betterments to graduations times 2022-08-31 19:23:06 +03:00
outisa 18a382167b Fix accessrights in navigation bar 2022-08-31 16:25:32 +03:00
outisa b4be0a3e6b [Faculty] Fix accessrights problems 2022-08-31 15:44:41 +03:00
outisa 596ac08ad4 Fix bug related to accessrights 2022-08-31 13:55:40 +03:00
outisa 9c6ad0027a Modify faculty accessrights in navigation bar 2022-08-31 12:05:03 +03:00
outisa 27ce374f35 [faculty] Modify access rights 2022-08-31 11:55:17 +03:00
Laura Sinikallio 7e6f311b62 Faculty - move grad times to new tab 2022-08-31 10:22:43 +03:00
Laura Sinikallio dd77fdff9e Initial pipeline for graduation times, WIP 2022-08-30 17:48:58 +03:00
outisa 0b7adf4888 [Faculty] Add messagebox for incomplete data 2022-08-30 14:12:57 +03:00
outisa 9deee8a384 [Faculty] Change color codes (perhaps) better 2022-08-30 13:58:11 +03:00
outisa 81791381a5 [Faculty] Make betterments for faculty view 2022-08-30 12:45:59 +03:00
outisa 9806639c1b [Faculty] Show total in on-hover tooltip 2022-08-30 12:13:51 +03:00
outisa 5e9a3c4b99 [Faculty] Change others to licentiates (thesis writers) 2022-08-30 12:01:38 +03:00
outisa 12f73f4dee [Faculty] Show numbers with max. 1 decimal 2022-08-30 11:54:04 +03:00
outisa 9d27dfebe5 [Faculty] Make betterments for basic view 2022-08-29 17:15:38 +03:00
outisa 7bebd4c845 Fix updating thesis writers 2022-08-29 17:14:05 +03:00
outisa 43d2e84b32 [Faculty] Add thesis writers to redis 2022-08-29 14:47:25 +03:00
Roni Lindholm ee1cb395a2 Fix custom population crash 2022-08-29 11:45:26 +03:00
Roni Lindholm 4e221d8867 Fix missing students from population of course when no attainment
Enrollments were retrieved by enrollment date instead of semester code
which corresponds to when the course is acually organized.
2022-08-29 11:11:03 +03:00
Roni Lindholm 3aab7b74a3 Fix counting of total enrollments in course stats attempts tab 2022-08-29 10:05:29 +03:00
Roni Lindholm 92651b8bc7 Make course stats search by course code case insenstive 2022-08-29 09:47:30 +03:00
Miko Keskimäki 08b53c34f2 Dependabot updates 2022-08-29 08:52:00 +03:00
Laura Sinikallio 8d6a311936 Add note to update tab 2022-08-27 12:59:05 +03:00
Laura Sinikallio 79b8b92b66 Implement faculty info texts 2022-08-27 12:03:04 +03:00
outisa fa8a07481a [Faculty] Add small adjustment for sorting 2022-08-27 10:45:23 +03:00
outisa f79d6ef0fb [Faculty] Find thesis writers only for new programmes 2022-08-26 21:43:44 +03:00
outisa bb25541202 Disable flaky faculty cypress test 2022-08-26 17:22:21 +03:00
outisa 3530723bd5 [Faculty] Change sort by selection 2022-08-26 16:41:31 +03:00
Laura Sinikallio f2ffaacc6c Info texts for faculty 2022-08-26 16:12:42 +03:00
Laura Sinikallio 14e95fe4df Add update statistics tab to faculty page, refactor programmes tab basis 2022-08-26 15:25:04 +03:00
Laura Sinikallio f69b12a1f9 Add faculty stats refresh to cronjob 2022-08-26 13:13:13 +03:00
Laura Sinikallio e4e527eaeb Imporove faculty filtering 2022-08-26 12:12:53 +03:00
Laura Sinikallio c4c8f6123b Tools for refreshing faculty stats with cronjob (unplugged) 2022-08-25 16:14:03 +03:00
Laura Sinikallio bdb71242a7 Redis edits from previous changes 2022-08-25 14:40:28 +03:00
Laura Sinikallio a3a36fcef1 Put back faculty frontside programme sorting 2022-08-25 14:17:56 +03:00
Laura Sinikallio adbcb1bb88 Faculty toggle for old programmes filters from all stats 2022-08-25 13:51:22 +03:00
outisa 37504f9e97 Fix minor bug for finding thesis writers 2022-08-24 18:29:13 +03:00
outisa 1722bcd7a3 Change order of the navigation bar 2022-08-24 17:10:37 +03:00
outisa a770873da5 [Faculty] Add language options for programme names 2022-08-24 16:45:18 +03:00
Laura Sinikallio a4c3385956 Redis for faculty credits and student info -> speed 2022-08-24 14:04:32 +03:00
Laura Sinikallio 9fe491e7cc Fix error, but better 2022-08-24 13:00:13 +03:00
Laura Sinikallio 42213948b7 Fix redis error 2022-08-24 12:28:10 +03:00
Laura Sinikallio 1014348e5b Redis for faculty programmes 2022-08-24 11:48:25 +03:00
outisa dcecd66e49 [Faculty] Fix sorting bug 2022-08-24 10:33:58 +03:00
Laura Sinikallio 4509e51f66 Refactor file structure, start of redis for faculty 2022-08-24 10:11:38 +03:00
outisa 90841369bb [Faculty] Modify calculation of thesis writers 2022-08-24 09:55:26 +03:00
outisa c3e41c8a60 [Faculty] Add small refactoring 2022-08-24 09:47:39 +03:00
Laura Sinikallio 81ffbf653e Basis for faculty graduation times 2022-08-23 10:20:28 +03:00
Laura Sinikallio b46e30c8fa Switch rest of faculty to new programme search 2022-08-22 15:52:46 +03:00
Miko Keskimäki 3619698744 Fix error with encryption when no selectable students for year exist 2022-08-22 14:21:35 +03:00
Laura Sinikallio 81ee6b8146 Improve faculty programmes retrieval 2022-08-22 14:03:49 +03:00
outisa c9b27fdc64 Merge branch 'master' of github.com:UniversityOfHelsinkiCS/oodikone 2022-08-22 12:55:02 +03:00
outisa 6b50c07ed7 [Faculty] Add toggle for showing only new programmes or all programmes 2022-08-22 12:54:47 +03:00
Roni Lindholm d2670e180c Fix empty study guidance group crash 2022-08-22 11:44:30 +03:00
Roni Lindholm 580e1b4ed0 Minor study guidance group tweaks 2022-08-22 11:08:42 +03:00
184 changed files with 10184 additions and 5374 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
+266 -36
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()
@@ -13,58 +11,290 @@ describe('Faculty overview', () => {
})
})
describe('Faculty basic information', () => {
describe('Faculty basic information: admin user', () => {
beforeEach(() => {
cy.init('/faculties', 'admin')
cy.contains('td', 'H90').click()
})
it('Credits produced by faculty are shown', () => {
cy.get(['data-cy=Section-CreditsProducedByTheFaculty'])
cy.get(['data-cy=Graph-CreditsProducedByTheFaculty'])
cy.get('[data-cy="Section-CreditsProducedByTheFaculty"]').should('be.visible')
cy.get('[data-cy="Graph-CreditsProducedByTheFaculty"]').should('be.visible')
})
it('Update statistics tab is shown', () => {
cy.get('[data-cy="FacultySegmentContainer"]').should('contain', 'Update statistics')
})
})
describe('Study programme information', () => {
describe('Faculty basic information: basic user', () => {
beforeEach(() => {
cy.init('/faculties', 'admin')
cy.init('/faculties')
cy.contains('td', 'H80').click()
})
it('Basic information tab show all graphs and tables', () => {
cy.get('[data-cy="Table-CreditsProducedByTheFaculty"]').should('be.visible')
cy.get('[data-cy="Graph-CreditsProducedByTheFaculty"]').should('be.visible')
cy.get('[data-cy="Table-ThesisWritersOfTheFaculty"]').should('be.visible')
cy.get('[data-cy="Graph-ThesisWritersOfTheFaculty"]').should('be.visible')
cy.get('[data-cy="Table-StudentsOfTheFaculty"]').should('be.visible')
cy.get('[data-cy="Graph-StudentsOfTheFaculty"]').should('be.visible')
cy.get('[data-cy="Table-GraduatedOfTheFaculty"]').should('be.visible')
cy.get('[data-cy="Graph-GraduatedOfTheFaculty"]').should('be.visible')
})
it('Correct tabs are shown', () => {
cy.get('[data-cy="FacultySegmentContainer"]').should('contain', 'Basic information')
cy.get('[data-cy="FacultySegmentContainer"]').should('contain', 'Graduation times')
cy.get('[data-cy="FacultySegmentContainer"]').should('contain', 'Programmes and student populations')
cy.get('[data-cy="FacultySegmentContainer"]').should('not.contain', 'Update statistics')
})
it('Toggle years works', () => {
cy.get('[data-cy="Table-CreditsProducedByTheFaculty"]').should('contain', '2022')
cy.get('[data-cy="Table-ThesisWritersOfTheFaculty"]').should('contain', '2022')
cy.get('[data-cy="Table-StudentsOfTheFaculty"]').should('contain', '2022')
cy.get('[data-cy="Table-GraduatedOfTheFaculty"]').should('contain', '2022')
cy.get('[data-cy="YearToggle"]').click()
cy.get('[data-cy="Table-CreditsProducedByTheFaculty"]').should('contain', '2022 - 2023')
cy.get('[data-cy="Table-ThesisWritersOfTheFaculty"]').should('contain', '2022 - 2023')
cy.get('[data-cy="Table-StudentsOfTheFaculty"]').should('contain', '2022 - 2023')
cy.get('[data-cy="Table-GraduatedOfTheFaculty"]').should('contain', '2022 - 2023')
})
it('Toggle programmes works', () => {
cy.get('[data-cy="FacultyProgrammesShownInfo"]').should('not.exist')
cy.get('[data-cy="ProgrammeToggle"]').click()
cy.get('[data-cy="FacultyProgrammesShownInfo"]').should('be.visible')
})
it('Students of the faculty infobox works', () => {
cy.get('[data-cy="StudentsOfTheFaculty-info-content"]').should('not.exist')
cy.get('[data-cy="StudentsOfTheFaculty-open-info"]').click()
cy.get('[data-cy="StudentsOfTheFaculty-info-content"]').should('be.visible')
cy.get('[data-cy="StudentsOfTheFaculty-close-info"]').click()
cy.get('[data-cy="StudentsOfTheFaculty-info-content"]').should('not.exist')
})
})
describe('Study programme information: users with access', () => {
beforeEach(() => {
cy.init('/faculties')
cy.contains('td', 'H80').click()
})
it('Study programme credit information is not visible in the beginning', () => {
cy.get('table[data-cy="Table-CreditsProducedByTheFaculty"]').should('be.visible')
cy.get('td[data-cy="Cell-CreditsProducedByTheFaculty-0"]').should('not.be.visible')
cy.get('td[data-cy="Cell-CreditsProducedByTheFaculty-1"]').should('not.be.visible')
cy.get('td[data-cy="Cell-CreditsProducedByTheFaculty-2"]').should('not.be.visible')
cy.get('td[data-cy="Cell-CreditsProducedByTheFaculty-3"]').should('not.be.visible')
cy.get('td[data-cy="Cell-CreditsProducedByTheFaculty-4"]').should('not.be.visible')
cy.get('td[data-cy="Cell-CreditsProducedByTheFaculty-5"]').should('not.be.visible')
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-0"]').should('not.be.visible')
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-1"]').should('not.be.visible')
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-2"]').should('not.be.visible')
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-3"]').should('not.be.visible')
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-4"]').should('not.be.visible')
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-5"]').should('not.be.visible')
})
it('Study programme credit information can be toggled', () => {
cy.get('table[data-cy="Table-CreditsProducedByTheFaculty"]').should('be.visible')
cy.get('[data-cy="Table-CreditsProducedByTheFaculty"]').should('be.visible')
cy.get('div[data-cy="Button-Show-CreditsProducedByTheFaculty-0"]').click()
cy.get('td[data-cy="Cell-CreditsProducedByTheFaculty-0"]').should('be.visible')
cy.get('td[data-cy="Cell-CreditsProducedByTheFaculty-1"]').should('not.be.visible')
cy.get('td[data-cy="Cell-CreditsProducedByTheFaculty-2"]').should('not.be.visible')
cy.get('td[data-cy="Cell-CreditsProducedByTheFaculty-3"]').should('not.be.visible')
cy.get('td[data-cy="Cell-CreditsProducedByTheFaculty-4"]').should('not.be.visible')
cy.get('td[data-cy="Cell-CreditsProducedByTheFaculty-5"]').should('not.be.visible')
cy.get('div[data-cy="Button-Show-CreditsProducedByTheFaculty-3"]').click()
cy.get('td[data-cy="Cell-CreditsProducedByTheFaculty-0"]').should('be.visible')
cy.get('td[data-cy="Cell-CreditsProducedByTheFaculty-1"]').should('not.be.visible')
cy.get('td[data-cy="Cell-CreditsProducedByTheFaculty-2"]').should('not.be.visible')
cy.get('td[data-cy="Cell-CreditsProducedByTheFaculty-3"]').should('be.visible')
cy.get('td[data-cy="Cell-CreditsProducedByTheFaculty-4"]').should('not.be.visible')
cy.get('td[data-cy="Cell-CreditsProducedByTheFaculty-5"]').should('not.be.visible')
cy.get('div[data-cy="Button-Hide-CreditsProducedByTheFaculty-0"]').click()
cy.get('td[data-cy="Cell-CreditsProducedByTheFaculty-0"]').should('not.be.visible')
cy.get('td[data-cy="Cell-CreditsProducedByTheFaculty-1"]').should('not.be.visible')
cy.get('td[data-cy="Cell-CreditsProducedByTheFaculty-2"]').should('not.be.visible')
cy.get('td[data-cy="Cell-CreditsProducedByTheFaculty-3"]').should('be.visible')
cy.get('td[data-cy="Cell-CreditsProducedByTheFaculty-4"]').should('not.be.visible')
cy.get('td[data-cy="Cell-CreditsProducedByTheFaculty-5"]').should('not.be.visible')
// I tried to fix this without cy.wait() method without effort. Sorting the programmes
// or something else causes rerendering of the view and yield to failing tests.
// eslint-disable-next-line cypress/no-unnecessary-waiting
cy.wait(10000)
cy.get('[data-cy="Button-Show-CreditsProducedByTheFaculty-0"]').click()
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-0"]').should('be.visible')
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-1"]').should('not.be.visible')
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-2"]').should('not.be.visible')
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-3"]').should('not.be.visible')
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-4"]').should('not.be.visible')
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-5"]').should('not.be.visible')
cy.get('[data-cy="Button-Show-CreditsProducedByTheFaculty-3"]').click()
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-0"]').should('be.visible')
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-1"]').should('not.be.visible')
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-2"]').should('not.be.visible')
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-3"]').should('be.visible')
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-4"]').should('not.be.visible')
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-5"]').should('not.be.visible')
cy.get('[data-cy="Button-Hide-CreditsProducedByTheFaculty-0"]').click()
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-0"]').should('not.be.visible')
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-1"]').should('not.be.visible')
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-2"]').should('not.be.visible')
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-3"]').should('be.visible')
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-4"]').should('not.be.visible')
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-5"]').should('not.be.visible')
})
it('Graph stays open when sorted', () => {
cy.get('[data-cy="Table-CreditsProducedByTheFaculty"]').should('be.visible')
// eslint-disable-next-line cypress/no-unnecessary-waiting
cy.wait(10000)
cy.get('[data-cy="Button-Show-CreditsProducedByTheFaculty-0"]').click()
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-0"]').should('be.visible')
cy.get('[data-cy="Menu-CreditsProducedByTheFaculty-Total"]').click()
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-0"]').should('be.visible')
})
})
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')
})
})
})
+37 -36
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,12 +103,12 @@ 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
cy.wait(10000)
cy.contains('No statistics found for the given query.')
@@ -118,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)')
})
@@ -134,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))
@@ -185,7 +184,7 @@ describe('Population Statistics tests', () => {
cy.get("[data-cy='credit-stats-tab'] > .menu > :nth-child(2)").click()
cy.get("[data-cy='credit-stats-table-name-header']").should('contain', 'Statistic for n = 170 Students')
cy.get("[data-cy='credit-stats-mean']").should('contain', '114.75')
cy.get("[data-cy='credit-stats-mean']").should('contain', '114.74')
cy.get("[data-cy='credit-stats-stdev']").should('contain', '65.53')
cy.get("[data-cy='credit-stats-min']").should('contain', '5')
cy.get("[data-cy='credit-stats-max']").should('contain', '314')
@@ -218,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')
@@ -238,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')
})
})
+118 -178
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,166 +202,97 @@ 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,
'100.0 %',
10,
'83.3 %',
0,
'0 %',
0,
'0 %',
0,
'0 %',
0,
'0 %',
10,
'83.3 %',
2,
'16.7 %',
12,
'100.0 %',
],
[
'2019 - 2020',
86,
'100.0 %',
86,
'100.0 %',
0,
'0 %',
0,
'0 %',
1,
'1.2 %',
1,
'1.2 %',
65,
'75.6 %',
21,
'24.4 %',
86,
'100.0 %',
],
[
'2018 - 2019',
162,
'100.0 %',
161,
'99.4 %',
0,
'0 %',
0,
'0 %',
0,
'0 %',
11,
'6.8 %',
118,
'72.8 %',
44,
'27.2 %',
161,
'99.4 %',
],
[
'2017 - 2018',
171,
'100.0 %',
171,
'100.0 %',
0,
'0 %',
0,
'0 %',
4,
'2.3 %',
42,
'24.6 %',
135,
'78.9 %',
36,
'21.1 %',
166,
'97.1 %',
],
[
'Total',
431,
'100.0 %',
431,
'100.0 %',
0,
'0 %',
0,
'0 %',
5,
'1.2 %',
54,
'12.5 %',
328,
'76.1 %',
103,
'23.9 %',
425,
'98.6 %',
],
['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, 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],
['2019 - 2020', 86, 26, 34, 21, 5, 0, 0, 0],
['2018 - 2019', 162, 10, 27, 47, 36, 26, 12, 4],
['2017 - 2018', 171, 20, 21, 26, 22, 22, 33, 27],
['Total', 431, 51, 64, 97, 79, 55, 51, 34],
['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, 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 30 credits')
.should('contain', 'Less than 15 credits')
.should('contain', '30-59 credits')
.should('contain', 'More than 180 credits')
.should('contain', '11.7%') // The percentage for less than 30 credits in 2017-2018, to check that the graph renders
.should('contain', '6.2%') // The percentage for less than 30 credits in 2018-2019
.should('contain', '30.2%') // The percentage for less than 30 credits in 2019-2020
.should('contain', '180 or more credits')
.should('contain', '4.1%') // The percentage for less than 15 credits in 2017-2018, to check that the graph renders
.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')
})
})
})
@@ -365,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()
})
@@ -385,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')
})
@@ -436,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()
@@ -452,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')
@@ -467,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()
@@ -490,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()
@@ -500,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]')
@@ -529,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
+310 -284
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.10",
"@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.4.0",
"eslint": "^8.22.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.29.3",
"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.10.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 }
+81 -76
View File
@@ -14,20 +14,20 @@
"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",
"moment": "^2.29.1",
"moment-timezone": "^0.5.34",
"moment-timezone": "^0.5.37",
"morgan": "^1.10.0",
"pg": "^8.7.3",
"pg": "^8.8.0",
"redis": "^3.1.2",
"sequelize": "^6.17.0",
"sequelize": "^6.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",
@@ -2209,9 +2209,9 @@
}
},
"node_modules/moment-timezone": {
"version": "0.5.34",
"resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.34.tgz",
"integrity": "sha512-3zAEHh2hKUs3EXLESx/wsgw6IQdusOT8Bxm3D9UrHPQR7zlMmzwybC8zHEM1tQ4LJwP7fcxrWr8tuBg05fFCbg==",
"version": "0.5.37",
"resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.37.tgz",
"integrity": "sha512-uEDzDNFhfaywRl+vwXxffjjq1q0Vzr+fcQpQ1bU0kbzorfS7zVtZnCnGc8mhWmF39d4g4YriF6kwA75mJKE/Zg==",
"dependencies": {
"moment": ">= 2.9.0"
},
@@ -2619,14 +2619,14 @@
"integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ=="
},
"node_modules/pg": {
"version": "8.7.3",
"resolved": "https://registry.npmjs.org/pg/-/pg-8.7.3.tgz",
"integrity": "sha512-HPmH4GH4H3AOprDJOazoIcpI49XFsHCe8xlrjHkWiapdbHK+HLtbm/GQzXYAZwmPju/kzKhjaSfMACG+8cgJcw==",
"version": "8.8.0",
"resolved": "https://registry.npmjs.org/pg/-/pg-8.8.0.tgz",
"integrity": "sha512-UXYN0ziKj+AeNNP7VDMwrehpACThH7LUl/p8TDFpEUuSejCUIwGSfxpHsPvtM6/WXFy6SU4E5RG4IJV/TZAGjw==",
"dependencies": {
"buffer-writer": "2.0.0",
"packet-reader": "1.0.0",
"pg-connection-string": "^2.5.0",
"pg-pool": "^3.5.1",
"pg-pool": "^3.5.2",
"pg-protocol": "^1.5.0",
"pg-types": "^2.1.0",
"pgpass": "1.x"
@@ -2635,7 +2635,7 @@
"node": ">= 8.0.0"
},
"peerDependencies": {
"pg-native": ">=2.0.0"
"pg-native": ">=3.0.1"
},
"peerDependenciesMeta": {
"pg-native": {
@@ -2657,9 +2657,9 @@
}
},
"node_modules/pg-pool": {
"version": "3.5.1",
"resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.5.1.tgz",
"integrity": "sha512-6iCR0wVrro6OOHFsyavV+i6KYL4lVNyYAB9RD18w66xSzN+d8b66HiwuP30Gp1SH5O9T82fckkzsRjlrhD0ioQ==",
"version": "3.5.2",
"resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.5.2.tgz",
"integrity": "sha512-His3Fh17Z4eg7oANLob6ZvH8xIVen3phEZh2QuyrIl4dQSDVEabNducv6ysROKpDNPSD+12tONZVWfSgMvDD9w==",
"peerDependencies": {
"pg": ">=8.0"
}
@@ -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.3",
"resolved": "https://registry.npmjs.org/sequelize/-/sequelize-6.21.3.tgz",
"integrity": "sha512-cJPrTTVCofUxaaNKoIETiXCYh2xJ+OFq5jMHJQqftp34M4kNoLpTfUMPSwYtRUeTcSh1/5HodfJXIBi7troIFA==",
"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",
@@ -5884,9 +5888,9 @@
"integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w=="
},
"moment-timezone": {
"version": "0.5.34",
"resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.34.tgz",
"integrity": "sha512-3zAEHh2hKUs3EXLESx/wsgw6IQdusOT8Bxm3D9UrHPQR7zlMmzwybC8zHEM1tQ4LJwP7fcxrWr8tuBg05fFCbg==",
"version": "0.5.37",
"resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.37.tgz",
"integrity": "sha512-uEDzDNFhfaywRl+vwXxffjjq1q0Vzr+fcQpQ1bU0kbzorfS7zVtZnCnGc8mhWmF39d4g4YriF6kwA75mJKE/Zg==",
"requires": {
"moment": ">= 2.9.0"
}
@@ -6207,14 +6211,14 @@
"integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ=="
},
"pg": {
"version": "8.7.3",
"resolved": "https://registry.npmjs.org/pg/-/pg-8.7.3.tgz",
"integrity": "sha512-HPmH4GH4H3AOprDJOazoIcpI49XFsHCe8xlrjHkWiapdbHK+HLtbm/GQzXYAZwmPju/kzKhjaSfMACG+8cgJcw==",
"version": "8.8.0",
"resolved": "https://registry.npmjs.org/pg/-/pg-8.8.0.tgz",
"integrity": "sha512-UXYN0ziKj+AeNNP7VDMwrehpACThH7LUl/p8TDFpEUuSejCUIwGSfxpHsPvtM6/WXFy6SU4E5RG4IJV/TZAGjw==",
"requires": {
"buffer-writer": "2.0.0",
"packet-reader": "1.0.0",
"pg-connection-string": "^2.5.0",
"pg-pool": "^3.5.1",
"pg-pool": "^3.5.2",
"pg-protocol": "^1.5.0",
"pg-types": "^2.1.0",
"pgpass": "1.x"
@@ -6231,9 +6235,9 @@
"integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw=="
},
"pg-pool": {
"version": "3.5.1",
"resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.5.1.tgz",
"integrity": "sha512-6iCR0wVrro6OOHFsyavV+i6KYL4lVNyYAB9RD18w66xSzN+d8b66HiwuP30Gp1SH5O9T82fckkzsRjlrhD0ioQ==",
"version": "3.5.2",
"resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.5.2.tgz",
"integrity": "sha512-His3Fh17Z4eg7oANLob6ZvH8xIVen3phEZh2QuyrIl4dQSDVEabNducv6ysROKpDNPSD+12tONZVWfSgMvDD9w==",
"requires": {}
},
"pg-protocol": {
@@ -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.3",
"resolved": "https://registry.npmjs.org/sequelize/-/sequelize-6.21.3.tgz",
"integrity": "sha512-cJPrTTVCofUxaaNKoIETiXCYh2xJ+OFq5jMHJQqftp34M4kNoLpTfUMPSwYtRUeTcSh1/5HodfJXIBi7troIFA==",
"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",
+5 -5
View File
@@ -15,20 +15,20 @@
"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",
"moment": "^2.29.1",
"moment-timezone": "^0.5.34",
"moment-timezone": "^0.5.37",
"morgan": "^1.10.0",
"pg": "^8.7.3",
"pg": "^8.8.0",
"redis": "^3.1.2",
"sequelize": "^6.17.0",
"sequelize": "^6.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: 'facultyStatistics',
group_info: 'grants access to faculty statistics',
},
])
},
down: queryInterface => {
return queryInterface.bulkDelete('access_groups', [
{
group_code: 'facultyStatistics',
},
])
},
}
@@ -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',
},
])
},
}
+30 -2
View File
@@ -5,6 +5,8 @@ const { refreshAssociationsInRedis } = require('./services/studyrights')
const { getAllProgrammes } = require('./services/studyrights')
const { updateBasicView, updateStudytrackView } = require('./services/studyprogrammeUpdates')
const { findAndSaveTeachers } = require('./services/topteachers')
const { faculties } = require('./services/organisations')
const { updateFacultyOverview, updateFacultyProgressOverview } = require('./services/faculty/facultyUpdates')
const { isProduction } = require('./conf-backend')
const { getCurrentSemester } = require('./services/semesters')
const logger = require('./util/logger')
@@ -16,11 +18,36 @@ const refreshStudyrightAssociations = async () => {
logger.info('Studyright associations refreshed!')
}
const refreshFaculties = async () => {
logger.info('Refreshing faculty overview statistics for all faculties')
const facultyList = (await faculties()).filter(f => !['Y', 'H99', 'Y01', 'H92', 'H930'].includes(f.code))
let ready = 0
for (const faculty of facultyList) {
try {
await updateFacultyOverview(faculty.code, 'ALL')
await updateFacultyProgressOverview(faculty.code)
ready += 1
} catch (e) {
logger.error({ message: `Failed to update stats for faculty ${faculty?.code} with type ALL`, meta: e })
}
logger.info(`${ready}/${facultyList.length} faculties done`)
}
logger.info('Faculty stats refreshed!')
}
const refreshNewOverviews = async () => {
logger.info('Refreshing studyprogramme and studytrack overview statistics for all programmes')
// 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 {
@@ -100,7 +127,7 @@ const startCron = () => {
logger.info('Cronjob for refreshing stats started: runs at 3am every day.')
// refresh 3am every day
schedule('0 3 * * *', async () => {
for (const func of [refreshStatistics, refreshTrends, refreshNewOverviews]) {
for (const func of [refreshStatistics, refreshTrends, refreshNewOverviews, refreshFaculties]) {
await func()
}
})
@@ -111,4 +138,5 @@ module.exports = {
startCron,
refreshStatistics,
refreshTrends,
refreshFaculties,
}
@@ -27,11 +27,16 @@ const accessLogger = morgan((tokens, req, res) => {
'ms',
].join(' ')
const usingIamRights = user.iamRights.some(programmeCode => tokens['url'](req, res).includes(programmeCode))
const onlyIamRights = !user.isAdmin && user.rights.length === 0
logger.info(message, {
// don't log student list which might be huge
..._.omit(meta, ['studentsUserCanAccess']),
// pass this as a custom field so we can filter by it in graylog
isUsageStats: true,
// needed for Grafana IAM users panel
isIamRights: onlyIamRights || usingIamRights,
})
})
@@ -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,
},
+5 -1
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,7 +59,9 @@ module.exports = (app, url) => {
app.use(url, tags)
app.use(url, programmeModules)
app.use(url, studyProgramme)
app.use(url, faculties)
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)
app.use(`${url}/users`, auth.roles(['admin']), users)
+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
+188 -96
View File
@@ -1,131 +1,223 @@
const router = require('express').Router()
const { faculties } = require('../services/organisations')
const { combineFacultyBasics } = require('../services/facultyBasics')
const { combineFacultyCredits } = require('../services/facultyCredits')
const { degreeProgrammesOfFaculty, findFacultyProgrammeCodes } = require('../services/faculty')
const { combineFacultyThesisWriters } = require('../services/facultyThesisWriters')
const { combineFacultyBasics } = require('../services/faculty/facultyBasics')
const { combineFacultyCredits } = require('../services/faculty/facultyCredits')
const { findFacultyProgrammeCodes } = require('../services/faculty/faculty')
const { combineFacultyThesisWriters } = require('../services/faculty/facultyThesisWriters')
const { countGraduationTimes } = require('../services/faculty/facultyGraduationTimes')
const { updateFacultyOverview, updateFacultyProgressOverview } = require('../services/faculty/facultyUpdates')
const { combineFacultyStudentProgress } = require('../services/faculty/facultyStudentProgress')
const { combineFacultyStudents } = require('../services/faculty/facultyStudents')
const {
getFacultyProgrammes,
setFacultyProgrammes,
getBasicStats,
setBasicStats,
getCreditStats,
setCreditStats,
getThesisWritersStats,
setThesisWritersStats,
getGraduationStats,
setGraduationStats,
getFacultyStudentStats,
setFacultyStudentStats,
getFacultyProgressStats,
setFacultyProgressStats,
} = require('../services/faculty/facultyService')
const logger = require('../util/logger')
// Faculty uses a lot of tools designed for Study programme.
// Some of them have been copied here and slightly edited for faculty purpose.
router.get('/faculties', async (req, res) => {
const facultyList = await faculties()
const getProgrammes = async (code, programmeFilter) => {
const programmes = await getFacultyProgrammes(code, programmeFilter)
if (programmes) return programmes
let updatedProgrammes = await findFacultyProgrammeCodes(code, programmeFilter)
if (updatedProgrammes) updatedProgrammes = await setFacultyProgrammes(code, updatedProgrammes, programmeFilter)
return updatedProgrammes
}
router.get('/', async (req, res) => {
const ignore = ['Y', 'H99', 'Y01', 'H92', 'H930']
const facultyList = (await faculties()).filter(f => !ignore.includes(f.code))
facultyList.sort((a, b) => (a.name.fi > b.name.fi ? 1 : -1))
res.json(facultyList)
})
router.get('/faculties/:id/basicstats', async (req, res) => {
router.get('/:id/basicstats', async (req, res) => {
const code = req.params.id
const yearType = req.query?.year_type
const programmeFilter = req.query?.programme_filter
const specialGroups = req.query?.special_groups
if (!code) return res.status(422).end()
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()
let allBasics = {
id: code,
years: [],
programmeNames: {},
studentInfo: {
tableStats: [],
graphStats: [],
titles: ['', 'Started studying', 'Graduated', 'Transferred inside', 'Transferred away', 'Transferred to'],
programmeTableStats: {},
},
graduationInfo: {
tableStats: [],
graphStats: [],
titles: ['', 'All graduations', 'Bachelors', 'Masters', 'Doctors', 'Others'],
programmeTableStats: {},
},
status: 'DONE',
lastUpdated: '',
//all programmes are required for correct sorting of transfers
let allProgrammeCodes = []
if (programmeFilter === 'NEW_STUDY_PROGRAMMES') {
const allProgs = await getProgrammes(code, 'ALL_PROGRAMMES')
allProgs?.data.forEach(prog => allProgrammeCodes.push(prog.code))
} else {
wantedProgrammes?.data.forEach(prog => allProgrammeCodes.push(prog.code))
}
const programmes = await findFacultyProgrammeCodes(code)
if (programmes) {
await combineFacultyBasics(allBasics, code, programmes, yearType)
let updatedStats = await combineFacultyBasics(
code,
wantedProgrammes.data,
yearType,
allProgrammeCodes,
programmeFilter,
specialGroups
)
if (updatedStats) {
updatedStats = await setBasicStats(updatedStats, yearType, programmeFilter, specialGroups)
}
return res.json(allBasics)
return res.json(updatedStats)
})
router.get('/faculties/:id/creditstats', async (req, res) => {
router.get('/:id/creditstats', async (req, res) => {
const code = req.params.id
const yearType = req.query?.year_type
const specialGroups = 'SPECIAL_INCLUDED' // req.query?.special_groups
const specialGroups = req.query?.special_groups
const programmeFilter = req.query?.programme_filter
if (!code) return res.status(422).end()
const data = await getCreditStats(code, yearType, programmeFilter, specialGroups)
if (data) return res.json(data)
let counts = {}
let years = []
let allCredits = {
id: code,
years: [],
tableStats: [],
graphStats: [],
programmeTableStats: {},
programmeNames: {},
titles: [
'',
'Total',
'Major students credits',
'Non-major faculty students credits',
'Non-major other faculty students credits',
'Non-degree student credits',
],
status: 'DONE',
lastUpdated: '',
const programmes = await getProgrammes(code, programmeFilter)
if (!programmes) return res.status(422).end()
// 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)
}
const programmes = await degreeProgrammesOfFaculty(code)
if (programmes) {
await combineFacultyCredits(allCredits, programmes, yearType, specialGroups, counts, years)
}
let majors = []
let facultyNonMajor = []
let otherNonMajor = []
let nonDegree = []
years.forEach(year => {
majors.push(counts[year][1])
facultyNonMajor.push(counts[year][2])
otherNonMajor.push(counts[year][3])
nonDegree.push(counts[year][4])
})
allCredits.graphStats = [
{ name: 'Major students credits', data: majors },
{ name: 'Non-major faculty students credits', data: facultyNonMajor },
{ name: 'Non-major other faculty students credits', data: otherNonMajor },
{ name: 'Non-degree credits', data: nonDegree },
]
return res.json(allCredits)
return res.json(updatedStats)
})
router.get('/faculties/:id/thesisstats', async (req, res) => {
router.get('/:id/thesisstats', async (req, res) => {
const code = req.params.id
const yearType = req.query?.year_type
//const specialGroups = 'SPECIAL_INCLUDED'
const specialGroups = req.query?.special_groups
const programmeFilter = req.query?.programme_filter
if (!code) return res.status(422).end()
let allThesisWriters = {
id: code,
years: [],
tableStats: [],
graphStats: [],
programmeTableStats: {},
titles: ['', 'All thesis writers', 'Bachelors', 'Masters', 'Doctors', 'Others'],
programmeNames: {},
status: 'Done',
lastUpdated: '',
const data = await getThesisWritersStats(code, yearType, programmeFilter, specialGroups)
if (data) return res.json(data)
const programmes = await getProgrammes(code, programmeFilter)
if (!programmes) return res.status(422).end()
let updateStats = await combineFacultyThesisWriters(code, programmes.data, yearType, specialGroups)
if (updateStats) {
updateStats = await setThesisWritersStats(updateStats, yearType, programmeFilter, specialGroups)
}
const programmes = await degreeProgrammesOfFaculty(code)
if (programmes) {
await combineFacultyThesisWriters(allThesisWriters, programmes, code, yearType)
}
return res.json(allThesisWriters)
return res.json(updateStats)
})
router.get('/:id/graduationtimes', async (req, res) => {
const code = req.params.id
const programmeFilter = req.query?.programme_filter
if (!code) return res.status(422).end()
const data = await getGraduationStats(code, programmeFilter)
if (data) {
return res.json(data)
}
let updatedStats = await countGraduationTimes(code, programmeFilter)
if (updatedStats) {
updatedStats = await setGraduationStats(updatedStats, programmeFilter)
}
return res.json(updatedStats)
})
router.get('/:id/progressstats', async (req, res) => {
const code = req.params.id
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)
let updateStats = await combineFacultyStudentProgress(
code,
programmes.data,
allProgrammeCodes,
specialGroups,
graduated
)
if (updateStats) {
updateStats = await setFacultyProgressStats(updateStats, specialGroups, graduated)
}
return res.json(updateStats)
})
router.get('/:id/studentstats', async (req, res) => {
const code = req.params.id
const programmeFilter = req.query.programme_filter
const specialGroups = req.query?.special_groups
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()
//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)
}
return res.json(updateStats)
})
router.get('/:id/update_basicview', async (req, res) => {
const code = req.params.id
const statsType = req.query?.stats_type
if (code) {
let result = null
try {
result = await updateFacultyOverview(code, statsType)
} catch (e) {
logger.error(`Failed to update faculty ${code} basic tab stats for ${statsType}: ${e}`)
}
return res.json(result)
}
return res.status(422).end()
})
router.get('/:id/update_progressview', async (req, res) => {
const code = req.params.id
if (code) {
let result = null
try {
result = await updateFacultyProgressOverview(code)
} catch (e) {
logger.error(`Failed to update faculty ${code} progress tab stats: ${e}`)
}
return res.json(result)
}
return res.status(422).end()
})
module.exports = router
+24 -1
View File
@@ -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')
@@ -48,7 +49,11 @@ router.post('/v2/populationstatistics/courses', async (req, res) => {
if (req.body.selectedStudentsByYear) {
req.body.selectedStudents = encrypted
? req.body.selectedStudents
.filter(({ encryptedData }) => req.body.selectedStudentsByYear[year].includes(encryptedData))
.filter(
({ encryptedData }) =>
req.body.selectedStudentsByYear[year] &&
req.body.selectedStudentsByYear[year].includes(encryptedData)
)
.map(decrypt)
: req.body.selectedStudentsByYear[year]
}
@@ -77,6 +82,20 @@ router.post('/v2/populationstatistics/courses', async (req, res) => {
return
}
//These cause problems in front (where they were originally calculated)
// Do them here for the moment
for (const course of result.coursestatistics) {
// course.students.notParticipated = {}
course.students.didNotPass = {}
for (const student of Object.keys(course.students.all)) {
if (
!Object.keys(course.students.passed).includes(student) &&
!Object.keys(course.students.retryPassed).includes(student)
)
course.students.didNotPass[student] = true
}
}
res.json(result)
}
})
@@ -250,6 +269,7 @@ router.get('/v3/populationstatistics', async (req, res) => {
...student,
firstnames: '',
lastname: '',
phone_number: '',
started: null,
iv,
studentNumber: encryptedData,
@@ -362,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,
@@ -411,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
+1 -1
View File
@@ -450,7 +450,7 @@ const codeLikeTerm = code =>
: {
code: {
// Starts with code or has AY/A in front of the code
[Op.regexp]: `^(AY|ay|A|a)?${code}`,
[Op.iRegexp]: `^(AY|ay|A|a)?${code}`,
},
}
-233
View File
@@ -1,233 +0,0 @@
const Sequelize = require('sequelize')
const { Op } = Sequelize
const {
ElementDetail,
Organization,
ProgrammeModule,
Studyright,
StudyrightElement,
Student,
Transfer,
Credit,
Course,
} = require('../models')
const {
facultyFormatStudyright,
facultyFormatProgramme,
formatFacultyTransfer,
formatFacultyThesisWriter,
formatOrganization,
} = require('./facultyHelpers')
const startedStudyrights = async (faculty, since) =>
(
await Studyright.findAll({
include: [
{
model: Student,
attributes: ['studentnumber'],
required: true,
},
{
model: StudyrightElement,
required: true,
include: {
model: ElementDetail,
required: true,
},
},
],
where: {
faculty_code: faculty,
studystartdate: {
[Op.gte]: since,
},
student_studentnumber: { [Op.not]: null },
},
})
).map(facultyFormatStudyright)
const graduatedStudyrights = async (faculty, since) =>
(
await Studyright.findAll({
include: [
{
model: Student,
attributes: ['studentnumber'],
required: true,
},
{
model: StudyrightElement,
required: true,
include: {
model: ElementDetail,
required: true,
},
},
],
where: {
faculty_code: faculty,
enddate: {
[Op.gte]: since,
},
graduated: 1,
student_studentnumber: { [Op.not]: null },
},
})
).map(facultyFormatStudyright)
const transferredInsideFaculty = async (programmes, since) =>
(
await Transfer.findAll({
where: {
transferdate: {
[Op.gte]: since,
},
sourcecode: programmes,
targetcode: programmes,
},
})
).map(formatFacultyTransfer)
const transferredAway = async (programmes, since) =>
(
await Transfer.findAll({
where: {
transferdate: {
[Op.gte]: since,
},
sourcecode: programmes,
targetcode: {
[Op.notIn]: programmes,
},
},
})
).map(formatFacultyTransfer)
const transferredTo = async (programmes, since) =>
(
await Transfer.findAll({
where: {
transferdate: {
[Op.gte]: since,
},
sourcecode: {
[Op.notIn]: programmes,
},
targetcode: programmes,
},
})
).map(formatFacultyTransfer)
const degreeProgrammesOfFaculty = async facultyCode =>
(
await ProgrammeModule.findAll({
attributes: ['code', 'name'],
include: {
model: Organization,
where: {
code: facultyCode,
},
},
})
).map(facultyFormatProgramme)
const facultyOgranizationId = async faculty => {
return await Organization.findOne({
attributes: ['id'],
where: {
code: faculty,
},
})
}
const getChildOrganizations = async facultyId =>
(
await Organization.findAll({
attributes: ['id', 'name', 'code', 'parent_id'],
where: {
parent_id: facultyId,
},
})
).map(formatOrganization)
const thesisWriters = async (providers, since, thesisTypes) =>
(
await Credit.findAll({
attributes: ['id', 'course_code', 'credits', 'attainment_date', 'student_studentnumber'],
include: {
model: Course,
attributes: ['code', 'course_unit_type'],
required: true,
where: {
course_unit_type: {
[Op.in]: thesisTypes,
},
},
include: {
model: Organization,
attributes: ['code'],
required: true,
where: {
code: {
[Op.in]: providers,
},
},
},
},
where: {
credittypecode: {
[Op.notIn]: [10, 9, 7],
},
isStudyModule: {
[Op.not]: true,
},
attainment_date: {
[Op.gte]: since,
},
student_studentnumber: { [Op.not]: null },
},
})
).map(formatFacultyThesisWriter)
// Some programme modules are not directly associated to a faculty (organization).
// Some have intermediate organizations, such as department, so the connection must be diggeg up
const findFacultyProgrammeCodes = async faculty => {
let allProgrammes = []
let allProgrammeCodes = []
const directAssociationProgrammes = await degreeProgrammesOfFaculty(faculty)
allProgrammes = allProgrammes.concat(directAssociationProgrammes)
allProgrammeCodes = allProgrammeCodes.concat(directAssociationProgrammes.map(prog => prog.code))
// find faculty's organization id and its direct child organizations
const { id } = await facultyOgranizationId(faculty)
const facultyChildOrganizations = await getChildOrganizations(id)
// get programme modules that have a found child as organization(_id)
for (const org of facultyChildOrganizations) {
const childAssociationProgrammes = await degreeProgrammesOfFaculty(org.code)
if (childAssociationProgrammes.length > 0) {
childAssociationProgrammes.forEach(prog => {
if (!(prog.code in allProgrammeCodes)) {
allProgrammes = allProgrammes.concat([prog])
allProgrammeCodes = allProgrammeCodes.concat([prog.code])
}
})
}
}
return allProgrammes
}
module.exports = {
startedStudyrights,
graduatedStudyrights,
transferredInsideFaculty,
transferredAway,
transferredTo,
degreeProgrammesOfFaculty,
thesisWriters,
findFacultyProgrammeCodes,
}
@@ -0,0 +1,401 @@
const Sequelize = require('sequelize')
const { Op } = Sequelize
const {
ElementDetail,
Organization,
ProgrammeModule,
Studyright,
StudyrightElement,
SemesterEnrollment,
Student,
Transfer,
Credit,
Course,
} = require('../../models')
const {
facultyFormatStudyright,
facultyFormatProgramme,
formatFacultyTransfer,
formatFacultyThesisWriter,
formatOrganization,
isNewProgramme,
mapCodesToIds,
facultyProgrammeStudents,
} = require('./facultyHelpers')
const getTransferredToAndAway = async (programmeCodes, allProgrammeCodes, since) => {
const awayTransfers = await transferredAway(programmeCodes, allProgrammeCodes, since)
const toTransfers = await transferredTo(programmeCodes, allProgrammeCodes, since)
return [...toTransfers, ...awayTransfers]
}
const getTransferredInside = async (programmeCodes, allProgrammeCodes, since) => {
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({
include: [
{
model: Student,
attributes: ['studentnumber'],
required: true,
},
{
model: StudyrightElement,
required: true,
include: {
model: ElementDetail,
required: true,
},
},
],
where: {
faculty_code: faculty,
studystartdate: {
[Op.gte]: since,
},
student_studentnumber: { [Op.not]: null },
...studyRightWhere,
},
})
).map(facultyFormatStudyright)
const graduatedStudyrights = async (faculty, since, studyrightWhere) =>
(
await Studyright.findAll({
include: [
{
model: Student,
attributes: ['studentnumber'],
required: true,
},
{
model: StudyrightElement,
required: true,
include: {
model: ElementDetail,
required: true,
},
},
],
where: {
faculty_code: faculty,
enddate: {
[Op.gte]: since,
},
graduated: 1,
student_studentnumber: { [Op.not]: null },
...studyrightWhere,
},
})
).map(facultyFormatStudyright)
const studyrightsByRightStartYear = async (faculty, since, graduated = 1) =>
(
await Studyright.findAll({
include: [
{
model: Student,
attributes: ['studentnumber'],
required: true,
},
{
model: StudyrightElement,
required: true,
include: {
model: ElementDetail,
required: true,
},
},
],
where: {
faculty_code: faculty,
startdate: {
[Op.gte]: since,
},
graduated: graduated,
student_studentnumber: { [Op.not]: null },
},
})
).map(facultyFormatStudyright)
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({
where: {
studyrightid: id,
extentcode: 2,
},
})
}
const transferredInsideFaculty = async (programmes, allProgrammeCodes, since) =>
(
await Transfer.findAll({
where: {
transferdate: {
[Op.gte]: since,
},
sourcecode: allProgrammeCodes,
targetcode: programmes,
},
})
).map(formatFacultyTransfer)
const transferredAway = async (programmes, allProgrammeCodes, since) =>
(
await Transfer.findAll({
where: {
transferdate: {
[Op.gte]: since,
},
sourcecode: programmes,
targetcode: {
[Op.notIn]: allProgrammeCodes,
},
},
})
).map(formatFacultyTransfer)
const transferredTo = async (programmes, allProgrammeCodes, since) =>
(
await Transfer.findAll({
where: {
transferdate: {
[Op.gte]: since,
},
sourcecode: {
[Op.notIn]: allProgrammeCodes,
},
targetcode: programmes,
},
})
).map(formatFacultyTransfer)
const degreeProgrammesOfFaculty = async facultyCode =>
(
await ProgrammeModule.findAll({
attributes: ['code', 'name'],
include: {
model: Organization,
where: {
code: {
[Op.startsWith]: facultyCode,
},
},
},
})
).map(facultyFormatProgramme)
const facultyOgranizationId = async faculty => {
return await Organization.findOne({
attributes: ['id'],
where: {
code: faculty,
},
})
}
const getChildOrganizations = async facultyId =>
(
await Organization.findAll({
attributes: ['id', 'name', 'code', 'parent_id'],
where: {
parent_id: facultyId,
},
})
).map(formatOrganization)
const thesisWriters = async (provider, since, thesisTypes, students) =>
(
await Credit.findAll({
attributes: ['id', 'course_code', 'attainment_date', 'student_studentnumber'],
include: {
model: Course,
attributes: ['course_unit_type'],
required: true,
where: {
course_unit_type: {
[Op.in]: thesisTypes,
},
},
include: {
model: Organization,
attributes: [],
required: true,
where: {
code: provider,
},
},
},
where: {
credittypecode: 4,
isStudyModule: {
[Op.not]: true,
},
attainment_date: {
[Op.gte]: since,
},
student_studentnumber: students.length > 0 ? students : { [Op.not]: null },
},
})
).map(formatFacultyThesisWriter)
// Some programme modules are not directly associated to a faculty (organization).
// Some have intermediate organizations, such as department, so the connection must be digged up
const findFacultyProgrammeCodes = async (faculty, programmeFilter) => {
let allProgrammes = []
let allProgrammeCodes = []
const directAssociationProgrammes = await degreeProgrammesOfFaculty(faculty)
allProgrammes = allProgrammes.concat(directAssociationProgrammes)
allProgrammeCodes = allProgrammeCodes.concat(directAssociationProgrammes.map(prog => prog.code))
// find faculty's organization id and its direct child organizations
const { id } = await facultyOgranizationId(faculty)
const facultyChildOrganizations = await getChildOrganizations(id)
// get programme modules that have a faculty child as organization(_id)
for (const org of facultyChildOrganizations) {
const childAssociationProgrammes = await degreeProgrammesOfFaculty(org.code)
if (childAssociationProgrammes.length > 0) {
childAssociationProgrammes.forEach(prog => {
if (!(prog.code in allProgrammeCodes)) {
allProgrammes = allProgrammes.concat([prog])
allProgrammeCodes = allProgrammeCodes.concat([prog.code])
}
})
} else {
// dig deeper
const grandChildren = await getChildOrganizations(org.id)
if (grandChildren.length > 0) {
for (const gcOrg of grandChildren) {
const associatedProgrammes = await degreeProgrammesOfFaculty(gcOrg.code)
if (associatedProgrammes.length > 0) {
associatedProgrammes.forEach(prog => {
if (!(prog.code in allProgrammeCodes)) {
allProgrammes = allProgrammes.concat([prog])
allProgrammeCodes = allProgrammeCodes.concat([prog.code])
}
})
}
}
}
}
}
if (programmeFilter === 'NEW_STUDY_PROGRAMMES') {
allProgrammes = allProgrammes.filter(prog => isNewProgramme(prog.code))
}
mapCodesToIds(allProgrammes)
return allProgrammes
}
module.exports = {
startedStudyrights,
graduatedStudyrights,
studyrightsByRightStartYear,
hasMasterRight,
transferredInsideFaculty,
transferredAway,
transferredTo,
degreeProgrammesOfFaculty,
thesisWriters,
findFacultyProgrammeCodes,
facultyOgranizationId,
getTransferredToAndAway,
getTransferredInside,
getStudyRightsByExtent,
getStudentsByStudentnumbers,
getTransfers,
}
@@ -0,0 +1,473 @@
const { indexOf } = require('lodash')
const moment = require('moment')
const {
startedStudyrights,
graduatedStudyrights,
transferredInsideFaculty,
transferredAway,
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)
// choose only the earlier started one, so we don't count start of masters as starting in faculty
let rightsToCount = {}
studyrights.forEach(right => {
const id = right.studyrightid.slice(0, -2)
const start = new Date(right.studystartdate)
if (id in rightsToCount) {
if (new Date(rightsToCount[id].studystartdate) > start) {
rightsToCount[id] = right
}
} else {
rightsToCount[id] = right
}
})
return Object.values(rightsToCount)
}
const addProgramme = async (
stats,
programme,
tableStats,
transferYear,
index,
allBasics,
facultyProgrammes,
includeAllSpecials
) => {
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[progId][year] = [0, 0, 0]))
} else {
Object.keys(tableStats).forEach(year => (stats[progId][year] = [0]))
}
}
stats[progId][transferYear][index] += 1
if (!(progId in allBasics.programmeNames)) {
allBasics.programmeNames[progId] = { ...name, code: code }
}
}
const getFacultyStarters = async (
faculty,
transfersToOrAwayStudyrights,
insideTransfersStudyrights,
since,
isAcademicYear,
yearsArray,
graphStats,
tableStats,
counts,
allBasics,
programmeData,
programmeFilter,
includeAllSpecials
) => {
const startedGraphStats = [...graphStats]
const startedTableStats = { ...tableStats }
const studyrightWhere = getExtentFilter(includeAllSpecials)
const studyrights = await startedStudyrights(faculty, since, studyrightWhere)
let filteredStudyrights = filterDuplicateStudyrights(studyrights)
const keys = Object.keys(codes)
const programmeTableStats = {}
if (!includeAllSpecials) {
filteredStudyrights = filteredStudyrights.filter(
s => !checkTransfers(s, insideTransfersStudyrights, transfersToOrAwayStudyrights)
)
}
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)
startedGraphStats[indexOf(yearsArray, startYear)] += 1
startedTableStats[startYear] += 1
if (!(programmeId in programmeTableStats)) {
programmeTableStats[programmeId] = { ...tableStats }
}
programmeTableStats[programmeId][startYear] += 1
if (!(programmeId in allBasics.programmeNames)) {
allBasics.programmeNames[programmeId] = { ...programmeName, code: programme }
}
}
})
allBasics.studentInfo.graphStats.push({ name: 'Started studying (new in faculty)', data: startedGraphStats })
programmeData['started'] = programmeTableStats
Object.keys(startedTableStats).forEach(year => {
counts[year] = [startedTableStats[year]]
})
}
const getFacultyGraduates = async (
faculty,
transfersToOrAwayStudyrights,
insideTransfersStudyrights,
since,
isAcademicYear,
yearsArray,
graphStats,
tableStats,
allBasics,
counts,
countsGraduations,
programmeData,
programmeFilter,
includeAllSpecials
) => {
const graduatedGraphStats = [[...graphStats], [...graphStats], [...graphStats], [...graphStats], [...graphStats]]
const graduatedTableStats = {}
const keys = Object.keys(codes)
let studyrightWhere = getExtentFilter(includeAllSpecials)
let graduatedRights = await graduatedStudyrights(faculty, since, studyrightWhere)
const programmeTableStats = {}
Object.keys(tableStats).forEach(year => (graduatedTableStats[year] = [0, 0, 0, 0, 0]))
if (!includeAllSpecials) {
graduatedRights = graduatedRights.filter(
s => !checkTransfers(s, insideTransfersStudyrights, transfersToOrAwayStudyrights)
)
}
graduatedRights.forEach(({ enddate, extentcode, studyrightElements }) => {
const { programme, programmeName } = findRightProgramme(studyrightElements, 'graduated')
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 (!(programmeId in programmeTableStats)) {
programmeTableStats[programmeId] = {}
Object.keys(tableStats).forEach(year => (programmeTableStats[programmeId][year] = [0, 0, 0, 0, 0]))
}
programmeTableStats[programmeId][endYear][0] += 1
if (extentcode === 1) {
graduatedGraphStats[1][indexOf(yearsArray, endYear)] += 1
graduatedTableStats[endYear][1] += 1
programmeTableStats[programmeId][endYear][1] += 1
} else if (extentcode === 2) {
graduatedGraphStats[2][indexOf(yearsArray, endYear)] += 1
graduatedTableStats[endYear][2] += 1
programmeTableStats[programmeId][endYear][2] += 1
} else if (extentcode === 4) {
graduatedGraphStats[3][indexOf(yearsArray, endYear)] += 1
graduatedTableStats[endYear][3] += 1
programmeTableStats[programmeId][endYear][3] += 1
} else {
graduatedGraphStats[4][indexOf(yearsArray, endYear)] += 1
graduatedTableStats[endYear][4] += 1
programmeTableStats[programmeId][endYear][4] += 1
}
if (!(programmeId in allBasics.programmeNames)) {
allBasics.programmeNames[programmeId] = { code: programme, ...programmeName }
}
}
})
allBasics.studentInfo.graphStats.push({ name: 'Graduated', data: graduatedGraphStats[0] })
allBasics.graduationInfo.graphStats.push({ name: 'Bachelors', data: graduatedGraphStats[1] })
allBasics.graduationInfo.graphStats.push({ name: 'Masters', data: graduatedGraphStats[2] })
allBasics.graduationInfo.graphStats.push({ name: 'Doctors', data: graduatedGraphStats[3] })
allBasics.graduationInfo.graphStats.push({ name: 'Others', data: graduatedGraphStats[4] })
programmeData['graduated'] = programmeTableStats
Object.keys(graduatedTableStats).forEach(year => {
counts[year] = counts[year].concat(graduatedTableStats[year][0])
countsGraduations[year] = graduatedTableStats[year]
})
}
// 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,
allProgrammeCodes,
since,
isAcademicYear,
yearsArray,
graphStats,
tableStats,
allBasics,
counts,
programmeData,
includeAllSpecials,
faculty
) => {
const insideTransfers = await getInsideTransfers(
programmeCodes,
allProgrammeCodes,
since,
includeAllSpecials,
faculty
)
let transferGraphStats = []
let transferTableStats = {}
if (includeAllSpecials) {
transferGraphStats = [[...graphStats], [...graphStats], [...graphStats]]
Object.keys(tableStats).forEach(year => (transferTableStats[year] = [0, 0, 0]))
} else {
transferGraphStats = [[...graphStats]]
Object.keys(tableStats).forEach(year => (transferTableStats[year] = [0]))
}
const programmeTableStats = {}
// inside transfer is counted towards target
for (const { transferdate, targetcode } of insideTransfers) {
const transferYear = defineYear(transferdate, isAcademicYear)
transferGraphStats[0][indexOf(yearsArray, transferYear)] += 1
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)
const toTransfers = await transferredTo(programmeCodes, allProgrammeCodes, since)
for (const { transferdate, sourcecode } of awayTransfers) {
const transferYear = defineYear(transferdate, isAcademicYear)
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)
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
)
}
}
}
allBasics.studentInfo.graphStats.push({ name: 'Transferred inside', data: transferGraphStats[0] })
if (includeAllSpecials) {
allBasics.studentInfo.graphStats.push({ name: 'Transferred away', data: transferGraphStats[1] })
allBasics.studentInfo.graphStats.push({ name: 'Transferred to', data: transferGraphStats[2] })
}
programmeData['transferred'] = programmeTableStats
Object.keys(transferTableStats).forEach(year => {
counts[year] = counts[year].concat(transferTableStats[year])
})
}
const combineFacultyBasics = async (faculty, programmes, yearType, allProgrammeCodes, programmeFilter, special) => {
let counts = {}
let countsGraduations = {}
let years = []
let programmeData = {}
const isAcademicYear = yearType === 'ACADEMIC_YEAR'
const includeAllSpecials = special === 'SPECIAL_INCLUDED'
const since = isAcademicYear ? new Date('2017-08-01') : new Date('2017-01-01')
const yearsArray = getYearsArray(since.getFullYear(), isAcademicYear)
const { graphStats, tableStats } = getStatsBasis(yearsArray)
Object.keys(tableStats).forEach(year => years.push(year))
const wantedProgrammeCodes = programmes.map(prog => prog.code)
let allBasics = {
id: faculty,
years: [],
programmeNames: {},
studentInfo: {
tableStats: [],
graphStats: [],
titles: includeAllSpecials
? ['', 'Started studying', 'Graduated', 'Transferred inside', 'Transferred away', 'Transferred to']
: ['', 'Started studying', 'Graduated', 'Transferred inside'],
programmeTableStats: {},
},
graduationInfo: {
tableStats: [],
graphStats: [],
titles: ['', 'All', 'Bachelors', 'Masters', 'Doctors', 'Others'],
programmeTableStats: {},
},
}
const transfersToAwayStudyrights = await getTransferredToAndAway(wantedProgrammeCodes, allProgrammeCodes, since)
const transferInsideStudyrights = await getTransferredInside(wantedProgrammeCodes, allProgrammeCodes, since)
// Started studying in faculty
await getFacultyStarters(
faculty,
transfersToAwayStudyrights,
transferInsideStudyrights,
since,
isAcademicYear,
yearsArray,
graphStats,
tableStats,
counts,
allBasics,
programmeData,
programmeFilter,
includeAllSpecials
)
// Graduated in faculty
await getFacultyGraduates(
faculty,
transfersToAwayStudyrights,
transferInsideStudyrights,
since,
isAcademicYear,
yearsArray,
graphStats,
tableStats,
allBasics,
counts,
countsGraduations,
programmeData,
programmeFilter,
includeAllSpecials
)
// Transfers
await getFacultyTransfers(
programmes,
wantedProgrammeCodes,
allProgrammeCodes,
since,
isAcademicYear,
yearsArray,
graphStats,
tableStats,
allBasics,
counts,
programmeData,
includeAllSpecials,
faculty
)
// combine tableStats from all categories
allBasics.years = years
years.forEach(year => {
allBasics.studentInfo.tableStats.push([year, ...counts[year]])
allBasics.graduationInfo.tableStats.push([year, ...countsGraduations[year]])
})
allBasics.studentInfo.tableStats.reverse()
allBasics.graduationInfo.tableStats.reverse()
//combine programme level tablestats
// all programmes are not present in all data types, check all found programmes and patch missing details with 0
let allCodes = Object.keys(programmeData.started).concat(
Object.keys(programmeData.graduated),
Object.keys(programmeData.transferred)
)
allCodes = [...new Set(allCodes)]
let studentInfo = {}
let graduationInfo = {}
allCodes.forEach(code => {
if (!(code in studentInfo)) studentInfo[code] = {}
for (const year of years) {
// started
if (code in programmeData.started) {
studentInfo[code][year] = [year, programmeData.started[code][year]]
} else {
studentInfo[code][year] = [year, 0]
}
// graduated total
if (code in programmeData.graduated) {
studentInfo[code][year] = studentInfo[code][year].concat([programmeData.graduated[code][year][0]])
if (!(code in graduationInfo)) graduationInfo[code] = {}
graduationInfo[code][year] = [year, ...programmeData.graduated[code][year]]
} else {
studentInfo[code][year] = studentInfo[code][year].concat([0])
}
// trasferred
if (code in programmeData.transferred) {
studentInfo[code][year] = studentInfo[code][year].concat(programmeData.transferred[code][year])
} else {
if (includeAllSpecials) {
studentInfo[code][year] = studentInfo[code][year].concat([0, 0, 0])
} else {
studentInfo[code][year] = studentInfo[code][year].concat([0])
}
}
}
})
Object.keys(studentInfo).forEach(prog => {
allBasics.studentInfo.programmeTableStats[prog] = [...Object.values(studentInfo[prog])].reverse()
})
Object.keys(graduationInfo).forEach(prog => {
allBasics.graduationInfo.programmeTableStats[prog] = Object.values(graduationInfo[prog]).reverse()
})
return allBasics
}
module.exports = { combineFacultyBasics }
@@ -1,5 +1,5 @@
const { indexOf } = require('lodash')
const { mapToProviders } = require('../util/utils')
const { mapToProviders } = require('../../util/utils')
const {
getStatsBasis,
defineYear,
@@ -8,14 +8,13 @@ const {
getYearsArray,
getStartDate,
isSpecialGroupCredit,
tableTitles,
} = require('./studyprogrammeHelpers')
} = require('../studyprogrammeHelpers')
const {
getStudyRights,
getCreditsForStudyProgramme,
allTransfers,
getCourseCodesForStudyProgramme,
} = require('./studyprogramme')
} = require('../studyprogramme')
const isFacultyNonMajorCredit = (studyrights, attainment_date, facultyProgrammes) => {
let right = ''
@@ -56,8 +55,8 @@ const getFacultyRegularCreditStats = async ({
const credits = await getCreditsForStudyProgramme(courses, since)
const students = [...new Set(credits.map(({ student_studentnumber }) => student_studentnumber))]
let studyrights = await getStudyRights(students, since)
const transfers = (await allTransfers(studyprogramme, since)).map(t => t.studyrightid)
let studyrights = await getStudyRights(students)
const transfers = (await allTransfers(studyprogramme, since)).map(t => t.studyrightid).filter(s => s !== 'undefined')
if (!includeAllSpecials) {
studyrights = studyrights.filter(s => !transfers.includes(s.studyrightid))
}
@@ -126,7 +125,6 @@ const getFacultyCreditStatsForStudytrack = async ({ studyprogramme, facultyProgr
const { majors, facultyNonMajors, otherNonMajors, nonDegree } = await getFacultyRegularCreditStats(queryParameters)
const reversedYears = getYearsArray(since.getFullYear(), isAcademicYear).reverse()
const titles = tableTitles['credits'][includeAllSpecials ? 'SPECIAL_INCLUDED' : 'SPECIAL_EXCLUDED']
const dataFound = [majors, facultyNonMajors, otherNonMajors].some(d => d.graphStats.length) // , transferred
if (!dataFound) return null
@@ -144,48 +142,44 @@ const getFacultyCreditStatsForStudytrack = async ({ studyprogramme, facultyProgr
otherNonMajors.tableStats[year],
nonDegree.tableStats[year],
]
: [
year,
majors.tableStats[year] +
facultyNonMajors.tableStats[year] +
otherNonMajors.tableStats[year] +
nonDegree.tableStats[year],
majors.tableStats[year],
]
: [year, majors.tableStats[year], majors.tableStats[year]]
)
const graphStats = [
{
name: 'Major students credits',
data: majors.graphStats,
},
{
name: 'Non-major faculty students credits',
data: facultyNonMajors.graphStats,
},
{
name: 'Non-major other faculty students credits 2',
data: otherNonMajors.graphStats,
},
{
name: 'Non-degree credits',
data: nonDegree.graphStats,
},
]
const graphStats = includeAllSpecials
? [
{
name: 'Major students credits',
data: majors.graphStats,
},
{
name: 'Non-major faculty students credits',
data: facultyNonMajors.graphStats,
},
{
name: 'Non-major other faculty students credits 2',
data: otherNonMajors.graphStats,
},
{
name: 'Non-degree credits',
data: nonDegree.graphStats,
},
]
: [
{
name: 'Major students credits',
data: majors.graphStats,
},
]
return {
id: studyprogramme,
years,
tableStats,
graphStats,
titles,
}
}
const getProgrammeCredits = async (code, yearType, specialGroups, facultyProgrammes) => {
// redis for faculty? Create faculty versions for out-commented functions
// const data = await getCreditStats(code, yearType, specialGroups)
// if (data) return data
const updatedStats = await getFacultyCreditStatsForStudytrack({
studyprogramme: code,
facultyProgrammes,
@@ -195,17 +189,41 @@ const getProgrammeCredits = async (code, yearType, specialGroups, facultyProgram
},
})
// if (updatedStats) await setCreditStats(updatedStats, yearType, specialGroups)
return updatedStats
}
const combineFacultyCredits = async (allCredits, programmes, yearType, specialGroups, counts, years) => {
const progCodes = programmes.map(p => p.code)
const combineFacultyCredits = async (faculty, programmes, allProgrammes, yearType, specialGroups) => {
let counts = {}
let years = []
const titles =
specialGroups === 'SPECIAL_INCLUDED'
? [
'',
'Total',
'Major students credits',
'Non-major faculty students credits',
'Non-major other faculty students credits',
'Non-degree student credits',
]
: ['', 'Total', 'Major students credits']
let allCredits = {
id: faculty,
years: [],
tableStats: [],
graphStats: [],
programmeTableStats: {},
programmeNames: {},
titles,
}
const progCodes = 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
@@ -221,6 +239,7 @@ const combineFacultyCredits = async (allCredits, programmes, yearType, specialGr
})
}
}
// save table stats and graph stats
years.forEach(year => {
allCredits.tableStats.push([year, ...counts[year]])
@@ -228,6 +247,30 @@ const combineFacultyCredits = async (allCredits, programmes, yearType, specialGr
years.sort()
allCredits.years = years
let majors = []
let facultyNonMajor = []
let otherNonMajor = []
let nonDegree = []
years.forEach(year => {
majors.push(counts[year][1])
facultyNonMajor.push(counts[year][2])
otherNonMajor.push(counts[year][3])
nonDegree.push(counts[year][4])
})
allCredits.graphStats =
specialGroups === 'SPECIAL_INCLUDED'
? [
{ name: 'Major students credits', data: majors },
{ name: 'Non-major faculty students credits', data: facultyNonMajor },
{ name: 'Non-major other faculty students credits', data: otherNonMajor },
{ name: 'Non-degree credits', data: nonDegree },
]
: [{ name: 'Major students credits', data: majors }]
return allCredits
}
module.exports = { combineFacultyCredits, getFacultyCreditStatsForStudytrack }
@@ -0,0 +1,344 @@
const moment = require('moment')
const { graduatedStudyrights, studyrightsByRightStartYear, hasMasterRight } = require('./faculty')
const { findRightProgramme, isNewProgramme } = require('./facultyHelpers')
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.startdate
return null
}
const getProgrammeObjectBasis = (years, levels, emptyObject = true) => {
return levels.reduce(
(acc, level) => ({ ...acc, [level]: years.reduce((acc, year) => ({ ...acc, [year]: emptyObject ? {} : 0 }), {}) }),
{}
)
}
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)) {
return false
}
if (programme === 'KH60_001') {
return !elements.some(el => ['EDUK-VO', '0371', '620050-ba', '620030-ba'].includes(el.dataValues.code))
}
if (programme === 'KH55_001') {
const result = await hasMasterRight(studyrightid.replace(/-1$/, '-2'))
if (!result) {
return false
}
}
return true
}
const addGraduation = async (
extentcode,
startdate,
studyrightid,
enddate,
studentnumber,
graduationAmounts,
graduationTimes,
year,
programmes,
programme
) => {
let actualStartdate = startdate
let level = null
if (programme === 'MH30_001' || programme === 'MH30_003') {
level = 'bcMsCombo'
} else if (extentcode === 1) {
level = 'bachelor'
} else if (extentcode === 2) {
if (studyrightid.slice(-2) === '-2') {
level = 'bcMsCombo'
actualStartdate = await findBachelorStartdate(studyrightid.replace(/-2$/, '-1'))
} else {
level = 'master'
}
} else if (extentcode === 4) {
level = 'doctor'
} else if (extentcode === 3) {
level = 'licentiate'
} else {
return
}
const absoluteTimeToGraduation = moment(enddate).diff(moment(actualStartdate), 'months')
const statutoryAbsences = await getStatutoryAbsences(studentnumber, actualStartdate, enddate)
const timeToGraduation = absoluteTimeToGraduation - statutoryAbsences
graduationAmounts[level][year] += 1
graduationTimes[level][year] = [...graduationTimes[level][year], timeToGraduation]
if (!(programme in programmes[level][year]))
programmes[level][year][programme] = { graduationTimes: [], graduationAmounts: 0 }
programmes[level][year][programme].graduationAmounts += 1
programmes[level][year][programme].graduationTimes = [
...programmes[level][year][programme].graduationTimes,
timeToGraduation,
]
}
const getClassSizes = async (faculty, since, classSizes, programmeFilter, years) => {
const studyrights = await studyrightsByRightStartYear(faculty, since, [0, 1])
// get all received studyrights for each year
// a transferred student is counted into new programmes class size i.e.
// students who received studyright on year X and graduated/will be graduating from programme Y
// if we only counted those who started fresh in the new programme we could get a bigger number of graduates
// than the class size, as the graduatation time count only looks at a degree as whole studyright
for (const right of studyrights) {
const { startdate, studyrightid, extentcode, studyrightElements } = right
const { programme } = findRightProgramme(studyrightElements, 'graduated')
if (programmeFilter === 'NEW_STUDY_PROGRAMMES' && !isNewProgramme(programme)) continue
let actualStartdate = startdate
let level = null
if (programme === 'MH30_001' || programme === 'MH30_003') {
level = 'bcMsCombo'
} else if (extentcode === 1) {
level = 'bachelor'
} else if (extentcode === 2) {
if (studyrightid.slice(-2) === '-2') {
level = 'bcMsCombo'
actualStartdate = await findBachelorStartdate(studyrightid.replace(/-2$/, '-1'))
} else {
level = 'master'
}
} else if (extentcode === 4) {
level = 'doctor'
} else if (extentcode === 3) {
level = 'licentiate'
} else {
continue
}
const startYear = defineYear(actualStartdate, false)
// On faculty level, count started bachelor rights to BcMsCombo as well --> useful class size even if master studies have yet not started
// On programme level BcMsCombo sizes are iffy as many students haven't started masters yet --> no Ms programme element to count
if (level !== 'bcMsCombo') {
classSizes[level][startYear] += 1
if (level === 'bachelor' && (await hasMS(programme, studyrightElements, studyrightid))) {
classSizes['bcMsCombo'][startYear] += 1
}
}
if (!(programme in classSizes.programmes)) {
if (extentcode === 2) {
classSizes.programmes[programme] = {}
classSizes.programmes[programme]['bcMsCombo'] = getYearsObject({ years, emptyArrays: false })
classSizes.programmes[programme]['master'] = getYearsObject({ years, emptyArrays: false })
} else {
classSizes.programmes[programme] = getYearsObject({ years, emptyArrays: false })
}
}
if (extentcode === 2) {
classSizes.programmes[programme][level][startYear] += 1
} else {
classSizes.programmes[programme][startYear] += 1
}
}
}
const count = async (faculty, since, years, yearsList, levels, programmeFilter, programmeNames, goals, mode) => {
let graduationAmounts = {}
let graduationTimes = {}
let programmes = getProgrammeObjectBasis(yearsList, levels)
let data = {
medians: {},
programmes: {
medians: getProgrammeObjectBasis(yearsList, levels),
},
}
levels.forEach(level => {
graduationAmounts[level] = getYearsObject({ years: yearsList })
graduationTimes[level] = getYearsObject({ years: yearsList, emptyArrays: true })
data.medians[level] = []
})
let studyrights = null
if (mode === 'gradYear') {
studyrights = await graduatedStudyrights(faculty, since)
} else {
studyrights = await studyrightsByRightStartYear(faculty, since)
}
for (const right of studyrights) {
const { enddate, startdate, studyrightid, extentcode, studyrightElements, studentnumber } = right
const { programme, programmeName } = findRightProgramme(studyrightElements, 'graduated')
if (programmeFilter === 'NEW_STUDY_PROGRAMMES' && !isNewProgramme(programme)) continue
if (!(programme in programmeNames)) {
programmeNames[programme] = programmeName
}
let year = null
if (mode === 'gradYear') {
year = defineYear(enddate, false)
} else {
year = defineYear(startdate, false)
}
await addGraduation(
extentcode,
startdate,
studyrightid,
enddate,
studentnumber,
graduationAmounts,
graduationTimes,
year,
programmes,
programme
)
}
years.forEach(year => {
levels.forEach(level => {
const median = getMedian(graduationTimes[level][year])
const statistics = { onTime: 0, yearOver: 0, wayOver: 0 }
// Programme level breakdown
data.programmes.medians[level][year] = { programmes: [], data: [] }
const programmeCodes = Object.keys(programmes[level][year])
const programmeIds = getSortedProgIds(programmeCodes)
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.code)) {
goal += goals.exceptions[prog.code]
}
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.code,
]
data.programmes.medians[level][year].data = [
...data.programmes.medians[level][year].data,
{
y: progMedian,
amount: programmes[level][year][prog.code].graduationAmounts,
statistics: progStatistics,
name: prog.name,
code: prog.code,
},
]
}
}
data.medians[level] = [
...data.medians[level],
{ y: median, amount: graduationAmounts[level][year], statistics, name: year },
]
})
})
return data
}
const countGraduationTimes = async (faculty, programmeFilter) => {
const isAcademicYear = false // = yearType === 'ACADEMIC_YEAR'
const since = isAcademicYear ? new Date('2017-08-01') : new Date('2017-01-01')
const yearsList = getYearsArray(since.getFullYear(), isAcademicYear)
const levels = ['bachelor', 'bcMsCombo', 'master', 'doctor', 'licentiate']
const programmeNames = {}
const years = [...yearsList].reverse()
const goals = {
bachelor: 36,
bcMsCombo: 60,
master: 24,
doctor: 48,
licentiate: 78,
exceptions: {
MH30_004: 6, //months more
'420420-ma': 6,
MH30_001: 12,
'320011-ma': 12,
'320001-ma': 12,
MH30_003: 12,
'320002-ma': 12,
},
}
if (faculty === 'H90') {
goals.bcMsCombo += 12
}
// We count studyrights (vs. studyright_elements)
// This way we get the whole time for a degree, even if the student was transferred to a new programme
// E.g. started 8/2016 in old Bc, transferred to new 10/2020, graduated from new 1/2021 --> total 53 months (not 3)
const byGradYear = await count(
faculty,
since,
years,
yearsList,
levels,
programmeFilter,
programmeNames,
goals,
'gradYear'
)
const byStartYear = await count(
faculty,
since,
years,
yearsList,
levels,
programmeFilter,
programmeNames,
goals,
'startYear'
)
const classSizes = getProgrammeObjectBasis(yearsList, levels, false)
classSizes['programmes'] = {}
await getClassSizes(faculty, since, classSizes, programmeFilter, years)
return { id: faculty, goals, byGradYear, byStartYear, programmeNames, classSizes }
}
module.exports = { countGraduationTimes }
@@ -1,3 +1,7 @@
const Sequelize = require('sequelize')
const { Op } = Sequelize
const { codes } = require('../../../config/programmeCodes')
const findRightProgramme = (studyrightElements, mode) => {
let programme = ''
let programmeName = ''
@@ -35,11 +39,13 @@ const facultyFormatStudyright = studyright => {
extentcode,
student,
studyright_elements,
startdate,
} = studyright
return {
studyrightid,
studystartdate,
startdate,
enddate,
givendate,
graduated,
@@ -50,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,
}
}
@@ -74,7 +89,6 @@ const formatFacultyThesisWriter = credit => {
attainment_date,
student_studentnumber,
courseUnitType: course.course_unit_type,
organizations: course.organizations.map(organization => organization.code),
}
}
@@ -83,6 +97,53 @@ const formatOrganization = org => {
return { id, name, code, parentId: parent_id }
}
const newProgrammes = [/^KH/, /^MH/, /^T/, /^LI/, /^K-/, /^FI/, /^00901$/, /^00910$/]
const isNewProgramme = code => {
for (let i = 0; i < newProgrammes.length; i++) {
if (newProgrammes[i].test(code)) {
return true
}
}
return false
}
const checkTransfers = (s, insideTransfersStudyrights, transfersToOrAwayStudyrights) => {
const allTransfers = [
...insideTransfersStudyrights.map(sr => sr.studentnumber),
...transfersToOrAwayStudyrights.map(sr => sr.studentnumber),
]
return allTransfers.includes(s.studentnumber)
}
const getExtentFilter = includeAllSpecials => {
const filteredExtents = [16] // always filter out secondary subject students
if (!includeAllSpecials) {
filteredExtents.push(7, 34, 22, 99, 14, 13, 9)
}
let studyrightWhere = {
extentcode: {
[Op.notIn]: filteredExtents,
},
}
return studyrightWhere
}
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,
@@ -90,4 +151,9 @@ module.exports = {
formatFacultyTransfer,
formatFacultyThesisWriter,
formatOrganization,
isNewProgramme,
checkTransfers,
getExtentFilter,
mapCodesToIds,
facultyProgrammeStudents,
}
@@ -0,0 +1,171 @@
const { redisClient } = require('../redis')
const moment = require('moment')
const createRedisKeyForFacultyProgrammes = (id, programmeFilter) => `FACULTY_PROGRAMMES_${id}_${programmeFilter}`
const createRedisKeyForBasicStats = (id, yearType, programmeFilter, specialGroups) =>
`FACULTY_BASIC_STATS_${id}_${yearType}_${programmeFilter}_${specialGroups}`
const createRedisKeyForCreditStats = (id, yearType, programmeFilter, specialGroups) =>
`FACULTY_CREDIT_STATS_${id}_${yearType}_${programmeFilter}_${specialGroups}`
const createRedisKeyForThesiswriters = (id, yearType, programmeFilter, specialGroups) =>
`FACULTY_THESIS_WRITERS_STATS_${id}_${yearType}_${programmeFilter}_${specialGroups}`
const createRedisKeyForGraduationTimeStats = (id, programmeFilter) =>
`FACULTY_GRADUATION_TIME_STATS_${id}_${programmeFilter}`
const createRediskeyForFacultyProgress = (id, special_groups, graduated) =>
`FACULTY_PROGRESS_STATS_${id}_${special_groups}_${graduated}`
const createRediskeyForFacultyStudents = (id, special_groups, graduated) =>
`FACULTY_STUDENTS_STATS_${id}_${special_groups}_${graduated}`
const setFacultyProgrammes = async (id, data, programmeFilter) => {
const redisKey = createRedisKeyForFacultyProgrammes(id, programmeFilter)
const dataToRedis = {
data: data,
status: 'DONE',
lastUpdated: moment().format(),
}
const setOperationStatus = await redisClient.setAsync(redisKey, JSON.stringify(dataToRedis))
if (setOperationStatus !== 'OK') return null
return dataToRedis
}
const getFacultyProgrammes = async (id, programmeFilter) => {
const redisKey = createRedisKeyForFacultyProgrammes(id, programmeFilter)
const dataFromRedis = await redisClient.getAsync(redisKey)
if (!dataFromRedis) return null
return JSON.parse(dataFromRedis)
}
const setBasicStats = async (data, yearType, programmeFilter, specialGroups) => {
const { id } = data
const redisKey = createRedisKeyForBasicStats(id, yearType, programmeFilter, specialGroups)
const dataToRedis = {
...data,
status: 'DONE',
lastUpdated: moment().format(),
}
const setOperationStatus = await redisClient.setAsync(redisKey, JSON.stringify(dataToRedis))
if (setOperationStatus !== 'OK') return null
return dataToRedis
}
const getBasicStats = async (id, yearType, programmeFilter, specialGroups) => {
const redisKey = createRedisKeyForBasicStats(id, yearType, programmeFilter, specialGroups)
const dataFromRedis = await redisClient.getAsync(redisKey)
if (!dataFromRedis) return null
return JSON.parse(dataFromRedis)
}
const setCreditStats = async (data, yearType, programmeFilter, specialGroups) => {
const { id } = data
const redisKey = createRedisKeyForCreditStats(id, yearType, programmeFilter, specialGroups)
const dataToRedis = {
...data,
status: 'DONE',
lastUpdated: moment().format(),
}
const setOperationStatus = await redisClient.setAsync(redisKey, JSON.stringify(dataToRedis))
if (setOperationStatus !== 'OK') return null
return dataToRedis
}
const getCreditStats = async (id, yearType, programmeFilter, specialGroups) => {
const redisKey = createRedisKeyForCreditStats(id, yearType, programmeFilter, specialGroups)
const dataFromRedis = await redisClient.getAsync(redisKey)
if (!dataFromRedis) return null
return JSON.parse(dataFromRedis)
}
const setThesisWritersStats = async (data, yearType, programmeFilter, specialGroups) => {
const { id } = data
const redisKey = createRedisKeyForThesiswriters(id, yearType, programmeFilter, specialGroups)
const dataToRedis = {
...data,
status: 'DONE',
lastUpdated: moment().format(),
}
const setOperationStatus = await redisClient.setAsync(redisKey, JSON.stringify(dataToRedis))
if (setOperationStatus !== 'OK') return null
return dataToRedis
}
const getThesisWritersStats = async (id, yearType, programmeFilter, specialGroups) => {
const redisKey = createRedisKeyForThesiswriters(id, yearType, programmeFilter, specialGroups)
const dataFromRedis = await redisClient.getAsync(redisKey)
if (!dataFromRedis) return null
return JSON.parse(dataFromRedis)
}
const setGraduationStats = async (data, programmeFilter) => {
const { id } = data
const redisKey = createRedisKeyForGraduationTimeStats(id, programmeFilter)
const dataToRedis = {
...data,
status: 'DONE',
lastUpdated: moment().format(),
}
const setOperationStatus = await redisClient.setAsync(redisKey, JSON.stringify(dataToRedis))
if (setOperationStatus !== 'OK') return null
return dataToRedis
}
const getGraduationStats = async (id, programmeFilter) => {
const redisKey = createRedisKeyForGraduationTimeStats(id, programmeFilter)
const dataFromRedis = await redisClient.getAsync(redisKey)
if (!dataFromRedis) return null
return JSON.parse(dataFromRedis)
}
const getFacultyProgressStats = async (id, specialGroups, graduated) => {
const redisKey = createRediskeyForFacultyProgress(id, specialGroups, graduated)
const dataFromRedis = await redisClient.getAsync(redisKey)
if (!dataFromRedis) return null
return JSON.parse(dataFromRedis)
}
const setFacultyProgressStats = async (data, specialGroups, graduated) => {
const { id } = data
const redisKey = createRediskeyForFacultyProgress(id, specialGroups, graduated)
const dataToRedis = {
...data,
status: 'DONE',
lastUpdated: moment().format(),
}
const setOperationStatus = await redisClient.setAsync(redisKey, JSON.stringify(dataToRedis))
if (setOperationStatus !== 'OK') return null
return dataToRedis
}
const getFacultyStudentStats = async (id, specialGroups, graduated) => {
const redisKey = createRediskeyForFacultyStudents(id, specialGroups, graduated)
const dataFromRedis = await redisClient.getAsync(redisKey)
if (!dataFromRedis) return null
return JSON.parse(dataFromRedis)
}
const setFacultyStudentStats = async (data, specialGroups, graduated) => {
const { id } = data
const redisKey = createRediskeyForFacultyStudents(id, specialGroups, graduated)
const dataToRedis = {
...data,
status: 'DONE',
lastUpdated: moment().format(),
}
const setOperationStatus = await redisClient.setAsync(redisKey, JSON.stringify(dataToRedis))
if (setOperationStatus !== 'OK') return null
return dataToRedis
}
module.exports = {
setFacultyProgrammes,
getFacultyProgrammes,
setBasicStats,
getBasicStats,
setCreditStats,
getCreditStats,
setThesisWritersStats,
getThesisWritersStats,
setGraduationStats,
getGraduationStats,
getFacultyProgressStats,
setFacultyProgressStats,
getFacultyStudentStats,
setFacultyStudentStats,
}
@@ -0,0 +1,321 @@
const { indexOf } = require('lodash')
const { Op } = require('sequelize')
const moment = require('moment')
const { getAcademicYearDates } = require('../../util/semester')
const {
getCreditThresholds,
getBachelorCreditGraphStats,
getMasterCreditGraphStats,
getDoctoralCreditGraphStats,
getYearsArray,
tableTitles,
getOnlyMasterCreditGraphStats,
getBcMsThresholds,
getVetenaryCreditGraphStats,
} = require('../studyprogrammeHelpers')
const { studytrackStudents } = require('../studyprogramme')
const { getStudyRightsByExtent, getTransferredInside, getTransferredToAndAway } = require('./faculty')
const { checkTransfers } = require('./facultyHelpers')
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).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
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 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, progData: programmeData }
}
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 includeAllSpecials = specialGroups === 'SPECIAL_INCLUDED'
const includeGraduated = graduated === 'GRADUATED_INCLUDED'
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 => {
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 { 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,
students,
creditThresholdKeys,
creditThresholdAmounts,
bachelorlimits,
limitKeys,
'KH'
)
if (!(progId in bachelorProgress)) {
bachelorProgress[progId] = 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
}
} 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)) {
progressStats.mastersTableStats[indexOf(reversedYears, year)][indexOf(creditThresholdKeys, key) + 2] +=
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)) {
progressStats.bcMsTableStats[indexOf(reversedYears, year)][indexOf(creditThresholdKeysBcMs, key) + 2] +=
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 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)
}
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
}
}
}
}
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 }
@@ -0,0 +1,279 @@
const moment = require('moment')
const { Op } = require('sequelize')
const { getStartDate, getYearsArray, getPercentage, getYearsObject, tableTitles } = require('../studyprogrammeHelpers')
const { getAcademicYearDates } = require('../../util/semester')
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,
allAbsent: 0,
allInactive: 0,
allGraduated: 0,
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)
}
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
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 (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()
}
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 += students.length
totals.allStarted += started.length
totals.allEnrolled += enrolled.length
totals.allAbsent += absent.length
totals.allInactive += inactive.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, allProgrammeCodes, specialGroups, graduated) => {
// Only academic years are considered
const includeAllSpecials = specialGroups === 'SPECIAL_INCLUDED'
const includeGraduated = graduated === 'GRADUATED_INCLUDED'
const since = getStartDate(code, true)
const years = getYearsArray(since.getFullYear(), true, true)
const settings = { includeAllSpecials, includeGraduated }
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()) {
if (year === 'Total') continue
const queryParams = {
faculty: code,
programmes,
allProgrammeCodes,
since,
settings,
year,
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['programmes'],
programmeNames: programmeNames,
}
return studentsData
}
module.exports = { combineFacultyStudents }
@@ -0,0 +1,115 @@
const { indexOf } = require('lodash')
const { mapToProviders } = require('../../util/utils')
const { thesisWriters } = require('./faculty')
const {
getStatsBasis,
getYearsArray,
defineYear,
getCorrectStudentnumbers,
alltimeEndDate,
alltimeStartDate,
} = require('../studyprogrammeHelpers')
const getFacultyThesisWriters = async ({ since, years, isAcademicYear, facultyProgrammes, includeAllSpecials }) => {
const thesisTypes = ['urn:code:course-unit-type:bachelors-thesis', 'urn:code:course-unit-type:masters-thesis']
let bachelors = getStatsBasis(years)
let masters = getStatsBasis(years)
let programmeCounts = {}
let programmeNames = {}
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],
startDate: alltimeStartDate,
endDate: alltimeEndDate,
includeAllSpecials,
})
const thesisCourseCodes = await thesisWriters(provider, since, thesisTypes, students)
thesisCourseCodes?.forEach(({ attainment_date, courseUnitType }) => {
const thesisYear = defineYear(attainment_date, isAcademicYear)
if (!(progId in programmeCounts)) {
programmeCounts[progId] = {}
Object.keys(bachelors.tableStats).forEach(year => (programmeCounts[progId][year] = [0, 0, 0]))
programmeNames[progId] = { ...name, code: code }
}
programmeCounts[progId][thesisYear][0] += 1
if (courseUnitType === thesisTypes[0]) {
bachelors.graphStats[indexOf(years, thesisYear)] += 1
bachelors.tableStats[thesisYear] += 1
programmeCounts[progId][thesisYear][1] += 1
} else if (courseUnitType === thesisTypes[1]) {
masters.graphStats[indexOf(years, thesisYear)] += 1
masters.tableStats[thesisYear] += 1
programmeCounts[progId][thesisYear][2] += 1
}
})
}
return { bachelors, masters, programmeCounts, programmeNames }
}
const getFacultyThesisWritersForProgrammes = async (
allThesisWriters,
facultyProgrammes,
isAcademicYear,
includeAllSpecials
) => {
const since = isAcademicYear ? new Date('2017-08-01') : new Date('2017-01-01')
const years = getYearsArray(since.getFullYear(), isAcademicYear)
const queryParameters = { since, years, isAcademicYear, facultyProgrammes, includeAllSpecials }
const { bachelors, masters, 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],
bachelors.tableStats[year],
masters.tableStats[year],
])
allThesisWriters.graphStats.push({ name: 'Bachelors', data: bachelors.graphStats })
allThesisWriters.graphStats.push({ name: 'Masters', data: masters.graphStats })
const programmes = programmeNames ? Object.keys(programmeNames) : facultyProgrammes.map(programme => programme.code)
programmes.forEach(programmeId => {
reversedYears.forEach(year => {
if (programmeCounts && programmeId in programmeCounts) {
if (!(programmeId in allThesisWriters.programmeTableStats)) {
allThesisWriters.programmeTableStats[programmeId] = []
}
allThesisWriters.programmeTableStats[programmeId].push([year, ...programmeCounts[programmeId][year]])
}
})
})
allThesisWriters.programmeNames = programmeNames
}
const combineFacultyThesisWriters = async (faculty, facultyProgrammes, yearType, specialGroups) => {
let allThesisWriters = {
id: faculty,
years: [],
tableStats: [],
graphStats: [],
programmeTableStats: {},
titles: ['', 'All', 'Bachelors', 'Masters'],
programmeNames: {},
status: 'Done',
lastUpdated: '',
}
const isAcademicYear = yearType === 'ACADEMIC_YEAR'
const includeAllSpecials = specialGroups === 'SPECIAL_INCLUDED'
await getFacultyThesisWritersForProgrammes(allThesisWriters, facultyProgrammes, isAcademicYear, includeAllSpecials)
return allThesisWriters
}
module.exports = { combineFacultyThesisWriters }
@@ -0,0 +1,196 @@
const logger = require('../../util/logger')
const { findFacultyProgrammeCodes } = require('./faculty')
const { combineFacultyBasics } = require('./facultyBasics')
const { combineFacultyCredits } = require('./facultyCredits')
const { combineFacultyThesisWriters } = require('./facultyThesisWriters')
const { countGraduationTimes } = require('./facultyGraduationTimes')
const {
setFacultyProgrammes,
setBasicStats,
setCreditStats,
setThesisWritersStats,
setGraduationStats,
setFacultyProgressStats,
setFacultyStudentStats,
} = require('./facultyService')
const { combineFacultyStudentProgress } = require('./facultyStudentProgress')
const { combineFacultyStudents } = require('./facultyStudents')
const updateFacultyOverview = async (faculty, statsType) => {
const calendarNewSpecial = {
yearType: 'CALENDAR_YEAR',
programmeFilter: 'NEW_STUDY_PROGRAMMES',
specialGroups: 'SPECIAL_INCLUDED',
}
const calendarNewSpecialOut = {
yearType: 'CALENDAR_YEAR',
programmeFilter: 'NEW_STUDY_PROGRAMMES',
specialGroups: 'SPECIAL_EXCLUDED',
}
const calendarAllSpecial = {
yearType: 'CALENDAR_YEAR',
programmeFilter: 'ALL_PROGRAMMES',
specialGroups: 'SPECIAL_INCLUDED',
}
const calendarAllSpecialOut = {
yearType: 'CALENDAR_YEAR',
programmeFilter: 'ALL_PROGRAMMES',
specialGroups: 'SPECIAL_EXCLUDED',
}
const academicNewSpecial = {
yearType: 'ACADEMIC_YEAR',
programmeFilter: 'NEW_STUDY_PROGRAMMES',
specialGroups: 'SPECIAL_INCLUDED',
}
const academicAllSpecial = {
yearType: 'ACADEMIC_YEAR',
programmeFilter: 'ALL_PROGRAMMES',
specialGroups: 'SPECIAL_INCLUDED',
}
const academicNewSpecialOut = {
yearType: 'ACADEMIC_YEAR',
programmeFilter: 'NEW_STUDY_PROGRAMMES',
specialGroups: 'SPECIAL_EXCLUDED',
}
const academicAllSpecialOut = {
yearType: 'ACADEMIC_YEAR',
programmeFilter: 'ALL_PROGRAMMES',
specialGroups: 'SPECIAL_EXCLUDED',
}
const options = [
calendarNewSpecial,
calendarAllSpecial,
academicNewSpecial,
academicAllSpecial,
calendarNewSpecialOut,
calendarAllSpecialOut,
academicNewSpecialOut,
academicAllSpecialOut,
]
let allProgrammes = []
let newProgrammes = []
let allProgrammeCodes = []
try {
const all = await findFacultyProgrammeCodes(faculty, 'ALL_PROGRAMMES')
const onlyNew = await findFacultyProgrammeCodes(faculty, 'NEW_STUDY_PROGRAMMES')
allProgrammes = await setFacultyProgrammes(faculty, all, 'ALL_PROGRAMMES')
newProgrammes = await setFacultyProgrammes(faculty, onlyNew, 'NEW_STUDY_PROGRAMMES')
allProgrammes?.data.forEach(prog => allProgrammeCodes.push(prog.code))
} catch (e) {
logger.error(e)
}
for (const option of options) {
const { yearType, programmeFilter, specialGroups } = option
try {
if (statsType === 'ALL' || statsType === 'STUDENT') {
const updatedStudentInfo = await combineFacultyBasics(
faculty,
programmeFilter === 'ALL_PROGRAMMES' ? allProgrammes.data : newProgrammes.data,
yearType,
allProgrammeCodes,
programmeFilter,
specialGroups
)
await setBasicStats(updatedStudentInfo, option.yearType, option.programmeFilter, option.specialGroups)
}
if (statsType === 'ALL' || statsType === 'CREDITS') {
const updatedCredits = await combineFacultyCredits(
faculty,
programmeFilter === 'ALL_PROGRAMMES' ? allProgrammes.data : newProgrammes.data,
allProgrammes.data,
yearType,
specialGroups
)
await setCreditStats(updatedCredits, yearType, programmeFilter, specialGroups)
}
if (statsType === 'ALL' || statsType === 'THESIS') {
const updateThesisWriters = await combineFacultyThesisWriters(
faculty,
programmeFilter === 'ALL_PROGRAMMES' ? allProgrammes.data : newProgrammes.data,
yearType,
specialGroups
)
await setThesisWritersStats(updateThesisWriters, yearType, programmeFilter, specialGroups)
}
} catch (e) {
logger.error(e)
}
}
try {
const updatedTimesAll = await countGraduationTimes(faculty, 'ALL_PROGRAMMES')
await setGraduationStats(updatedTimesAll, 'ALL_PROGRAMMES')
const updatedTimesNew = await countGraduationTimes(faculty, 'NEW_STUDY_PROGRAMMES')
await setGraduationStats(updatedTimesNew, 'NEW_STUDY_PROGRAMMES')
} catch (e) {
logger.error(e)
}
return 'OK'
}
const updateFacultyProgressOverview = async faculty => {
const specialGraduated = {
specialGroups: 'SPECIAL_INCLUDED',
graduated: 'GRADUATED_INCLUDED',
}
const specialNotGraduated = {
specialGroups: 'SPECIAL_INCLUDED',
graduated: 'GRADUATED_EXCLUDED',
}
const notSpecialGraduated = {
specialGroups: 'SPECIAL_EXCLUDED',
graduated: 'GRADUATED_INCLUDED',
}
const notSpecialNotGraduated = {
specialGroups: 'SPECIAL_EXCLUDED',
graduated: 'GRADUATED_EXCLUDED',
}
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)
}
for (const option of options) {
const { specialGroups, graduated } = option
try {
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)
}
}
return 'OK'
}
module.exports = { updateFacultyOverview, updateFacultyProgressOverview }
@@ -1,316 +0,0 @@
const { indexOf } = require('lodash')
const {
startedStudyrights,
graduatedStudyrights,
transferredInsideFaculty,
transferredAway,
transferredTo,
} = require('./faculty')
const { getStatsBasis, getYearsArray, defineYear } = require('./studyprogrammeHelpers')
const { findRightProgramme } = require('./facultyHelpers')
const filterDuplicateStudyrights = studyrights => {
// bachelor+master students have two studyrights (separated by two last digits in studyrightid)
// choose only the earlier started one, so we don't count start of masters as starting in faculty
let rightsToCount = {}
studyrights.forEach(right => {
const id = right.studyrightid.slice(0, -2)
const start = new Date(right.studystartdate)
if (id in rightsToCount) {
if (new Date(rightsToCount[id].studystartdate) > start) {
rightsToCount[id] = right
}
} else {
rightsToCount[id] = right
}
})
return Object.values(rightsToCount)
}
const addProgramme = async (stats, programme, tableStats, transferYear, index, allBasics, facultyProgrammes) => {
if (!(programme in stats)) {
stats[programme] = {}
Object.keys(tableStats).forEach(year => (stats[programme][year] = [0, 0, 0]))
}
stats[programme][transferYear][index] += 1
if (!(programme in allBasics.programmeNames)) {
const name = facultyProgrammes.filter(prog => prog.code === programme)[0]?.name
allBasics.programmeNames[programme] = name
}
}
const getFacultyStarters = async (
faculty,
since,
isAcademicYear,
yearsArray,
graphStats,
tableStats,
counts,
allBasics,
programmeData
) => {
const startedGraphStats = [...graphStats]
const startedTableStats = { ...tableStats }
const studyrights = await startedStudyrights(faculty, since)
const filteredStudyrights = filterDuplicateStudyrights(studyrights)
const programmeTableStats = {}
filteredStudyrights.forEach(({ studystartdate, studyrightElements }) => {
const { programme, programmeName } = findRightProgramme(studyrightElements, 'started')
const startYear = defineYear(studystartdate, isAcademicYear)
startedGraphStats[indexOf(yearsArray, startYear)] += 1
startedTableStats[startYear] += 1
if (!(programme in programmeTableStats)) {
programmeTableStats[programme] = { ...tableStats }
}
programmeTableStats[programme][startYear] += 1
if (!(programme in allBasics.programmeNames)) {
allBasics.programmeNames[programme] = programmeName
}
})
allBasics.studentInfo.graphStats.push({ name: 'Started studying', data: startedGraphStats })
programmeData['started'] = programmeTableStats
Object.keys(startedTableStats).forEach(year => {
counts[year] = [startedTableStats[year]]
})
}
const getFacultyGraduates = async (
faculty,
since,
isAcademicYear,
yearsArray,
graphStats,
tableStats,
allBasics,
counts,
countsGraduations,
programmeData
) => {
const graduatedGraphStats = [[...graphStats], [...graphStats], [...graphStats], [...graphStats], [...graphStats]]
const graduatedTableStats = {}
Object.keys(tableStats).forEach(year => (graduatedTableStats[year] = [0, 0, 0, 0, 0]))
const programmeTableStats = {}
const graduatedRights = await graduatedStudyrights(faculty, since)
graduatedRights.forEach(({ enddate, extentcode, studyrightElements }) => {
const { programme, programmeName } = findRightProgramme(studyrightElements, 'graduated')
const endYear = defineYear(enddate, isAcademicYear)
graduatedGraphStats[0][indexOf(yearsArray, endYear)] += 1
graduatedTableStats[endYear][0] += 1
if (!(programme in programmeTableStats)) {
programmeTableStats[programme] = {}
Object.keys(tableStats).forEach(year => (programmeTableStats[programme][year] = [0, 0, 0, 0, 0]))
}
programmeTableStats[programme][endYear][0] += 1
if (extentcode === 1) {
graduatedGraphStats[1][indexOf(yearsArray, endYear)] += 1
graduatedTableStats[endYear][1] += 1
programmeTableStats[programme][endYear][1] += 1
} else if (extentcode === 2) {
graduatedGraphStats[2][indexOf(yearsArray, endYear)] += 1
graduatedTableStats[endYear][2] += 1
programmeTableStats[programme][endYear][2] += 1
} else if (extentcode === 4) {
graduatedGraphStats[3][indexOf(yearsArray, endYear)] += 1
graduatedTableStats[endYear][3] += 1
programmeTableStats[programme][endYear][3] += 1
} else {
graduatedGraphStats[4][indexOf(yearsArray, endYear)] += 1
graduatedTableStats[endYear][4] += 1
programmeTableStats[programme][endYear][4] += 1
}
if (!(programme in allBasics.programmeNames)) {
allBasics.programmeNames[programme] = programmeName
}
})
allBasics.studentInfo.graphStats.push({ name: 'Graduated', data: graduatedGraphStats[0] })
// Is this needed in graph
// allBasics.graduationInfo.graphStats.push({ name: 'All graduations', data: graduatedGraphStats[0] })
allBasics.graduationInfo.graphStats.push({ name: 'Bachelors', data: graduatedGraphStats[1] })
allBasics.graduationInfo.graphStats.push({ name: 'Masters', data: graduatedGraphStats[2] })
allBasics.graduationInfo.graphStats.push({ name: 'Doctors', data: graduatedGraphStats[3] })
allBasics.graduationInfo.graphStats.push({ name: 'Others', data: graduatedGraphStats[4] })
programmeData['graduated'] = programmeTableStats
Object.keys(graduatedTableStats).forEach(year => {
counts[year] = counts[year].concat(graduatedTableStats[year][0])
countsGraduations[year] = graduatedTableStats[year]
})
}
const getFacultyTransfers = async (
programmes,
since,
isAcademicYear,
yearsArray,
graphStats,
tableStats,
allBasics,
counts,
programmeData
) => {
const programmeCodes = programmes.map(prog => prog.code)
const insideTransfers = await transferredInsideFaculty(programmeCodes, since)
const awayTransfers = await transferredAway(programmeCodes, since)
const toTransfers = await transferredTo(programmeCodes, since)
const transferGraphStats = [[...graphStats], [...graphStats], [...graphStats]]
const transferTableStats = {}
Object.keys(tableStats).forEach(year => (transferTableStats[year] = [0, 0, 0]))
const programmeTableStats = {}
// inside transfer is counted towards target
for (const { transferdate, targetcode } of insideTransfers) {
const transferYear = defineYear(transferdate, isAcademicYear)
transferGraphStats[0][indexOf(yearsArray, transferYear)] += 1
transferTableStats[transferYear][0] += 1
await addProgramme(programmeTableStats, targetcode, tableStats, transferYear, 0, allBasics, programmes)
}
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)
}
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)
}
allBasics.studentInfo.graphStats.push({ name: 'Transferred inside', data: transferGraphStats[0] })
allBasics.studentInfo.graphStats.push({ name: 'Transferred away', data: transferGraphStats[1] })
allBasics.studentInfo.graphStats.push({ name: 'Transferred to', data: transferGraphStats[2] })
programmeData['transferred'] = programmeTableStats
Object.keys(transferTableStats).forEach(year => {
counts[year] = counts[year].concat(transferTableStats[year])
})
}
const combineFacultyBasics = async (allBasics, faculty, programmes, yearType) => {
let counts = {}
let countsGraduations = {}
let years = []
let programmeData = {}
const isAcademicYear = yearType === 'ACADEMIC_YEAR'
const since = isAcademicYear ? new Date('2017-08-01') : new Date('2017-01-01')
const yearsArray = getYearsArray(since.getFullYear(), isAcademicYear)
const { graphStats, tableStats } = getStatsBasis(yearsArray)
Object.keys(tableStats).forEach(year => years.push(year))
// Started studying in faculty
await getFacultyStarters(
faculty,
since,
isAcademicYear,
yearsArray,
graphStats,
tableStats,
counts,
allBasics,
programmeData
)
// Graduated in faculty
await getFacultyGraduates(
faculty,
since,
isAcademicYear,
yearsArray,
graphStats,
tableStats,
allBasics,
counts,
countsGraduations,
programmeData
)
// Transfers
await getFacultyTransfers(
programmes,
since,
isAcademicYear,
yearsArray,
graphStats,
tableStats,
allBasics,
counts,
programmeData
)
// combine tableStats from all categories
allBasics.years = years
years.forEach(year => {
allBasics.studentInfo.tableStats.push([year, ...counts[year]])
allBasics.graduationInfo.tableStats.push([year, ...countsGraduations[year]])
})
allBasics.studentInfo.tableStats.reverse()
allBasics.graduationInfo.tableStats.reverse()
//combine programme level tablestats
// all programmes are not present in all data types, check all found programmes and patch missing details with 0
let allCodes = Object.keys(programmeData.started).concat(
Object.keys(programmeData.graduated),
Object.keys(programmeData.transferred)
)
allCodes = [...new Set(allCodes)]
let studentInfo = {}
let graduationInfo = {}
allCodes.forEach(code => {
if (!(code in studentInfo)) studentInfo[code] = {}
for (const year of years) {
// started
if (code in programmeData.started) {
studentInfo[code][year] = [year, programmeData.started[code][year]]
} else {
studentInfo[code][year] = [year, 0]
}
// graduated total
if (code in programmeData.graduated) {
studentInfo[code][year] = studentInfo[code][year].concat([programmeData.graduated[code][year][0]])
if (!(code in graduationInfo)) graduationInfo[code] = {}
graduationInfo[code][year] = [year, ...programmeData.graduated[code][year]]
} else {
studentInfo[code][year] = studentInfo[code][year].concat([0])
}
// trasferred
if (code in programmeData.transferred) {
studentInfo[code][year] = studentInfo[code][year].concat(programmeData.transferred[code][year])
} else {
studentInfo[code][year] = studentInfo[code][year].concat([0, 0, 0])
}
}
})
Object.keys(studentInfo).forEach(prog => {
allBasics.studentInfo.programmeTableStats[prog] = [...Object.values(studentInfo[prog])].reverse()
})
Object.keys(graduationInfo).forEach(prog => {
allBasics.graduationInfo.programmeTableStats[prog] = Object.values(graduationInfo[prog]).reverse()
})
}
module.exports = { combineFacultyBasics }
@@ -1,124 +0,0 @@
const { indexOf } = require('lodash')
const { mapToProviders } = require('../util/utils')
const { thesisWriters } = require('./faculty')
const { getStatsBasis, getYearsArray, defineYear } = require('./studyprogrammeHelpers')
const getFacultyThesisWriters = async ({ since, years, isAcademicYear, facultyProgrammes }) => {
const programmeProviders = facultyProgrammes.reduce(
(results, studyprogramme) => ({
...results,
[mapToProviders([studyprogramme.code])[0]]: { ...studyprogramme },
}),
{}
)
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',
'urn:code:course-unit-type:bachelors-thesis-seminar',
'urn:code:course-unit-type:masters-thesis-seminar',
'',
]
const thesisCourseCodes = await thesisWriters(Object.keys(programmeProviders), since, thesisTypes)
let bachelors = getStatsBasis(years)
let masters = getStatsBasis(years)
let doctors = getStatsBasis(years)
let licentiates = getStatsBasis(years)
let programmeCounts = {}
let programmeNames = {}
thesisCourseCodes.forEach(({ attainment_date, courseUnitType, organizations }) => {
const thesisYear = defineYear(attainment_date, isAcademicYear)
const programme = programmeProviders[organizations[0]]
if (!(programme.code in programmeCounts)) {
programmeCounts[programme.code] = {}
Object.keys(bachelors.tableStats).forEach(year => (programmeCounts[programme.code][year] = [0, 0, 0, 0, 0]))
programmeNames[programme.code] = programme.name
}
programmeCounts[programme.code][thesisYear][0] += 1
if (courseUnitType === thesisTypes[0] || courseUnitType == thesisTypes[4]) {
bachelors.graphStats[indexOf(years, thesisYear)] += 1
bachelors.tableStats[thesisYear] += 1
programmeCounts[programme.code][thesisYear][1] += 1
} else if (courseUnitType === thesisTypes[1] || courseUnitType == thesisTypes[5]) {
masters.graphStats[indexOf(years, thesisYear)] += 1
masters.tableStats[thesisYear] += 1
programmeCounts[programme.code][thesisYear][2] += 1
} else if (courseUnitType === thesisTypes[2]) {
doctors.graphStats[indexOf(years, thesisYear)] += 1
doctors.tableStats[thesisYear] += 1
programmeCounts[programme.code][thesisYear][3] += 1
} else if (courseUnitType === thesisTypes[3]) {
licentiates.graphStats[indexOf(years, thesisYear)] += 1
licentiates.tableStats[thesisYear] += 1
programmeCounts[programme.code][thesisYear][4] += 1
}
})
if (
bachelors.graphStats.every(year => year === 0) &&
masters.graphStats.every(year => year === 0) &&
doctors.graphStats.every(year => year === 0) &&
licentiates.graphStats.every(year => year === 0)
) {
return {
bachelors: { graphStats: [], tableStats: {} },
masters: { graphStats: [], tableStats: {} },
doctors: { graphStats: [], tableStats: {} },
licentiates: { graphStats: [], tableStats: {} },
programmeCounts: {},
}
}
return { bachelors, masters, doctors, licentiates, programmeCounts, programmeNames }
}
const getFacultyThesisWritersForStudyTrack = async (allThesisWriters, facultyProgrammes, code, isAcademicYear) => {
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 }
const { bachelors, masters, doctors, licentiates, programmeCounts, programmeNames } = await getFacultyThesisWriters(
queryParameters
)
allThesisWriters.years = years.map(year => year.toString())
const reversedYears = years.reverse()
allThesisWriters.tableStats = reversedYears.map(year => [
year,
bachelors.tableStats[year] + masters.tableStats[year] + doctors.tableStats[year],
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: 'Others', data: licentiates.graphStats })
facultyProgrammes.forEach(programme => {
reversedYears.forEach(year => {
if (programme.code in programmeCounts) {
if (!(programme.code in allThesisWriters.programmeTableStats)) {
allThesisWriters.programmeTableStats[programme.code] = []
}
allThesisWriters.programmeTableStats[programme.code].push([year, ...programmeCounts[programme.code][year]])
}
})
})
allThesisWriters.programmeNames = programmeNames
}
const combineFacultyThesisWriters = async (allThesisWriters, facultyProgrammes, faculty, yearType) => {
const isAcademicYear = yearType === 'ACADEMIC_YEAR' ? true : false
await getFacultyThesisWritersForStudyTrack(allThesisWriters, facultyProgrammes, faculty, isAcademicYear)
return allThesisWriters
}
module.exports = { combineFacultyThesisWriters }
@@ -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 }
+7 -13
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 || [],
@@ -210,7 +211,7 @@ const getStudentsIncludeCoursesBetween = async (studentnumbers, startDate, endDa
raw: true,
}),
Enrollment.findAll({
attributes: ['course_code', 'state', 'enrollment_date_time', 'studentnumber'],
attributes: ['course_code', 'state', 'enrollment_date_time', 'studentnumber', 'semestercode'],
where: {
enrollment_date_time: {
[Op.between]: [attainmentDateFrom, endDate],
@@ -232,6 +233,7 @@ const getStudentsIncludeCoursesBetween = async (studentnumbers, startDate, endDa
'creditcount',
'abbreviatedname',
'email',
'phone_number',
'updatedAt',
'gender_code',
'birthdate',
@@ -414,7 +416,7 @@ const studentnumbersWithAllStudyrightElements = async (
// db startdate is formatted to utc so need to change it when querying
const formattedStartDate = new Date(moment.tz(startDate, 'Europe/Helsinki').format()).toUTCString()
const filteredExtents = [10] // always filter out secondary subject students
const filteredExtents = [16] // always filter out secondary subject students
let studyrightWhere = {
extentcode: {
[Op.notIn]: filteredExtents,
@@ -424,7 +426,7 @@ const studentnumbersWithAllStudyrightElements = async (
filteredExtents.push(7, 34)
}
if (!nondegreeStudents) {
filteredExtents.push(33, 99, 14, 13)
filteredExtents.push(22, 99, 14, 13)
}
let studentWhere = {}
@@ -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}`,
+3 -3
View File
@@ -121,7 +121,7 @@ const getUnifyStatus = unifyCourses => {
}
const findByCourseAndSemesters = async (coursecodes, from, to, separate, unifyCourses = 'unifyStats') => {
const { startdate } = await Semester.findOne({
const { startdate, semestercode: fromSemester } = await Semester.findOne({
where: {
[separate ? 'semestercode' : 'yearcode']: from,
},
@@ -129,7 +129,7 @@ const findByCourseAndSemesters = async (coursecodes, from, to, separate, unifyCo
limit: 1,
raw: true,
})
const { enddate } = await Semester.findOne({
const { enddate, semestercode: toSemester } = await Semester.findOne({
where: {
[separate ? 'semestercode' : 'yearcode']: to,
},
@@ -164,7 +164,7 @@ const findByCourseAndSemesters = async (coursecodes, from, to, separate, unifyCo
WHERE
e.studentnumber = s.studentnumber
AND e.course_code IN (:coursecodes)
AND e.enrollment_date_time BETWEEN '${startdate.toISOString()}' AND '${enddate.toISOString()}'
AND e.semestercode BETWEEN ${fromSemester} AND ${toSemester}
AND e.enrollment_date_time >= '2021-05-31'
AND e.state IN ('ENROLLED', 'CONFIRMED')
);
@@ -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,8 +1,7 @@
const { mean } = require('lodash')
const { studentnumbersWithAllStudyrightElements } = require('./populations')
const { codes } = require('../../config/programmeCodes')
// Helper functions
const getCorrectStudentnumbers = async ({ codes, startDate, endDate, includeAllSpecials, includeGraduated = true }) => {
let studentnumbers = []
const exchangeStudents = includeAllSpecials
@@ -45,6 +44,7 @@ const resolveStudyRightCode = studyright_elements => {
const formatStudyright = studyright => {
const {
studyrightid,
startdate,
studystartdate,
enddate,
givendate,
@@ -58,6 +58,7 @@ const formatStudyright = studyright => {
return {
studyrightid,
startdate,
studystartdate: defineStartDate(studyright_elements, studystartdate),
enddate,
givendate,
@@ -151,15 +152,16 @@ const isNonMajorCredit = (studyrights, attainment_date) =>
)
})
const isSpecialGroupCredit = (studyrights, attainment_date, transfers) =>
const isSpecialGroupCredit = (studyrights, attainment_date, transfers) => {
studyrights.some(studyright => {
if (!studyright) return true // If there is no studyright matching the credit, is not a major student credit
if (studyright.studystartdate > attainment_date) return true // Credits before the studyright started are not major student credits
if (studyright.enddate && attainment_date > studyright.enddate) return true // Credits after studyright are not major student credits
if ([7, 9, 16, 34, 33, 99, 14, 13].includes(studyright.extentcode)) return true // Excludes non-degree studyrights and exchange students
if ([7, 9, 16, 34, 22, 99, 14, 13].includes(studyright.extentcode)) return true // Excludes non-degree studyrights and exchange students
if (transfers.includes(studyright.studyrightid)) return true // Excludes both transfers in and out of the programme
return false
})
}
const getMedian = values => {
if (values.length === 0) return 0
@@ -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()
@@ -224,8 +221,12 @@ const getPercentage = (value, total) => {
const getEmptyArray = length => new Array(length).fill(0)
const getBachelorCreditGraphStats = years => ({
lte15: {
name: 'Less than 15 credits',
data: getEmptyArray(years.length),
},
lte30: {
name: 'Less than 30 credits',
name: '15-29 credits',
data: getEmptyArray(years.length),
},
lte60: {
@@ -249,7 +250,7 @@ const getBachelorCreditGraphStats = years => ({
data: getEmptyArray(years.length),
},
mte180: {
name: 'More than 180 credits',
name: '180 or more credits',
data: getEmptyArray(years.length),
},
})
@@ -280,61 +281,123 @@ const getMasterCreditGraphStats = years => ({
data: getEmptyArray(years.length),
},
mte300: {
name: 'More than 300 credits',
name: '300 or more credits',
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: {
name: 'Less than 15 credits',
data: getEmptyArray(years.length),
},
lte30: {
name: '15-29 credits',
data: getEmptyArray(years.length),
},
lte60: {
name: '30-59 credits',
data: getEmptyArray(years.length),
},
lte90: {
name: '60-89 credits',
data: getEmptyArray(years.length),
},
lte120: {
name: '90-119 credits',
data: getEmptyArray(years.length),
},
mte120: {
name: '120 or more credits',
data: getEmptyArray(years.length),
},
})
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: 'More than 300 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 = ['lte30', 'lte60', 'lte90', 'lte120', 'lte150', 'lte180', 'mte180']
const bachelorCreditThresholds = ['lte15', 'lte30', 'lte60', 'lte90', 'lte120', 'lte150', 'lte180', 'mte180']
const masterCreditThresholds = ['lte200', 'lte220', 'lte240', 'lte260', 'lte280', 'lte300', 'mte300']
const doctoralCreditThresholds = ['lte50', 'lte100', 'lte150', 'lte200', 'lte250', 'lte300', 'mte300']
const bachelorCreditAmounts = [30, 60, 90, 120, 150, 180, 180]
const onlyMasterCreditThresholds = ['lte15', 'lte30', 'lte60', 'lte90', 'lte120', 'mte120']
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 doctoralCreditAmounts = [50, 100, 150, 200, 250, 300, 300]
const getCreditThresholds = studyprogramme => {
const onlyMasterCreditAmounts = [15, 30, 60, 90, 120, 120]
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, 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 }
}
@@ -350,8 +413,8 @@ const tableTitles = {
'',
'Total',
'Major students credits',
'Non major students credits',
'Non degree students credits',
'Non-major students credits',
'Non-degree students credits',
'Transferred credits',
],
},
@@ -359,13 +422,14 @@ const tableTitles = {
bachelor: [
'',
'All',
'< 30 credits',
'< 15 credits',
'15-29 credits',
'30-59 credits',
'60-89 credits',
'90-119 credits',
'120-149 credits',
'150-179 credits',
'> 180 credits',
'180 ≤ credits',
],
master: [
'',
@@ -376,19 +440,19 @@ const tableTitles = {
'240-259 credits',
'260-279 credits',
'280-299 credits',
'> 300 credits',
'300 ≤ credits',
],
doctoral: [
masterOnly: [
'',
'All',
'< 50 credits',
'50-99 credits',
'100-149 credits',
'150-199 credits',
'200-249 credits',
'250-299 credits',
'> 300 credits',
'< 15 credits',
'15-29 credits',
'30-59 credits',
'60-89 credits',
'90-119 credits',
'120 ≤ credits',
],
doctoral: ['', 'All', '< 10 credits', '10-19 credits', '20-29 credits', '30-39 credits', '40 ≤ credits'],
},
studytracks: [
'',
@@ -400,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,
@@ -422,7 +541,6 @@ module.exports = {
isMajorStudentCredit,
isSpecialGroupCredit,
getMedian,
getMean,
defineYear,
getStartDate,
alltimeStartDate,
@@ -431,10 +549,16 @@ module.exports = {
getPercentage,
getBachelorCreditGraphStats,
getMasterCreditGraphStats,
getVetenaryCreditGraphStats,
getDoctoralCreditGraphStats,
getOnlyMasterCreditGraphStats,
getCreditGraphStats,
getCreditThresholds,
tableTitles,
getCreditProgressTableTitles,
getBcMsThresholds,
isNonMajorCredit,
mapCodesToIds,
getId,
getGoal,
}
@@ -30,7 +30,6 @@ const updateBasicView = async code => {
}
const options = [specialCalendar, specialAcademic, specialExcludedCalendar, specialExcludedAcademic]
for (const option of options) {
try {
const basicStats = await getBasicStatsForStudytrack({
@@ -62,8 +61,8 @@ const updateBasicView = async code => {
} catch (e) {
logger.error(e)
}
return 'OK'
}
return 'OK'
}
const updateStudytrackView = async code => {
+160 -122
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,43 +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
data[thresholdKeys[6]] += creditcount >= thresholdAmounts[6] ? 1 : 0
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 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),
]
}
@@ -103,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,
@@ -121,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) {
@@ -138,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
@@ -235,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 {
@@ -256,11 +292,11 @@ const getEmptyStatsObjects = (years, studytracks, studyprogramme) => {
mainStatsByTrack,
creditGraphStats,
creditTableStats,
graduationMedianTime,
graduationMeanTime,
graduationAmounts,
graduationTimes,
totalAmounts,
emptyTracks,
totals,
}
}
@@ -279,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),
}
}
+46 -20
View File
@@ -8,10 +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 hyOneGroup = 'hy-one'
const toskaGroup = 'grp-toska'
const facultyStatisticsGroup = 'grp-oodikone-users'
// Max 25 users can be stored in the cache
const userDataCache = new LRU({
@@ -168,30 +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))
const rights = _.uniqBy([...programmes, ...enrichProgrammesFromFaculties(faculties)]).concat(extraRights)
const {
dataValues: {
@@ -220,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'),
@@ -231,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)
}
@@ -253,21 +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(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({
@@ -285,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
}
+1 -1
View File
@@ -8,7 +8,7 @@ const getSemester = date => (isSpring(date) ? 'SPRING' : 'FALL')
const semesterStart = {
SPRING: '01-01',
FALL: '07-31',
FALL: '08-01',
}
const semesterEnd = {
+14 -14
View File
@@ -6,7 +6,7 @@
"": {
"name": "frontend",
"dependencies": {
"@reduxjs/toolkit": "^1.8.4",
"@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.4",
"resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-1.8.4.tgz",
"integrity": "sha512-IpFq1WI7sCYeLQpDCGvlcQY9wn70UpAM3cOLq78HRnVn1746RI+l3y5xcuOeVOxORaxABJh3cfJMxycD2IwH5w==",
"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.4",
"resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-1.8.4.tgz",
"integrity": "sha512-IpFq1WI7sCYeLQpDCGvlcQY9wn70UpAM3cOLq78HRnVn1746RI+l3y5xcuOeVOxORaxABJh3cfJMxycD2IwH5w==",
"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.4",
"@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",
@@ -0,0 +1,112 @@
export default {
ProgrammeToggle: `
New study programmes: Näyttää kaikissa luvuissa ja kuvaajissa pelkästään uuden opetussuunnitelman ohjelmat.
All programmes: Näyttää kaikissa luvuissa ja kuvaajissa kuvaajissa myös vanhan opetussuunnitelman ohjelmat.`,
YearToggle: `
Academic year: Näyttää tilastot akateemisille vuosille jaoteltuna, esimerkiksi 1.8.2019-31.7.2020
Calendar year: Näyttää tilastot kalenterivuosille jaoteltuna, esimerkiksi 1.1.2019-31.12.2019
`,
StudentsOfTheFaculty: `
**Started studying**: Sisältää kyseisenä vuonna tiedekunnassa aloittaneet opiskelijat (aloitetut opinto-oikeudet) riippumatta siitä, minä vuonna kyseinen opiskeluoikeus on myönnetty. Opiskelija on myös voinut myöhemmin keskeyttää opintonsa tai siirtyä pois ohjelmasta.
Kandi-maisteri-opinto-oikeudellisista opiskelijoista lasketaan vain kandissa ts. tiedekunnassa aloitus, ei toistamiseen enää maisteriopintojen aloitusta.\n
**Graduated**: Sisältää kyseisenä vuonna valmistuneet opiskelijat riippumatta siitä, minä vuonna opiskeluoikeus ohjelmaan on myönnetty. Jaottelu on tehty opinto-oikeuden päättymispäivän (=valmistumispäivän) perusteella.
**Transferred inside**: Sisältää kyseisenä vuonna tiedekunnan sisällä siirtyneet opiskelijat (opinto-oikeudet). Ohjelmakohtaisissa tilastoissa siirto on laskettu kohdeohjelman tilastoihin.
**Transferred away**: Sisältää kyseisenä vuonna pois tiedekunnasta siirtyneet opiskelijat (opinto-oikeudet). Sama opiskelija voi esiintyä tilastoissa useana vuonna, mikäli hän on siirtynyt tiedekuntaan ja siitä pois useasti.
**Transferred to**: Sisältää kyseisenä vuonna tiedekuntaan siirtyneet opiskelijat (opinto-oikeudet).
HUOM! Siirtyneiden määrä on erityisen suuri vuonna 2020, jolloin vanhoista koulutusohjelmista luovuttiin ja opiskelijat siirtyivät uusiin ohjelmiin.
`,
CreditsProducedByTheFaculty: `
Sisältää opintopisteet suoritusvuosittain. Suoritukset on jaoteltu Sisussa näkyvän kurssin suorituspäivän mukaan.
**Major students credits**: Sisältää ne opintopisteet, joiden suorittajalla on suoritushetkellä ollut ensisijainen opinto-oikeus johonkin tiedekunnan koulutusohjelmaan. Sisältää myös tiedekunnan ohjelmaan siirtyneet ensisijaiset opiskelijat.
**Non-major faculty students credits**: Sisältää tiedekunnan koulutusohjelmien tuottamat opintopisteet, joiden suorittajalla EI ole ollut suoritushetkellä ensisijaista opinto-oikeutta kyseiseen koulutusohjelmaan,
MUTTA hänellä on sellainen johonkin toiseen tiedekunnan ohjelmaan. Sisältää esimerkiksi vaihto-opiskelijat ja erillis-opinto-oikeudella opiskelevat.
Sisältää myös opintopisteet, jotka opiskelija on suorittanut koulutusohjelmaan ennen opintojen aloittamista koulutusohjelmassa tai opintojen päätyttyä.
**Non-major other faculty students credits**: Sisältää tiedekunnan koulutusohjelmien tuottamat opintopisteet, joiden suorittajalla EI ole ollut suoritushetkellä ensisijaista opinto-oikeutta kyseiseen koulutusohjelmaan tai mihinkään muuhun kyseisen tiedekunnan koulutusohjelmaan.
Sisältää esimerkiksi vaihto-opiskelijat ja erillis-opinto-oikeudella opiskelevat. Sisältää myös opintopisteet, jotka opiskelija on suorittanut koulutusohjelmaan ennen opintojen aloittamista koulutusohjelmassa tai opintojen päätyttyä.
**Non-degree student credits**: Sisältää tiedekuntaan suoritetut opintopisteet, joiden suorittajilla ei ole tutkinto-oikeutta Helsingin yliopistossa.
`,
GraduatedOfTheFaculty: `Sisältää kyseisenä vuonna tiedekunnasta valmistuneet opiskelijat riippumatta siitä, minä vuonna opiskeluoikeus ohjelmaan on myönnetty. Jaottelu on tehty opinto-oikeuden päättymispäivän (=valmistumispäivän) perusteella.`,
ThesisWritersOfTheFaculty: `Sisältää kyseisenä vuonna kandidaatintutkielman 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: `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.
- Bachelor: Kandidaatin tutkinnon suoritusajat
- Bachelor + Master: Kandi+maisteriopinto-oikeudella opiskelleiden ja siitä maisteriksi valmistuneiden suoritusajat
- Master: Pelkällä maisterintutkinnon opinto-oikeudella opiskelleiden suoritusajat
- Doctor: Tohtorin tutkinnon suoritusajat
- Licentiate: Lisensiaatin tutkinnon suoritusajat
Valmistumisajoista on **vähennetty lakisääteiset poissaolot**.
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:
**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.
**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.
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: `
Graduated included: Valinnassa ovat mukana ohjelmasta jo valmistuneet opiskelijat.
Graduated excluded: Valinnasta on poistettu ohjelmasta jo valmistuneet opiskelijat.
`,
StudentsStatsOfTheFaculty: `
Kuvaa tiedekunnan ohjelmissa opiskelevien opiskelijoden tilastoja. Esimerkiksi 2020-2021 aloittaneiden opintopistejakaumaa.
HUOM!
- **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. `,
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. 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. 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.
`,
}
@@ -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.',
},
}
@@ -3,6 +3,7 @@ import CoolDataScience from './coolDataScience'
import CourseStatistics from './courseStatistics'
import CoursePopulation from './coursePopulation'
import Studyprogramme from './studyprogramme'
import Faculty from './faculty'
export default {
PopulationStatistics,
@@ -10,4 +11,5 @@ export default {
CourseStatistics,
CoursePopulation,
Studyprogramme,
Faculty,
}
@@ -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)
@@ -65,7 +65,7 @@ const countTotalStats = (formattedStats, userHasAccessToAllStats) => {
categories: { passed, failed },
grades: cgrades,
enrollmentsByState: { ...acc.attempts.enrollmentsByState },
totalEnrollments: acc.students.totalEnrollments + (curr.students.totalEnrollments || 0),
totalEnrollments: acc.attempts.totalEnrollments + (curr.attempts.totalEnrollments || 0),
},
students: {
totalEnrollments: acc.students.totalEnrollments + (curr.students.totalEnrollments || 0),
@@ -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)
@@ -1,6 +1,6 @@
import React from 'react'
import { Divider, Loader } from 'semantic-ui-react'
import { Divider, Loader, Popup, Button, Message } from 'semantic-ui-react'
import xlsx from 'xlsx'
import {
useGetFacultyCreditStatsQuery,
useGetFacultyBasicStatsQuery,
@@ -12,27 +12,91 @@ import InteractiveDataTable from '../InteractiveDataView'
import Toggle from '../../StudyProgramme/Toggle'
import InfoBox from '../../Info/InfoBox'
import InfotoolTips from '../../../common/InfoToolTips'
import sortProgrammeKeys from '../facultyHelpers'
import '../faculty.css'
const Overview = ({ faculty, academicYear, setAcademicYear }) => {
const toolTips = InfotoolTips.Studyprogramme
const Overview = ({
faculty,
academicYear,
setAcademicYear,
studyProgrammes,
setStudyProgrammes,
specialGroups,
setSpecialGroups,
language,
}) => {
const toolTips = InfotoolTips.Faculty
const yearType = academicYear ? 'ACADEMIC_YEAR' : 'CALENDAR_YEAR'
const special = 'SPECIAL_INCLUDED' // specialGroups ? 'SPECIAL_EXCLUDED' : 'SPECIAL_INCLUDED'
const credits = useGetFacultyCreditStatsQuery({ id: faculty?.code, yearType, specialGroups: special })
const basics = useGetFacultyBasicStatsQuery({ id: faculty?.code, yearType, specialGroups: special })
const thesisWriters = useGetFacultyThesisStatsQuery({ id: faculty?.code, yearType })
// basic.data.studentsInfo --> data for students of faculty table
// basics.data.graduationInfo --> data for graduated and thesis writers table
const studyProgrammeFilter = studyProgrammes ? 'ALL_PROGRAMMES' : 'NEW_STUDY_PROGRAMMES'
const special = specialGroups ? 'SPECIAL_EXCLUDED' : 'SPECIAL_INCLUDED'
const credits = useGetFacultyCreditStatsQuery({
id: faculty?.code,
yearType,
studyProgrammeFilter,
specialGroups: special,
})
const basics = useGetFacultyBasicStatsQuery({
id: faculty?.code,
yearType,
studyProgrammeFilter,
specialGroups: special,
})
const thesisWriters = useGetFacultyThesisStatsQuery({
id: faculty?.code,
yearType,
studyProgrammeFilter,
specialGroups: special,
})
const getDivider = (title, toolTipText) => (
const downloadCsv = (titles, tableStats, programmeStats, programmeNames, toolTipText) => {
const headers = titles.map(title => ({ label: title === '' ? 'Year' : title, key: title === '' ? 'Year' : title }))
const csvData = sortProgrammeKeys(Object.keys(programmeStats)).reduce(
(results, programme) => [
...results,
...programmeStats[programme].map(yearRow => {
return {
Programme: programme,
Name: programmeNames[programme][language]
? programmeNames[programme][language]
: programmeNames[programme].fi,
...yearRow.reduce((result, value, valueIndex) => ({ ...result, [headers[valueIndex].key]: value }), {}),
}
}),
],
[]
)
const tableStatsAsCsv = tableStats.map(yearArray =>
yearArray.reduce((result, value, yearIndex) => ({ ...result, [headers[yearIndex].key]: value }), {})
)
const book = xlsx.utils.book_new()
const tableSheet = xlsx.utils.json_to_sheet(tableStatsAsCsv)
xlsx.utils.book_append_sheet(book, tableSheet, 'TableStats')
const sheet = xlsx.utils.json_to_sheet(csvData)
xlsx.utils.book_append_sheet(book, sheet, 'ProgrammeStats')
xlsx.writeFile(book, `${faculty.code}-${toolTipText}.xlsx`)
}
const getDivider = (title, toolTipText, titles, tableStats, programmeStats, programmeNames) => (
<>
<div className="divider">
<Divider data-cy={`Section-${toolTipText}`} horizontal>
{title}
</Divider>
</div>
<InfoBox content="Sisältää opintopisteet suoritusvuosittain. Suoritukset on jaoteltu Sisussa näkyvän kurssin suorituspäivän mukaan. Kaikki valmistuneet -lukumäärä sisältää kaikki mahdolliset suoritetuksi merkityt opiskeluoikeudet." />
{/* <InfoBox content={toolTips[toolTipText]} /> */}
<InfoBox content={toolTips[toolTipText]} cypress={toolTipText} />
<Popup
content="Download statistics as csv"
trigger={
<Button
icon="download"
floated="right"
onClick={() => downloadCsv(titles, tableStats, programmeStats, programmeNames, toolTipText)}
style={{ backgroundColor: 'white', borderRadius: 0 }}
/>
}
/>
</>
)
const isFetchingOrLoading =
@@ -54,39 +118,69 @@ const Overview = ({ faculty, academicYear, setAcademicYear }) => {
if (isError) return <h3>Something went wrong, please try refreshing the page.</h3>
/*
Order of the programme keys: KH -> MH -> T -> FI -> K- -> Numbers containing letters at end -> Y- -> Numbers
*/
const regexValuesAll = [
/^KH/,
/^MH/,
/^T/,
/^LI/,
/^K-/,
/^FI/,
/^00901$/,
/^00910$/,
/^\d.*a$/,
/^Y/,
/\d$/,
/^\d.*e$/,
]
// const regexValuesNewProgrammes = [/^KH/, /^MH/, /^T/, /^LI/, /^K-/, /^FI/, /^00901$/, /^00910$/]
// const regexValuesOldProgrammes = [/^\d.*a$/, /\^d.*$/, /^Y/, /^\d.*e$/]
let creditShortTitles = []
let transferShortTitles = []
if (special === 'SPECIAL_INCLUDED') {
creditShortTitles = ['Code', 'Total', 'Major', 'Non-major', 'Non-major other', 'Non-degree']
transferShortTitles = ['Code', 'Started', 'Graduated', 'Transferred in', 'Transferred away', 'Transferred to']
} else {
creditShortTitles = ['Code', 'Major']
transferShortTitles = ['Code', 'Started', 'Graduated']
}
const testKey = value => {
for (let i = 0; i < regexValuesAll.length; i++) {
if (regexValuesAll[i].test(value)) {
return i
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 6
}
const sortProgrammeKeys = programmeKeys => {
return programmeKeys.sort((a, b) => {
return testKey(a) - testKey(b)
})
return plotLinePlaces
}
return (
<div className="faculty-overview">
<div className="toggle-container">
@@ -98,96 +192,234 @@ const Overview = ({ faculty, academicYear, setAcademicYear }) => {
value={academicYear}
setValue={setAcademicYear}
/>
<Toggle
cypress="ProgrammeToggle"
toolTips={toolTips.ProgrammeToggle}
firstLabel="New study programmes"
secondLabel="All study programmes"
value={studyProgrammes}
setValue={setStudyProgrammes}
/>
<Toggle
cypress="StudentToggle"
toolTips={toolTips.StudentToggle}
firstLabel="All studyrights"
secondLabel="Special studyrights excluded"
value={specialGroups}
setValue={setSpecialGroups}
/>
</div>
{isFetchingOrLoading ? (
<Loader active style={{ marginTop: '10em' }} />
<Loader active style={{ marginTop: '15em' }} />
) : (
<>
{studyProgrammeFilter === 'ALL_PROGRAMMES' && (
<Message data-cy="FacultyProgrammesShownInfo">
Please note that the data is complete only for current Bachelor, Masters and Doctoral programmes.
Especially, credits and thesis writers contain only data for current programmes.
</Message>
)}
{special === 'SPECIAL_EXCLUDED' && (
<Message data-cy="FacultyExcludeSpecialsInfo">
Please note that the data might be inaccurate in some fields. Checking the values is in progress.
</Message>
)}
{basics.isSuccess && basics.data && (
<>
{getDivider('Students of the faculty', 'StudentsOfTheFaculty')}
{getDivider(
'Students of the faculty',
'StudentsOfTheFaculty',
basics?.data?.studentInfo.titles,
basics?.data?.studentInfo.tableStats,
basics?.data?.studentInfo.programmeTableStats,
basics?.data?.programmeNames
)}
<div className="section-container">
<LineGraph
cypress="StudentsOfTheFaculty"
data={{ ...basics?.data.studentInfo, years: basics.data.years }}
/>
<InteractiveDataTable
cypress="StudentsOfTheFaculty"
dataStats={basics?.data?.studentInfo.tableStats}
dataProgrammeStats={basics?.data?.studentInfo.programmeTableStats}
programmeNames={basics?.data?.programmeNames}
sortedKeys={sortProgrammeKeys(Object.keys(basics?.data?.studentInfo.programmeTableStats))}
titles={basics?.data?.studentInfo.titles}
sliceStart={1}
language="fi"
/>
<div className="graph-container-narrow">
<LineGraph
cypress="StudentsOfTheFaculty"
data={{ ...basics?.data.studentInfo, years: basics.data.years }}
/>
</div>
<div className="table-container-wide">
<InteractiveDataTable
cypress="StudentsOfTheFaculty"
dataStats={basics?.data?.studentInfo.tableStats}
dataProgrammeStats={basics?.data?.studentInfo.programmeTableStats}
programmeNames={basics?.data?.programmeNames}
sortedKeys={sortProgrammeKeys(
Object.keys(basics?.data?.studentInfo.programmeTableStats).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}
yearsVisible={Array(basics?.data?.studentInfo.tableStats.length).fill(false)}
shortNames={transferShortTitles}
/>
</div>
</div>
</>
)}
{credits.isSuccess && credits.data && (
<>
{getDivider('Credits produced by the faculty', 'CreditsProducedByTheFaculty')}
{getDivider(
'Credits produced by the faculty',
'CreditsProducedByTheFaculty',
credits?.data?.titles,
credits?.data?.tableStats,
credits?.data?.programmeTableStats,
credits?.data?.programmeNames
)}
<div className="section-container">
<StackedBarChart
cypress="CreditsProducedByTheSFaculty"
data={credits?.data?.graphStats}
labels={credits?.data?.years}
/>
<InteractiveDataTable
cypress="CreditsProducedByTheFaculty"
dataStats={credits?.data?.tableStats}
dataProgrammeStats={credits?.data?.programmeTableStats}
programmeNames={credits?.data?.programmeNames}
sortedKeys={sortProgrammeKeys(Object.keys(credits?.data?.programmeTableStats))}
titles={credits?.data?.titles}
extraHeight="EXTRA HEIGHT"
sliceStart={2}
language="fi"
/>
<div className="graph-container-narrow">
<StackedBarChart
cypress="CreditsProducedByTheFaculty"
data={credits?.data?.graphStats}
labels={credits?.data?.years}
wideTable="narrow"
/>
</div>
<div className="table-container-wide">
<InteractiveDataTable
cypress="CreditsProducedByTheFaculty"
dataStats={credits?.data?.tableStats}
dataProgrammeStats={credits?.data?.programmeTableStats}
programmeNames={credits?.data?.programmeNames}
sortedKeys={sortProgrammeKeys(
Object.keys(credits?.data.programmeTableStats).map(obj => [
obj,
credits?.data?.programmeNames[obj].code,
]),
faculty.code
).map(listObj => listObj[0])}
plotLinePlaces={getChartPlotLinePlaces(
sortProgrammeKeys(
Object.keys(credits?.data?.programmeTableStats).map(obj => [
obj,
credits?.data?.programmeNames[obj].code,
]),
faculty.code
)
)}
titles={credits?.data?.titles}
extraHeight="EXTRA HEIGHT"
sliceStart={2}
language={language}
yearsVisible={Array(credits?.data?.tableStats.length).fill(false)}
shortNames={creditShortTitles}
/>
</div>
</div>
</>
)}
{basics.isSuccess && basics.data && (
<>
{getDivider('Graduated of the faculty', 'GraduatedOfTheFaculty')}
{getDivider(
'Graduated of the faculty',
'GraduatedOfTheFaculty',
basics?.data?.graduationInfo.titles,
basics?.data?.graduationInfo.tableStats,
basics?.data?.graduationInfo.programmeTableStats,
basics?.data?.programmeNames
)}
<div className="section-container">
<LineGraph
cypress="GraduatedOfTheFaculty"
data={{ ...basics?.data.graduationInfo, years: basics.data.years }}
/>
<InteractiveDataTable
cypress="GraduatedOfTheFaculty"
dataStats={basics?.data?.graduationInfo.tableStats}
dataProgrammeStats={basics?.data?.graduationInfo.programmeTableStats}
programmeNames={basics?.data?.programmeNames}
sortedKeys={sortProgrammeKeys(Object.keys(basics?.data?.graduationInfo.programmeTableStats))}
titles={basics?.data?.graduationInfo.titles}
sliceStart={2}
language="fi"
/>
<div className="graph-container-narrow">
<LineGraph
cypress="GraduatedOfTheFaculty"
data={{ ...basics?.data.graduationInfo, years: basics.data.years }}
/>
</div>
<div className="table-container-wide">
<InteractiveDataTable
cypress="GraduatedOfTheFaculty"
dataStats={basics?.data?.graduationInfo.tableStats}
dataProgrammeStats={basics?.data?.graduationInfo.programmeTableStats}
programmeNames={basics?.data?.programmeNames}
sortedKeys={sortProgrammeKeys(
Object.keys(basics?.data?.graduationInfo.programmeTableStats).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}
yearsVisible={Array(basics?.data?.graduationInfo.tableStats.length).fill(false)}
/>
</div>
</div>
</>
)}
{thesisWriters.isSuccess && thesisWriters.data && (
<>
{getDivider('Thesis writers of the faculty', 'ThesisWritersOfTheFaculty')}
{getDivider(
'Thesis writers of the faculty',
'ThesisWritersOfTheFaculty',
thesisWriters?.data?.titles,
thesisWriters?.data.tableStats,
thesisWriters?.data.programmeTableStats,
thesisWriters?.data?.programmeNames
)}
<div className="section-container">
<LineGraph
cypress="ThesisWritersOfTheFaculty"
data={{ ...thesisWriters?.data, years: thesisWriters?.data.years }}
/>
<InteractiveDataTable
cypress="ThesisWritersOfTheFaculty"
dataStats={thesisWriters?.data.tableStats}
dataProgrammeStats={thesisWriters?.data.programmeTableStats}
programmeNames={thesisWriters?.data.programmeNames}
sortedKeys={sortProgrammeKeys(Object.keys(thesisWriters?.data.programmeTableStats))}
titles={thesisWriters?.data?.titles}
sliceStart={2}
extraHeight="EXTRA HEIGHT"
language="fi"
/>
<div className="graph-container-narrow">
<LineGraph
cypress="ThesisWritersOfTheFaculty"
data={{ ...thesisWriters?.data, years: thesisWriters?.data.years }}
/>
</div>
<div className="table-container-wide">
<InteractiveDataTable
cypress="ThesisWritersOfTheFaculty"
dataStats={thesisWriters?.data.tableStats}
dataProgrammeStats={thesisWriters?.data.programmeTableStats}
programmeNames={thesisWriters?.data.programmeNames}
sortedKeys={sortProgrammeKeys(
Object.keys(thesisWriters?.data.programmeTableStats).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"
language={language}
yearsVisible={Array(thesisWriters?.data.tableStats.length).fill(false)}
/>
</div>
</div>
</>
)}
@@ -0,0 +1,70 @@
import React from 'react'
const ReactHighcharts = require('react-highcharts')
const getColors = len => {
if (len < 8) return ['#f57368', '#fb8c6e', '#fba678', '#dbda7d', '#9ec27c', '#60a866', '#008c59']
return ['#e66067', '#f57368', '#fb8c6e', '#fba678', '#dbda7d', '#9ec27c', '#60a866', '#008c59']
}
const FacultyBarChart = ({ cypress, data }) => {
if (!data) return null
const colors = getColors(Object.keys(data.stats).length)
const dataWithColors = Object.values(data?.stats).map((series, index) => ({ ...series, color: colors[index] }))
const getFileName = () => {
return `oodikone_progress_of_students_in_${data?.id}_by_study_start_year`
}
const defaultConfig = {
title: {
text: '',
},
series: dataWithColors,
credits: {
text: 'oodikone | TOSKA',
},
xAxis: {
categories: data?.years,
},
chart: {
type: 'column',
height: '450px',
},
plotOptions: {
column: {
stacking: 'percent',
dataLabels: {
enabled: true,
format: '{point.percentage:.1f}%',
},
},
},
exporting: {
filename: getFileName(),
width: 2200,
height: 1400,
sourceWidth: 1200,
sourceHeight: 600,
buttons: {
contextButton: {
menuItems: ['viewFullscreen', 'downloadPNG', 'downloadSVG', 'downloadPDF'],
},
},
},
yAxis: {
allowDecimals: false,
min: 0,
reversed: false,
title: '',
},
}
return (
<div className="graph-container" data-cy={`Graph-${cypress}`}>
<ReactHighcharts config={defaultConfig} />
</div>
)
}
export default FacultyBarChart
@@ -0,0 +1,86 @@
import React, { useState } from 'react'
import { Table } from 'semantic-ui-react'
import ProgrammeProgressChart from './ProgrammeProgressChart'
import BasicRow from '../InteractiveDataView/BasicRow'
const FacultyProgressTable = ({
data,
programmeStats,
titles,
sortedKeys,
progressYearsVisible,
programmeNames,
language,
cypress,
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>
<Table data-cy={cypress} compact celled>
<Table.Header>
<Table.Row key={`${cypress}-${Math.random()}`} textAlign="center">
{titles?.map(title => (
<Table.HeaderCell key={title}>{title}</Table.HeaderCell>
))}
</Table.Row>
</Table.Header>
<Table.Body>
{data?.map(
(yearArray, yearIndex) =>
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.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>
)
}
export default FacultyProgressTable
@@ -0,0 +1,220 @@
/* eslint-disable react/no-array-index-key */
import PopulationLink from 'components/StudyProgramme/StudytrackOverview/PopulationLink'
import React, { useState } from '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,
language,
cypress,
requiredRights,
}) => {
const [yearsVisible, setVisible] = useState(new Array(years.length).fill(false))
const [showPercentages, setShowPercentages] = useState(false)
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={cypress} celled structured>
<Table.Header>
<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"
>
{title}
</Table.HeaderCell>
))}
</Table.Row>
</Table.Header>
<Table.Body>
{years.map((year, yearIndex) => {
return (
<>
<Table.Row key={`${year}-faculty-row}`} className="header-row">
{tableStats[year].map((value, valueIdx) => {
if (valueIdx === 0)
return (
<Table.Cell key={`${year}-faculty-cell}`}>
<Button
key={`${year}-studentsTableButton}`}
as="div"
onClick={() => toggleVisibility(yearIndex)}
labelPosition="right"
style={{ backgroundColor: 'white', borderRadius: 0, padding: 0, margin: 0 }}
data-cy={`Button-${cypress}-${yearIndex}`}
>
<Button icon style={{ backgroundColor: 'white', borderRadius: 0, padding: 0, margin: 0 }}>
<Icon name={yearsVisible[yearIndex] ? 'angle down' : 'angle right'} />
</Button>
<Label as="a" style={{ backgroundColor: 'white', borderRadius: 0, padding: 0, margin: 0 }}>
<b>{value}</b>
</Label>
</Button>
</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, 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(programmeNames[programme].code)) && (
<PopulationLink
studyprogramme={programmeNames[programme].code}
year={year}
years={calendarYears}
/>
)}
</Table.Cell>
{getRows(idx, programme, year)}
</Table.Row>
)
})}
</>
)
})}
</Table.Body>
</Table>
</div>
)
}
export default FacultyStudentDataTable
@@ -0,0 +1,119 @@
/* eslint-disable react/no-this-in-sfc */
import React from 'react'
import Highcharts from 'highcharts'
import ReactHighcharts from 'react-highcharts'
const getColors = len => {
if (len < 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']
}
// 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 (
<>
<b>No data available</b>
</>
)
const transpose = matrix => {
return matrix.reduce((prev, next) => next.map((_item, i) => (prev[i] || []).concat(next[i])), [])
}
const colors = getColors(data[0]?.length || 6)
const dataTranspose = transpose(data)
.map((obj, idx) => ({ name: names[idx], data: obj, color: colors[idx] }))
.reverse()
// Tooltip formatter function: this.<some value> needs to be used in order to obtain access to the values.
const defaultConfig = {
chart: {
type: 'bar',
marginTop: 60,
height: getFlexHeight(labels.length, needsExtra),
},
credits: {
text: 'oodikone | TOSKA',
},
title: {
text: '',
style: {
display: 'none',
},
},
series: dataTranspose,
xAxis: {
categories: labels,
fontSize: '24px',
},
yAxis: {
allowDecimals: false,
min: 0,
reversed: true,
title: '',
},
legend: {
layout: 'horizontal',
align: 'left',
fontSize: '24px',
x: 10,
verticalAlign: 'top',
y: -10,
floating: true,
backgroundColor: Highcharts.defaultOptions.legend.backgroundColor || 'white',
borderColor: '#CCC',
borderWidth: 1,
shadow: false,
},
tooltip: {
shared: true,
backgroundColor: 'white',
fontSize: '25px',
formatter() {
let tooltipString = `<b>${
longLabels[this.x] && longLabels[this.x][language] ? longLabels[this.x][language] : longLabels[this.x].fi
} </b><br /><p>${this.x} - ${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 />`
})
return tooltipString
},
},
plotOptions: {
series: {
stacking: 'percent',
pointWidth: 20,
dataLabels: {
enabled: true,
format: '{point.percentage:.1f}%',
color: '#000000',
align: 'left',
style: {
textOutline: 'none',
},
filter: {
property: 'percentage',
operator: '>',
value: 3,
},
},
},
},
}
return <ReactHighcharts config={defaultConfig} />
}
export default ProgrammeProgressChart
@@ -1,7 +1,295 @@
import React from 'react'
import { Divider, Loader, Message } from 'semantic-ui-react'
import { useGetFacultyProgressStatsQuery, useGetFacultyStudentStatsQuery } from 'redux/facultyStats'
import FacultyProgressTable from './FacultyProgressTable'
import FacultyBarChart from './FacultyBarChart'
import Toggle from '../../StudyProgramme/Toggle'
import InfoBox from '../../Info/InfoBox'
import InfotoolTips from '../../../common/InfoToolTips'
import '../faculty.css'
import FacultyStudentDataTable from './FacultyStudentDataTable'
import sortProgrammeKeys from '../facultyHelpers'
const FacultyProgrammeOverview = () => {
return <div className="programmes-overview">Faculty programmes</div>
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} cypress={cypress} />}
</>
)
const getKey = (programmeKeys, idx) => {
return programmeKeys[idx][1].startsWith('T') ? 'T' : programmeKeys[idx][1].slice(0, 2)
}
const FacultyProgrammeOverview = ({
faculty,
language,
graduatedGroup,
setGraduatedGroup,
specialGroups,
setSpecialGroups,
requiredRights,
}) => {
const toolTips = InfotoolTips.Faculty
const studyProgrammeFilter = 'NEW_STUDY_PROGRAMMES'
const specials = specialGroups ? 'SPECIAL_EXCLUDED' : 'SPECIAL_INCLUDED'
const graduated = graduatedGroup ? 'GRADUATED_EXCLUDED' : 'GRADUATED_INCLUDED'
const progressStats = useGetFacultyProgressStatsQuery({
id: faculty?.code,
studyProgrammeFilter,
specialGroups: specials,
graduated,
})
const studentStats = useGetFacultyStudentStatsQuery({
id: faculty.code,
studyProgrammeFilter,
specialGroups: specials,
graduated,
})
const isFetchingOrLoading =
progressStats.isLoading || progressStats.isFetching || studentStats.isLoading || studentStats.isFetching
const isError =
progressStats.isError ||
studentStats.isError ||
(progressStats.isSuccess && !progressStats.data) ||
(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">
<Toggle
cypress="StudentToggle"
toolTips={toolTips.StudentToggle}
firstLabel="All studyrights"
secondLabel="Special studyrights excluded"
value={specialGroups}
setValue={setSpecialGroups}
/>
<Toggle
cypress="GraduatedToggle"
toolTips={toolTips.GraduatedToggle}
firstLabel="Graduated included"
secondLabel="Graduated excluded"
value={graduatedGroup}
setValue={setGraduatedGroup}
/>
</div>
{isFetchingOrLoading ? (
<Loader active style={{ marginTop: '15em' }} />
) : (
<div className="programmes-overview">
{studentStats && studentStats.data && (
<>
{getDivider(
'Students of the faculty By Starting year',
'StudentsOfTheFacultyByStartingYear',
toolTips.StudentsStatsOfTheFaculty,
'InfoFacultyStudentTable'
)}
<div>
<FacultyStudentDataTable
tableStats={studentStats?.data.facultyTableStats}
programmeStats={studentStats?.data.programmeStats}
programmeNames={studentStats?.data.programmeNames}
titles={studentStats?.data.titles}
years={studentStats?.data.years}
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}
/>
</div>
</>
)}
{progressStats.isSuccess && progressStats.data && (
<>
{getDivider(
'Progress of students of the faculty ',
'BachelorStudentsOfTheFacultyByStartingYear',
toolTips.StudentProgress,
'InfoFacultyProgress'
)}
{getDivider('Bachelor', 'BachelorStudentsOfTheFacultyByStartingYear', 'no-infobox')}
<div className="section-container">
<div className="graph-container">
<FacultyBarChart
cypress="FacultyBachelorsProgress"
data={{
id: faculty.code,
stats: progressStats?.data.bachelorsGraphStats,
years: progressStats?.data.years,
}}
/>
</div>
<div className="table-container">
<FacultyProgressTable
data={progressStats?.data.bachelorsTableStats}
programmeStats={progressStats?.data.bachelorsProgStats}
titles={progressStats?.data.bachelorTitles}
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
cypress="FacultyBachelorMastersProgress"
data={{
id: faculty.code,
stats: progressStats?.data.bcMsGraphStats,
years: progressStats?.data.years,
}}
/>
</div>
<div className="table-container">
<FacultyProgressTable
data={progressStats?.data.bcMsTableStats}
programmeStats={progressStats?.data.bcMsProgStats}
titles={progressStats?.data.bcMsTitles}
sortedKeys={sortProgrammeKeys(
Object.keys(progressStats?.data.bcMsProgStats).map(obj => [
obj,
progressStats?.data?.programmeNames[obj].code,
]),
faculty.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>
{!(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
cypress="FacultyDoctoralProgress"
data={{
id: faculty.code,
stats: progressStats?.data.doctoralGraphStats,
years: progressStats?.data.years,
}}
/>
</div>
<div className="table-container">
<FacultyProgressTable
data={progressStats?.data.doctoralTableStats}
programmeStats={progressStats?.data.doctoralProgStats}
titles={progressStats?.data.doctoralTitles}
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="FacultyDoctoralProgressTable"
/>
</div>
</div>
</>
)}
</div>
)}
</div>
)
}
export default FacultyProgrammeOverview
@@ -0,0 +1,37 @@
import React from 'react'
import { Button, Icon, Table, Label } from 'semantic-ui-react'
const roundValue = value => {
return Number.isInteger(value) ? value : value.toFixed(1)
}
const BasicRow = ({ icon, yearArray, cypress, yearIndex, toggleVisibility, styles }) => {
return (
<Table.Row style={styles} key={`Basic-row-${cypress}-${yearIndex}`}>
{yearArray?.map((value, idx) => (
<Table.Cell key={`random-button-cell-key-${Math.random()}`}>
{idx === 0 ? (
<Button
as="div"
onClick={toggleVisibility}
labelPosition="right"
style={{ backgroundColor: 'white', borderRadius: 0, padding: 0, margin: 0 }}
data-cy={`Button-${cypress}-${yearIndex}`}
>
<Button icon style={{ backgroundColor: 'white', borderRadius: 0, padding: 0, margin: 0 }}>
<Icon name={icon} />
</Button>
<Label as="a" style={{ backgroundColor: 'white', borderRadius: 0, padding: 0, margin: 0 }}>
{value}
</Label>
</Button>
) : (
roundValue(value)
)}
</Table.Cell>
))}
</Table.Row>
)
}
export default BasicRow
@@ -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', '#434348', '#90ed7d', '#f7a35c', '#8085e9', '#2b908f', '#f45b5b', '#91e8e1']
const colors = ['#7cb5ec', '#90ed7d', '#434348', '#f7a35c', '#FFF000', '#2b908f', '#f45b5b', '#91e8e1']
const CollapsedStackedBar = ({ data, labels, longLabels, names, language, differenceData, 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,
@@ -109,9 +136,9 @@ const CollapsedStackedBar = ({ data, labels, longLabels, names, language, differ
backgroundColor: 'white',
fontSize: '25px',
formatter() {
let tooltipString = `<b>${longLabels[this.x] ? longLabels[this.x][language] : ''}</b><br /><p>${
this.x
}</p><br />`
let tooltipString = `<b>${
longLabels[this.x] && longLabels[this.x][language] ? longLabels[this.x][language] : longLabels[this.x].fi
}</b><br /><p>${this.x} - ${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>
@@ -119,15 +146,21 @@ const CollapsedStackedBar = ({ data, labels, longLabels, names, language, differ
diffArray[point.series.name]
)}</span>)<br />`
})
tooltipString += `<b>Total: ${this.points.reduce((prev, current) => prev + current.y, 0)}</b>`
return tooltipString
},
},
plotOptions: {
series: {
stacking: 'normal',
pointWidth: 24,
pointWidth: 20,
dataLabels: {
enabled: true,
align: 'left',
formatter() {
if (Number.isInteger(this.y)) return `${this.y}`
return `${this.y.toFixed(1)}`
},
fontSize: '24px',
filter: {
property: 'y',
@@ -1,75 +0,0 @@
import React, { useState, useImperativeHandle } from 'react'
import { Button, Icon, Table, Label } from 'semantic-ui-react'
const BasicRow = ({ icon, yearArray, cypress, yearIndex, toggleVisibility, styles }) => {
return (
<Table.Row style={styles} key={`Basic-row-${cypress}-${yearIndex}`}>
{yearArray?.map((value, idx) => (
<Table.Cell key={`random-button-cell-key-${Math.random()}`}>
{idx === 0 ? (
<Button
as="div"
onClick={toggleVisibility}
labelPosition="right"
style={{ backgroundColor: 'white', borderRadius: 0 }}
data-cy={`Button-${cypress}-${yearIndex}`}
>
<Button icon style={{ backgroundColor: 'white', borderRadius: 0 }}>
<Icon name={icon} />
</Button>
<Label as="a" style={{ backgroundColor: 'white', borderRadius: 0 }}>
{value}
</Label>
</Button>
) : (
value
)}
</Table.Cell>
))}
</Table.Row>
)
}
const ToggleTableView = React.forwardRef((props, ref) => {
const [visible, setVisible] = useState(false)
const toggleVisibility = () => {
setVisible(!visible)
}
useImperativeHandle(ref, () => {
return toggleVisibility
})
const chartRowStyles = {
display: visible ? '' : 'none',
}
const basicRowStyles = {
display: visible ? 'none' : '',
}
return (
<React.Fragment key={`random-fragment-key-${Math.random()}`}>
<BasicRow
icon="chevron right"
yearArray={props.yearArray}
cypress={`Show-${props.cypress}`}
yearIndex={props.yearIndex}
toggleVisibility={toggleVisibility}
styles={basicRowStyles}
/>
<BasicRow
icon="chevron down"
yearArray={props.yearArray}
cypress={`Hide-${props.cypress}`}
yearIndex={props.yearIndex}
toggleVisibility={toggleVisibility}
styles={chartRowStyles}
/>
<Table.Row key={`stack-row-key-${Math.random()}`} style={chartRowStyles}>
{props.children}
</Table.Row>
</React.Fragment>
)
})
ToggleTableView.displayName = 'ToggleTableView'
export default ToggleTableView

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