Compare commits

...
190 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
106 changed files with 3542 additions and 4168 deletions
+20
View File
@@ -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
+86 -9
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()
@@ -154,6 +152,12 @@ describe('Faculty overview', () => {
})
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)
@@ -196,6 +200,7 @@ describe('Faculty overview', () => {
})
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')
@@ -209,15 +214,87 @@ describe('Faculty overview', () => {
cy.get('[data-cy="GraduationTimeToggle"]').click()
cy.get('[data-cy="Section-master"]').within(() => {
cy.contains('1 graduated (100 % of class)').click()
cy.get('div[class="programmes-graph"]').should('be.visible')
cy.get('div[class="programmes-graph"]').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('1 graduated').trigger('mouseover')
cy.contains('From class of 2018, 1/1 students have graduated')
cy.contains('mean study time: 25 months')
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')
})
})
})
+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')
})
})
})
+15 -17
View File
@@ -1,19 +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'
@@ -24,6 +13,18 @@ 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 class').should('be.disabled')
cy.contains('Search for class')
@@ -66,9 +67,6 @@ describe('Population Statistics tests', () => {
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('.header').within(() => {
cy.contains('Tietojenkäsittelytieteen kandiohjelma')
cy.contains('class size 170 students')
@@ -105,7 +103,6 @@ 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 class').click()
@@ -220,11 +217,11 @@ describe('Population Statistics tests', () => {
})
})
/*
describe('when using IAM user', () => {
beforeEach(() => {
cy.init(pathToCSBach2017, 'onlyiamrights')
})
it('Population statistics is visible', () => {
cy.reload()
@@ -241,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
})
})
*/
})
+62 -16
View File
@@ -181,11 +181,15 @@ describe('Studyprogramme overview', () => {
.should('contain', 25)
.should('contain', 35)
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')
@@ -198,19 +202,51 @@ 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 student populations').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', () => {
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', () => {
@@ -220,7 +256,7 @@ describe('Studyprogramme overview', () => {
['2019 - 2020', 86, 86, 0, 0, 1, 1, 65, 21, 86],
['2018 - 2019', 162, 161, 0, 0, 0, 11, 118, 44, 161],
['2017 - 2018', 171, 171, 0, 0, 4, 42, 135, 36, 166],
['Total', 431, 431, 0, 0, 5, 54, 328, 103, 425],
['Total', 431, 428, 0, 0, 5, 54, 328, 103, 425],
]
cy.checkTableStats(tableContents, 'StudytrackOverview')
@@ -232,7 +268,7 @@ describe('Studyprogramme overview', () => {
['2019 - 2020', 86, 11, 15, 35, 20, 5, 0, 0, 0],
['2018 - 2019', 162, 3, 9, 28, 50, 33, 25, 11, 3],
['2017 - 2018', 171, 7, 13, 22, 32, 18, 23, 32, 24],
['Total', 431, 16, 35, 65, 103, 75, 56, 50, 31],
['Total', 431, 33, 37, 85, 102, 56, 48, 43, 27],
]
cy.checkTableStats(tableContents, 'StudytrackProgress')
@@ -247,10 +283,16 @@ describe('Studyprogramme overview', () => {
.should('contain', '5.6%') // The percentage for 15-29 credits in 2018-2019
.should('contain', '12.8%') // The percentage for less than 15 credits in 2019-2020
cy.get('[data-cy=Graph-2018-AverageGraduationTimes]')
.should('contain', '2018 - 2019')
.should('contain', 'n = 11 / 162')
.should('contain', '22 kk')
cy.get('[data-cy=graduation-times-graph-breakdown]')
cy.get('[data-cy=GraduationTimeToggle]').click()
cy.get('[data-cy=graduation-times-graph]').within(() => {
cy.contains('Start year')
cy.contains('2019 - 2020')
cy.contains('42 graduated').trigger('mouseover')
cy.contains('study time: 31.5 months')
cy.contains('39 graduated on time')
})
})
})
@@ -258,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()
})
@@ -334,6 +378,8 @@ 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()
})
@@ -394,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]')
@@ -423,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"]
}
+12
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,
+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
+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
-438
View File
@@ -1,438 +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 universityServices = 'hy-ypa-opa-dojo'
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(universityServices)
.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
+1 -1
View File
@@ -125,7 +125,7 @@ const codes = {
T922106: 'foodhealth',
T923101: 'papu',
T923102: 'geodoc',
T923103: 'atm',
T923103: 'atm-dp',
T923104: 'chems',
T923105: 'domast',
T923106: 'matrena',
+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,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: () => {},
}
+8 -1
View File
@@ -38,9 +38,16 @@ const refreshFaculties = async () => {
const refreshNewOverviews = async () => {
logger.info('Refreshing studyprogramme and studytrack overview statistics for all programmes')
// Filters out old programmes and special ones like Fitech studies. Filters also out the programmes starting with
// 2_ or ending with _2. Those programmes are mapped to correct programme (studentProgrammeModuleFixer.js)
const codes = (await getAllProgrammes())
.map(p => p.code)
.filter(code => code.includes('KH') || code.includes('MH') || /^(T)[0-9]{6}$/.test(code))
.filter(
code =>
(code.includes('KH') && !code.startsWith('2_KH') && !code.endsWith('_2')) ||
(code.includes('MH') && !code.startsWith('2_MH') && !code.endsWith('_2')) ||
/^(T)[0-9]{6}$/.test(code)
)
let ready = 0
for (const code of codes) {
try {
@@ -3,8 +3,8 @@ const { ApplicationError } = require('../util/customErrors')
const { getUser, getMockedUser } = require('../services/userService')
const { requiredGroup } = require('../conf-backend')
const _ = require('lodash')
const { relevantIAMs } = require('../../config/IAMConfig')
const { getOrganizationAccess } = require('../util/organizationAccess')
const logger = require('../util/logger')
const parseIamGroups = iamGroups => iamGroups?.split(';').filter(Boolean) ?? []
@@ -27,11 +27,12 @@ const currentUserMiddleware = async (req, _res, next) => {
throw new ApplicationError('Not enough data in request headers', 403, { logoutUrl })
}
const iamGroups = parseIamGroups(hygroupcn).filter(iam => relevantIAMs.includes(iam))
const iamRights = Object.keys(await getOrganizationAccess({ iamGroups }))
const iamGroups = parseIamGroups(hygroupcn)
const { access = {}, specialGroup = {} } = await getOrganizationAccess({ iamGroups })
const iamRights = Object.keys(access)
if (!hasRequiredIamGroup(iamGroups, iamRights)) {
logger.error({ message: 'User does not have required iam group', meta: { iamGroups, iamRights } })
throw new ApplicationError('User does not have required iam group', 403, { logoutUrl })
}
@@ -40,6 +41,8 @@ const currentUserMiddleware = async (req, _res, next) => {
name,
email,
iamGroups,
iamRights,
specialGroup,
sisId,
})
@@ -1,3 +1,4 @@
const { STRING } = require('sequelize')
const Sequelize = require('sequelize')
const { sequelizeKone } = require('../database/connection')
@@ -143,6 +144,35 @@ const OpenUniPopulationSearch = sequelizeKone.define(
{ 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',
{
@@ -188,4 +218,5 @@ module.exports = {
sequelizeKone,
ExcludedCourse,
StudyGuidanceGroupTag,
StudyProgrammeCriteria,
}
+1 -1
View File
@@ -25,7 +25,7 @@ Student.init(
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: {
+2
View File
@@ -31,6 +31,7 @@ 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')
@@ -58,6 +59,7 @@ module.exports = (app, url) => {
app.use(url, tags)
app.use(url, programmeModules)
app.use(url, studyProgramme)
app.use(`${url}/programmecriteria`, studyProgrammeCriteria)
app.use(`${url}/openunisearch`, auth.roles(['openUniSearch']), customOpenUniSearch)
app.use(`${url}/faculties`, auth.roles(['facultyStatistics']), faculties)
app.use(`${url}/updater`, auth.roles(['admin']), updater)
@@ -1,5 +1,5 @@
const router = require('express').Router()
var moment = require('moment-timezone')
const moment = require('moment-timezone')
const { getCustomOpenUniCourses } = require('../services/openUni/openUniStats')
const {
getOpenUniSearches,
+21 -4
View File
@@ -144,13 +144,25 @@ router.get('/:id/progressstats', async (req, res) => {
const programmeFilter = req.query.programme_filter
const specialGroups = req.query?.special_groups
const graduated = req.query?.graduated
const programmes = await getProgrammes(code, programmeFilter)
if (!programmes) return res.status(422).end()
//all programmes are required for correct sorting of transfers
let allProgrammeCodes = []
const allProgs = await getProgrammes(code, 'ALL_PROGRAMMES')
allProgs?.data.forEach(prog => allProgrammeCodes.push(prog.code))
if (!code) return res.status(422).end()
const data = await getFacultyProgressStats(code, specialGroups, graduated)
if (data) return res.json(data)
const programmes = await getProgrammes(code, programmeFilter)
if (!programmes) return res.status(422).end()
let updateStats = await combineFacultyStudentProgress(code, programmes.data, specialGroups, graduated)
let updateStats = await combineFacultyStudentProgress(
code,
programmes.data,
allProgrammeCodes,
specialGroups,
graduated
)
if (updateStats) {
updateStats = await setFacultyProgressStats(updateStats, specialGroups, graduated)
}
@@ -168,7 +180,12 @@ router.get('/:id/studentstats', async (req, res) => {
if (data) return res.json(data)
const programmes = await getProgrammes(code, programmeFilter)
if (!programmes) return res.status(422).end()
let updateStats = await combineFacultyStudents(code, programmes.data, specialGroups, graduated)
//all programmes are required for correct sorting of transfers
let allProgrammeCodes = []
const allProgs = await getProgrammes(code, 'ALL_PROGRAMMES')
allProgs?.data.forEach(prog => allProgrammeCodes.push(prog.code))
let updateStats = await combineFacultyStudents(code, programmes.data, allProgrammeCodes, specialGroups, graduated)
if (updateStats) {
updateStats = await setFacultyStudentStats(updateStats, specialGroups, graduated)
}
@@ -0,0 +1,30 @@
const {
getCriteria,
saveYearlyCourseCriteria,
saveYearlyCreditCriteria,
} = require('../services/studyProgrammeCriteria')
const router = require('express').Router()
router.get('/', async (req, res) => {
const studyProgramme = req.query?.programmecode
if (!studyProgramme) return res.status(422).end()
const studyProgrammeCriteria = await getCriteria(studyProgramme)
return res.json(studyProgrammeCriteria)
})
router.post('/courses', async (req, res) => {
const { code, courses, year } = req.body
if (!code || !courses || !year) return res.status(400).end()
const studyProgrammeCriteria = await saveYearlyCourseCriteria(code, courses, year)
return res.json(studyProgrammeCriteria)
})
router.post('/credits', async (req, res) => {
const { code, credits } = req.body
if (!code || !credits) return res.status(400).end()
const studyProgrammeCriteria = await saveYearlyCreditCriteria(code, credits)
return res.json(studyProgrammeCriteria)
})
module.exports = router
@@ -4,10 +4,9 @@ const {
ElementDetail,
Organization,
ProgrammeModule,
Semester,
SemesterEnrollment,
Studyright,
StudyrightElement,
SemesterEnrollment,
Student,
Transfer,
Credit,
@@ -19,24 +18,54 @@ const {
formatFacultyTransfer,
formatFacultyThesisWriter,
formatOrganization,
formatAbsence,
isNewProgramme,
mapCodesToIds,
facultyProgrammeStudents,
} = require('./facultyHelpers')
const getTransferredToAndAway = async (programmeCodes, allProgrammeCodes, since) => {
const awayTransfers = (await transferredAway(programmeCodes, allProgrammeCodes, since)).map(t => t.studyrightid)
const toTransfers = (await transferredTo(programmeCodes, allProgrammeCodes, since)).map(t => t.studyrightid)
const transferredToOrAway = [...toTransfers, ...awayTransfers]
return transferredToOrAway
const awayTransfers = await transferredAway(programmeCodes, allProgrammeCodes, since)
const toTransfers = await transferredTo(programmeCodes, allProgrammeCodes, since)
return [...toTransfers, ...awayTransfers]
}
const getTransferredInside = async (programmeCodes, allProgrammeCodes, since) => {
const transfersInside = (await transferredInsideFaculty(programmeCodes, allProgrammeCodes, since)).map(
t => t.studyrightid
)
return transfersInside
return await transferredInsideFaculty(programmeCodes, allProgrammeCodes, since)
}
const getTransfers = async (programmeCode, allProgrammeCodes, since, end) => {
return await transferredFaculty(programmeCode, allProgrammeCodes, since, end)
}
const transferredFaculty = async (programme, allProgrammeCodes, since, end) =>
(
await Transfer.findAll({
where: {
transferdate: {
[Op.between]: [since, end],
},
[Op.or]: [
{
sourcecode: allProgrammeCodes,
targetcode: programme,
},
{
sourcecode: programme,
targetcode: {
[Op.notIn]: allProgrammeCodes,
},
},
{
sourcecode: {
[Op.notIn]: allProgrammeCodes,
},
targetcode: programme,
},
],
},
})
).map(formatFacultyTransfer)
const startedStudyrights = async (faculty, since, studyRightWhere) =>
(
await Studyright.findAll({
@@ -125,15 +154,58 @@ const studyrightsByRightStartYear = async (faculty, since, graduated = 1) =>
})
).map(facultyFormatStudyright)
const bachelorStudyright = async id => {
return await Studyright.findOne({
attributes: ['studystartdate'],
where: {
studyrightid: id,
extentcode: 1,
},
})
}
const getStudyRightsByExtent = async (faculty, elementStart, studyrightStart, code, extents, graduated) =>
(
await Studyright.findAll({
include: [
{
model: Student,
attributes: ['studentnumber'],
required: true,
},
{
model: StudyrightElement,
required: true,
where: {
code: code,
...elementStart,
},
include: {
model: ElementDetail,
required: true,
where: {
type: 20,
},
},
},
],
where: {
faculty_code: faculty,
extentcode: {
[Op.in]: extents,
},
graduated: {
[Op.in]: graduated,
},
studystartdate: { [Op.not]: null },
...studyrightStart,
student_studentnumber: { [Op.not]: null },
},
})
).map(facultyFormatStudyright)
const getStudentsByStudentnumbers = async studentnumbers =>
(
await Student.findAll({
where: {
studentnumber: studentnumbers,
},
include: {
model: SemesterEnrollment,
attributes: ['semestercode', 'enrollmenttype'],
},
})
).map(facultyProgrammeStudents)
const hasMasterRight = async id => {
return await Studyright.findOne({
@@ -256,29 +328,6 @@ const thesisWriters = async (provider, since, thesisTypes, students) =>
})
).map(formatFacultyThesisWriter)
const statutoryAbsences = async (studentnumber, startdate, enddate) =>
(
await SemesterEnrollment.findAll({
attributes: ['semestercode'],
include: {
model: Semester,
attributes: ['startdate', 'enddate'],
where: {
startdate: {
[Op.gte]: startdate,
},
enddate: {
[Op.lte]: enddate,
},
},
},
where: {
studentnumber: studentnumber,
statutory_absence: true,
},
})
).map(formatAbsence)
// Some programme modules are not directly associated to a faculty (organization).
// Some have intermediate organizations, such as department, so the connection must be digged up
const findFacultyProgrammeCodes = async (faculty, programmeFilter) => {
@@ -336,7 +385,6 @@ module.exports = {
startedStudyrights,
graduatedStudyrights,
studyrightsByRightStartYear,
bachelorStudyright,
hasMasterRight,
transferredInsideFaculty,
transferredAway,
@@ -345,7 +393,9 @@ module.exports = {
thesisWriters,
findFacultyProgrammeCodes,
facultyOgranizationId,
statutoryAbsences,
getTransferredToAndAway,
getTransferredInside,
getStudyRightsByExtent,
getStudentsByStudentnumbers,
getTransfers,
}
@@ -1,4 +1,5 @@
const { indexOf } = require('lodash')
const moment = require('moment')
const {
startedStudyrights,
graduatedStudyrights,
@@ -7,6 +8,7 @@ const {
transferredTo,
getTransferredToAndAway,
getTransferredInside,
studyrightsByRightStartYear,
} = require('./faculty')
const { getStatsBasis, getYearsArray, defineYear } = require('../studyprogrammeHelpers')
const { findRightProgramme, isNewProgramme, checkTransfers, getExtentFilter } = require('./facultyHelpers')
@@ -109,7 +111,7 @@ const getFacultyStarters = async (
}
})
allBasics.studentInfo.graphStats.push({ name: 'Started studying', data: startedGraphStats })
allBasics.studentInfo.graphStats.push({ name: 'Started studying (new in faculty)', data: startedGraphStats })
programmeData['started'] = programmeTableStats
Object.keys(startedTableStats).forEach(year => {
@@ -153,6 +155,7 @@ const getFacultyGraduates = async (
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
@@ -202,7 +205,18 @@ const getFacultyGraduates = async (
countsGraduations[year] = graduatedTableStats[year]
})
}
// If includeAllSpecial false, insidetransfers should contain only students started after 1.8.2017.
const getInsideTransfers = async (programmeCodes, allProgrammeCodes, since, includeAllSpecials, faculty) => {
if (includeAllSpecials) {
return await transferredInsideFaculty(programmeCodes, allProgrammeCodes, since)
}
const insiders = await transferredInsideFaculty(programmeCodes, allProgrammeCodes, since)
const studyrights = (await studyrightsByRightStartYear(faculty, moment('2017-08-01', 'YYYY-MM-DD'))).map(
sr => sr.studyrightid
)
const filteredTransfers = insiders.filter(tr => studyrights.includes(tr.studyrightid))
return filteredTransfers
}
const getFacultyTransfers = async (
programmes,
programmeCodes,
@@ -215,10 +229,16 @@ const getFacultyTransfers = async (
allBasics,
counts,
programmeData,
includeAllSpecials
includeAllSpecials,
faculty
) => {
const insideTransfers = await transferredInsideFaculty(programmeCodes, allProgrammeCodes, since)
// Filter still away those that have started studying before 1.8.2017, when include all specials false.
const insideTransfers = await getInsideTransfers(
programmeCodes,
allProgrammeCodes,
since,
includeAllSpecials,
faculty
)
let transferGraphStats = []
let transferTableStats = {}
if (includeAllSpecials) {
@@ -234,17 +254,19 @@ const getFacultyTransfers = async (
for (const { transferdate, targetcode } of insideTransfers) {
const transferYear = defineYear(transferdate, isAcademicYear)
transferGraphStats[0][indexOf(yearsArray, transferYear)] += 1
transferTableStats[transferYear][0] += 1
await addProgramme(
programmeTableStats,
targetcode,
tableStats,
transferYear,
0,
allBasics,
programmes,
includeAllSpecials
)
if (isAcademicYear || !(transferYear > moment().year())) {
transferTableStats[transferYear][0] += 1
await addProgramme(
programmeTableStats,
targetcode,
tableStats,
transferYear,
0,
allBasics,
programmes,
includeAllSpecials
)
}
}
if (includeAllSpecials) {
const awayTransfers = await transferredAway(programmeCodes, allProgrammeCodes, since)
@@ -252,34 +274,38 @@ const getFacultyTransfers = async (
for (const { transferdate, sourcecode } of awayTransfers) {
const transferYear = defineYear(transferdate, isAcademicYear)
transferGraphStats[1][indexOf(yearsArray, transferYear)] += 1
transferTableStats[transferYear][1] += 1
await addProgramme(
programmeTableStats,
sourcecode,
tableStats,
transferYear,
1,
allBasics,
programmes,
includeAllSpecials
)
if (isAcademicYear || !(transferYear > moment().year())) {
transferGraphStats[1][indexOf(yearsArray, transferYear)] += 1
transferTableStats[transferYear][1] += 1
await addProgramme(
programmeTableStats,
sourcecode,
tableStats,
transferYear,
1,
allBasics,
programmes,
includeAllSpecials
)
}
}
for (const { transferdate, targetcode } of toTransfers) {
const transferYear = defineYear(transferdate, isAcademicYear)
transferGraphStats[2][indexOf(yearsArray, transferYear)] += 1
transferTableStats[transferYear][2] += 1
await addProgramme(
programmeTableStats,
targetcode,
tableStats,
transferYear,
2,
allBasics,
programmes,
includeAllSpecials
)
if (isAcademicYear || !(transferYear > moment().year())) {
transferGraphStats[2][indexOf(yearsArray, transferYear)] += 1
transferTableStats[transferYear][2] += 1
await addProgramme(
programmeTableStats,
targetcode,
tableStats,
transferYear,
2,
allBasics,
programmes,
includeAllSpecials
)
}
}
}
@@ -377,7 +403,8 @@ const combineFacultyBasics = async (faculty, programmes, yearType, allProgrammeC
allBasics,
counts,
programmeData,
includeAllSpecials
includeAllSpecials,
faculty
)
// combine tableStats from all categories
@@ -423,7 +450,11 @@ const combineFacultyBasics = async (faculty, programmes, yearType, allProgrammeC
if (code in programmeData.transferred) {
studentInfo[code][year] = studentInfo[code][year].concat(programmeData.transferred[code][year])
} else {
studentInfo[code][year] = studentInfo[code][year].concat([0, 0, 0])
if (includeAllSpecials) {
studentInfo[code][year] = studentInfo[code][year].concat([0, 0, 0])
} else {
studentInfo[code][year] = studentInfo[code][year].concat([0])
}
}
}
})
@@ -219,6 +219,7 @@ const combineFacultyCredits = async (faculty, programmes, allProgrammes, yearTyp
const progCodes = allProgrammes.map(p => p.code)
for (const prog of programmes) {
if (prog.code === 'MH70_008_2') continue
const data = await getProgrammeCredits(prog.code, yearType, specialGroups, progCodes)
if (data) {
allCredits.programmeNames[prog.progId] = { code: prog.code, ...prog.name }
@@ -1,14 +1,9 @@
const moment = require('moment')
const {
graduatedStudyrights,
studyrightsByRightStartYear,
bachelorStudyright,
hasMasterRight,
statutoryAbsences,
} = require('./faculty')
const { graduatedStudyrights, studyrightsByRightStartYear, hasMasterRight } = require('./faculty')
const { findRightProgramme, isNewProgramme } = require('./facultyHelpers')
const { getYearsArray, getYearsObject, getMean, getMedian, defineYear } = require('../studyprogrammeHelpers')
const { getYearsArray, getYearsObject, getMedian, defineYear } = require('../studyprogrammeHelpers')
const { codes } = require('../../../config/programmeCodes')
const { bachelorStudyright, countTimeCategories, getStatutoryAbsences } = require('../graduationHelpers')
const sortProgrammes = data => {
const check = name => {
@@ -26,7 +21,8 @@ const getSortedProgIds = progs => {
for (const prog of progs) {
if (Object.keys(codes).includes(prog)) {
programmes = [...programmes, { name: codes[prog].toUpperCase(), code: 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 }]
}
@@ -37,7 +33,7 @@ const getSortedProgIds = progs => {
const findBachelorStartdate = async id => {
const studyright = await bachelorStudyright(id)
if (studyright) return studyright.studystartdate
if (studyright) return studyright.startdate
return null
}
@@ -48,25 +44,6 @@ const getProgrammeObjectBasis = (years, levels, emptyObject = true) => {
)
}
const getStatutoryAbsences = async (studentnumber, startdate, enddate) => {
const absences = await statutoryAbsences(studentnumber, startdate, enddate)
if (absences.length) {
const absentMonths = absences.reduce((sum, ab) => sum + moment(ab.end).diff(moment(ab.start), 'months'), 0)
return absentMonths
}
return 0
}
const countTimeCategories = (times, goal) => {
const statistics = { onTime: 0, yearOver: 0, wayOver: 0 }
times.forEach(time => {
if (time <= goal) statistics.onTime += 1
else if (time <= goal + 12) statistics.yearOver += 1
else statistics.wayOver += 1
})
return statistics
}
const 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)) {
@@ -204,11 +181,9 @@ const count = async (faculty, since, years, yearsList, levels, programmeFilter,
let graduationTimes = {}
let programmes = getProgrammeObjectBasis(yearsList, levels)
let data = {
means: {},
medians: {},
programmes: {
medians: getProgrammeObjectBasis(yearsList, levels),
means: getProgrammeObjectBasis(yearsList, levels),
},
}
@@ -216,7 +191,6 @@ const count = async (faculty, since, years, yearsList, levels, programmeFilter,
graduationAmounts[level] = getYearsObject({ years: yearsList })
graduationTimes[level] = getYearsObject({ years: yearsList, emptyArrays: true })
data.medians[level] = []
data.means[level] = []
})
let studyrights = null
@@ -259,19 +233,16 @@ const count = async (faculty, since, years, yearsList, levels, programmeFilter,
years.forEach(year => {
levels.forEach(level => {
const median = getMedian(graduationTimes[level][year])
const mean = getMean(graduationTimes[level][year])
const statistics = { onTime: 0, yearOver: 0, wayOver: 0 }
// Programme level breakdown
data.programmes.medians[level][year] = { programmes: [], data: [] }
data.programmes.means[level][year] = { programmes: [], data: [] }
const programmeCodes = Object.keys(programmes[level][year])
const programmeIds = getSortedProgIds(programmeCodes)
if (programmeIds.length > 0) {
for (const prog of programmeIds) {
const progMedian = getMedian(programmes[level][year][prog.code].graduationTimes)
const progMean = getMean(programmes[level][year][prog.code].graduationTimes)
let goal = goals[level]
if (faculty === 'H30' && Object.keys(goals.exceptions).includes(prog.code)) {
@@ -295,28 +266,12 @@ const count = async (faculty, since, years, yearsList, levels, programmeFilter,
code: prog.code,
},
]
data.programmes.means[level][year].programmes = [...data.programmes.means[level][year].programmes, prog.code]
data.programmes.means[level][year].data = [
...data.programmes.means[level][year].data,
{
y: progMean,
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 },
]
data.means[level] = [
...data.means[level],
{ y: mean, amount: graduationAmounts[level][year], statistics, name: year },
]
})
})
@@ -56,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,
}
}
@@ -88,11 +97,6 @@ const formatOrganization = org => {
return { id, name, code, parentId: parent_id }
}
const formatAbsence = absence => {
const { semestercode, semester } = absence
return { semestercode, start: semester.startdate, end: semester.enddate }
}
const newProgrammes = [/^KH/, /^MH/, /^T/, /^LI/, /^K-/, /^FI/, /^00901$/, /^00910$/]
const isNewProgramme = code => {
@@ -105,10 +109,11 @@ const isNewProgramme = code => {
}
const checkTransfers = (s, insideTransfersStudyrights, transfersToOrAwayStudyrights) => {
if (transfersToOrAwayStudyrights.includes(s.studyrightid)) return true
if (insideTransfersStudyrights.includes(s.studyrightid) && new Date(s.studystartdate) < new Date('2017-08-01'))
return true
return false
const allTransfers = [
...insideTransfersStudyrights.map(sr => sr.studentnumber),
...transfersToOrAwayStudyrights.map(sr => sr.studentnumber),
]
return allTransfers.includes(s.studentnumber)
}
const getExtentFilter = includeAllSpecials => {
@@ -130,6 +135,9 @@ const mapCodesToIds = data => {
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
}
@@ -143,9 +151,9 @@ module.exports = {
formatFacultyTransfer,
formatFacultyThesisWriter,
formatOrganization,
formatAbsence,
isNewProgramme,
checkTransfers,
getExtentFilter,
mapCodesToIds,
facultyProgrammeStudents,
}
@@ -1,4 +1,5 @@
const { indexOf } = require('lodash')
const { Op } = require('sequelize')
const moment = require('moment')
const { getAcademicYearDates } = require('../../util/semester')
const {
@@ -7,37 +8,42 @@ const {
getMasterCreditGraphStats,
getDoctoralCreditGraphStats,
getYearsArray,
getCorrectStudentnumbers,
tableTitles,
getOnlyMasterCreditGraphStats,
getOnlyMasterThresholds,
getBcMsThresholds,
getVetenaryCreditGraphStats,
} = require('../studyprogrammeHelpers')
const { studytrackStudents, allStudyrights } = require('../studyprogramme')
const { studytrackStudents } = require('../studyprogramme')
const { getStudyRightsByExtent, getTransferredInside, getTransferredToAndAway } = require('./faculty')
const { checkTransfers } = require('./facultyHelpers')
const getStudentData = (startDate, students, thresholdKeys, thresholdAmounts, limits, limitKeys, prog) => {
const getStudentData = (startDate, students, thresholdKeys, thresholdAmounts, limits = [], limitKeys = [], prog) => {
let data = {}
let programmeData = {}
thresholdKeys.forEach(t => (data[t] = 0))
limitKeys.forEach(t => (programmeData[t] = 0))
students.forEach(({ credits }) => {
const creditcount = credits
.filter(credit => moment(credit.attainment_date).isAfter(startDate))
.reduce((prev, curr) => (prev += curr.credits), 0)
.filter(credit => moment(credit.attainment_date).isSameOrAfter(startDate))
.reduce((prev, curr) => prev + curr.credits, 0)
data[thresholdKeys[0]] += creditcount < thresholdAmounts[0] ? 1 : 0
data[thresholdKeys[1]] += creditcount >= thresholdAmounts[0] && creditcount < thresholdAmounts[1] ? 1 : 0
data[thresholdKeys[2]] += creditcount >= thresholdAmounts[1] && creditcount < thresholdAmounts[2] ? 1 : 0
data[thresholdKeys[3]] += creditcount >= thresholdAmounts[2] && creditcount < thresholdAmounts[3] ? 1 : 0
data[thresholdKeys[4]] += creditcount >= thresholdAmounts[3] && creditcount < thresholdAmounts[4] ? 1 : 0
if (thresholdKeys.length === 8) {
data[thresholdKeys[4]] += creditcount >= thresholdAmounts[3] && creditcount < thresholdAmounts[4] ? 1 : 0
data[thresholdKeys[5]] += creditcount >= thresholdAmounts[4] && creditcount < thresholdAmounts[5] ? 1 : 0
data[thresholdKeys[6]] += creditcount >= thresholdAmounts[5] && creditcount < thresholdAmounts[6] ? 1 : 0
data[thresholdKeys[7]] += creditcount >= thresholdAmounts[6] ? 1 : 0
} else if (thresholdKeys.length === 7) {
data[thresholdKeys[4]] += creditcount >= thresholdAmounts[3] && creditcount < thresholdAmounts[4] ? 1 : 0
data[thresholdKeys[5]] += creditcount >= thresholdAmounts[4] && creditcount < thresholdAmounts[5] ? 1 : 0
data[thresholdKeys[6]] += creditcount >= thresholdAmounts[5] ? 1 : 0
} else {
} else if (thresholdKeys.length === 6) {
data[thresholdKeys[4]] += creditcount >= thresholdAmounts[3] && creditcount < thresholdAmounts[4] ? 1 : 0
data[thresholdKeys[5]] += creditcount >= thresholdAmounts[4] ? 1 : 0
} else {
data[thresholdKeys[4]] += creditcount >= thresholdAmounts[3] ? 1 : 0
}
if (['KH', 'MH'].includes(prog)) {
programmeData[limitKeys[0]] += creditcount <= limits[5][0] ? 1 : 0
@@ -49,151 +55,151 @@ const getStudentData = (startDate, students, thresholdKeys, thresholdAmounts, li
}
})
return [data, programmeData]
return { data, progData: programmeData }
}
const combineFacultyStudentProgress = async (faculty, programmes, specialGroups, graduated) => {
const createLimits = (months, creditsToAdd) => {
return [
[Math.ceil(months * (60 / 12)) + creditsToAdd, null],
[Math.ceil(months * (45 / 12)) + creditsToAdd, Math.ceil(months * (60 / 12)) + creditsToAdd],
[Math.ceil(months * (30 / 12)) + creditsToAdd, Math.ceil(months * (45 / 12)) + creditsToAdd],
[Math.ceil(months * (15 / 12)) + creditsToAdd, Math.ceil(months * (30 / 12)) + creditsToAdd],
[creditsToAdd + 1, Math.ceil(months * (15 / 12)) + creditsToAdd],
[creditsToAdd, null],
]
}
const createYearlyTitles = (start, limitList) => {
return [
`${start} Credits`,
`${limitList[4][0]} ≤ Credits < ${limitList[4][1]}`,
`${limitList[3][0]} ≤ Credits < ${limitList[3][1]}`,
`${limitList[2][0]} ≤ Credits < ${limitList[2][1]}`,
`${limitList[1][0]} ≤ Credits < ${limitList[1][1]}`,
`${limitList[0][0]} ≤ Credits`,
]
}
const filterOutTransfers = async (studyrights, programmes, allProgrammeCodes, since) => {
const transferredInAndOut = await getTransferredToAndAway(programmes, allProgrammeCodes, since)
const insideTransfers = await getTransferredInside(programmes, allProgrammeCodes, since)
const filteredStudyrights = studyrights.filter(sr => !checkTransfers(sr, insideTransfers, transferredInAndOut))
return filteredStudyrights
}
const combineFacultyStudentProgress = async (faculty, programmes, allProgrammeCodes, specialGroups, graduated) => {
const since = new Date('2017-08-01')
const isAcademicYear = true
const includeYearsCombined = true
const includeAllSpecials = specialGroups === 'SPECIAL_INCLUDED'
const includeGraduated = graduated === 'GRADUATED_INCLUDED'
const yearsArray = getYearsArray(since.getFullYear(), isAcademicYear, includeYearsCombined)
let allBachelorGraphStats = getBachelorCreditGraphStats(yearsArray)
let allBsMsGraphstats = getMasterCreditGraphStats(yearsArray)
let allMastersGraphStats = getOnlyMasterCreditGraphStats(yearsArray)
let allDoctoralGraphStats = getDoctoralCreditGraphStats(yearsArray)
let bachelorsProgrammeStats = {}
let bcMsProgrammeStats = {}
let mastersProgrammeStats = {}
let doctoralProgrammeStats = {}
let bachelorsTableStats = new Array(yearsArray.length)
let bachelorMastersTableStats = new Array(yearsArray.length)
let mastersTableStats = new Array(yearsArray.length)
let doctoralTableStats = new Array(yearsArray.length)
let yearlyBachelorTitles = []
let yearlyBcMsTitles = []
let yearlyMasterTitles = []
const graduationStatus = [0]
if (includeGraduated) {
graduationStatus.push(1)
}
const yearsArray = getYearsArray(since.getFullYear(), true, true)
const titlesVet = [
'',
'All',
'< 210 credits',
'210-239 credits',
'240-269 credits',
'270-299 credits',
'300-329 credits',
'330-359 credits',
' 360 ≤ credits ',
]
const progressStats = {
id: faculty,
years: yearsArray,
bachelorsTableStats: new Array(yearsArray.length),
bcMsTableStats: new Array(yearsArray.length),
mastersTableStats: new Array(yearsArray.length),
doctoralTableStats: new Array(yearsArray.length),
bachelorsGraphStats: getBachelorCreditGraphStats(yearsArray),
bcMsGraphStats: faculty === 'H90' ? getVetenaryCreditGraphStats(yearsArray) : getMasterCreditGraphStats(yearsArray),
mastersGraphStats: getOnlyMasterCreditGraphStats(yearsArray),
doctoralGraphStats: getDoctoralCreditGraphStats(yearsArray),
bachelorTitles: tableTitles.creditProgress.bachelor,
bcMsTitles: faculty === 'H90' ? titlesVet : tableTitles.creditProgress.master,
mastersTitles: tableTitles.creditProgress.masterOnly,
doctoralTitles: tableTitles.creditProgress.doctoral,
yearlyBachelorTitles: [],
yearlyBcMsTitles: [],
yearlyMasterTitles: [],
programmeNames: {},
bachelorsProgStats: {},
bcMsProgStats: {},
mastersProgStats: {},
doctoralProgStats: {},
}
const reversedYears = [...yearsArray].reverse()
reversedYears.forEach(year => {
bachelorsTableStats[indexOf(reversedYears, year)] = [year, 0, 0, 0, 0, 0, 0, 0, 0, 0]
bachelorMastersTableStats[indexOf(reversedYears, year)] = [year, 0, 0, 0, 0, 0, 0, 0, 0]
mastersTableStats[indexOf(reversedYears, year)] = [year, 0, 0, 0, 0, 0, 0, 0]
doctoralTableStats[indexOf(reversedYears, year)] = [year, 0, 0, 0, 0, 0, 0, 0, 0]
progressStats.bachelorsTableStats[indexOf(reversedYears, year)] = [year, 0, 0, 0, 0, 0, 0, 0, 0, 0]
progressStats.bcMsTableStats[indexOf(reversedYears, year)] = [year, 0, 0, 0, 0, 0, 0, 0, 0]
progressStats.mastersTableStats[indexOf(reversedYears, year)] = [year, 0, 0, 0, 0, 0, 0, 0]
progressStats.doctoralTableStats[indexOf(reversedYears, year)] = [year, 0, 0, 0, 0, 0, 0]
})
const limitKeys = ['zero', 't5', 't4', 't3', 't2', 't1']
let bachelorlimits = []
let masterlimits = []
let bcmslimits = []
const bachelorProgress = {}
const masterProgress = {}
const bcmsProgress = {}
const doctoralProgress = {}
for (const year of reversedYears) {
if (year === 'Total') continue
const { startDate, endDate } = getAcademicYearDates(year, since)
const today = moment()
const lastDayOfMonth = moment(today).endOf('month')
const lastDayOfMonth = moment().endOf('month')
let months = Math.round(moment.duration(moment(lastDayOfMonth).diff(moment(startDate))).asMonths())
if (months >= 36) {
bachelorlimits = [
[Math.ceil(36 * (60 / 12)), null],
[Math.ceil(36 * (45 / 12)), Math.ceil(36 * (60 / 12))],
[Math.ceil(36 * (30 / 12)), Math.ceil(36 * (45 / 12))],
[Math.ceil(36 * (15 / 12)), Math.ceil(36 * (30 / 12))],
[1, Math.ceil(36 * (15 / 12))],
[0, null],
]
bachelorlimits = createLimits(36, 0)
} else {
bachelorlimits = [
[Math.ceil(months * (60 / 12)), null],
[Math.ceil(months * (45 / 12)), Math.ceil(months * (60 / 12))],
[Math.ceil(months * (30 / 12)), Math.ceil(months * (45 / 12))],
[Math.ceil(months * (15 / 12)), Math.ceil(months * (30 / 12))],
[1, Math.ceil(months * (15 / 12))],
[0, null],
]
bachelorlimits = createLimits(months, 0)
}
if (months >= 24) {
masterlimits = [
[Math.ceil(24 * (60 / 12)), null],
[Math.ceil(24 * (45 / 12)), Math.ceil(24 * (60 / 12))],
[Math.ceil(24 * (30 / 12)), Math.ceil(24 * (45 / 12))],
[Math.ceil(24 * (15 / 12)), Math.ceil(24 * (30 / 12))],
[1, Math.ceil(20 * (15 / 12))],
[0, null],
]
masterlimits = createLimits(24, 0)
} else {
masterlimits = [
[Math.ceil(months * (60 / 12)), null],
[Math.ceil(months * (45 / 12)), Math.ceil(months * (60 / 12))],
[Math.ceil(months * (30 / 12)), Math.ceil(months * (45 / 12))],
[Math.ceil(months * (15 / 12)), Math.ceil(months * (30 / 12))],
[1, Math.ceil(months * (15 / 12))],
[0, null],
]
masterlimits = createLimits(months, 0)
}
if (months >= 60) {
bcmslimits = [
[Math.ceil(60 * (60 / 12)) + 180, null],
[Math.ceil(60 * (45 / 12)) + 180, Math.ceil(60 * (60 / 12)) + 180],
[Math.ceil(60 * (30 / 12)) + 180, Math.ceil(60 * (45 / 12)) + 180],
[Math.ceil(60 * (15 / 12)) + 180, Math.ceil(60 * (30 / 12)) + 180],
[181, Math.ceil(60 * (15 / 12)) + 180],
[180, null],
]
if (months >= 72 && faculty === 'H90') {
bcmslimits = createLimits(72, 0)
} else if (months >= 60) {
bcmslimits = createLimits(60, 0)
} else {
bcmslimits = [
[Math.ceil(months * (60 / 12)) + 180, null],
[Math.ceil(months * (45 / 12)) + 180, Math.ceil(months * (60 / 12)) + 180],
[Math.ceil(months * (30 / 12)) + 180, Math.ceil(months * (45 / 12)) + 180],
[Math.ceil(months * (15 / 12)) + 180, Math.ceil(months * (30 / 12)) + 180],
[181, Math.ceil(months * (15 / 12)) + 180],
[180, null],
]
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)
yearlyBachelorTitles.push([
'0 Credits',
`${bachelorlimits[4][0]} ≤ Credits < ${bachelorlimits[4][1]}`,
`${bachelorlimits[3][0]} ≤ Credits < ${bachelorlimits[3][1]}`,
`${bachelorlimits[2][0]} ≤ Credits < ${bachelorlimits[2][1]}`,
`${bachelorlimits[1][0]} ≤ Credits < ${bachelorlimits[1][1]}`,
`${bachelorlimits[0][0]} ≤ Credits`,
])
yearlyMasterTitles.push([
'0 Credits',
`${masterlimits[4][0]} ≤ Credits < ${masterlimits[4][1]}`,
`${masterlimits[3][0]} ≤ Credits < ${masterlimits[3][1]}`,
`${masterlimits[2][0]} ≤ Credits < ${masterlimits[2][1]}`,
`${masterlimits[1][0]} ≤ Credits < ${masterlimits[1][1]}`,
`${masterlimits[0][0]} ≤ Credits`,
])
yearlyBcMsTitles.push([
'180 Credits',
`${bcmslimits[4][0]} ≤ Credits < ${bcmslimits[4][1]}`,
`${bcmslimits[3][0]} ≤ Credits < ${bcmslimits[3][1]}`,
`${bcmslimits[2][0]} ≤ Credits < ${bcmslimits[2][1]}`,
`${bcmslimits[1][0]} ≤ Credits < ${bcmslimits[1][1]}`,
`${bcmslimits[0][0]} ≤ Credits`,
])
for (const { progId, code } of programmes) {
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) return
const studentnumbers = await getCorrectStudentnumbers({
codes: [code],
startDate,
endDate,
includeAllSpecials,
includeGraduated,
})
const all = await allStudyrights(code, studentnumbers)
const students = await studytrackStudents(studentnumbers)
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 data = getStudentData(
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,
@@ -202,122 +208,114 @@ const combineFacultyStudentProgress = async (faculty, programmes, specialGroups,
limitKeys,
'KH'
)
const studentData = data[0]
const programmeData = data[1]
if (!(progId in bachelorsProgrammeStats)) {
bachelorsProgrammeStats[progId] = new Array(reversedYears.length - 1)
}
bachelorsTableStats[indexOf(reversedYears, year)][1] += all.length || 0
if (year !== 'Total') {
bachelorsProgrammeStats[progId][indexOf(reversedYears, year)] = limitKeys.map(key => programmeData[key])
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)) {
bachelorsTableStats[indexOf(reversedYears, year)][indexOf(creditThresholdKeys, key) + 2] +=
progressStats.bachelorsTableStats[indexOf(reversedYears, year)][indexOf(creditThresholdKeys, key) + 2] +=
studentData[key] || 0
allBachelorGraphStats[key].data[indexOf(yearsArray, year)] += 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 all_BcMs = all.filter(sr => sr.studyrightid.slice(-2) === '-2')
const all_Ms = all.filter(sr => sr.studyrightid.slice(-2) !== '-2')
const all_MsStudentnumbers = all_Ms.map(sr => sr.studentnumber)
const all_BcMsStudentnumbers = all_BcMs.map(sr => sr.studentnumber)
mastersTableStats[indexOf(reversedYears, year)][1] += all_Ms.length || 0
bachelorMastersTableStats[indexOf(reversedYears, year)][1] += all_BcMs.length || 0
const 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)
const msStudents = students.filter(student => all_MsStudentnumbers.includes(student.studentnumber))
const bcMsStudents = students.filter(student => all_BcMsStudentnumbers.includes(student.studentnumber))
const data = getStudentData(
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,
creditThresholdKeys,
creditThresholdAmounts,
creditThresholdKeysBcMs,
creditThresholdAmountsBcMs,
bcmslimits,
limitKeys,
'MH'
)
const bcMsStudentdata = data[0]
const bcMsProgrammedata = data[1]
const { msOnlyCreditThresholdKeys, msOnlyCreditThresholdAmount } = getOnlyMasterThresholds()
const data2 = getStudentData(
const { data: msStudentdata, progData: msProgdata } = getStudentData(
startDate,
msStudents,
msOnlyCreditThresholdKeys,
msOnlyCreditThresholdAmount,
creditThresholdKeys,
creditThresholdAmounts,
masterlimits,
limitKeys,
'MH'
)
const msStudentdata = data2[0]
const msProgrammedata = data2[1]
if (!(progId in mastersProgrammeStats)) {
mastersProgrammeStats[progId] = new Array(reversedYears.length - 1)
}
if (!(progId in bcMsProgrammeStats)) {
bcMsProgrammeStats[progId] = new Array(reversedYears.length - 1)
}
if (year !== 'Total') {
mastersProgrammeStats[progId][indexOf(reversedYears, year)] = limitKeys.map(key => msProgrammedata[key])
bcMsProgrammeStats[progId][indexOf(reversedYears, year)] = limitKeys.map(key => bcMsProgrammedata[key])
if (!(progId in masterProgress)) {
masterProgress[progId] = new Array(reversedYears.length - 1)
bcmsProgress[progId] = new Array(reversedYears.length - 1)
}
masterProgress[progId][indexOf(reversedYears, year)] = limitKeys.map(key => msProgdata[key])
bcmsProgress[progId][indexOf(reversedYears, year)] = limitKeys.map(key => bcMsProgdata[key])
for (const key of Object.keys(msStudentdata)) {
mastersTableStats[indexOf(reversedYears, year)][indexOf(msOnlyCreditThresholdKeys, key) + 2] +=
progressStats.mastersTableStats[indexOf(reversedYears, year)][indexOf(creditThresholdKeys, key) + 2] +=
msStudentdata[key] || 0
allMastersGraphStats[key].data[indexOf(yearsArray, year)] += msStudentdata[key] || 0
progressStats.mastersTableStats[indexOf(reversedYears, 'Total')][indexOf(creditThresholdKeys, key) + 2] +=
msStudentdata[key] || 0
progressStats.mastersGraphStats[key].data[indexOf(yearsArray, year)] += msStudentdata[key] || 0
progressStats.mastersGraphStats[key].data[indexOf(yearsArray, 'Total')] += msStudentdata[key] || 0
}
for (const key of Object.keys(bcMsStudentdata)) {
bachelorMastersTableStats[indexOf(reversedYears, year)][indexOf(creditThresholdKeys, key) + 2] +=
progressStats.bcMsTableStats[indexOf(reversedYears, year)][indexOf(creditThresholdKeysBcMs, key) + 2] +=
bcMsStudentdata[key] || 0
allBsMsGraphstats[key].data[indexOf(yearsArray, year)] += bcMsStudentdata[key] || 0
progressStats.bcMsTableStats[indexOf(reversedYears, 'Total')][indexOf(creditThresholdKeysBcMs, key) + 2] +=
bcMsStudentdata[key] || 0
progressStats.bcMsGraphStats[key].data[indexOf(yearsArray, year)] += bcMsStudentdata[key] || 0
progressStats.bcMsGraphStats[key].data[indexOf(yearsArray, 'Total')] += bcMsStudentdata[key] || 0
}
} else {
const data = getStudentData(startDate, students, creditThresholdKeys, creditThresholdAmounts, [], [], 'T')
const studentData = data[0]
doctoralTableStats[indexOf(reversedYears, year)][1] += all.length || 0
if (!(progId in doctoralProgrammeStats)) {
doctoralProgrammeStats[progId] = new Array(reversedYears.length - 1)
const all = studyrights.filter(sr => sr.extentcode === 4 || sr.extentcode === 3).map(sr => sr.studentnumber)
const doctoralStudents = await studytrackStudents(all)
const { data } = getStudentData(startDate, doctoralStudents, creditThresholdKeys, creditThresholdAmounts, 'T')
progressStats.doctoralTableStats[indexOf(reversedYears, year)][1] += doctoralStudents.length || 0
progressStats.doctoralTableStats[indexOf(reversedYears, 'Total')][1] += doctoralStudents.length || 0
if (!(progId in doctoralProgress)) {
doctoralProgress[progId] = new Array(reversedYears.length - 1)
}
if (year !== 'Total') {
doctoralProgrammeStats[progId][indexOf(reversedYears, year)] = creditThresholdKeys.map(
key => studentData[key]
)
}
for (const key of Object.keys(studentData)) {
doctoralTableStats[indexOf(reversedYears, year)][indexOf(creditThresholdKeys, key) + 2] +=
studentData[key] || 0
allDoctoralGraphStats[key].data[indexOf(yearsArray, year)] += studentData[key] || 0
doctoralProgress[progId][indexOf(reversedYears, year)] = creditThresholdKeys.map(key => data[key])
for (const key of Object.keys(data)) {
progressStats.doctoralTableStats[indexOf(reversedYears, year)][indexOf(creditThresholdKeys, key) + 2] +=
data[key] || 0
progressStats.doctoralTableStats[indexOf(reversedYears, 'Total')][indexOf(creditThresholdKeys, key) + 2] +=
data[key] || 0
progressStats.doctoralGraphStats[key].data[indexOf(yearsArray, year)] += data[key] || 0
progressStats.doctoralGraphStats[key].data[indexOf(yearsArray, 'Total')] += data[key] || 0
}
}
}
}
const facultyProgressStats = {
id: faculty,
years: yearsArray,
bachelorsTableStats: bachelorsTableStats,
bcMsTableStats: bachelorMastersTableStats,
mastersTableStats: mastersTableStats,
doctoralTableStats: doctoralTableStats,
bachelorsGraphStats: allBachelorGraphStats,
bcMsGraphStats: allBsMsGraphstats,
mastersGraphStats: allMastersGraphStats,
doctoralGraphStats: allDoctoralGraphStats,
bachelorTitles: tableTitles.creditProgress.bachelor,
bcMsTitles: tableTitles.creditProgress.master,
mastersTitles: tableTitles.creditProgress.masterOnly,
doctoralTitles: tableTitles.creditProgress.doctoral,
yearlyBachelorTitles: yearlyBachelorTitles,
yearlyBcMsTitles: yearlyBcMsTitles,
yearlyMasterTitles: yearlyMasterTitles,
programmeNames: programmes.reduce(
(obj, dataItem) => ({ ...obj, [dataItem.progId]: { code: dataItem.code, ...dataItem.name } }),
{}
),
bachelorsProgrammeStats: bachelorsProgrammeStats,
bcMsProgrammeStats: bcMsProgrammeStats,
mastersProgrammeStats: mastersProgrammeStats,
doctoralProgrammeStats: doctoralProgrammeStats,
}
return facultyProgressStats
progressStats.bachelorsProgStats = Object.keys(bachelorProgress)
.filter(prog => !bachelorProgress[prog].every(year => year.every(value => value === 0)))
.reduce((result, prog) => ({ ...result, [prog]: bachelorProgress[prog] }), {})
progressStats.mastersProgStats = Object.keys(masterProgress)
.filter(prog => !masterProgress[prog].every(year => year.every(value => value === 0)))
.reduce((result, prog) => ({ ...result, [prog]: masterProgress[prog] }), {})
progressStats.bcMsProgStats = Object.keys(bcmsProgress)
.filter(prog => !bcmsProgress[prog].every(year => year.every(value => value === 0)))
.reduce((result, prog) => ({ ...result, [prog]: bcmsProgress[prog] }), {})
progressStats.doctoralProgStats = Object.keys(doctoralProgress)
.filter(prog => !doctoralProgress[prog].every(year => year.every(value => value === 0)))
.reduce((result, prog) => ({ ...result, [prog]: doctoralProgress[prog] }), {})
return progressStats
}
module.exports = { combineFacultyStudentProgress }
@@ -1,39 +1,13 @@
const {
getStartDate,
getYearsArray,
getPercentage,
getYearsObject,
getCorrectStudentnumbers,
tableTitles,
} = require('../studyprogrammeHelpers')
const {
studytrackStudents,
allStudyrights,
startedStudyrights,
inactiveStudyrights,
enrolledStudents,
absentStudents,
graduatedStudyRights,
} = require('../studyprogramme')
const moment = require('moment')
const { Op } = require('sequelize')
const { getStartDate, getYearsArray, getPercentage, getYearsObject, tableTitles } = require('../studyprogrammeHelpers')
const { getAcademicYearDates } = require('../../util/semester')
const getStudentData = students => {
let data = { female: 0, male: 0, finnish: 0 }
students.forEach(({ gender_code, home_country_en }) => {
data.male += gender_code === '1' ? 1 : 0
data.female += gender_code === '2' ? 1 : 0
data.finnish += home_country_en === 'Finland' ? 1 : 0
})
return data
}
// Goes through the faculty and its study programmes for the year
const getFacultyDataForYear = async ({ programmes, since, settings, year, years, programmeTableStats }) => {
const { includeAllSpecials, includeGraduated } = settings
const { startDate, endDate } = getAcademicYearDates(year, since)
// Totals for the year into table stats
const totals = {
const { getStudyRightsByExtent, getStudentsByStudentnumbers, getTransfers } = require('./faculty')
const { getCurrentSemester } = require('../semesters')
const { checkTransfers } = require('./facultyHelpers')
const { inactiveStudyrights, absentStudents } = require('../studyprogramme')
const emptyTotals = () => {
return {
total: 0,
allStarted: 0,
allEnrolled: 0,
@@ -43,116 +17,261 @@ const getFacultyDataForYear = async ({ programmes, since, settings, year, years,
allFinnish: 0,
allFemale: 0,
allMale: 0,
allOtherUnknown: 0,
allOtherCountries: 0,
}
}
const toltalsCalculation = totals => {
return [
totals.total,
totals.allStarted,
getPercentage(totals.allStarted, totals.total),
totals.allEnrolled,
getPercentage(totals.allEnrolled, totals.total),
totals.allAbsent,
getPercentage(totals.allAbsent, totals.total),
totals.allInactive,
getPercentage(totals.allInactive, totals.total),
totals.allGraduated,
getPercentage(totals.allGraduated, totals.total),
totals.allMale,
getPercentage(totals.allMale, totals.total),
totals.allFemale,
getPercentage(totals.allFemale, totals.total),
totals.allOtherUnknown,
getPercentage(totals.allOtherUnknown, totals.total),
totals.allFinnish,
getPercentage(totals.allFinnish, totals.total),
totals.allOtherCountries,
getPercentage(totals.allOtherCountries, totals.total),
]
}
const addTotals = (facultyTableStats, year, totals) => {
facultyTableStats[year] = [year, ...toltalsCalculation(totals)]
}
const addTotalsProgramme = (programmeTableStats, progId, year, totals) => {
programmeTableStats[progId][year] = toltalsCalculation(totals)
}
for (const { progId, code } of programmes) {
const studentnumbers = await getCorrectStudentnumbers({
codes: [code],
startDate,
endDate,
includeAllSpecials,
includeGraduated,
})
const getStudentData = (students, facultyExtra, year, code) => {
let data = { female: 0, male: 0, finnish: 0, otherCountries: 0, otherUnknown: 0 }
students.forEach(({ gender_code, home_country_en }) => {
data.male += gender_code === '1' ? 1 : 0
data.female += gender_code === '2' ? 1 : 0
data.otherUnknown += ['0', '3'].includes(gender_code) ? 1 : 0
facultyExtra[year][code].otherG += gender_code === '3' ? 1 : 0
facultyExtra[year][code].unknownG += gender_code === '0' ? 1 : 0
facultyExtra['Total'][code].otherG += gender_code === '3' ? 1 : 0
facultyExtra['Total'][code].unknownG += gender_code === '0' ? 1 : 0
data.finnish += home_country_en === 'Finland' ? 1 : 0
data.otherCountries += home_country_en !== 'Finland' ? 1 : 0
if (!Object.keys(facultyExtra[year][code].countries).includes(home_country_en))
facultyExtra[year][code].countries[home_country_en] = 0
if (!Object.keys(facultyExtra['Total'][code].countries).includes(home_country_en))
facultyExtra['Total'][code].countries[home_country_en] = 0
facultyExtra[year][code].countries[home_country_en] += 1
facultyExtra['Total'][code].countries[home_country_en] += 1
})
return data
}
// Goes through the faculty and its study programmes for the year
const getFacultyDataForYear = async ({
faculty,
programmes,
allProgrammeCodes,
since,
settings,
year,
years,
programmeTableStats,
facultyExtra,
allTotals,
progTotals,
programmeNames,
}) => {
const { includeAllSpecials, includeGraduated } = settings
const { startDate, endDate } = getAcademicYearDates(year, since)
// Totals for the year into table stats
let extents = [1, 2, 3, 4]
let graduated = [0]
if (includeAllSpecials) {
extents = [...extents, ...[7, 9, 34, 22, 99, 14, 13]]
}
if (includeGraduated) {
graduated = [...graduated, 1]
}
const totals = emptyTotals()
for (const { progId, code, name } of programmes) {
let elementStart = {}
let studyrightStart = {}
const start = moment(`${year.slice(0, 4)}-08-01`, 'YYYY-MM-DD')
const end = moment(`${year.slice(-4)}-07-31`, 'YYYY-MM-DD').endOf('day')
const allTransfers = await getTransfers(code, allProgrammeCodes, start, end)
if (!Object.keys(facultyExtra[year]).includes(code))
facultyExtra[year][code] = { unknownG: 0, otherG: 0, countries: {} }
if (!Object.keys(facultyExtra['Total']).includes(code))
facultyExtra['Total'][code] = { unknownG: 0, otherG: 0, countries: {} }
const startDateWhere = {
startdate: {
[Op.and]: {
[Op.gte]: startDate,
[Op.lte]: endDate,
},
},
}
if (code.includes('MH')) {
elementStart = startDateWhere
} else {
studyrightStart = startDateWhere
}
const allStudyrights = await getStudyRightsByExtent(
faculty,
elementStart,
studyrightStart,
code,
extents,
graduated
)
let studyrights = allStudyrights
let allStudents = []
studyrights = allStudyrights.filter(s => !checkTransfers(s, allTransfers, allTransfers))
if (includeAllSpecials) {
allStudents = [...new Set([...allTransfers.map(sr => sr.studentnumber).filter(smbr => smbr !== null)])]
}
// Get all the studyrights and students for the calculations
let all = await allStudyrights(code, studentnumbers)
const students = await studytrackStudents(studentnumbers)
const studentData = getStudentData(students)
const started = await startedStudyrights(code, startDate, studentnumbers)
const enrolled = await enrolledStudents(code, startDate, studentnumbers)
const absent = await absentStudents(code, studentnumbers)
const inactive = await inactiveStudyrights(code, studentnumbers)
const graduated = await graduatedStudyRights(code, startDate, studentnumbers)
const currentSemester = await getCurrentSemester()
const studentNbrs = studyrights.map(sr => sr.studentnumber)
allStudents = [...studentNbrs, ...allStudents]
const students = await getStudentsByStudentnumbers(allStudents)
const studentData = getStudentData(students, facultyExtra, year, code)
const started = await getStudentsByStudentnumbers(studentNbrs)
const inactive = await inactiveStudyrights(code, studentNbrs)
const graduatedStudents = studyrights.filter(sr => sr.graduated === 1)
const absent = await absentStudents(code, studentNbrs)
const enrolled = students.filter(
s =>
s.semesters.filter(semester => semester.semestercode === currentSemester.semestercode)[0]?.enrollmenttype === 1
)
// Count stats for the programme
if (students.length === 0) continue
if (!(progId in programmeTableStats)) {
programmeTableStats[progId] = years.reduce((resultObj, yearKey) => {
return { ...resultObj, [yearKey]: [] }
}, {})
progTotals[progId] = emptyTotals()
}
if (!(progId in programmeNames)) {
programmeNames[progId] = { code: code, ...name }
progTotals[progId] = emptyTotals()
}
programmeTableStats[progId][year] = [
all.length,
started.length,
getPercentage(started.length, all.length),
enrolled.length,
getPercentage(enrolled.length, all.length),
absent.length,
getPercentage(absent.length, all.length),
inactive.length,
getPercentage(inactive.length, all.length),
graduated.length,
getPercentage(graduated.length, all.length),
studentData.male,
getPercentage(studentData.male, all.length),
studentData.female,
getPercentage(studentData.female, all.length),
studentData.finnish,
getPercentage(studentData.finnish, all.length),
]
const yearTotals = {
total: students.length,
allStarted: started.length,
allEnrolled: enrolled.length,
allAbsent: absent.length,
allInactive: inactive.length,
allGraduated: graduatedStudents.length,
allMale: studentData.male,
allFemale: studentData.female,
allOtherUnknown: studentData.otherUnknown,
allFinnish: studentData.finnish,
allOtherCountries: studentData.otherCountries,
}
addTotalsProgramme(programmeTableStats, progId, year, yearTotals)
totals.total += all.length
totals.total += students.length
totals.allStarted += started.length
totals.allEnrolled += enrolled.length
totals.allAbsent += absent.length
totals.allInactive += inactive.length
totals.allGraduated += graduated.length
totals.allGraduated += graduatedStudents.length
totals.allFemale += studentData.female
totals.allMale += studentData.male
totals.allOtherUnknown += studentData.otherUnknown
totals.allFinnish += studentData.finnish
}
totals.allOtherCountries += studentData.otherCountries
allTotals.total += students.length
allTotals.allStarted += started.length
allTotals.allEnrolled += enrolled.length
allTotals.allAbsent += absent.length
allTotals.allInactive += inactive.length
allTotals.allGraduated += graduatedStudents.length
allTotals.allFemale += studentData.female
allTotals.allMale += studentData.male
allTotals.allOtherUnknown += studentData.otherUnknown
allTotals.allFinnish += studentData.finnish
allTotals.allOtherCountries += studentData.otherCountries
progTotals[progId].total += students.length
progTotals[progId].allStarted += started.length
progTotals[progId].allEnrolled += enrolled.length
progTotals[progId].allAbsent += absent.length
progTotals[progId].allInactive += inactive.length
progTotals[progId].allGraduated += graduatedStudents.length
progTotals[progId].allFemale += studentData.female
progTotals[progId].allMale += studentData.male
progTotals[progId].allOtherUnknown += studentData.otherUnknown
progTotals[progId].allFinnish += studentData.finnish
progTotals[progId].allOtherCountries += studentData.otherCountries
}
return totals
}
// Combines all the data for faculty students table
const combineFacultyStudents = async (code, programmes, specialGroups, graduated) => {
const combineFacultyStudents = async (code, programmes, allProgrammeCodes, specialGroups, graduated) => {
// Only academic years are considered
const isAcademicYear = true
const includeAllSpecials = specialGroups === 'SPECIAL_INCLUDED'
const includeGraduated = graduated === 'GRADUATED_INCLUDED'
const includeYearsCombined = true
const since = getStartDate(code, isAcademicYear)
const years = getYearsArray(since.getFullYear(), isAcademicYear, includeYearsCombined)
const since = getStartDate(code, true)
const years = getYearsArray(since.getFullYear(), true, true)
const settings = { includeAllSpecials, includeGraduated }
let facultyTableStats = getYearsObject({ years, emptyArrays: true })
let programmeTableStats = {}
const facultyTableStats = getYearsObject({ years, emptyArrays: true })
const programmeTableStats = {}
const facultyExtra = years.reduce((acc, year) => ({ ...acc, [year]: {} }), {})
const progTotals = {}
const allTotals = emptyTotals()
const programmeNames = {}
for (const year of years.reverse()) {
const queryParams = { programmes, since, settings, year, years, programmeTableStats }
const totals = await getFacultyDataForYear(queryParams)
facultyTableStats[year] = [
if (year === 'Total') continue
const queryParams = {
faculty: code,
programmes,
allProgrammeCodes,
since,
settings,
year,
totals.total,
totals.allStarted,
getPercentage(totals.allStarted, totals.total),
totals.allEnrolled,
getPercentage(totals.allEnrolled, totals.total),
totals.allAbsent,
getPercentage(totals.allAbsent, totals.total),
totals.allInactive,
getPercentage(totals.allInactive, totals.total),
totals.allGraduated,
getPercentage(totals.allGraduated, totals.total),
totals.allMale,
getPercentage(totals.allMale, totals.total),
totals.allFemale,
getPercentage(totals.allFemale, totals.total),
totals.allFinnish,
getPercentage(totals.allFinnish, totals.total),
]
years,
programmeTableStats,
facultyExtra,
allTotals,
progTotals,
programmeNames,
}
const totals = await getFacultyDataForYear(queryParams)
if (totals.total !== 0) {
addTotals(facultyTableStats, year, totals)
}
}
addTotals(facultyTableStats, 'Total', allTotals)
Object.keys(progTotals).forEach(programme =>
addTotalsProgramme(programmeTableStats, programme, 'Total', progTotals[programme])
)
const studentsData = {
id: code,
years: years,
facultyTableStats: facultyTableStats,
facultyTableStatsExtra: facultyExtra,
programmeStats: programmeTableStats,
titles: tableTitles['studytracks'],
programmeNames: programmes.reduce(
(obj, dataItem) => ({ ...obj, [dataItem.progId]: { code: dataItem.code, ...dataItem.name } }),
{}
),
titles: tableTitles['programmes'],
programmeNames: programmeNames,
}
return studentsData
}
@@ -11,21 +11,15 @@ const {
} = require('../studyprogrammeHelpers')
const getFacultyThesisWriters = async ({ since, years, isAcademicYear, facultyProgrammes, includeAllSpecials }) => {
const thesisTypes = [
'urn:code:course-unit-type:bachelors-thesis',
'urn:code:course-unit-type:masters-thesis',
'urn:code:course-unit-type:doctors-thesis',
'urn:code:course-unit-type:licentiate-thesis',
]
const thesisTypes = ['urn:code:course-unit-type:bachelors-thesis', 'urn:code:course-unit-type:masters-thesis']
let bachelors = getStatsBasis(years)
let masters = getStatsBasis(years)
let doctors = getStatsBasis(years)
let licentiates = getStatsBasis(years)
let programmeCounts = {}
let programmeNames = {}
for (const { progId, code, name } of facultyProgrammes) {
if (code === 'MH70_008_2' || code.startsWith('LIS') || code.startsWith('T')) continue
const provider = mapToProviders([code])[0]
const students = await getCorrectStudentnumbers({
codes: [code],
@@ -33,6 +27,7 @@ const getFacultyThesisWriters = async ({ since, years, isAcademicYear, facultyPr
endDate: alltimeEndDate,
includeAllSpecials,
})
const thesisCourseCodes = await thesisWriters(provider, since, thesisTypes, students)
thesisCourseCodes?.forEach(({ attainment_date, courseUnitType }) => {
const thesisYear = defineYear(attainment_date, isAcademicYear)
@@ -40,7 +35,7 @@ const getFacultyThesisWriters = async ({ since, years, isAcademicYear, facultyPr
if (!(progId in programmeCounts)) {
programmeCounts[progId] = {}
Object.keys(bachelors.tableStats).forEach(year => (programmeCounts[progId][year] = [0, 0, 0, 0, 0]))
Object.keys(bachelors.tableStats).forEach(year => (programmeCounts[progId][year] = [0, 0, 0]))
programmeNames[progId] = { ...name, code: code }
}
programmeCounts[progId][thesisYear][0] += 1
@@ -53,22 +48,14 @@ const getFacultyThesisWriters = async ({ since, years, isAcademicYear, facultyPr
masters.graphStats[indexOf(years, thesisYear)] += 1
masters.tableStats[thesisYear] += 1
programmeCounts[progId][thesisYear][2] += 1
} else if (courseUnitType === thesisTypes[2]) {
doctors.graphStats[indexOf(years, thesisYear)] += 1
doctors.tableStats[thesisYear] += 1
programmeCounts[progId][thesisYear][3] += 1
} else if (courseUnitType === thesisTypes[3]) {
licentiates.graphStats[indexOf(years, thesisYear)] += 1
licentiates.tableStats[thesisYear] += 1
programmeCounts[progId][thesisYear][4] += 1
}
})
}
return { bachelors, masters, doctors, licentiates, programmeCounts, programmeNames }
return { bachelors, masters, programmeCounts, programmeNames }
}
const getFacultyThesisWritersForStudyTrack = async (
const getFacultyThesisWritersForProgrammes = async (
allThesisWriters,
facultyProgrammes,
isAcademicYear,
@@ -77,24 +64,18 @@ const getFacultyThesisWritersForStudyTrack = async (
const since = isAcademicYear ? new Date('2017-08-01') : new Date('2017-01-01')
const years = getYearsArray(since.getFullYear(), isAcademicYear)
const queryParameters = { since, years, isAcademicYear, facultyProgrammes, includeAllSpecials }
const { bachelors, masters, doctors, licentiates, programmeCounts, programmeNames } = await getFacultyThesisWriters(
queryParameters
)
const { bachelors, masters, programmeCounts, programmeNames } = await getFacultyThesisWriters(queryParameters)
allThesisWriters.years = years.map(year => year.toString())
const reversedYears = years.reverse()
allThesisWriters.tableStats = reversedYears.map(year => [
year,
bachelors.tableStats[year] + masters.tableStats[year] + doctors.tableStats[year] + licentiates.tableStats[year],
bachelors.tableStats[year] + masters.tableStats[year],
bachelors.tableStats[year],
masters.tableStats[year],
doctors.tableStats[year],
licentiates.tableStats[year],
])
allThesisWriters.graphStats.push({ name: 'Bachelors', data: bachelors.graphStats })
allThesisWriters.graphStats.push({ name: 'Masters', data: masters.graphStats })
allThesisWriters.graphStats.push({ name: 'Doctors', data: doctors.graphStats })
allThesisWriters.graphStats.push({ name: 'Licentiates', data: licentiates.graphStats })
const programmes = programmeNames ? Object.keys(programmeNames) : facultyProgrammes.map(programme => programme.code)
programmes.forEach(programmeId => {
@@ -118,7 +99,7 @@ const combineFacultyThesisWriters = async (faculty, facultyProgrammes, yearType,
tableStats: [],
graphStats: [],
programmeTableStats: {},
titles: ['', 'All', 'Bachelors', 'Masters', 'Doctors', 'Licentiates'],
titles: ['', 'All', 'Bachelors', 'Masters'],
programmeNames: {},
status: 'Done',
lastUpdated: '',
@@ -126,7 +107,7 @@ const combineFacultyThesisWriters = async (faculty, facultyProgrammes, yearType,
const isAcademicYear = yearType === 'ACADEMIC_YEAR'
const includeAllSpecials = specialGroups === 'SPECIAL_INCLUDED'
await getFacultyThesisWritersForStudyTrack(allThesisWriters, facultyProgrammes, isAcademicYear, includeAllSpecials)
await getFacultyThesisWritersForProgrammes(allThesisWriters, facultyProgrammes, isAcademicYear, includeAllSpecials)
return allThesisWriters
}
@@ -104,6 +104,7 @@ const updateFacultyOverview = async (faculty, statsType) => {
const updatedCredits = await combineFacultyCredits(
faculty,
programmeFilter === 'ALL_PROGRAMMES' ? allProgrammes.data : newProgrammes.data,
allProgrammes.data,
yearType,
specialGroups
)
@@ -154,9 +155,14 @@ const updateFacultyProgressOverview = async faculty => {
const options = [specialGraduated, specialNotGraduated, notSpecialGraduated, notSpecialNotGraduated]
let newProgrammes = []
let allProgrammes = []
let allProgrammeCodes = []
try {
const all = await findFacultyProgrammeCodes(faculty, 'ALL_PROGRAMMES')
const onlyNew = await findFacultyProgrammeCodes(faculty, 'NEW_STUDY_PROGRAMMES')
allProgrammes = await setFacultyProgrammes(faculty, all, 'ALL_PROGRAMMES')
newProgrammes = await setFacultyProgrammes(faculty, onlyNew, 'NEW_STUDY_PROGRAMMES')
allProgrammes?.data.forEach(prog => allProgrammeCodes.push(prog.code))
} catch (e) {
logger.error(e)
}
@@ -167,6 +173,7 @@ const updateFacultyProgressOverview = async faculty => {
const updateFacultyStudentStats = await combineFacultyStudents(
faculty,
newProgrammes.data,
allProgrammeCodes,
specialGroups,
graduated
)
@@ -174,6 +181,7 @@ const updateFacultyProgressOverview = async faculty => {
const updateFacultyProgressStats = await combineFacultyStudentProgress(
faculty,
newProgrammes.data,
allProgrammeCodes,
specialGroups,
graduated
)
@@ -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,
}
@@ -9,7 +9,6 @@ const {
} = require('./openUniHelpers')
const { Op } = require('sequelize')
// 1. iteration: time is hardcoded. Check hyväksytty grades
const getCredits = async (courseCodes, startdate) =>
(
await Credit.findAll({
@@ -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,7 +9,6 @@ const {
getYearsObject,
getYearsArray,
getMedian,
getMean,
getStartDate,
getThesisType,
alltimeStartDate,
@@ -25,33 +24,26 @@ const {
allStudyrights,
allTransfers,
} = require('./studyprogramme')
const { bachelorStudyright } = require('./faculty/faculty')
const { countTimeCategories, bachelorStudyright, getStatutoryAbsences } = require('./graduationHelpers')
const { getAllProgrammes } = require('./studyrights')
const countTimeCategories = (times, goal) => {
const statistics = { onTime: 0, yearOver: 0, wayOver: 0 }
times.forEach(time => {
if (time <= goal) statistics.onTime += 1
else if (time <= goal + 12) statistics.yearOver += 1
else statistics.wayOver += 1
})
return statistics
}
const checkStartdate = async (id, studyStartDate) => {
const checkStartdate = async (id, startdate) => {
if (id.slice(-2) === '-1') {
return studyStartDate
return startdate
}
const studyright = await bachelorStudyright(id.replace(/-2$/, '-1'))
if (studyright) return studyright.studystartdate
if (studyright) return studyright.startdate
return null
}
const addGraduation = async (studyright, isAcademicYear, amounts, times) => {
const studyStartDate = await checkStartdate(studyright.studyrightid, studyright.studystartdate)
const startdate = await checkStartdate(studyright.studyrightid, studyright.startdate)
const graduationYear = defineYear(studyright.enddate, isAcademicYear)
const timeToGraduation = moment(studyright.enddate).diff(moment(studyStartDate), 'months')
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]
}
@@ -123,70 +115,34 @@ const getGraduationTimeStats = async ({ studyprogramme, since, years, isAcademic
const studyrights = await graduatedStudyRights(studyprogramme, since, studentnumbers)
// for masters, separate bc+ms combo from just ms students (except some medical progs)
if (doCombo) {
for (const right of studyrights) {
if (right.studyrightid.slice(-2) === '-2') {
await addGraduation(right, isAcademicYear, graduationAmountsCombo, graduationTimesCombo)
} else {
await addGraduation(right, isAcademicYear, graduationAmounts, graduationTimes)
}
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)
}
} else {
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
const medians = getYearsObject({ years, emptyArrays: true })
const means = getYearsObject({ years, emptyArrays: true })
// HighCharts graph require the data to have this format (ie. actual value, "empty value")
years.forEach(year => {
const median = getMedian(graduationTimes[year])
const mean = getMean(graduationTimes[year])
medians[year] = [
['', median],
['', comparisonValue - median],
]
means[year] = [
['', mean],
['', comparisonValue - mean],
]
})
// new version
const goal = getGoal(studyprogramme)
const times = { medians: [], means: [], goal }
const comboTimes = { medians: [], means: [], goal: goal + 36 }
const times = { medians: [], goal }
const comboTimes = { medians: [], goal: goal + 36 }
const rev = [...years].reverse()
for (const year of rev) {
const median = getMedian(graduationTimes[year])
const mean = getMean(graduationTimes[year])
const statistics = countTimeCategories(graduationTimes[year], goal)
times.medians = [...times.medians, { y: median, amount: graduationAmounts[year], name: year, statistics }]
times.means = [...times.means, { y: mean, amount: graduationAmounts[year], name: year, statistics }]
if (doCombo) {
const median = getMedian(graduationTimesCombo[year])
const mean = getMean(graduationTimesCombo[year])
const statistics = countTimeCategories(graduationTimesCombo[year], goal + 36)
comboTimes.medians = [
...comboTimes.medians,
{ y: median, amount: graduationAmountsCombo[year], name: year, statistics },
]
comboTimes.means = [
...comboTimes.means,
{ y: mean, amount: graduationAmountsCombo[year], name: year, statistics },
]
}
}
return { medians, means, graduationAmounts, times, doCombo, comboTimes }
return { times, doCombo, comboTimes }
}
// Creates (studyrightid, transfer)-map out of programmes transfers
@@ -304,28 +260,39 @@ const getGraduationStatsForStudytrack = async ({ studyprogramme, settings }) =>
const reversedYears = getYearsArray(since.getFullYear(), isAcademicYear).reverse()
const titles = ['', 'Graduated', 'Wrote thesis']
const titles =
studyprogramme.includes('LIS') || studyprogramme.includes('T')
? ['', 'Graduated']
: ['', 'Graduated', 'Wrote thesis']
const programmesBeforeOrAfterTitles = ['Code', 'Id', 'Programme', ...years]
const tableStats = reversedYears.map(year => [year, graduated.tableStats[year], thesis.tableStats[year]])
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,
@@ -1,4 +1,3 @@
const { mean } = require('lodash')
const { studentnumbersWithAllStudyrightElements } = require('./populations')
const { codes } = require('../../config/programmeCodes')
@@ -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,
@@ -170,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()
@@ -289,6 +285,36 @@ const getMasterCreditGraphStats = years => ({
data: getEmptyArray(years.length),
},
})
const getVetenaryCreditGraphStats = years => ({
lte210: {
name: 'Less than 210 credits',
data: getEmptyArray(years.length),
},
lte240: {
name: '210-239 credits',
data: getEmptyArray(years.length),
},
lte270: {
name: '240-269 credits',
data: getEmptyArray(years.length),
},
lte300: {
name: '270-299 credits',
data: getEmptyArray(years.length),
},
lte330: {
name: '300-329 credits',
data: getEmptyArray(years.length),
},
lte360: {
name: '330-359 credits',
data: getEmptyArray(years.length),
},
mte360: {
name: '360 or more credits',
data: getEmptyArray(years.length),
},
})
const getOnlyMasterCreditGraphStats = years => ({
lte15: {
@@ -318,60 +344,60 @@ const getOnlyMasterCreditGraphStats = years => ({
})
const getDoctoralCreditGraphStats = years => ({
lte50: {
name: 'Less than 50 credits',
lte10: {
name: 'Less than 10 credits',
data: getEmptyArray(years.length),
},
lte100: {
name: '50-99 credits',
lte20: {
name: '10-19 credits',
data: getEmptyArray(years.length),
},
lte150: {
name: '100-149 credits',
lte30: {
name: '20-29 credits',
data: getEmptyArray(years.length),
},
lte200: {
name: '150-199 credits',
lte40: {
name: '30-39 credits',
data: getEmptyArray(years.length),
},
lte250: {
name: '200-249 credits',
data: getEmptyArray(years.length),
},
lte300: {
name: '250-299 credits',
data: getEmptyArray(years.length),
},
mte300: {
name: '300 or more credits',
mte40: {
name: '40 or more credits',
data: getEmptyArray(years.length),
},
})
const getCreditGraphStats = (studyprogramme, years) => {
const getCreditGraphStats = (studyprogramme, years, studyprog = false) => {
if (studyprogramme.includes('KH')) return getBachelorCreditGraphStats(years)
if (studyprogramme.includes('MH')) return getMasterCreditGraphStats(years)
if (studyprogramme === 'MH90_001')
return studyprog ? getBachelorCreditGraphStats(years) : getVetenaryCreditGraphStats(years)
if (studyprogramme.includes('MH')) return getOnlyMasterCreditGraphStats(years)
return getDoctoralCreditGraphStats(years)
}
const bachelorCreditThresholds = ['lte15', 'lte30', 'lte60', 'lte90', 'lte120', 'lte150', 'lte180', 'mte180']
const masterCreditThresholds = ['lte200', 'lte220', 'lte240', 'lte260', 'lte280', 'lte300', 'mte300']
const onlyMasterCreditThresholds = ['lte15', 'lte30', 'lte60', 'lte90', 'lte120', 'mte120']
const doctoralCreditThresholds = ['lte50', 'lte100', 'lte150', 'lte200', 'lte250', 'lte300', 'mte300']
const doctoralCreditThresholds = ['lte10', 'lte20', 'lte30', 'lte40', 'mte40']
const bachelorCreditAmounts = [15, 30, 60, 90, 120, 150, 180, 180]
const masterCreditAmounts = [200, 220, 240, 260, 280, 300, 300]
const onlyMasterCreditAmounts = [15, 30, 60, 90, 120, 120]
const doctoralCreditAmounts = [50, 100, 150, 200, 250, 300, 300]
const getOnlyMasterThresholds = () => {
return { msOnlyCreditThresholdKeys: onlyMasterCreditThresholds, msOnlyCreditThresholdAmount: onlyMasterCreditAmounts }
const doctoralCreditAmounts = [10, 20, 30, 40, 40]
const vetenaryCreditThresholds = ['lte210', 'lte240', 'lte270', 'lte300', 'lte330', 'lte360', 'mte360']
const vetenaryCreditAmounts = [210, 240, 270, 300, 330, 360, 360]
const getBcMsThresholds = () => {
return { creditThresholdKeysBcMs: masterCreditThresholds, creditThresholdAmountsBcMs: masterCreditAmounts }
}
const getCreditThresholds = studyprogramme => {
const getCreditThresholds = (studyprogramme, studyProg = false) => {
if (studyprogramme === 'MH90_001') {
return studyProg
? { creditThresholdKeys: bachelorCreditThresholds, creditThresholdAmounts: bachelorCreditAmounts }
: { creditThresholdKeys: vetenaryCreditThresholds, creditThresholdAmounts: vetenaryCreditAmounts }
}
if (studyprogramme.includes('KH')) {
return { creditThresholdKeys: bachelorCreditThresholds, creditThresholdAmounts: bachelorCreditAmounts }
}
if (studyprogramme.includes('MH')) {
return { creditThresholdKeys: masterCreditThresholds, creditThresholdAmounts: masterCreditAmounts }
return { creditThresholdKeys: onlyMasterCreditThresholds, creditThresholdAmounts: onlyMasterCreditAmounts }
}
return { creditThresholdKeys: doctoralCreditThresholds, creditThresholdAmounts: doctoralCreditAmounts }
}
@@ -403,7 +429,7 @@ const tableTitles = {
'90-119 credits',
'120-149 credits',
'150-179 credits',
'>= 180 credits',
'180 ≤ credits',
],
master: [
'',
@@ -414,7 +440,7 @@ const tableTitles = {
'240-259 credits',
'260-279 credits',
'280-299 credits',
'>= 300 credits',
'300 ≤ credits',
],
masterOnly: [
'',
@@ -424,19 +450,9 @@ const tableTitles = {
'30-59 credits',
'60-89 credits',
'90-119 credits',
'>= 120 credits',
],
doctoral: [
'',
'All',
'< 50 credits',
'50-99 credits',
'100-149 credits',
'150-199 credits',
'200-249 credits',
'250-299 credits',
'>= 300 credits',
'120 ≤ credits',
],
doctoral: ['', 'All', '< 10 credits', '10-19 credits', '20-29 credits', '30-39 credits', '40 ≤ credits'],
},
studytracks: [
'',
@@ -448,13 +464,27 @@ 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
}
@@ -511,7 +541,6 @@ module.exports = {
isMajorStudentCredit,
isSpecialGroupCredit,
getMedian,
getMean,
defineYear,
getStartDate,
alltimeStartDate,
@@ -520,13 +549,14 @@ module.exports = {
getPercentage,
getBachelorCreditGraphStats,
getMasterCreditGraphStats,
getVetenaryCreditGraphStats,
getDoctoralCreditGraphStats,
getOnlyMasterCreditGraphStats,
getCreditGraphStats,
getCreditThresholds,
tableTitles,
getCreditProgressTableTitles,
getOnlyMasterThresholds,
getBcMsThresholds,
isNonMajorCredit,
mapCodesToIds,
getId,
+155 -123
View File
@@ -4,7 +4,6 @@ const moment = require('moment')
const { getAssociations } = require('./studyrights')
const {
getMedian,
getMean,
getStartDate,
getYearsArray,
getPercentage,
@@ -14,6 +13,7 @@ const {
getCreditGraphStats,
tableTitles,
getCreditProgressTableTitles,
getGoal,
} = require('./studyprogrammeHelpers')
const {
studytrackStudents,
@@ -25,11 +25,11 @@ const {
graduatedStudyRights,
} = require('./studyprogramme')
const { getAcademicYearDates } = require('../util/semester')
const { countTimeCategories } = require('./graduationHelpers')
const getStudentData = (startDate, students, thresholdKeys, thresholdAmounts) => {
let data = { female: 0, male: 0, finnish: 0 }
thresholdKeys.forEach(t => (data[t] = 0))
students.forEach(({ gender_code, home_country_en, credits }) => {
const creditcount = credits
.filter(credit => moment(credit.attainment_date).isAfter(startDate))
@@ -42,49 +42,67 @@ const getStudentData = (startDate, students, thresholdKeys, thresholdAmounts) =>
data[thresholdKeys[1]] += creditcount >= thresholdAmounts[0] && creditcount < thresholdAmounts[1] ? 1 : 0
data[thresholdKeys[2]] += creditcount >= thresholdAmounts[1] && creditcount < thresholdAmounts[2] ? 1 : 0
data[thresholdKeys[3]] += creditcount >= thresholdAmounts[2] && creditcount < thresholdAmounts[3] ? 1 : 0
data[thresholdKeys[4]] += creditcount >= thresholdAmounts[3] && creditcount < thresholdAmounts[4] ? 1 : 0
data[thresholdKeys[5]] += creditcount >= thresholdAmounts[4] && creditcount < thresholdAmounts[5] ? 1 : 0
// bachelor programme has more columns than master and doctoral programmes
if (thresholdKeys.length > 7) {
data[thresholdKeys[4]] += creditcount >= thresholdAmounts[3] && creditcount < thresholdAmounts[4] ? 1 : 0
data[thresholdKeys[5]] += creditcount >= thresholdAmounts[4] && creditcount < thresholdAmounts[5] ? 1 : 0
data[thresholdKeys[6]] += creditcount >= thresholdAmounts[5] && creditcount < thresholdAmounts[6] ? 1 : 0
data[thresholdKeys[7]] += creditcount >= thresholdAmounts[6] ? 1 : 0
} else {
} else if (thresholdKeys.length > 6) {
data[thresholdKeys[4]] += creditcount >= thresholdAmounts[3] && creditcount < thresholdAmounts[4] ? 1 : 0
data[thresholdKeys[5]] += creditcount >= thresholdAmounts[4] && creditcount < thresholdAmounts[5] ? 1 : 0
data[thresholdKeys[6]] += creditcount >= thresholdAmounts[5] ? 1 : 0
} else if (thresholdKeys.length > 5) {
data[thresholdKeys[4]] += creditcount >= thresholdAmounts[3] && creditcount < thresholdAmounts[4] ? 1 : 0
data[thresholdKeys[5]] += creditcount >= thresholdAmounts[4] ? 1 : 0
} else {
data[thresholdKeys[4]] += creditcount >= thresholdAmounts[3] ? 1 : 0
}
})
return data
}
const getGraduationTimeStats = async ({
year,
graduated,
studyprogramme,
track,
graduationMeanTime,
graduationMedianTime,
graduationAmounts,
}) => {
const getGraduationTimeStats = async ({ year, graduated, track, graduationAmounts, graduationTimes, classSize }) => {
if (year === 'Total') {
return
}
// Count how long each student took to graduate
let graduationTimes = []
graduated.forEach(({ enddate, studystartdate }) => {
const timeToGraduation = moment(enddate).diff(moment(studystartdate), 'months')
let times = []
graduated.forEach(({ enddate, startdate }) => {
const timeToGraduation = moment(enddate).diff(moment(startdate), 'months')
graduationAmounts[track][year] += 1
graduationTimes = [...graduationTimes, timeToGraduation]
times = [...times, timeToGraduation]
})
// The maximum amount of months in the graph depends on the studyprogramme intended graduation time
const comparisonValue = studyprogramme.includes('KH') ? 72 : 48
const median = getMedian(times)
const statistics = countTimeCategories(times, graduationTimes.goal)
// HighCharts graph requires the data to have this format (ie. actual value, "empty value")
const median = getMedian(graduationTimes)
const mean = getMean(graduationTimes)
graduationMedianTime[track][year] = [
['', median],
['', comparisonValue - median],
graduationTimes[track].medians = [
...graduationTimes[track].medians,
{ y: median, amount: graduationAmounts[track][year], name: year, statistics, classSize },
]
graduationMeanTime[track][year] = [
['', mean],
['', comparisonValue - mean],
}
const getStats = (all, started, enrolled, absent, inactive, graduated, studentData) => {
return [
all.length,
getPercentage(all.length, all.length),
started.length,
getPercentage(started.length, all.length),
enrolled.length,
getPercentage(enrolled.length, all.length),
absent.length,
getPercentage(absent.length, all.length),
inactive.length,
getPercentage(inactive.length, all.length),
graduated.length,
getPercentage(graduated.length, all.length),
studentData.male,
getPercentage(studentData.male, all.length),
studentData.female,
getPercentage(studentData.female, all.length),
studentData.finnish,
getPercentage(studentData.finnish, all.length),
]
}
@@ -109,17 +127,16 @@ const getStudytrackDataForTheYear = async ({
mainStatsByTrack,
creditGraphStats,
creditTableStats,
graduationMeanTime,
graduationMedianTime,
graduationAmounts,
graduationTimes,
totalAmounts,
emptyTracks,
totals,
} = data
await Promise.all(
studytracks.map(async track => {
const codes = studyprogramme === track ? [studyprogramme] : [studyprogramme, track]
const studentnumbers = await getCorrectStudentnumbers({
codes,
startDate,
@@ -127,16 +144,42 @@ const getStudytrackDataForTheYear = async ({
includeAllSpecials,
includeGraduated,
})
// Get all the studyrights and students for the calculations
const all = await allStudyrights(track, studentnumbers)
const students = await studytrackStudents(studentnumbers)
const studentData = getStudentData(startDate, students, creditThresholdKeys, creditThresholdAmounts)
const started = await startedStudyrights(track, startDate, studentnumbers)
const enrolled = await enrolledStudents(track, startDate, studentnumbers)
const absent = await absentStudents(track, studentnumbers)
const inactive = await inactiveStudyrights(track, studentnumbers)
const graduated = await graduatedStudyRights(track, startDate, studentnumbers)
let all = []
let studentData = {}
let started = []
let enrolled = []
let absent = []
let inactive = []
let graduated = []
// Get all the studyrights and students for the calculations
if (year !== 'Total') {
all = await allStudyrights(track, studentnumbers)
studentData = getStudentData(startDate, students, creditThresholdKeys, creditThresholdAmounts)
started = await startedStudyrights(track, startDate, studentnumbers)
enrolled = await enrolledStudents(track, startDate, studentnumbers)
absent = await absentStudents(track, studentnumbers)
inactive = await inactiveStudyrights(track, studentnumbers)
graduated = await graduatedStudyRights(track, startDate, studentnumbers)
totals[track].all = [...totals[track].all, ...all]
totals[track].studentData.male += studentData.male
totals[track].studentData.female += studentData.female
totals[track].studentData.finnish += studentData.finnish
totals[track].started = [...totals[track].started, ...started]
totals[track].enrolled = [...totals[track].enrolled, ...enrolled]
totals[track].absent = [...totals[track].absent, ...absent]
totals[track].inactive = [...totals[track].inactive, ...inactive]
totals[track].graduated = [...totals[track].graduated, ...graduated]
} else {
all = totals[track].all
studentData = totals[track].studentData
started = totals[track].started
enrolled = totals[track].enrolled
absent = totals[track].absent
inactive = totals[track].inactive
graduated = totals[track].graduated
}
// If the track has no stats for that year, it should be removed from the table and dropdown options
if (all.length === 0) {
@@ -144,82 +187,63 @@ const getStudytrackDataForTheYear = async ({
return
}
// Count stats for creditgraph for the year per track
creditThresholdKeys.forEach(
threshold => (creditGraphStats[track][threshold].data[indexOf(years, year)] = studentData[threshold])
)
// Count stats for the credit progress table for the year per track
creditTableStats[track] = [
...creditTableStats[track],
[year, all.length, ...creditThresholdKeys.map(threshold => studentData[threshold])],
]
if (year !== 'Total') {
// Count stats for creditgraph for the year per track
creditThresholdKeys.forEach(
threshold => (creditGraphStats[track][threshold].data[indexOf(years, year)] += studentData[threshold])
)
creditThresholdKeys.forEach(
threshold => (creditGraphStats[track][threshold].data[indexOf(years, 'Total')] += studentData[threshold])
)
// Count stats for the credit progress table for the year per track
creditTableStats[track] = [
[year, all.length, ...creditThresholdKeys.map(threshold => studentData[threshold])],
...creditTableStats[track],
]
} else {
const totalCredits = creditTableStats[track].reduce(
(a, b) => a.map((colVal, idx) => colVal + (b[idx] || 0)),
Array(creditThresholdKeys.length + 2).fill(0)
)
creditTableStats[track] = [[year, ...totalCredits.slice(1)], ...creditTableStats[track]]
}
// Count stats for the main studytrack table grouped by tracks
mainStatsByTrack[track] = [
[year, ...getStats(all, started, enrolled, absent, inactive, graduated, studentData)],
...mainStatsByTrack[track],
[
year,
all.length,
getPercentage(all.length, all.length),
started.length,
getPercentage(started.length, all.length),
enrolled.length,
getPercentage(enrolled.length, all.length),
absent.length,
getPercentage(absent.length, all.length),
inactive.length,
getPercentage(inactive.length, all.length),
graduated.length,
getPercentage(graduated.length, all.length),
studentData.male,
getPercentage(studentData.male, all.length),
studentData.female,
getPercentage(studentData.female, all.length),
studentData.finnish,
getPercentage(studentData.finnish, all.length),
],
]
// Count stats for the main studytrack table grouped by year
mainStatsByYear[year] = [
...mainStatsByYear[year],
[
studytrackNames[track]?.name['fi'] ? `${studytrackNames[track]?.name['fi']}, ${track}` : year,
all.length,
getPercentage(all.length, all.length),
started.length,
getPercentage(started.length, all.length),
enrolled.length,
getPercentage(enrolled.length, all.length),
absent.length,
getPercentage(absent.length, all.length),
inactive.length,
getPercentage(inactive.length, all.length),
graduated.length,
getPercentage(graduated.length, all.length),
studentData.male,
getPercentage(studentData.male, all.length),
studentData.female,
getPercentage(studentData.female, all.length),
studentData.finnish,
getPercentage(studentData.finnish, all.length),
...getStats(all, started, enrolled, absent, inactive, graduated, studentData),
],
...mainStatsByYear[year],
]
// Count stats for the graduation time charts grouped by year
totalAmounts[track][year] = await all.length
if (!(track in graduationTimes)) {
graduationTimes[track] = { medians: [] }
}
totalAmounts[track][year] = all.length
await getGraduationTimeStats({
year,
graduated,
studyprogramme,
track,
graduationMedianTime,
graduationMeanTime,
graduationAmounts,
graduationTimes,
classSize: totalAmounts[track][year],
})
})
)
Object.keys(graduationTimes).forEach(track => {
if (track !== 'goal') {
const sortedMedians = graduationTimes[track].medians.sort((a, b) => b.name.localeCompare(a.name))
graduationTimes[track].medians = sortedMedians
}
})
}
// Defines the studytrack names for the studytrack selector
@@ -241,20 +265,26 @@ const getEmptyStatsObjects = (years, studytracks, studyprogramme) => {
const mainStatsByTrack = {}
const creditGraphStats = {}
const creditTableStats = {}
const graduationMedianTime = {}
const graduationMeanTime = {}
const graduationAmounts = {}
const graduationTimes = { goal: getGoal(studyprogramme) }
const totalAmounts = {}
const emptyTracks = new Map()
const totals = {}
studytracks.forEach(async track => {
mainStatsByTrack[track] = []
creditGraphStats[track] = getCreditGraphStats(studyprogramme, years)
creditGraphStats[track] = getCreditGraphStats(studyprogramme, years, true)
creditTableStats[track] = []
graduationMedianTime[track] = getYearsObject({ years, emptyArrays: true })
graduationMeanTime[track] = getYearsObject({ years, emptyArrays: true })
graduationAmounts[track] = getYearsObject({ years })
totalAmounts[track] = getYearsObject({ years })
totals[track] = {
all: [],
started: [],
enrolled: [],
absent: [],
inactive: [],
graduated: [],
studentData: { male: 0, female: 0, finnish: 0 },
}
})
return {
@@ -262,11 +292,11 @@ const getEmptyStatsObjects = (years, studytracks, studyprogramme) => {
mainStatsByTrack,
creditGraphStats,
creditTableStats,
graduationMedianTime,
graduationMeanTime,
graduationAmounts,
graduationTimes,
totalAmounts,
emptyTracks,
totals,
}
}
@@ -285,35 +315,37 @@ const getStudytrackStatsForStudyprogramme = async ({ studyprogramme, settings })
const studytrackNames = associations.studyTracks
const data = getEmptyStatsObjects(years, studytracks, studyprogramme)
const { creditThresholdKeys, creditThresholdAmounts } = getCreditThresholds(studyprogramme)
const { creditThresholdKeys, creditThresholdAmounts } = getCreditThresholds(studyprogramme, true)
await Promise.all(
years.map(async year => {
return await getStudytrackDataForTheYear({
studyprogramme,
since,
settings,
studytracks,
studytrackNames,
year,
years,
data,
creditThresholdKeys,
creditThresholdAmounts,
})
const yearsReversed = [...years].reverse()
for (const year of yearsReversed) {
await getStudytrackDataForTheYear({
studyprogramme,
since,
settings,
studytracks,
studytrackNames,
year,
years,
data,
creditThresholdKeys,
creditThresholdAmounts,
})
)
}
const studytrackOptions = getStudytrackOptions(studyprogramme, studytrackNames, studytracks, data.emptyTracks, years)
return {
id: studyprogramme,
years: getYearsArray(since.getFullYear(), isAcademicYear, includeYearsCombined),
years: years,
...data,
studytrackOptions,
includeGraduated: settings.graduated,
populationTitles: tableTitles['studytracks'],
creditTableTitles: getCreditProgressTableTitles(studyprogramme),
creditTableTitles:
studyprogramme === 'MH90_001'
? tableTitles['creditProgress']['bachelor']
: getCreditProgressTableTitles(studyprogramme),
}
}
+39 -25
View File
@@ -8,12 +8,10 @@ const { checkStudyGuidanceGroupsAccess, getAllStudentsUserHasInGroups } = requir
const _ = require('lodash')
const { User, UserElementDetails, AccessGroup, UserFaculties, sequelizeUser } = require('../models/models_user')
const { getOrganizationAccess } = require('../util/organizationAccess')
const { getUserIams, getAllUserAccess } = require('../util/jami')
const courseStatisticsGroup = 'grp-oodikone-basic-users'
const facultyStatisticsGroup = 'grp-oodikone-users'
const hyOneGroup = 'hy-one'
const toskaGroup = 'grp-toska'
const openUniGroup = 'hy-ypa-opa-dojo'
// Max 25 users can be stored in the cache
const userDataCache = new LRU({
@@ -170,32 +168,34 @@ const getAccessGroups = async () => await AccessGroup.findAll()
const enrichProgrammesFromFaculties = faculties =>
facultiesAndProgrammesForTrends.filter(f => faculties.includes(f.faculty_code)).map(f => f.programme_code)
const findAll = async () =>
(
await User.findAll({
include: userIncludes,
})
).map(user => ({
const findAll = async () => {
const allUsers = await User.findAll({
include: userIncludes,
})
const userAccess = await getAllUserAccess()
const formattedUsers = allUsers.map(user => ({
...user.get(),
is_enabled: true,
iam_groups: userAccess.find(({ id }) => id === user.sisu_person_id)?.iamGroups || [],
elementdetails: _.uniqBy([
...user.programme.map(p => p.elementDetailCode),
...enrichProgrammesFromFaculties(user.faculty.map(f => f.faculty_code)),
]),
}))
const formatUser = async userFromDb => {
return formattedUsers
}
const formatUser = async (userFromDb, extraRights) => {
const [accessGroups, programmes, faculties] = [
['accessgroup', 'group_code'],
['programme', 'elementDetailCode'],
['faculty', 'faculty_code'],
].map(([field, code]) => userFromDb[field].map(entry => entry[code]))
const rights = _.uniqBy([...programmes, ...enrichProgrammesFromFaculties(faculties)])
const iamRights = Object.keys(await getOrganizationAccess(userFromDb)).filter(
programmeCode => !rights.includes(programmeCode)
)
const rights = _.uniqBy([...programmes, ...enrichProgrammesFromFaculties(faculties)]).concat(extraRights)
const {
dataValues: {
@@ -224,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'),
@@ -235,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)
}
@@ -257,23 +269,24 @@ const getUser = async ({ username, name, email, iamGroups, sisId }) => {
email,
language,
sisu_person_id: sisId,
iam_groups: iamGroups,
last_login: new Date(),
})
const { kosu, jory, hyOne, superAdmin, openUni } = specialGroup
const userFromDb = await byUsername(username)
const formattedUser = await formatUser(userFromDb)
const formattedUser = await formatUser(userFromDb, kosu ? iamRights : [])
const { roles: currentAccessGroups } = formattedUser
// Modify accessgroups based on current groups and iamGroups
let newAccessGroups = []
if (await checkStudyGuidanceGroupsAccess(sisId)) newAccessGroups.push('studyGuidanceGroups')
if (iamGroups.includes(courseStatisticsGroup)) newAccessGroups.push('courseStatistics')
if (iamGroups.includes(facultyStatisticsGroup) || iamGroups.some(element => /-jory$/.test(element)))
newAccessGroups.push('facultyStatistics')
if (iamGroups.includes(hyOneGroup) || currentAccessGroups.includes('teachers')) newAccessGroups.push('teachers')
if (iamGroups.includes(toskaGroup) || currentAccessGroups.includes('admin')) newAccessGroups.push('admin')
if (iamGroups.includes(openUniGroup)) newAccessGroups.push('openUniSearch')
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 (
@@ -297,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,
}
+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
}
@@ -34,7 +34,7 @@ export default {
**Non-degree student credits**: Sisältää tiedekuntaan suoritetut opintopisteet, joiden suorittajilla ei ole tutkinto-oikeutta Helsingin yliopistossa.
`,
GraduatedOfTheFaculty: `Sisältää kyseisenä vuonna tiedekunnasta valmistuneet opiskelijat riippumatta siitä, minä vuonna opiskeluoikeus ohjelmaan on myönnetty. Jaottelu on tehty opinto-oikeuden päättymispäivän (=valmistumispäivän) perusteella.`,
ThesisWritersOfTheFaculty: `Sisältää kyseisenä vuonna kandidaatintutkielman, pro gradu -tutkielman, väitöskirjan tai lisensiaatintyön hyväksytysti suorittaneet opiskelijat. Mukana ei ole mahdollisia arvosanan korotuksia.`,
ThesisWritersOfTheFaculty: `Sisältää kyseisenä vuonna kandidaatintutkielman tai pro gradu -tutkielman suorittaneet opiskelijat. Mukana ei ole mahdollisia arvosanan korotuksia.`,
StudentToggle: `
All studyrights: Valinnassa ovat mukana vaihto-opiskelijat, erillisoikeudella opiskelevat ja ohjelmaan siirtyneet opiskelijat sekä ohjelmasta pois siirtyneet opiskelijat.\n
Special studyrights excluded: Valinnasta ja kaikista luvuista on poissuljettu kaikki edellä mainitut erikoisryhmät.
@@ -55,25 +55,27 @@ export default {
Valmistumisajoista on **vähennetty lakisääteiset poissaolot**.
Vihreä **katkoviiva** kuvastaa tavoiteaikaa. Keltainen katkoviiva on tavoiteaika + yksi vuosi.\n
Vihreä **vaakapalkki** merkitsee, että keskimääräinen valmistumisaika on alle tai yhtä kuin tavoiteaika, keltainen palkki merkitsee korkeintaan vuoden ylitystä
ja punainen yli vuoden tavoiteajan ylitystä.
Palkissa oleva lukumäärä kertoo **montako opiskelijaa** kuhunkin valmistuneiden ryhmään kuuluu.
Tarkastellessa valmistumisaikoja aloitusvuosittain palkissa näkyy myös **kuinka suuri osa** kyseisenä vuonna tiedekuntaan/opinto-ohjelmaan opinto-oikeuden saaneista on valmistunut.
Palkissa oleva lukumäärä kertoo **montako opiskelijaa** kuhunkin valmistuneiden ryhmään kuuluu.
Vihreä **vaakapalkki** kuvaa tavoiteajassa (tai alle) valmistuneista, keltainen palkki korkeintaan vuodella tavoiteajan ylittäneitä
ja punainen yli vuodella tavoiteajan ylittäneitä.
Säädöt:
**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.
**Median time**: Näyttää kyseisenä vuonna valmistuneiden opiskelijoiden opintojen keston **mediaanin** kuukausina.
Mediaaninäkymässä vihreä **katkoviiva** kuvastaa tavoiteaikaa. Keltainen katkoviiva on tavoiteaika + yksi vuosi.\n
**Mean time**: Näyttää kyseisenä vuonna valmistuneiden opiskelijoiden opintojen keston **keskiarvon** kuukausina.`,
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: `
@@ -85,33 +87,26 @@ export default {
Kuvaa tiedekunnan ohjelmissa opiskelevien opiskelijoden tilastoja. Esimerkiksi 2020-2021 aloittaneiden opintopistejakaumaa.
HUOM!
**All** Sisältää kaikki opiskelijat, joiden opinto-oikeudella on aloituspäivä kyseisenä vuonna, riippumatta siitä, onko opiskelija aloittanut opiskelemaan ko. vuonna.
**Currently enrolled** Sisältää kaikki tällä hetkellä läsnäolevat olevat opiskelija.
**Absent** Sisältää kaikki lakisääteisistä syistä poissaolevat opiskelijat.
**Started studying** Sisältää kaikki opiskelijat, jotka ovat aloittaneet opiskelemaan koulutusohjelmassa kyseisenä lukuvuonna.
**Inactive** Sisältää kaikki opinto-oikeudet, jotka ovat alkaneet kyseisenä vuonna, mutta jotka ovat sittemmin vanhentuneet ilman, että opiskelija on valmistunut ohjelmasta. Sisältää myös opinto-oikeudet,
- **All** Sisältää opiskelijat, jotka siirtyneet ohjelmaan tai siitä ulos sekä aloittaneet ohjelmassa
- **Currently enrolled** Sisältää kaikki tällä hetkellä läsnäolevat olevat opiskelija.
- **Absent** Sisältää kaikki lakisääteisistä syistä poissaolevat opiskelijat.
- **Started studying** Sisältää kaikki opiskelijat, jotka ovat aloittaneet opiskelemaan koulutusohjelmassa kyseisenä lukuvuonna.
- **Inactive** Sisältää kaikki opinto-oikeudet, jotka ovat alkaneet kyseisenä vuonna, mutta jotka ovat sittemmin vanhentuneet ilman, että opiskelija on valmistunut ohjelmasta. Sisältää myös opinto-oikeudet,
joissa opiskelija on laiminlyönyt ilmoittautumisen tälle lukukaudelle.
Mukana ovat oletusarvoisesti myös vaihto-opiskelijat, ohjelmaan ja siitä pois siirtyneet opiskelijat sekä erillisopinto-oikeudella opiskelevat.
Taulukko näyttää myös näistä opiskelijoista miesten, naisten ja suomalaisten osuudet sekä valmistuneiden määrän.
Yläosan valikosta on mahdollista valita tarkasteluun yhden opinto-ohjelman opiskelijat tietylle vuodelle. Sekä opiskelijoiden yleistiedot että edistyminen opintopisteittäin
kuvaavat tällöin kyseisen opintosuunnan opiskelijoita.`,
Taulukko näyttää myös näistä opiskelijoista miesten, naisten ja suomalaisten osuudet sekä valmistuneiden määrän. `,
StudentProgress: `
Kuvaa tiedekuntaan kuuluvien eri ohjemien opinto-oikeudella varustettujen opiskelijoiden etenemistä opintopisteissä mitattuna. Esimerkiksi 2021-2022 aloittaneiden opintopistejakaumaa.
- Bachelor: Kandidaatin opinto-oikeudella varustettujen opiskelijoiden eteneminen
- Bachelor + Master: Kandi+maisteriopinto-oikeudella opiskelleiden ja siitä maisteriksi ohjelmiin siirtyneiden etenenminen
- 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 ja lisensiaatin opinto-oikeudella varustettujen opiskelijoiden eteneminen
- Doctor: Tohtorin opinto-oikeudella varustettujen opiskelijoiden eteneminen
HUOM! Mukana ovat myös muuhun kuin valittuun koulutusohjelmaan tai opintosuuntaan tehdyt opintopisteet.
HUOM! Mukana ovat myös muuhun kuin valittuun koulutusohjelmaan tai opintosuuntaan tehdyt opintopisteet. Opintopisteet on laskettu opinto-oikeuden alkamisesta, eikä tällä hetkellä ota huomioon mahdollisia hyväksilukuja.
Mukana ovat oletusarvoisesti myös vaihto-opiskelijat, erillisopinto-oikeudella opiskelevat ja ohjelmaan siirtyneet.
Mikäli yläosan valikosta valittuna on jokin tietty opintosuunta, myös nämä graafit ja taulukot kuvaavat kyseisen opintosuunnan opiskelijoita.
**Total** Kuvaa opiskelijoinden opintopisteiden määrien kertymää alkaen 1.8.2017. Luvut ovat isompia kuin vuosittain lasketut, sillä nyt kaikki 1.8.2017 jälkeen
suoritetut opintopisteet lasketaan mukaan, vaikka ne olisi suoritettu ennen varsinaista opinto-oikeuden alkua.
`,
}
@@ -1,8 +1,8 @@
export default {
enrollmentStatus: {
label: 'Filter students present or absent',
label: 'Filter students present, absent or passive',
short:
'Suodata opiskelijoita sen mukaan ovatko he ilmoittautuneet läsnä- tai poissaoleviksi kyseiselle lukukaudelle',
'Suodata opiskelijoita sen mukaan ovatko he joko ilmoittautuneet läsnä- tai poissaoleviksi tai jääneet passiivisiksi kyseiselle lukukaudelle. Huom. osa passiivisista opiskelijoista on jo valmistunut ko. ohjelmasta.',
},
studyrightStatus: {
label: 'Rajaa opiskelijoita opinto-oikeuden statuksen mukaan',
@@ -66,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.
`,
@@ -83,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.
@@ -92,10 +92,19 @@ const CustomOpenUniSearch = ({ setValues, savedSearches, location, history }) =>
const courseList = input.split(/[\s,]+/).map(code => code.trim().toUpperCase())
const query = {
courseCode: courseList,
startdate: moment(startdate).format('DD-MM-YYYY'),
enddate: moment(enddate).format('DD-MM-YYYY'),
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: startdate.toISOString(), enddate: enddate.toISOString() })
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()
}
@@ -130,7 +139,7 @@ const CustomOpenUniSearch = ({ setValues, savedSearches, location, history }) =>
return (
<Modal
trigger={
<Button size="small" color="blue" onClick={() => setModal(true)} data-cy="custom-pop-search-button">
<Button size="small" color="blue" onClick={() => setModal(true)} data-cy="open-uni-search-button">
Fetch Open Uni Students
</Button>
}
@@ -142,7 +151,7 @@ const CustomOpenUniSearch = ({ setValues, savedSearches, location, history }) =>
<Form>
<h2> Fetch open uni course population</h2>
<Form.Field>
<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 !== ''}
@@ -152,12 +161,7 @@ const CustomOpenUniSearch = ({ setValues, savedSearches, location, history }) =>
/>
</Form.Field>
<em>Insert course code(s)</em>
<TextArea
value={input}
placeholder="TKT12345, PSYK-123"
onChange={e => setInput(e.target.value)}
data-cy="s-no-input"
/>
<TextArea value={input} placeholder="TKT12345, PSYK-123" onChange={e => setInput(e.target.value)} />
</Form.Field>
<SearchHistory
header="Saved populations"
@@ -170,7 +174,7 @@ const CustomOpenUniSearch = ({ setValues, savedSearches, location, history }) =>
updateItem={() => null}
handleSearch={onSelectSearch}
/>
<Form.Field>
<Form.Field data-cy="begin-of-search">
<em>Select beginning</em>
<Datetime
value={startdate}
@@ -181,7 +185,7 @@ const CustomOpenUniSearch = ({ setValues, savedSearches, location, history }) =>
closeOnSelect
/>
</Form.Field>
<Form.Field>
<Form.Field data-cy="end-of-search">
<em>Select ending for enrollments:</em>
<br />
<em>Attainments are fetched until today.</em>
@@ -198,6 +202,7 @@ const CustomOpenUniSearch = ({ setValues, savedSearches, location, history }) =>
</Modal.Content>
<Modal.Actions>
<Button
data-cy="save-search"
disabled={!name || updateIsLoading || createIsLoading}
loading={updateIsLoading || createIsLoading}
floated="left"
@@ -225,16 +225,18 @@ const getColumns = (labelsToCourses, language) => {
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
}
@@ -263,7 +265,7 @@ const OpenUniPopulationResults = ({ fieldValues, language }) => {
return (
<div style={{ paddingBottom: '50px' }}>
<div style={{ maxHeight: '80vh', width: '100%' }}>
<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)`}
@@ -87,7 +87,7 @@ const CustomPopulationSearch = ({
const onClicker = e => {
e.preventDefault()
const studentnumbers = input.match(/[0-9]+/g)
const studentnumbers = input.match(/[0-9]+/g).map(sNumber => (sNumber.length === 8 ? `0${sNumber}` : sNumber))
getCustomPopulationDispatch({ studentnumberlist: studentnumbers, onProgress })
getCustomPopulationCoursesByStudentnumbers({ studentnumberlist: studentnumbers })
selectCustomPopulationSearchDispatch(selectedSearchId || null)
@@ -128,6 +128,59 @@ const Overview = ({
transferShortTitles = ['Code', 'Started', 'Graduated']
}
const options = {
KH: 'Bachelors',
MH: 'Masters',
T: 'Doctors and Licentiates',
LIS: 'Doctors and Licentiates',
OTHER: 'Other',
}
const getChartPlotLinePlaces = programmeKeys => {
if (programmeKeys.length === 0) return []
let key = programmeKeys[0][1].slice(0, 2)
if (!['KH', 'MH', 'T', 'LIS'].includes(key)) {
key = 'OTHER'
}
const plotLinePlaces = [[0, options[key]]]
for (let i = 0; i < programmeKeys.length - 1; i++) {
if (
(programmeKeys[i][1].startsWith('KH') && programmeKeys[i + 1][1].startsWith('MH')) ||
(programmeKeys[i][1].startsWith('MH') && programmeKeys[i + 1][1].startsWith('KH')) ||
(programmeKeys[i][1].startsWith('MH') && programmeKeys[i + 1][1].startsWith('T')) ||
((programmeKeys[i][1].startsWith('T') || programmeKeys[i][1].startsWith('LIS')) &&
(programmeKeys[i + 1][1].startsWith('KH') || programmeKeys[i + 1][1].startsWith('MH'))) ||
((programmeKeys[i][1].startsWith('T') ||
programmeKeys[i][1].startsWith('LIS') ||
programmeKeys[i][1].startsWith('KH') ||
programmeKeys[i][1].startsWith('MH')) &&
programmeKeys[i + 1][1].startsWith('K-'))
) {
let key = programmeKeys[i + 1][1].slice(0, 2)
if (!['KH', 'MH'].includes(key)) {
const keyT = programmeKeys[i + 1][1].slice(0, 1)
const keyLis = programmeKeys[i + 1][1].slice(0, 3)
if (keyT === 'T') {
key = keyT
} else if (keyLis === 'LIS') {
key = keyLis
} else {
key = 'OTHER'
}
}
if (
!programmeKeys[i + 1][1].includes(faculty.code) &&
(programmeKeys[i + 1][1].startsWith('MH') || programmeKeys[i + 1][1].startsWith('KH'))
) {
plotLinePlaces.push([i + 1, `${options[key]} secondary`])
} else {
plotLinePlaces.push([i + 1, options[key]])
}
}
}
return plotLinePlaces
}
return (
<div className="faculty-overview">
<div className="toggle-container">
@@ -199,8 +252,18 @@ const Overview = ({
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}
@@ -240,8 +303,18 @@ const Overview = ({
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}
@@ -280,8 +353,18 @@ const Overview = ({
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}
@@ -318,8 +401,18 @@ const Overview = ({
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"
@@ -13,6 +13,7 @@ const FacultyProgressTable = ({
language,
cypress,
progressTitles,
needsExtra = 'NO EXTRA',
}) => {
const [visible, setVisible] = useState(progressYearsVisible)
if (!data) return null
@@ -21,6 +22,7 @@ const FacultyProgressTable = ({
arrayToModify[yearIndex] = !visible[yearIndex]
setVisible(arrayToModify)
}
const lenOfYearArray = data.length - 1
return (
<div>
@@ -34,43 +36,47 @@ const FacultyProgressTable = ({
</Table.Header>
<Table.Body>
{data?.map((yearArray, yearIndex) => (
<React.Fragment key={`random-fragment-key-${Math.random()}`}>
{yearIndex < lenOfYearArray ? (
<BasicRow
icon={visible[yearIndex] ? 'angle down' : 'angle right'}
yearArray={yearArray}
cypress={visible[yearIndex] ? `Hide-${cypress}` : `Show-${cypress}`}
yearIndex={yearIndex}
toggleVisibility={() => toggleVisibility(yearIndex)}
/>
) : (
<Table.Row>
{yearArray?.map(value => (
<Table.Cell key={`last-row-${Math.random()}`}>{value}</Table.Cell>
))}
</Table.Row>
)}
{yearIndex < lenOfYearArray ? (
<Table.Row
key={`${cypress}-row-${Math.random()}`}
style={{ display: visible[yearIndex] ? '' : 'none' }}
>
<Table.Cell data-cy={`Cell-${cypress}-${yearIndex}`} colSpan={100}>
<ProgrammeProgressChart
data={sortedKeys?.map(programme => programmeStats[programme][yearIndex])}
longLabels={programmeNames}
labels={sortedKeys}
language={language}
names={progressTitles ? progressTitles[yearIndex] : titles?.slice(2)}
{data?.map(
(yearArray, yearIndex) =>
yearArray?.slice(1).some(value => value !== 0) && (
<React.Fragment key={`random-fragment-key-${Math.random()}`}>
{yearIndex < lenOfYearArray ? (
<BasicRow
icon={visible[yearIndex] ? 'angle down' : 'angle right'}
yearArray={yearArray}
cypress={visible[yearIndex] ? `Hide-${cypress}` : `Show-${cypress}`}
yearIndex={yearIndex}
toggleVisibility={() => toggleVisibility(yearIndex)}
/>
</Table.Cell>
</Table.Row>
) : (
<></>
)}
</React.Fragment>
))}
) : (
<Table.Row>
{yearArray?.map(value => (
<Table.Cell key={`last-row-${Math.random()}`}>{value}</Table.Cell>
))}
</Table.Row>
)}
{yearIndex < lenOfYearArray ? (
<Table.Row
key={`${cypress}-row-${Math.random()}`}
style={{ display: visible[yearIndex] ? '' : 'none' }}
>
<Table.Cell data-cy={`Cell-${cypress}-${yearIndex}`} colSpan={100}>
<ProgrammeProgressChart
data={sortedKeys?.map(programme => programmeStats[programme][yearIndex])}
longLabels={programmeNames}
labels={sortedKeys}
language={language}
names={progressTitles ? progressTitles[yearIndex] : titles?.slice(2)}
needsExtra={needsExtra === 'EXTRA HEIGHT'}
/>
</Table.Cell>
</Table.Row>
) : (
<></>
)}
</React.Fragment>
)
)}
</Table.Body>
</Table>
</div>
@@ -1,12 +1,38 @@
/* eslint-disable react/no-array-index-key */
import PopulationLink from 'components/StudyProgramme/StudytrackOverview/PopulationLink'
import React, { useState } from 'react'
import { Table, Button, Icon, Label } from 'semantic-ui-react'
import { Table, Button, Icon, Label, Popup } from 'semantic-ui-react'
import Toggle from '../../StudyProgramme/Toggle'
const getStyle = idx => {
if ([4, 12].includes(idx)) return { backgroundColor: '#f9f9f9', borderLeftWidth: 'thick' }
if ([1, 4, 5, 8, 9, 12, 13, 16, 17, 20, 21].includes(idx)) return { backgroundColor: '#f9f9f9' }
if (idx === 18) return { borderLeftWidth: 'thick' }
return {}
}
const backgroundColors = { KH: '#ffffff', MH: '#f2f6f7', T: '#e7edee' }
const backgroundColorsDarks = { KH: '#f9f9f9', MH: '#eeeeee', T: '#dddddd' }
const getTitlePopup = idx => {
if (idx === 0) return <p>All</p>
if ([1, 2].includes(idx)) return <p>Started studying</p>
if ([3, 4].includes(idx)) return <p>Currently enrolled</p>
if ([5, 6].includes(idx)) return <p>Absent</p>
if ([7, 8].includes(idx)) return <p>Inactive</p>
if ([9, 10].includes(idx)) return <p>Graduated</p>
if ([11, 12].includes(idx)) return <p>Men</p>
if ([13, 14].includes(idx)) return <p>Women</p>
if ([15, 16].includes(idx)) return <p>Other/Unknown</p>
if ([17, 18].includes(idx)) return <p>Finnish</p>
return <p>Other</p>
}
const FacultyStudentDataTable = ({
tableStats,
programmeStats,
programmeNames,
tableLinePlaces,
titles,
years,
sortedKeys,
@@ -16,35 +42,90 @@ const FacultyStudentDataTable = ({
}) => {
const [yearsVisible, setVisible] = useState(new Array(years.length).fill(false))
const [showPercentages, setShowPercentages] = useState(false)
const calendarYears = years.reduce((all, year) => {
if (year === 'Total') return all
return all.concat(Number(year.slice(0, 4)))
}, [])
const toggleVisibility = yearIndex => {
const arrayToModify = [...yearsVisible]
arrayToModify[yearIndex] = !yearsVisible[yearIndex]
setVisible(arrayToModify)
}
const getRowStyle = (idx, dark = false) => {
if (tableLinePlaces.length >= 3 && tableLinePlaces[2][0] <= idx)
return dark
? { backgroundColor: backgroundColorsDarks[tableLinePlaces[2][1]] }
: { backgroundColor: backgroundColors[tableLinePlaces[2][1]] }
if (tableLinePlaces.length >= 3 && tableLinePlaces[2][0] > idx && tableLinePlaces[1][0] <= idx)
return dark
? { backgroundColor: backgroundColorsDarks[tableLinePlaces[1][1]] }
: { backgroundColor: backgroundColors[tableLinePlaces[1][1]] }
if (tableLinePlaces.length >= 2 && tableLinePlaces[1][0] > idx && tableLinePlaces[0][0] <= idx)
return dark
? { backgroundColor: backgroundColorsDarks[tableLinePlaces[0][1]] }
: { backgroundColor: backgroundColors[tableLinePlaces[0][1]] }
return dark
? { backgroundColor: backgroundColorsDarks[tableLinePlaces[0][1]] }
: { backgroundColor: backgroundColors[tableLinePlaces[0][1]] }
}
const calendarYears = years.reduce((all, year) => {
if (year === 'Total') return all
return all.concat(Number(year.slice(0, 4)))
}, [])
const getRows = (idx, programme, year) => {
return programmeStats[programme][year].map((value, valIdx) => {
if (!showPercentages && typeof value === 'string' && (value.includes('%') || value.includes('NA'))) return null
return (
<Popup
content={getTitlePopup(valIdx)}
trigger={
<Table.Cell
key={`${year}-${programme}-color-${valIdx}`}
style={
getStyle(valIdx + 1)?.backgroundColor
? { borderLeftWidth: getStyle(valIdx + 1).borderLeftWidth, ...getRowStyle(idx, true) }
: { ...getStyle(valIdx + 1), ...getRowStyle(idx) }
}
>
{value}
</Table.Cell>
}
/>
)
})
}
return (
<div className="datatable">
<Toggle
cypress="HidePercentagesToggle"
firstLabel="Hide percentages"
secondLabel="Show percentages"
value={showPercentages}
setValue={setShowPercentages}
/>
<Table data-cy="Table-StudytrackOverview" celled>
<Table data-cy={cypress} celled structured>
<Table.Header>
<Table.Row>
<Table.Row key="FirstHeader">
<Table.HeaderCell colSpan={!showPercentages ? 3 : 4} />
<Table.HeaderCell colSpan={!showPercentages ? 4 : 8} style={{ borderLeftWidth: 'thick' }}>
Status
</Table.HeaderCell>
<Table.HeaderCell colSpan={!showPercentages ? 3 : 6} style={{ borderLeftWidth: 'thick' }}>
Gender
</Table.HeaderCell>
<Table.HeaderCell colSpan={!showPercentages ? 2 : 4} style={{ borderLeftWidth: 'thick' }}>
Country
</Table.HeaderCell>
</Table.Row>
<Table.Row key="secondHeader">
{titles.map((title, index) => (
<Table.HeaderCell
key={title}
colSpan={index === 0 || index === 1 || !showPercentages ? 1 : 2}
style={
[3, 7, 10].includes(index) ? { fontWeight: 'bold', borderLeftWidth: 'thick' } : { fontWeight: 'bold' }
}
textAlign="left"
style={{ fontWeight: 'bold' }}
>
{title}
</Table.HeaderCell>
@@ -56,12 +137,13 @@ const FacultyStudentDataTable = ({
{years.map((year, yearIndex) => {
return (
<>
<Table.Row key={`${year}-faculty-row-${Math.random()}`} className="header-row">
<Table.Row key={`${year}-faculty-row}`} className="header-row">
{tableStats[year].map((value, valueIdx) => {
if (valueIdx === 0)
return (
<Table.Cell key={`${year}-faculty-cell-${Math.random()}`}>
<Table.Cell key={`${year}-faculty-cell}`}>
<Button
key={`${year}-studentsTableButton}`}
as="div"
onClick={() => toggleVisibility(yearIndex)}
labelPosition="right"
@@ -79,27 +161,39 @@ const FacultyStudentDataTable = ({
)
if (!showPercentages && typeof value === 'string' && (value.includes('%') || value.includes('NA')))
return null
if ([1, 4, 5, 8, 9, 12, 13, 16, 17].includes(valueIdx))
return (
<Table.Cell style={{ backgroundColor: '#f9f9f9' }} key={`${year}-cell-color-${Math.random()}`}>
{value}
</Table.Cell>
)
return <Table.Cell key={`${year}$-${value}-cell-${Math.random()}`}>{value}</Table.Cell>
return (
<Table.Cell
style={getStyle(valueIdx)}
key={`${year}$-cell-colorless-${valueIdx + Math.random()}`}
>
{value}
</Table.Cell>
)
})}
</Table.Row>
{yearsVisible[yearIndex] &&
sortedKeys.map(programme => {
return (
<Table.Row className="regular-row" key={`${year}-regular-row-${Math.random()}`}>
<Table.Cell textAlign="left" style={{ paddingLeft: '50px' }} key={`${year}-${programme}`}>
<b>
{programme} - {programmeNames[programme].code}
<br />
{programmeNames[programme][language]
? programmeNames[programme][language]
: programmeNames[programme].fi}
</b>
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)) && (
@@ -110,26 +204,7 @@ const FacultyStudentDataTable = ({
/>
)}
</Table.Cell>
{programmeStats[programme][year].map((value, valIdx) => {
if (
!showPercentages &&
typeof value === 'string' &&
(value.includes('%') || value.includes('NA'))
)
return null
if ([0, 3, 4, 7, 8, 11, 12, 15, 16].includes(valIdx))
return (
<Table.Cell
key={`${year}-${programme}-color-${Math.random()}`}
style={{ backgroundColor: '#f9f9f9' }}
>
{value}
</Table.Cell>
)
return (
<Table.Cell key={`${year}-${programme}-colorless-${Math.random()}`}>{value}</Table.Cell>
)
})}
{getRows(idx, programme, year)}
</Table.Row>
)
})}
@@ -4,11 +4,23 @@ import Highcharts from 'highcharts'
import ReactHighcharts from 'react-highcharts'
const getColors = len => {
if (len < 6) return ['#fba678', '#dbda7d', '#9ec27c', '#60a866', '#008c59']
if (len < 8) return ['#f57368', '#fb8c6e', '#fba678', '#dbda7d', '#9ec27c', '#60a866', '#008c59']
return ['#e66067', '#f57368', '#fb8c6e', '#fba678', '#dbda7d', '#9ec27c', '#60a866', '#008c59']
}
const ProgrammeProgressChart = ({ data, labels, longLabels, names, language }) => {
// Point width is 24 px different multipliers adjusts the height.
const getFlexHeight = (len, needsExtra) => {
if (len > 7 && needsExtra) return `${len * 24 * 1.5}px`
if (len > 5 && !needsExtra) return `${len * 24 * 1.5}px`
if (needsExtra && len <= 2) return `${len * 24 * 6}px`
if (needsExtra && len <= 4) return `${len * 24 * 3}px`
if (needsExtra) return `${len * 24 * 2}px`
if (len < 2) return `${len * 24 * 5}px`
if (len < 3) return `${len * 24 * 4}px`
if (len <= 4) return `${len * 24 * 3}px`
return `${len * 24}px`
}
const ProgrammeProgressChart = ({ data, labels, longLabels, names, language, needsExtra }) => {
if (!data || data.length === 0)
return (
<>
@@ -29,6 +41,7 @@ const ProgrammeProgressChart = ({ data, labels, longLabels, names, language }) =
chart: {
type: 'bar',
marginTop: 60,
height: getFlexHeight(labels.length, needsExtra),
},
credits: {
text: 'oodikone | TOSKA',
@@ -1,5 +1,5 @@
import React from 'react'
import { Divider, Loader } from 'semantic-ui-react'
import { Divider, Loader, Message } from 'semantic-ui-react'
import { useGetFacultyProgressStatsQuery, useGetFacultyStudentStatsQuery } from 'redux/facultyStats'
import FacultyProgressTable from './FacultyProgressTable'
import FacultyBarChart from './FacultyBarChart'
@@ -10,17 +10,19 @@ import '../faculty.css'
import FacultyStudentDataTable from './FacultyStudentDataTable'
import sortProgrammeKeys from '../facultyHelpers'
const getDivider = (title, toolTipText, content) => (
const getDivider = (title, toolTipText, content, cypress) => (
<>
<div className="divider">
<Divider data-cy={`Section-${toolTipText}`} horizontal>
{title}
</Divider>
</div>
{content === 'no-infobox' ? null : <InfoBox content={content} />}
{content === 'no-infobox' ? null : <InfoBox content={content} cypress={cypress} />}
</>
)
const getKey = (programmeKeys, idx) => {
return programmeKeys[idx][1].startsWith('T') ? 'T' : programmeKeys[idx][1].slice(0, 2)
}
const FacultyProgrammeOverview = ({
faculty,
language,
@@ -57,6 +59,22 @@ const FacultyProgrammeOverview = ({
(studentStats.isSuccess && !studentStats.data)
if (isError) return <h3>Something went wrong, please try refreshing the page.</h3>
const getTableLinePlaces = programmeKeys => {
if (programmeKeys.length === 0) return []
const key = getKey(programmeKeys, 0)
const plotLinePlaces = [[0, key]]
for (let i = 0; i < programmeKeys.length - 1; i++) {
if (
(programmeKeys[i][1].startsWith('KH') && programmeKeys[i + 1][1].startsWith('MH')) ||
(programmeKeys[i][1].startsWith('MH') && programmeKeys[i + 1][1].startsWith('T'))
) {
const key = getKey(programmeKeys, i + 1)
plotLinePlaces.push([i + 1, key])
}
}
return plotLinePlaces
}
return (
<div className="faculty-overview">
<div className="toggle-container">
@@ -86,7 +104,8 @@ const FacultyProgrammeOverview = ({
{getDivider(
'Students of the faculty By Starting year',
'StudentsOfTheFacultyByStartingYear',
toolTips.StudentsStatsOfTheFaculty
toolTips.StudentsStatsOfTheFaculty,
'InfoFacultyStudentTable'
)}
<div>
<FacultyStudentDataTable
@@ -98,9 +117,19 @@ const FacultyProgrammeOverview = ({
sortedKeys={sortProgrammeKeys(
Object.keys(studentStats?.data.programmeStats).map(obj => [
obj,
studentStats?.data?.programmeNames[obj].code,
])
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}
@@ -114,7 +143,8 @@ const FacultyProgrammeOverview = ({
{getDivider(
'Progress of students of the faculty ',
'BachelorStudentsOfTheFacultyByStartingYear',
toolTips.StudentProgress
toolTips.StudentProgress,
'InfoFacultyProgress'
)}
{getDivider('Bachelor', 'BachelorStudentsOfTheFacultyByStartingYear', 'no-infobox')}
<div className="section-container">
@@ -131,13 +161,14 @@ const FacultyProgrammeOverview = ({
<div className="table-container">
<FacultyProgressTable
data={progressStats?.data.bachelorsTableStats}
programmeStats={progressStats?.data.bachelorsProgrammeStats}
programmeStats={progressStats?.data.bachelorsProgStats}
titles={progressStats?.data.bachelorTitles}
sortedKeys={sortProgrammeKeys(
Object.keys(progressStats?.data.bachelorsProgrammeStats).map(obj => [
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}
@@ -148,6 +179,11 @@ const FacultyProgrammeOverview = ({
</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
@@ -162,54 +198,62 @@ const FacultyProgrammeOverview = ({
<div className="table-container">
<FacultyProgressTable
data={progressStats?.data.bcMsTableStats}
programmeStats={progressStats?.data.bcMsProgrammeStats}
programmeStats={progressStats?.data.bcMsProgStats}
titles={progressStats?.data.bcMsTitles}
sortedKeys={sortProgrammeKeys(
Object.keys(progressStats?.data.bcMsProgrammeStats).map(obj => [
Object.keys(progressStats?.data.bcMsProgStats).map(obj => [
obj,
progressStats?.data?.programmeNames[obj].code,
])
]),
faculty.code
).map(listObj => listObj[0])}
progressYearsVisible={Array(progressStats?.data.years.slice(1).length).fill(false)}
programmeNames={progressStats?.data.programmeNames}
language={language}
cypress="FacultyBachelorMasterProgressTable"
progressTitles={progressStats?.data.yearlyBcMsTitles}
needsExtra={faculty.code === 'H60' ? 'NO EXTRA' : 'EXTRA HEIGHT'}
/>
</div>
</div>
{getDivider('Master', 'MasterStudentsOfTheFacultyByStartingYear', 'no-infobox')}
<div className="section-container">
<div className="graph-container">
<FacultyBarChart
cypress="FacultyMastersProgress"
data={{
id: faculty.code,
stats: progressStats?.data.mastersGraphStats,
years: progressStats?.data.years,
}}
/>
</div>
<div className="table-container">
<FacultyProgressTable
data={progressStats?.data.mastersTableStats}
programmeStats={progressStats?.data.mastersProgrammeStats}
titles={progressStats?.data.mastersTitles}
sortedKeys={sortProgrammeKeys(
Object.keys(progressStats?.data.mastersProgrammeStats).map(obj => [
obj,
progressStats?.data?.programmeNames[obj].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.yearlyMasterTitles}
/>
</div>
</div>
{getDivider('Doctor and licentiate', 'DoctoralStudentsOfTheFacultyByStartingYear', 'no-infobox')}
{!(faculty.code === 'H90') && (
<>
{getDivider('Master', 'MasterStudentsOfTheFacultyByStartingYear', 'no-infobox')}
<div className="section-container">
<div className="graph-container">
<FacultyBarChart
cypress="FacultyMastersProgress"
data={{
id: faculty.code,
stats: progressStats?.data.mastersGraphStats,
years: progressStats?.data.years,
}}
/>
</div>
<div className="table-container">
<FacultyProgressTable
data={progressStats?.data.mastersTableStats}
programmeStats={progressStats?.data.mastersProgStats}
titles={progressStats?.data.mastersTitles}
sortedKeys={sortProgrammeKeys(
Object.keys(progressStats?.data.mastersProgStats).map(obj => [
obj,
progressStats?.data?.programmeNames[obj].code,
]),
faculty.code
).map(listObj => listObj[0])}
progressYearsVisible={Array(progressStats?.data.years.slice(1).length).fill(false)}
programmeNames={progressStats?.data.programmeNames}
language={language}
cypress="FacultyMastersProgressTable"
progressTitles={progressStats?.data.yearlyMasterTitles}
needsExtra={faculty.code === 'H60' ? 'NO EXTRA' : 'EXTRA HEIGHT'}
/>
</div>
</div>
</>
)}
{getDivider('Doctor', 'DoctoralStudentsOfTheFacultyByStartingYear', 'no-infobox')}
<div className="section-container">
<div className="graph-container">
<FacultyBarChart
@@ -224,18 +268,19 @@ const FacultyProgrammeOverview = ({
<div className="table-container">
<FacultyProgressTable
data={progressStats?.data.doctoralTableStats}
programmeStats={progressStats?.data.doctoralProgrammeStats}
programmeStats={progressStats?.data.doctoralProgStats}
titles={progressStats?.data.doctoralTitles}
sortedKeys={sortProgrammeKeys(
Object.keys(progressStats?.data.doctoralProgrammeStats).map(obj => [
Object.keys(progressStats?.data.doctoralProgStats).map(obj => [
obj,
progressStats?.data?.programmeNames[obj].code,
])
]),
faculty.code
).map(listObj => listObj[0])}
progressYearsVisible={Array(progressStats?.data.years.slice(1).length).fill(false)}
programmeNames={progressStats?.data.programmeNames}
language={language}
cypress="FacultyBachelorsProgressTable"
cypress="FacultyDoctoralProgressTable"
/>
</div>
</div>
@@ -1,15 +1,23 @@
/* eslint-disable react/no-this-in-sfc */
import React from 'react'
import Highcharts from 'highcharts'
import ReactHighcharts from 'react-highcharts'
const colors = ['#7cb5ec', '#90ed7d', '#434348', '#f7a35c', '#FFF000', '#2b908f', '#f45b5b', '#91e8e1']
const CollapsedStackedBar = ({ data, labels, longLabels, names, language, differenceData, extraHeight }) => {
const CollapsedStackedBar = ({
data,
labels,
longLabels,
names,
plotLinePlaces,
language,
differenceData,
extraHeight,
}) => {
const transpose = matrix => {
return matrix.reduce((prev, next) => next.map((_item, i) => (prev[i] || []).concat(next[i])), [])
}
if (names[0] === 'Started studying') names[0] += ' (new in faculty)'
const needsExtra = extraHeight === 'EXTRA HEIGHT'
const dataTranspose = transpose(data)
.map((obj, idx) => ({ name: names[idx], data: obj, color: colors[idx] }))
@@ -33,6 +41,27 @@ 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) => {
if (len > 7 && needsExtra) return `${len * 24 * 1.5}px`
@@ -73,6 +102,7 @@ const CollapsedStackedBar = ({ data, labels, longLabels, names, language, differ
title: {
text: '',
},
plotLines: chartPlotLinePlaces,
fontSize: '24px',
},
yAxis: {
@@ -83,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',
},
@@ -96,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,
@@ -9,6 +9,7 @@ const InteractiveDataTable = ({
dataProgrammeStats,
programmeNames,
sortedKeys,
plotLinePlaces,
titles,
language,
sliceStart,
@@ -28,14 +29,32 @@ const InteractiveDataTable = ({
const keys = {}
if (!(columnIndex === 0)) {
const numbersOfYears = dataStats.length
const data = Object.entries(dataProgrammeStats)
const data = sortedKeys?.map(programme => [programme, dataProgrammeStats[programme]])
const groupIndices = plotLinePlaces.length > 0 ? plotLinePlaces.map(val => val[0]) : []
for (let yearIdx = 0; yearIdx < numbersOfYears; yearIdx++) {
const yearlySortedKeys = data
.sort((a, b) => {
if (sortDir === -1) return a[1][yearIdx][columnIndex] - b[1][yearIdx][columnIndex]
return b[1][yearIdx][columnIndex] - a[1][yearIdx][columnIndex]
})
.map(yearlyProgrammes => yearlyProgrammes[0])
let yearlySortedKeys = []
if (groupIndices.length > 0) {
for (let i = 0; i < groupIndices.length; i++) {
const ending = i === groupIndices.length - 1 ? data.length : groupIndices[i + 1]
yearlySortedKeys = [
...yearlySortedKeys,
...data
.slice(groupIndices[i], ending)
.sort((a, b) => {
if (sortDir === -1) return a[1][yearIdx][columnIndex] - b[1][yearIdx][columnIndex]
return b[1][yearIdx][columnIndex] - a[1][yearIdx][columnIndex]
})
.map(yearlyProgrammes => yearlyProgrammes[0]),
]
}
} else {
yearlySortedKeys = data
.sort((a, b) => {
if (sortDir === -1) return a[1][yearIdx][columnIndex] - b[1][yearIdx][columnIndex]
return b[1][yearIdx][columnIndex] - a[1][yearIdx][columnIndex]
})
.map(yearlyProgrammes => yearlyProgrammes[0])
}
keys[yearIdx] = yearlySortedKeys
}
}
@@ -132,6 +151,7 @@ const InteractiveDataTable = ({
)
}
labels={sortbyColumn === 0 ? sortedKeys : keyOrder[yearIndex]}
plotLinePlaces={plotLinePlaces}
differenceData={Object.keys(differenceToPrevYears)?.reduce(
(yearlyObject, programme) => ({
...yearlyObject,
@@ -0,0 +1,122 @@
/* eslint-disable react/no-this-in-sfc */
import React from 'react'
import ReactHighcharts from 'react-highcharts'
const BarChart = ({ data, handleClick, facultyGraph = true, year = null, label, programmeNames, language = null }) => {
const statData = [
{ name: 'On time', color: '#90A959', data: [] },
{ name: 'Max. year overtime', color: '#FEE191', data: [] },
{ name: 'Overtime', color: '#FB6962', data: [] },
]
let categories = []
const codeMap = {}
// eslint-disable-next-line no-restricted-syntax
for (const item of data) {
statData[0].data = [...statData[0].data, item.statistics.onTime]
statData[1].data = [...statData[1].data, item.statistics.yearOver]
statData[2].data = [...statData[2].data, item.statistics.wayOver]
categories = [...categories, item.name]
if (!facultyGraph) {
codeMap[item.name || item.code] = item.code
}
}
const getHeight = () => {
if (data.length === 1) {
return 250
}
const t = data.length > 8 ? 80 : 110
return data.length * t + 100
}
const getTooltipText = (programmeId, seriesName, amount) => {
if (!facultyGraph) {
const code = codeMap[programmeId]
return `<b>${
programmeNames[code]?.[language] ? programmeNames[code]?.[language] : programmeNames[code]?.fi
}</b><br />${code}<br /><b>${seriesName}</b>: ${amount}`
}
return `<b>${seriesName}</b>: ${amount}`
}
const config = {
chart: {
type: 'bar',
height: getHeight(),
margin: [70, 0],
},
title: { text: ' ' },
series: statData,
tooltip: {
backgroundColor: 'white',
fontSize: '25px',
// outside: true,
// eslint-disable-next-line
formatter: function() {
return getTooltipText(this.x, this.series.name, this.y)
},
},
xAxis: {
type: 'category',
categories,
title: {
text: facultyGraph ? label : 'Programme',
align: 'high',
offset: 0,
rotation: 0,
y: -10,
},
},
yAxis: {
min: 0,
// max: maxValue,
title: { text: 'Number of students' },
labels: {
overflow: 'justify',
},
allowDecimals: false,
showFirstLabel: false,
},
credits: {
text: 'oodikone | TOSKA',
},
plotOptions: {
bar: {
dataLabels: {
enabled: true,
align: 'right',
color: '#424949',
style: {
textOutline: 'none',
},
formatter() {
// eslint-disable-next-line react/no-this-in-sfc
return this.y !== 0 ? this.y : ''
},
},
},
series: {
pointWidth: data.length > 8 ? 16 : 20,
groupPadding: 0.15,
point: {
events: {
click(e) {
handleClick(e, facultyGraph, categories[this.x])
},
},
},
},
},
}
if (!facultyGraph) config.title.text = `Year ${year} by ${label.toLowerCase()}`
return (
<div className={`${facultyGraph ? 'faculty' : 'programmes'}-breakdown-graph`}>
<ReactHighcharts config={config} />
</div>
)
}
export default BarChart
@@ -1,9 +1,110 @@
import React, { useState } from 'react'
import { Divider, Message } from 'semantic-ui-react'
import BarChart from './BarChart'
import MedianBarChart from './MedianBarChart'
import BreakdownBarChart from './BreakdownBarChart'
import useLanguage from '../../LanguagePicker/useLanguage'
import '../faculty.css'
const MedianDisplay = ({
handleClick,
data,
level,
goal,
label,
levelProgrammeData,
programmeNames,
classSizes,
groupBy,
goalExceptions,
year,
language,
programmeData,
}) => {
return (
<div>
{level === 'bcMsCombo' && groupBy === 'byStartYear' && (
<div className="graduations-message">
<Message compact>
Programme class sizes for recent years are not reliable as students might still lack relevant master studies
data in Sisu
</Message>
</div>
)}
{goalExceptions.needed && ['master', 'bcMsCombo'].includes(level) && (
<div className="graduations-message">
<Message compact>
<b>Different goal times</b> have been taken into account in all numbers and programme level bar coloring,
but the faculty level bar color is based on the typical goal time of {goal} months
</Message>
</div>
)}
<div className="graduations-chart-container">
<MedianBarChart
data={data}
goal={goal}
handleClick={handleClick}
label={label}
programmeNames={programmeNames}
classSizes={classSizes?.[level]}
/>
{!programmeData ? (
<div className="graduations-message">
<Message compact>Click a bar to view that year's programme level breakdown</Message>
</div>
) : (
<MedianBarChart
data={levelProgrammeData[year]?.data}
goal={goal}
facultyGraph={false}
handleClick={handleClick}
year={year}
label={label}
programmeNames={programmeNames}
language={language}
classSizes={classSizes?.programmes}
level={level}
goalExceptions={goalExceptions}
/>
)}
</div>
</div>
)
}
const BreakdownDislay = ({
handleClick,
data,
label,
levelProgrammeData,
programmeNames,
year,
language,
programmeData,
}) => {
return (
<div>
<div className="graduations-chart-container">
<BreakdownBarChart data={data} handleClick={handleClick} label={label} />
{!programmeData ? (
<div className="graduations-message">
<Message compact>Click a bar to view that year's programme level breakdown</Message>
</div>
) : (
<BreakdownBarChart
data={levelProgrammeData[year]?.data}
handleClick={handleClick}
facultyGraph={false}
year={year}
label={label}
programmeNames={programmeNames}
language={language}
/>
)}
</div>
</div>
)
}
const GraduationTimes = ({
title,
data,
@@ -12,7 +113,7 @@ const GraduationTimes = ({
label,
levelProgrammeData,
programmeNames,
showMeanTime,
showMedian,
classSizes,
groupBy,
goalExceptions,
@@ -22,9 +123,9 @@ const GraduationTimes = ({
const { language } = useLanguage()
if (!data.some(a => a.amount > 0)) return null
const handleClick = (e, isFacultyGraph) => {
const handleClick = (e, isFacultyGraph, seriesCategory = null) => {
if (isFacultyGraph) {
setYear(e.point.name)
setYear(seriesCategory || e.point.name)
setProgrammeData(true)
} else {
setProgrammeData(false)
@@ -35,55 +136,35 @@ const GraduationTimes = ({
return (
<div className={`graduation-times-${level}`} data-cy={`Section-${level}`}>
<Divider horizontal>{title}</Divider>
<div>
{level === 'bcMsCombo' && groupBy === 'byStartYear' && (
<div className="graduations-message">
<Message compact>
Programme class sizes for recent years are not reliable as students might still lack relevant master
studies data in Sisu
</Message>
</div>
)}
{goalExceptions.needed && ['master', 'bcMsCombo'].includes(level) && (
<div className="graduations-message">
<Message compact>
<b>Different goal times</b> have been taken into account in all numbers and programme level bar coloring,
but the faculty level bar color is based on the typical goal time of {goal} months
</Message>
</div>
)}
<div className="graduations-chart-container">
<BarChart
data={data}
goal={goal}
handleClick={handleClick}
label={label}
programmeNames={programmeNames}
showMeanTime={showMeanTime}
classSizes={classSizes?.[level]}
/>
{!programmeData ? (
<div className="graduations-message">
<Message compact>Click a bar to view that year's programme level breakdown</Message>
</div>
) : (
<BarChart
data={levelProgrammeData[year]?.data}
goal={goal}
facultyGraph={false}
handleClick={handleClick}
year={year}
label={label}
programmeNames={programmeNames}
language={language}
showMeanTime={showMeanTime}
classSizes={classSizes?.programmes}
level={level}
goalExceptions={goalExceptions}
/>
)}
</div>
</div>
{!showMedian ? (
<BreakdownDislay
handleClick={handleClick}
data={data}
level={level}
label={label}
levelProgrammeData={levelProgrammeData}
programmeNames={programmeNames}
year={year}
language={language}
programmeData={programmeData}
/>
) : (
<MedianDisplay
handleClick={handleClick}
data={data}
level={level}
goal={goal}
label={label}
levelProgrammeData={levelProgrammeData}
programmeNames={programmeNames}
classSizes={classSizes}
groupBy={groupBy}
goalExceptions={goalExceptions}
year={year}
language={language}
programmeData={programmeData}
/>
)}
</div>
)
}
@@ -11,7 +11,6 @@ const BarChart = ({
label,
programmeNames,
language = null,
showMeanTime,
classSizes,
level,
goalExceptions,
@@ -69,9 +68,7 @@ const BarChart = ({
label === 'Start year'
? `<b>From class of ${facultyGraph ? name : year}, ${amount}/${getClassSize(code)} students have graduated</b>`
: `<b>${amount} students graduated in year ${facultyGraph ? name : year}</b>`
const timeText = `<br /><p>${sortingText}, <br /><b>${
showMeanTime ? 'mean' : 'median'
} study time: ${y} months</p></b>`
const timeText = `<br /><p>${sortingText}, <br /><b> median study time: ${y} months</p></b>`
const statisticsText = `<br /><p>${statistics.onTime} graduated on time</p><br />
<p>${statistics.yearOver} graduated max year overtime</p>
<br /><p>${statistics.wayOver} graduated over year late</p>`
@@ -96,6 +93,7 @@ const BarChart = ({
tooltip: {
backgroundColor: 'white',
fontSize: '25px',
// outside: true,
// eslint-disable-next-line
formatter: function() {
return getTooltipText(
@@ -10,22 +10,20 @@ import '../faculty.css'
const TimesAndPathsView = ({ faculty, studyProgrammes, setStudyProgrammes }) => {
const toolTips = InfotoolTips.Faculty
const [showMeanTime, setShowMeanTime] = useState(false)
const [showMedian, setShowMedian] = useState(false)
const [groupByStartYear, setGroupByStartYear] = useState(false)
const studyProgrammeFilter = studyProgrammes ? 'ALL_PROGRAMMES' : 'NEW_STUDY_PROGRAMMES'
const graduationStats = useGetFacultyGraduationTimesQuery({ id: faculty?.code, studyProgrammeFilter })
const groupBy = groupByStartYear ? 'byStartYear' : 'byGradYear'
const label = groupByStartYear ? 'Start year' : 'Graduation year'
const data = showMeanTime ? graduationStats?.data?.[groupBy].means : graduationStats?.data?.[groupBy].medians
const data = graduationStats?.data?.[groupBy].medians
const goals = graduationStats?.data?.goals
const goalExceptions = { ...goals?.exceptions, needed: faculty?.code === 'H30' }
const programmeData = showMeanTime
? graduationStats?.data?.[groupBy].programmes.means
: graduationStats?.data?.[groupBy].programmes.medians
const programmeData = graduationStats?.data?.[groupBy].programmes.medians
const programmeNames = graduationStats?.data?.programmeNames
const classSizes = graduationStats?.data?.classSizes
const commonProps = { label, programmeNames, showMeanTime, classSizes, goalExceptions }
const commonProps = { label, programmeNames, showMedian, classSizes, goalExceptions }
const isFetchingOrLoading = graduationStats.isLoading || graduationStats.isFetching
@@ -65,6 +63,13 @@ const TimesAndPathsView = ({ faculty, studyProgrammes, setStudyProgrammes }) =>
<>
{getDivider('Average graduation times', 'AverageGraduationTimes')}
<div className="toggle-container">
<Toggle
cypress="GraduationTimeToggle"
firstLabel="Breakdown"
secondLabel="Median times"
value={showMedian}
setValue={setShowMedian}
/>
<Toggle
cypress="GroupByToggle"
firstLabel="Group by: Graduation year"
@@ -72,13 +77,6 @@ const TimesAndPathsView = ({ faculty, studyProgrammes, setStudyProgrammes }) =>
value={groupByStartYear}
setValue={setGroupByStartYear}
/>
<Toggle
cypress="GraduationTimeToggle"
firstLabel="Median time"
secondLabel="Mean time"
value={showMeanTime}
setValue={setShowMeanTime}
/>
</div>
<div>
<GraduationTimes
@@ -27,16 +27,23 @@ const testKey = value => {
return 6
}
const sortProgrammeKeys = programmeKeys => {
return programmeKeys.sort((a, b) => {
if (testKey(a[1]) - testKey(b[1]) === 0 && testKey(a[0]) - testKey(b[0])) {
return a[1].localeCompare(b[1])
}
if (testKey(a[1]) - testKey(b[1]) === 0) {
return a[0].localeCompare(b[0])
}
return testKey(a[1]) - testKey(b[1])
})
const sortProgrammeKeys = (programmeKeys, faculty) => {
try {
return programmeKeys.sort((a, b) => {
if (a[1].includes(faculty) && !b[1].includes(faculty)) return -1
if (!a[1].includes(faculty) && b[1].includes(faculty)) return 1
if (a[1].startsWith('T') && !b[1].includes(faculty) && !b[1].includes('T')) return -1
if (!a[1].includes(faculty) && b[1].startsWith('T') && !a[1].includes('T')) return 1
if (a[1].startsWith('LIS') && !b[1].includes(faculty) && !b[1].includes('LIS')) return -1
if (!a[1].includes(faculty) && b[1].startsWith('LIS') && !a[1].includes('LIS')) return 1
if (testKey(a[1]) - testKey(b[1]) === 0) {
return a[0].localeCompare(b[0])
}
return testKey(a[1]) - testKey(b[1])
})
} catch (e) {
return programmeKeys
}
}
export default sortProgrammeKeys
@@ -8,10 +8,10 @@ const filterTexts = {
[FilterType.ALL]: { label: 'All' },
[FilterType.PASSED]: { label: 'Passed' },
[FilterType.PASSED_AFTER_FAILURE]: { label: 'Passed After Failure' },
[FilterType.FAILED]: { label: 'Failed' },
[FilterType.FAILED_MANY_TIMES]: { label: 'Failed Multiple Times' },
// [FilterType.FAILED]: { label: 'Failed' },
// [FilterType.FAILED_MANY_TIMES]: { label: 'Failed Multiple Times' },
// [FilterType.NOT_PARTICIPATED]: { label: 'Not Participated' },
[FilterType.DID_NOT_PASS]: { label: "Didn't Pass" },
// [FilterType.DID_NOT_PASS]: { label: "Didn't Pass" },
}
// a bandaid solution to prevent oodikone crashing
const translate = {
@@ -25,6 +25,8 @@ const translate = {
}
const CourseCard = ({ course, filterType, onChange }) => {
// Object.entries(filterTexts).map(([type, { label, info }]) => console.log(type, label, translate[type]))
// console.log('course', course)
const { language } = useLanguage()
const name = 'courseFilter'
@@ -37,7 +39,7 @@ const CourseCard = ({ course, filterType, onChange }) => {
return (
<>
<Label style={{ marginTop: '0.5rem' }}>
{getTextIn(course.course.name, language)}
{getTextIn(course?.course?.name, language)}
<Dropdown
text={filterTexts[filterType].label}
@@ -60,7 +62,7 @@ const CourseCard = ({ course, filterType, onChange }) => {
/>
)
}
if (Object.keys(course?.students[translate[type]]).length === 0)
if (course?.students[translate[type]] && Object.keys(course?.students[translate[type]]).length === 0)
return <Dropdown.Item key={label} text={label} value={type} onClick={onClick} disabled />
return <Dropdown.Item key={label} text={label} value={type} onClick={onClick} />
})}
@@ -15,7 +15,6 @@ const CourseFilterCard = ({ courseStats, options, onOptionsChange }) => {
const { courseFilters } = options
const { language } = useLanguage()
const name = 'course-filter-card'
// Wrestle course stats into something semantic-ui eats without throwing up.
const makeLabel = cs => `${cs.course.code} - ${getTextIn(cs.course.name, language)}`
const dropdownOptions = Object.values(courseStats)
@@ -81,9 +80,9 @@ const filterFunctions = {
[FilterType.ALL]: createFilterFunc('all'),
[FilterType.PASSED]: createFilterFunc('passed'),
[FilterType.PASSED_AFTER_FAILURE]: createFilterFunc('retryPassed'),
[FilterType.FAILED]: createFilterFunc('failed'),
[FilterType.FAILED_MANY_TIMES]: createFilterFunc('failedMany'),
[FilterType.DID_NOT_PASS]: createFilterFunc('didNotPass'),
// [FilterType.FAILED]: createFilterFunc('failed'),
// [FilterType.FAILED_MANY_TIMES]: createFilterFunc('failedMany'),
// [FilterType.DID_NOT_PASS]: createFilterFunc('didNotPass'),
// [FilterType.NOT_PARTICIPATED]: createFilterFunc('all', true),
// [FilterType.DID_NOT_PASS]: ({ studentNumber }, { students }) =>
// !Object.keys(students.all).includes(studentNumber) || Object.keys(students.failed).includes(studentNumber),
@@ -103,9 +102,7 @@ const filter = createFilter({
filter(student, { courseFilters }, { precomputed: courseMap }) {
return Object.entries(courseFilters).reduce((result, [code, filterType]) => {
const stats = courseMap[code]
if (!stats) return false
return result && filterFunctions[filterType](student, stats)
}, true)
},
@@ -8,6 +8,7 @@ import createFilter from './createFilter'
const STATUS_OPTIONS = [
{ key: 'enrl-status-present', text: 'Present', value: 1 },
{ key: 'enrl-status-absent', text: 'Absent', value: 2 },
{ key: 'enrl-status-inactive', text: 'Passive', value: 3 },
]
const EnrollmentStatusFilterCard = ({ options, onOptionsChange, allSemesters, language, semesterCodes }) => {
@@ -179,7 +179,7 @@ const StudentNumberFilterCard = ({ options, onOptionsChange, withoutSelf }) => {
export default createFilter({
key: 'studentNumber',
title: 'Student number *',
title: 'Student Number',
defaultOptions: {
allowlist: [],
@@ -49,7 +49,7 @@ const StudyrightStatusFilterCard = ({ options, onOptionsChange }) => {
export default createFilter({
key: 'studyrightStatusFilter',
title: 'Studyright status',
title: 'Studyright Status',
info: filterInfo.studyrightStatus,
@@ -13,7 +13,7 @@ const InfoBox = ({ content, cypress }) => {
<div className="content-container" data-cy={`${cypress}-info-content`}>
<Icon name="info circle" size="huge" />
<Message.Content className="ok-infobox content">
<ReactMarkdown escapeHtml={false}>{content}</ReactMarkdown>
<ReactMarkdown>{content}</ReactMarkdown>
</Message.Content>
</div>
<Button onClick={toggleOpen} className="ok-infobox-close" data-cy={`${cypress}-close-info`}>
@@ -6,6 +6,7 @@ import { Link } from 'react-router-dom'
import CourseFilterToggle from '../CourseFilterToggle'
import { UsePopulationCourseContext } from '../PopulationCourseContext'
import { getTextIn } from '../../../common'
import './passFailEnrollments.css'
const createModuleAggregateRow = ({ definition, children }) => ({
label_code: definition.module.code,
@@ -93,7 +94,7 @@ const PassFailEnrollments = ({ flat, onlyIamRights }) => {
children: [
{
key: 'total',
title: 'Total students',
title: 'Total\nstudents',
helpText: 'Total count of students, including enrolled students with no grade.',
cellStyle: { textAlign: 'right' },
filterType: 'range',
@@ -115,7 +116,7 @@ const PassFailEnrollments = ({ flat, onlyIamRights }) => {
},
{
key: 'totalEnrolledNoGrade',
title: 'Enrolled no grade',
title: 'Enrolled, \nno grade',
helpText: 'Students with a valid enrollment and no passing or failing grade.',
cellStyle: { textAlign: 'right' },
filterType: 'range',
@@ -159,14 +160,14 @@ const PassFailEnrollments = ({ flat, onlyIamRights }) => {
},
{
key: 'passed-after-retry',
title: 'Passed after retry',
title: 'Passed\nafter retry',
cellStyle: { textAlign: 'right' },
filterType: 'range',
getRowVal: row => row.stats?.retryPassed ?? 0,
},
{
key: 'failed-many',
title: 'Failed multiple times',
title: 'Failed\nmultiple times',
cellStyle: { textAlign: 'right' },
filterType: 'range',
getRowVal: row => row.stats?.failedMany ?? 0,
@@ -234,6 +235,7 @@ const PassFailEnrollments = ({ flat, onlyIamRights }) => {
return (
<>
<SortableTable
tableId="pass-and-fail-statistics-of-courses"
title="Pass and fail statistics of courses with course enrollment details"
data={data}
columns={columns}
@@ -0,0 +1,19 @@
table#pass-and-fail-statistics-of-courses tbody tr td:nth-child(n + 1) {
z-index: -1;
}
table#pass-and-fail-statistics-of-courses tbody tr:nth-child(2n) td:nth-child(2) {
position: sticky;
left: 0;
background: rgb(242 242 242) !important;
overflow: hidden;
z-index: 0;
}
table#pass-and-fail-statistics-of-courses tbody tr:nth-child(2n + 1) td:nth-child(2) {
position: sticky;
left: 0;
background: white !important;
overflow: hidden;
z-index: 0;
}
@@ -1,9 +1,14 @@
import moment from 'moment/moment'
import React from 'react'
import { Table } from 'semantic-ui-react'
import studyPlanFilter from 'components/FilterView/filters/hops'
import CollapsibleCreditRow from './CollapsibleCreditRow'
import useFilters from '../../FilterView/useFilters'
const CreditsGainedTable = ({ filteredStudents, type, year, creditDateFilterOptions }) => {
const { useFilterSelector } = useFilters()
const studyPlanFilterIsActive = useFilterSelector(studyPlanFilter.selectors.isActive)
if (!filteredStudents || !filteredStudents.length || !type) return null
let creditList = []
const studentCount = (min, max = Infinity) =>
@@ -79,6 +84,18 @@ const CreditsGainedTable = ({ filteredStudents, type, year, creditDateFilterOpti
]
const monthString = months === 1 ? 'Month' : 'Months'
const CreditsHeader = () => {
if (studyPlanFilterIsActive) {
return <p>All credits in studyplan</p>
}
return (
<p>
Gredits Gained Between <br /> {title} <br /> ({months} {monthString})
</p>
)
}
return (
<div className="credits-gained-table" data-cy={`credits-gained-table-${type}`}>
<h3>{type}</h3>
@@ -87,7 +104,7 @@ const CreditsGainedTable = ({ filteredStudents, type, year, creditDateFilterOpti
<Table.Row key={`credits-gained-table-${type}`}>
<Table.HeaderCell collapsing />
<Table.HeaderCell key={`${title}-${type}`}>
Gredits Gained Between <br /> {title} <br /> ({months} {monthString})
<CreditsHeader />
</Table.HeaderCell>
<Table.HeaderCell key={`credits-number-of-students-${type}`}>
Number of Students
@@ -132,9 +132,6 @@ const PopulationSearchForm = props => {
setState({ query: initialQuery() })
props.getProgrammes()
}
/* if (!semesters.years) {
props.getSemesters()
} */
if (location.search) {
fetchPopulationFromUrlParams()
}
@@ -1,11 +1,10 @@
/* eslint-disable babel/no-invalid-this, class-methods-use-this */
import React, { Component, useEffect } from 'react'
import React, { useEffect, useState } from 'react'
import { connect } from 'react-redux'
import { func, shape, object, bool, arrayOf } from 'prop-types'
import { Form, Button } from 'semantic-ui-react'
import moment from 'moment'
import qs from 'query-string'
import Datetime from 'react-datetime'
import { get as lodashGet } from 'lodash'
import PopulationQueryCard from '../PopulationQueryCard'
import { removePopulation } from '../../redux/populations'
@@ -42,111 +41,37 @@ const PopulationsQueryTSA = ({ programmeCode, unitData }) => {
return null
}
class PopulationSearchHistory extends Component {
static propTypes = {
removePopulation: func.isRequired,
populations: shape({
pending: bool,
error: bool,
data: shape({}),
query: object,
}).isRequired,
units: object, // eslint-disable-line
tags: arrayOf(shape({})).isRequired,
history: shape({}).isRequired,
}
const getMonths = (year, term) => {
const start = term === 'FALL' ? `${year}-08-01` : moment(`${year}-01-01`).add(1, 'years')
return Math.ceil(moment.duration(moment().diff(moment(start))).asMonths())
}
constructor() {
super()
this.state = {
showAdvancedSettings: false,
query: {
studentStatuses: [],
semesters: ['FALL', 'SPRING'],
months: 0,
},
const PopulationSearchHistory = props => {
const [showAdvancedSettings, setShowAdvancedSettings] = useState(false)
const [semesters, setSemesters] = useState(
props.populations.query?.semesters ? props.populations.query?.semesters : ['FALL', 'SPRING']
)
const [studentStatuses, setStudentStatus] = useState(
props.populations.query?.studentStatuses ? props.populations.query?.studentStatuses : []
)
const [months, setMonths] = useState(props.populations.query?.months ? props.populations.query?.months : 0)
const handleSemesterSelection = (e, { value }) => {
const newSemesters = semesters.includes(value) ? semesters.filter(s => s !== value) : [...semesters, value]
if (!props.populations.query.tag) {
setSemesters(newSemesters)
setMonths(getMonths(props.populations.query.year, semesters.includes('FALL') ? 'FALL' : 'SPRING'))
}
}
componentWillReceiveProps(nextProps) {
const { query } = this.state
if (nextProps.populations.query) {
this.setState({
query: {
...query,
months: nextProps.populations.query.months,
year: nextProps.populations.query.year,
semesters: nextProps.populations.query.semesters,
studentStatuses: nextProps.populations.query.studentStatuses || [],
},
})
}
const handleStudentStatusSelection = (e, { value }) => {
setStudentStatus(
studentStatuses.includes(value) ? studentStatuses.filter(s => s !== value) : [...studentStatuses, value]
)
}
months = (year, term) => {
const start = term === 'FALL' ? `${year}-08-01` : moment(`${year}-01-01`).add(1, 'years')
return Math.ceil(moment.duration(moment().diff(moment(start))).asMonths())
}
getMonths = (year, end, term) => {
if (moment.isMoment(end)) {
const lastDayOfMonth = moment(end).endOf('month')
const start = term === 'FALL' ? `${year}-08-01` : `${year}-01-01`
return Math.round(moment.duration(moment(lastDayOfMonth).diff(moment(start))).asMonths())
}
return -1
}
handleSemesterSelection = (e, { value }) => {
const { query } = this.state
const semesters = query.semesters.includes(value)
? query.semesters.filter(s => s !== value)
: [...query.semesters, value]
if (!query.tag) {
this.setState({
query: {
...query,
semesters,
months: this.months(this.props.populations.query.year, semesters.includes('FALL') ? 'FALL' : 'SPRING'),
},
})
}
}
handleStudentStatusSelection = (e, { value }) => {
const { query } = this.state
const studentStatuses = query.studentStatuses.includes(value)
? query.studentStatuses.filter(s => s !== value)
: [...query.studentStatuses, value]
this.setState({
query: {
...query,
studentStatuses,
},
})
}
handleMonthsChange = value => {
const { query } = this.state
const months = this.getMonths(query.year, value, query.semesters.includes('FALL') ? 'FALL' : 'SPRING')
this.setState({
query: {
...query,
months,
},
})
}
getMonthValue = (year, months) => {
const start = `${year}-08-01`
return moment(start)
.add(months - 1, 'months')
.format('MMMM YYYY')
}
pushQueryToUrl = () => {
const { studyRights, tag, years } = this.props.populations.query
const { studentStatuses, semesters, months, year } = this.state.query
const pushQueryToUrl = () => {
const { studyRights, tag, year } = props.populations.query
const queryObject = {
tag,
@@ -155,42 +80,21 @@ class PopulationSearchHistory extends Component {
studentStatuses,
semesters,
studyRights: JSON.stringify(studyRights),
years,
}
const searchString = qs.stringify(queryObject)
this.props.history.push({ search: searchString })
props.history.push({ search: searchString })
}
getMinSelection = (year, semester) => (semester === 'FALL' ? `${year}-08-01` : `${year}-01-01`)
const removeThisPopulation = uuid => props.removePopulation(uuid)
removePopulation = uuid => this.props.removePopulation(uuid)
renderAdvancedSettingsSelector = () => {
if (!this.state.showAdvancedSettings) {
const renderAdvancedSettingsSelector = () => {
if (!showAdvancedSettings) {
return null
}
const { query } = this.state
const { populations } = this.props
const { semesters, studentStatuses } = query
const { populations } = props
return (
<Form.Group style={{ flexDirection: 'column' }}>
<Form.Field error={query.months < 0}>
<b>Statistics until</b>
<Datetime
dateFormat="MMMM YYYY"
closeOnSelect
defaultValue={this.getMonthValue(query.year, query.months)}
onChange={value => this.handleMonthsChange(value)}
isValidDate={current =>
current.isBefore(moment()) &&
current.isAfter(this.getMinSelection(query.year, query.semesters[1] || query.semesters[0]))
}
className="adv-stats-until"
/>
</Form.Field>
{!populations.query.tag ? (
<Form.Field style={{ marginTop: '15px' }}>
<b>Starting semesters</b>
@@ -201,7 +105,7 @@ class PopulationSearchHistory extends Component {
value="FALL"
name="semesterGroup"
checked={semesters.includes('FALL')}
onChange={this.handleSemesterSelection}
onChange={handleSemesterSelection}
data-cy="toggle-fall"
/>
<Form.Checkbox
@@ -211,22 +115,22 @@ class PopulationSearchHistory extends Component {
value="SPRING"
name="semesterGroup"
checked={semesters.includes('SPRING')}
onChange={this.handleSemesterSelection}
onChange={handleSemesterSelection}
data-cy="toggle-spring"
/>
</Form.Field>
) : null}
<Form.Field style={{ marginTop: '15px' }}>
<b>Include</b>
{/* Hide buggy options for now */}
{/* <Form.Checkbox
{/* Investigate why not working
<Form.Checkbox
className="populationStatisticsRadio"
key="EXCHANGE"
label="Exchange students"
value="EXCHANGE"
name="studentStatusGroup"
checked={studentStatuses.includes('EXCHANGE')}
onChange={this.handleStudentStatusSelection}
onChange={handleStudentStatusSelection}
/>
<Form.Checkbox
className="populationStatisticsRadio"
@@ -235,7 +139,7 @@ class PopulationSearchHistory extends Component {
value="NONDEGREE"
name="studentStatusGroup"
checked={studentStatuses.includes('NONDEGREE')}
onChange={this.handleStudentStatusSelection}
onChange={handleStudentStatusSelection}
/> */}
<Form.Checkbox
className="populationStatisticsRadio"
@@ -244,11 +148,11 @@ class PopulationSearchHistory extends Component {
value="TRANSFERRED"
name="studentStatusGroup"
checked={studentStatuses.includes('TRANSFERRED')}
onChange={this.handleStudentStatusSelection}
onChange={handleStudentStatusSelection}
/>
</Form.Field>
<Form.Field style={{ marginTop: '15px' }}>
<Button type="button" onClick={this.pushQueryToUrl}>
<Button type="button" onClick={pushQueryToUrl}>
Fetch class with new settings
</Button>
</Form.Field>
@@ -256,9 +160,8 @@ class PopulationSearchHistory extends Component {
)
}
renderQueryCards = () => {
const { populations, units, tags } = this.props
const { showAdvancedSettings } = this.state
const renderQueryCards = () => {
const { populations, units, tags } = props
const { Advanced, QueryCard } = infotooltips.PopulationStatistics
if (!units.data.programmes || !populations.query || !populations.data.students) {
@@ -277,7 +180,7 @@ class PopulationSearchHistory extends Component {
query={populations.query}
queryId={0}
units={[units.data.programmes[programmeCode], units.data.studyTracks[studyTrackCode]].filter(Boolean)}
removeSampleFn={this.removePopulation}
removeSampleFn={removeThisPopulation}
updating={populations.updating}
tags={tags}
/>
@@ -286,29 +189,40 @@ class PopulationSearchHistory extends Component {
</div>
</div>
<div style={{ marginLeft: '100px' }}>
<Form.Group>
<Form.Field>
<Form.Radio
data-cy="advanced-toggle"
toggle
checked={showAdvancedSettings}
onClick={() => {
this.setState({ showAdvancedSettings: !showAdvancedSettings })
}}
label="Advanced settings"
/>
</Form.Field>
</Form.Group>
<div>{this.renderAdvancedSettingsSelector()}</div>
{populations.query.year !== 'All' && (
<Form.Group>
<Form.Field>
<Form.Radio
data-cy="advanced-toggle"
toggle
checked={showAdvancedSettings}
onClick={() => setShowAdvancedSettings(!showAdvancedSettings)}
label="Advanced settings"
/>
</Form.Field>
</Form.Group>
)}
<div>{renderAdvancedSettingsSelector()}</div>
<div>{showAdvancedSettings ? <InfoBox content={Advanced} /> : <FilterActiveNote />}</div>
</div>
</div>
)
}
render() {
return <div className="historyContainer">{this.renderQueryCards()}</div>
}
return <div className="historyContainer">{renderQueryCards()}</div>
}
PopulationSearchHistory.propTypes = {
removePopulation: func.isRequired,
populations: shape({
pending: bool,
error: bool,
data: shape({}),
query: object,
}).isRequired,
units: object, // eslint-disable-line
tags: arrayOf(shape({})).isRequired,
history: shape({}).isRequired,
}
const mapStateToProps = ({ populations, populationProgrammes, tags }) => ({
@@ -23,7 +23,7 @@ const PopulationSearch = ({ populationFound, history, location, loading }) => {
{title && <Header size="medium">{title}</Header>}
{(!populationFound || !history.location.search) && (
<>
<InfoBox content={infotoolTips.PopulationStatistics.Search} />
<InfoBox content={infotoolTips.PopulationStatistics.Search} cypress="PopulationSearch" />
<Divider />
</>
)}
@@ -1,4 +1,4 @@
import { sortBy } from 'lodash'
import { flatten, sortBy } from 'lodash'
import React from 'react'
import { useSelector } from 'react-redux'
import { Dropdown } from 'semantic-ui-react'
@@ -73,9 +73,16 @@ export default ({ students }) => {
return studyright?.startdate ? studyright.startdate : ''
}
const getStartedInStudyright = studyrights => {
const studyright = findCorrectStudyright(studyrights)
return studyright?.studystartdate ? studyright.studystartdate : ''
const getStartedInProgramme = studyrights => {
const code = queryStudyrights[0]
if (!code || !studyrights) return ''
const element = flatten(
studyrights.reduce((acc, curr) => {
acc.push(curr.studyright_elements)
return acc
}, [])
).filter(e => e.code === code)
return new Date(Math.max(new Date(element[0]?.startdate), new Date(getStartOfStudyright(studyrights))))
}
const getAdmissionType = studyrights => {
@@ -143,7 +150,7 @@ export default ({ students }) => {
tags: tags(s.tags),
'start year at university': reformatDate(s.started, 'YYYY'),
'start of studyright': reformatDate(getStartOfStudyright(s.studyrights), 'YYYY-MM-DD'),
'started in studyright': reformatDate(getStartedInStudyright(s.studyrights), 'YYYY-MM-DD'),
'started in programme': reformatDate(getStartedInProgramme(s.studyrights), 'YYYY-MM-DD'),
'admission type': parseInt(queryYear, 10 >= 2020) ? getAdmissionType(s.studyrights) : undefined,
'updated at': reformatDate(s.updatedAt, 'YYYY-MM-DD hh:mm:ss'),
'mandatory total passed': totalMandatoryPassed(s.studentNumber, codes),
@@ -30,6 +30,10 @@ import {
hopsFilter,
} from '../FilterView/filters'
const getYearText = year => {
if (year === 'All') return ''
return `${year} - ${Number(year) + 1}`
}
const PopulationStatistics = () => {
const location = useLocation()
const { language } = useLanguage()
@@ -96,9 +100,7 @@ const PopulationStatistics = () => {
}, [samples, programmeCode])
const title =
location.search === ''
? 'Class statistics'
: `${getTextIn(programmeName, language)} ${query?.year} - ${Number(query?.year) + 1}`
location.search === '' ? 'Class statistics' : `${getTextIn(programmeName, language)} ${getYearText(query?.year)}`
return (
<FilterView
@@ -119,6 +121,9 @@ const PopulationStatistics = () => {
<div className="segmentContainer" style={{ flexGrow: 1 }}>
<Header className="segmentTitle" size="large" align="center">
{title}
{location.search !== '' && query?.studyRights?.studyTrack && query?.studyRights?.studyTrack !== '' && (
<Header.Subheader> studytrack {query?.studyRights?.studyTrack}</Header.Subheader>
)}
{location.search !== '' && (
<Header.Subheader>
studytime {query?.months} months, class size {students.length} students
@@ -163,21 +163,28 @@ const GeneralTab = ({
}
const studentToStudyrightStartMap = selectedStudents.reduce((res, sn) => {
const currentStudyright = students[sn].studyrights.find(studyright =>
studyright.studyright_elements.some(e => e.code === programmeCode)
)
if (currentStudyright?.studyrightid && currentStudyright.studyrightid.slice(-2) === '-2') {
const bachelorId = currentStudyright.studyrightid.replace(/-2$/, '-1')
const bacherlorStudyright = students[sn].studyrights.find(studyright => studyright.studyrightid === bachelorId)
res[sn] = bacherlorStudyright?.startdate || null
} else {
res[sn] = currentStudyright?.startdate || null
}
return res
}, {})
const studentToProgrammeStartMap = selectedStudents.reduce((res, sn) => {
const targetStudyright = _.flatten(
students[sn].studyrights.reduce((acc, curr) => {
acc.push(curr.studyright_elements)
return acc
}, [])
).filter(e => e.code === programmeCode)
res[sn] = targetStudyright[0] ? targetStudyright[0].startdate : null
return res
}, {})
const studentToStudyrightActualStartMap = selectedStudents.reduce((res, sn) => {
const targetStudyright = students[sn].studyrights.find(studyright =>
studyright.studyright_elements.some(e => e.code === programmeCode)
)
res[sn] = targetStudyright ? targetStudyright.studystartdate : null
// clean up odd bachelor start dates, (givendate)
res[sn] = new Date(Math.max(new Date(targetStudyright[0]?.startdate), new Date(studentToStudyrightStartMap[sn])))
return res
}, {})
@@ -189,18 +196,6 @@ const GeneralTab = ({
return res
}, {})
const getActualStartDate = studentNumber => {
const studyRightStart = studentToStudyrightStartMap[studentNumber]
const studyRightStartActual = studentToStudyrightActualStartMap[studentNumber]
if (!studyRightStart) return studyRightStartActual
if (!studyRightStartActual) return studyRightStart
return new Date(studyRightStart).getTime() > new Date(studyRightStartActual).getTime()
? studyRightStart
: studyRightStartActual
}
const getStarted = ({ obfuscated, started }) => {
if (obfuscated || !started) return ''
return moment(started).get('year')
@@ -256,7 +251,7 @@ const GeneralTab = ({
const shouldShowAdmissionType = parseInt(query?.year, 10) >= 2020 || parseInt(group?.tags?.year, 10) >= 2020
let creditColumnTitle = 'Since Start of Studyright'
let creditColumnTitle = 'Since start\nin programme'
if (creditDateFilterOptions) {
const { startDate, endDate } = creditDateFilterOptions
@@ -297,7 +292,7 @@ const GeneralTab = ({
getRowVal: s => {
const credits = getStudentTotalCredits({
...s,
courses: s.courses.filter(c => new Date(c.date) >= new Date(studentToStudyrightStartMap[s.studentNumber])),
courses: s.courses.filter(c => new Date(c.date) >= studentToProgrammeStartMap[s.studentNumber]),
})
return credits
},
@@ -329,8 +324,8 @@ const GeneralTab = ({
// All columns components user is able to use
const columnsAvailable = {
lastname: { key: 'lastname', title: 'last name', getRowVal: s => s.lastname },
firstname: { key: 'firstname', title: 'given names', getRowVal: s => s.firstnames },
lastname: { key: 'lastname', title: 'Last name', getRowVal: s => s.lastname },
firstname: { key: 'firstname', title: 'Given names', getRowVal: s => s.firstnames },
'studentnumber-parent': {
key: 'studentnumber-parent',
mergeHeader: true,
@@ -394,54 +389,28 @@ const GeneralTab = ({
return grade
},
},
transferredFrom: {
key: 'transferredFrom',
title: 'Transferred From',
getRowVal: s => (s.transferredStudyright ? transferFrom(s) : ''),
},
studyTrack: containsStudyTracks && {
key: 'studyTrack',
title: 'Study Track',
getRowVal: s => studytrack(s.studyrights).map(st => st.name)[0],
},
priority: {
key: 'priority',
title: 'priority',
getRowVal: s => priorityText(s.studyrights),
},
extent: {
key: 'extent',
title: 'extent',
getRowVal: s => extentCodes(s.studyrights),
},
semesterEnrollments: {
key: 'semesterEnrollments',
title: 'semesters',
helpText: 'Number of present semesters',
getRowVal: s => semesterEnrollments(s.semesterenrollments),
},
tags: {
key: 'tags',
title: 'Tags',
getRowVal: s => (!s.obfuscated ? tags(s.tags) : ''),
studyrightStart: {
key: 'studyrightStart',
title: 'Start of\nstudyright',
filterType: 'date',
getRowVal: s => studentToStudyrightStartMap[s.studentNumber], // new Date(studentToStudyrightStartMap[s.studentNumber]),
formatValue: value => reformatDate(new Date(value), 'YYYY-MM-DD'),
},
studyStartDate: {
key: 'studyStartDate',
title: 'start of studyright',
title: 'Started in\nprogramme',
filterType: 'date',
getRowVal: s => new Date(studentToStudyrightStartMap[s.studentNumber]),
getRowVal: s => studentToProgrammeStartMap[s.studentNumber],
formatValue: value => reformatDate(new Date(value), 'YYYY-MM-DD'),
},
studyStartDateActual: {
key: 'studyStartDateActual',
title: 'started in studyright',
filterType: 'date',
formatValue: value => reformatDate(new Date(value), 'YYYY-MM-DD'),
getRowVal: s => new Date(getActualStartDate(s.studentNumber)),
},
endDate: {
key: 'endDate',
title: 'graduation date',
title: 'Graduation date',
filterType: 'date',
getRowVal: s =>
studentToStudyrightEndMap[s.studentNumber] ? new Date(studentToStudyrightEndMap[s.studentNumber]) : '',
@@ -450,9 +419,24 @@ const GeneralTab = ({
? reformatDate(studentToStudyrightEndMap[s.studentNumber], 'YYYY-MM-DD')
: '',
},
semesterEnrollments: {
key: 'semesterEnrollments',
title: 'Semesters\npresent',
getRowVal: s => semesterEnrollments(s.semesterenrollments),
},
tags: {
key: 'tags',
title: 'Tags',
getRowVal: s => (!s.obfuscated ? tags(s.tags) : ''),
},
transferredFrom: {
key: 'transferredFrom',
title: 'Transferred\nfrom',
getRowVal: s => (s.transferredStudyright ? transferFrom(s) : ''),
},
admissionType: shouldShowAdmissionType && {
key: 'admissionType',
title: 'admission type',
title: 'Admission type',
getRowVal: s => {
const studyright = s.studyrights.find(sr => sr.studyright_elements.some(e => e.code === programmeCode))
return studyright && studyright.admission_type ? studyright.admission_type : 'Ei valintatapaa'
@@ -498,6 +482,16 @@ const GeneralTab = ({
title: cleanedQueryStudyrights.some(code => code.startsWith('MH')) ? 'Bachelor' : 'Master',
getRowVal: s => (s.option ? getTextIn(s.option.name, language) : ''),
},
priority: {
key: 'priority',
title: 'Priority',
getRowVal: s => priorityText(s.studyrights),
},
extent: {
key: 'extent',
title: 'Extent',
getRowVal: s => extentCodes(s.studyrights),
},
email: {
mergeHeader: true,
merge: true,
@@ -507,7 +501,7 @@ const GeneralTab = ({
key: 'emailValue',
title: (
<>
email
Email
<Popup
trigger={<Icon link name="copy" onClick={copyToClipboardAll} style={{ float: 'right' }} />}
content="Copied email list!"
@@ -610,7 +604,8 @@ const GeneralTabContainer = ({ studyGuidanceGroup, variant, ...props }) => {
const getStudyGuidanceGroupColumns = () => {
const cols = ['programme', 'startYear']
if (studyGuidanceGroup?.tags?.year) cols.push('credits.startYear')
if (studyGuidanceGroup?.tags?.studyProgramme) cols.push('studyStartDate', 'studyStartDateActual', 'endDate')
if (studyGuidanceGroup?.tags?.studyProgramme)
cols.push('studyrightStart', 'studyStartDate', 'studyStartDateActual', 'endDate')
if (studyGuidanceGroup?.tags?.studyProgramme && studyGuidanceGroup?.tags?.year) {
cols.push('admissionType')
}
@@ -627,6 +622,7 @@ const GeneralTabContainer = ({ studyGuidanceGroup, variant, ...props }) => {
'priority',
'extent',
'semesterEnrollments',
'studyrightStart',
'studyStartDate',
'studyStartDateActual',
'endDate',
@@ -638,7 +634,7 @@ const GeneralTabContainer = ({ studyGuidanceGroup, variant, ...props }) => {
const baseColumns = ['credits', 'credits.all', 'studentnumber-parent', 'tags', 'updatedAt', 'option']
const nameColumnsToAdd = namesVisible ? ['email', 'lastname', 'firstname'] : []
const adminColumnsToFilter = isAdmin ? [] : ['priority', 'extent', 'studyStartDate', 'updatedAt']
const adminColumnsToFilter = isAdmin ? [] : ['priority', 'extent', 'updatedAt']
const columnKeysToInclude = _.chain(baseColumns)
.union(columnsByVariant[variant])
@@ -89,41 +89,51 @@ const CoursesTable = ({ students }) => {
)
const columns = useMemo(() => {
const nameColumns = [
const nameColumns = []
if (namesVisible) {
nameColumns.push(
{
key: 'lastname',
title: 'Last name',
getRowVal: s => (s.total ? null : s.lastname),
cellProps: { title: 'last name' },
child: true,
},
{
key: 'firstname',
title: 'Given names',
getRowVal: s => (s.total ? null : s.firstnames),
cellProps: { title: 'first names' },
child: true,
}
)
}
nameColumns.push(
{
key: 'studentnumber-parent',
mergeHeader: true,
// merge: true,
title: 'Student Number',
children: [
{
key: 'studentnumber',
title: 'Student Number',
cellProps: { title: 'student number' },
getRowVal: s => (s.total ? '*' : s.studentNumber),
getRowContent: s => (s.total ? 'Summary:' : s.studentNumber),
child: true,
},
{
key: 'icon',
title: 'Icon',
export: false,
getRowVal: s =>
!s.total && (
<Item
as={Link}
to={`/students/${s.studentNumber}`}
onClick={() => {
sendAnalytics('Student details button clicked', 'Mandatory courses table')
}}
>
<Icon name="user outline" />
</Item>
),
cellProps: { className: 'iconCell' },
child: true,
},
],
cellProps: { title: 'student number', className: 'studentNumber' },
getRowVal: s => (s.total ? '*' : s.studentNumber),
getRowContent: s =>
s.total ? (
'Summary:'
) : (
<div>
<span>{s.studentNumber}</span>
<Item
as={Link}
to={`/students/${s.studentNumber}`}
onClick={() => {
sendAnalytics('Student details button clicked', 'Mandatory courses table')
}}
>
<Icon style={{ borderLeft: '1em' }} name="user outline" />
</Item>
</div>
),
child: true,
},
{
key: 'totalpassed',
@@ -138,34 +148,8 @@ const CoursesTable = ({ students }) => {
: totalMandatoryPassed(s.studentNumber),
cellProps: { title: 'total passed' },
child: true,
},
]
if (namesVisible) {
nameColumns.push(
{
key: 'lastname',
title: 'last name',
getRowVal: s => (s.total ? null : s.lastname),
cellProps: { title: 'last name' },
child: true,
},
{
key: 'firstname',
title: 'given names',
getRowVal: s => (s.total ? null : s.firstnames),
cellProps: { title: 'first names' },
child: true,
},
{
key: 'email',
title: 'email',
getRowVal: s => (s.total ? null : s.email),
cellProps: { title: 'emails' },
child: true,
}
)
}
}
)
const mandatoryCourseLabels = []
@@ -239,12 +223,24 @@ const CoursesTable = ({ students }) => {
textTitle: m.code,
vertical: true,
forceToolsMode: 'dangling',
cellProps: {
title: `${m.code}, ${getTextIn(m.name)}`,
style: {
verticalAlign: 'middle',
textAlign: 'center',
},
cellProps: s => {
let grade = s.courses
? s.courses.find(course => course.course_code === m.code || course.course_code === `AY${m.code}`)
?.grade
: null
grade =
s.courses && !grade && hasPassedMandatory(s.studentNumber, m.code)
? s.courses.find(course => course.course_code === m.label_code)?.grade
: grade
const gradeText = grade ? `\nGrade: ${grade}` : ''
const studentCode = s.studentNumber ? `\nStudent number: ${s.studentNumber}` : ``
return {
title: `${m.code}, ${getTextIn(m.name)}${studentCode} ${gradeText}`,
style: {
verticalAlign: 'middle',
textAlign: 'center',
},
}
},
headerProps: { title: `${m.code}, ${getTextIn(m.name)}` },
getRowVal: s => {
@@ -272,6 +268,14 @@ const CoursesTable = ({ students }) => {
}))
)
columns.push({
key: 'email',
title: 'Email',
getRowVal: s => (s.total ? null : s.email),
cellProps: { title: 'emails' },
child: true,
})
return columns
}, [namesVisible, mandatoryCourses, getTextIn])
@@ -298,6 +302,7 @@ const CoursesTable = ({ students }) => {
<div style={{ maxHeight: '80vh', width: '100%' }}>
{mandatoryCourses.length > 0 && (
<SortableTable
tableId="course-of-population-students"
title={`Courses of population's students`}
getRowKey={s => (s.total ? 'totals' : s.studentNumber)}
tableProps={{
@@ -501,7 +506,7 @@ const PopulationStudentsContainer = ({ ...props }) => {
const contentByVariant = {
population: {
panesToInclude: ['General', 'Courses', 'Tags'],
infotoolTipContent: infotoolTips.PopulationStatistics.Students,
infotoolTipContent: infotoolTips.PopulationStatistics.StudentsClass,
},
coursePopulation: {
panesToInclude: ['General'],
@@ -509,11 +514,11 @@ const PopulationStudentsContainer = ({ ...props }) => {
},
customPopulation: {
panesToInclude: ['General'],
infotoolTipContent: infotoolTips.PopulationStatistics.Students,
infotoolTipContent: infotoolTips.PopulationStatistics.StudentsCustom,
},
studyGuidanceGroupPopulation: {
panesToInclude: ['General'],
infotoolTipContent: infotoolTips.PopulationStatistics.Students,
infotoolTipContent: infotoolTips.PopulationStatistics.StudentsGuidanceGroups,
},
}
@@ -19,3 +19,35 @@
white-space: initial;
padding: 1px;
}
table#course-of-population-students td.studentNumber:not(:first-child) {
padding: 0 !important;
}
table#course-of-population-students .studentNumber div span {
padding-left: 0.1em;
}
table#course-of-population-students .studentNumber div a {
padding-left: 0.7em;
}
table#course-of-population-students tbody tr td:nth-child(n + 1) {
z-index: -1;
}
table#course-of-population-students tbody tr:nth-child(2n) td:first-child {
position: sticky;
left: 0;
background: rgb(242 242 242) !important;
overflow: hidden;
z-index: 0;
}
table#course-of-population-students tbody tr:nth-child(2n + 1) td:first-child {
position: sticky;
left: 0;
background: white !important;
overflow: hidden;
z-index: 0;
}
@@ -26,6 +26,14 @@ export default class SortingFilteringVisitor extends DataVisitor {
const va = getColumnValue(pCtx.withItem(a), column)
const vb = getColumnValue(pCtx.withItem(b), column)
if (va === 'Not saved') {
return 0
}
if (va !== 'Not saved' && vb === 'Not saved') {
return -1
}
if (va === vb) {
return 0
}
@@ -23,9 +23,14 @@ const DefaultColumnFilterComponent = ({ column, options, dispatch }) => {
if (!values) {
return []
}
const t = _.uniq(values)
.filter(value => search === '' || `${value}`.indexOf(search) > -1)
.filter(
value =>
search === '' ||
(typeof value === 'string' &&
typeof search === 'string' &&
`${value.toLowerCase()}`.indexOf(search.toLowerCase()) > -1)
)
.sort((a, b) => {
if (typeof a === 'number' && typeof b === 'number') {
return a - b
@@ -446,7 +446,7 @@ const ColumnHeaderContent = React.memo(({ column, colSpan, state, dispatch, rowS
}}
>
<SizeMeasurer onSizeChange={onTitleSizeChange} style={{ display: 'inline-flex', alignItems: 'center' }}>
{column.title}
<p style={{ whiteSpace: 'pre' }}>{column.title}</p>
{column.helpText && !forcedTitleWidth && (
<Icon
name="question circle outline"
@@ -575,7 +575,7 @@ const createHeaders = (columns, columnDepth, dispatch) => {
const [column] = stack.splice(0, 1)
if (column.parent?.mergeHeader) {
continue; // eslint-disable-line
continue // eslint-disable-line
}
const currentDepth = column.depth ?? 0
@@ -692,7 +692,8 @@ const tableStateReducer = (...args) =>
state.expandedGroups.push(group)
}
},
}[type]())
// eslint-disable-next-line prettier/prettier
})[type]()
})
const injectParentPointers = columns => {
@@ -801,6 +802,7 @@ const extractColumnKeys = columns => {
}
const SortableTable = ({
tableId,
columns: pColumns,
title,
data,
@@ -921,7 +923,7 @@ const SortableTable = ({
}
const content = (
<table className={classNames.join(' ')} style={tableStyles}>
<table className={classNames.join(' ')} id={tableId} style={tableStyles}>
<thead style={{ position: 'sticky', top: 0, zIndex: 1 }}>{headers}</thead>
<tbody>
{sortedData.map(item => (
@@ -26,6 +26,8 @@ const CourseParticipationTable = ({ student, language, clearCourseStats, studyri
if (isStudyModuleCredit) {
icon = <Icon name="certificate" color="purple" />
} else if (c.credittypecode === 9) {
icon = <Icon name="file alternative outline" title="Credit transfer" color="green" />
} else if (passed) {
icon = <Icon name="check circle outline" color="green" />
} else {
@@ -91,10 +91,36 @@ const StudentSearch = ({
// so that the loading spinner doesn't go on top of the search box
return <div style={{ margin: 100 }} />
}
// let studentsSorted = students
if (students.length <= 0) {
return <div>No search results or search term is not accurate enough</div>
}
const studentsSorted = students.sort((a, b) => {
let aDateSplit = a.started.split('.')
let bDateSplit = b.started.split('.')
let aDate
let bDate
if (aDateSplit[0] === 'Unavailable') {
aDateSplit = null
} else {
aDate = new Date(aDateSplit[2], aDateSplit[1], aDateSplit[0])
}
if (bDateSplit[0] === 'Unavailable') {
bDateSplit = null
} else {
bDate = new Date(bDateSplit[2], bDateSplit[1], bDateSplit[0])
}
if (aDate && bDate) {
return bDate - aDate
}
if (aDate && !bDate) {
return -1
}
return 0
})
const columns = [
{
key: 'studentnumber',
@@ -224,14 +250,13 @@ const StudentSearch = ({
columns.splice(0, 0, ...nameColumns)
}
return (
<SortableTable
figure={false}
getRowKey={s => s.studentNumber}
tableProps={{ celled: false }}
columns={columns}
data={students.slice(0, 200)}
data={studentsSorted.slice(0, 200)}
chunkifyBy="studentNumber"
/>
)
@@ -1,87 +0,0 @@
import React from 'react'
const ReactHighcharts = require('react-highcharts')
const GaugeChart = ({ cypress, data, year, graduationAmount, studyprogramme }) => {
if (!data || !graduationAmount || !data.length) return null
const thresholdValues = studyprogramme.includes('KH') ? [36, 41] : [24, 27]
const getColor = data => {
if (!studyprogramme.includes('KH') && !studyprogramme.includes('MH')) return ['#1d44a1', '#EBECF0']
if (data <= thresholdValues[0]) return ['#90A959', '#EBECF0']
if (data > thresholdValues[0] && data <= thresholdValues[1]) return ['#FEE191', '#EBECF0']
return ['#FB6962', '#EBECF0']
}
if (!data) return null
const defaultConfig = {
chart: {
plotBackgroundColor: null,
plotBorderWidth: 0,
plotShadow: false,
width: 260,
height: 300,
},
title: {
text: `${data[0][1]} kk`,
align: 'center',
verticalAlign: 'middle',
y: 60,
},
subtitle: {
text: `${year}<br/> n = ${graduationAmount}`,
style: {
fontWeight: 'bold',
fontSize: '15px',
},
},
credits: {
text: '',
},
exporting: {
enabled: false,
},
accessibility: {
point: {
valueSuffix: '%',
},
},
plotOptions: {
pie: {
dataLabels: {
title: '{point.percentage:.1f}%',
distance: -50,
style: {
fontWeight: 'bold',
color: 'white',
},
},
startAngle: -90,
endAngle: 90,
center: ['50%', '75%'],
size: '100%',
colors: getColor(data[0][1]),
},
},
tooltip: {
enabled: false,
},
series: [
{
type: 'pie',
name: '',
innerSize: '40%',
data,
},
],
}
return (
<div data-cy={`Graph-${cypress}`}>
<ReactHighcharts config={defaultConfig} />
</div>
)
}
export default GaugeChart
@@ -5,8 +5,8 @@ import { useGetBasicStatsQuery, useGetCreditStatsQuery, useGetGraduationStatsQue
import LineGraph from './LineGraph'
import StackedBarChart from './StackedBarChart'
import BarChart from './BarChart'
// import GaugeChart from './GaugeChart'
import TimeBarChart from './TimeBarChart'
import MedianTimeBarChart from '../MedianTimeBarChart'
import BreakdownBarChart from '../BreakdownBarChart'
import DataTable from './DataTable'
import Toggle from '../Toggle'
import InfoBox from '../../Info/InfoBox'
@@ -17,13 +17,15 @@ import '../studyprogramme.css'
const isNewProgramme = code => code.includes('KH') || code.includes('MH') || /^(T)[0-9]{6}$/.test(code)
const Overview = ({ studyprogramme, specialGroups, setSpecialGroups, academicYear, setAcademicYear }) => {
const [showMeanTime, setShowMeanTime] = useState(false)
const [showMedian, setShowMedian] = useState(false)
const toolTips = InfotoolTips.Studyprogramme
const yearType = academicYear ? 'ACADEMIC_YEAR' : 'CALENDAR_YEAR'
const special = specialGroups ? 'SPECIAL_EXCLUDED' : 'SPECIAL_INCLUDED'
const basics = useGetBasicStatsQuery({ id: studyprogramme, yearType, specialGroups: special })
const credits = useGetCreditStatsQuery({ id: studyprogramme, yearType, specialGroups: special })
const graduations = useGetGraduationStatsQuery({ id: studyprogramme, yearType, specialGroups: special })
const doCombo = graduations?.data?.doCombo
const timesData = graduations?.data?.graduationTimes
const getDivider = (title, toolTipText) => (
<>
@@ -36,6 +38,34 @@ const Overview = ({ studyprogramme, specialGroups, setSpecialGroups, academicYea
</>
)
const displayMedian = () => (
<>
{doCombo && (
<MedianTimeBarChart
data={graduations?.data?.comboTimes?.medians}
goal={graduations?.data?.comboTimes?.goal}
title="Bachelor + Master studyright"
byStartYear={false}
/>
)}
<MedianTimeBarChart
data={timesData?.medians}
goal={graduations?.data.graduationTimes?.goal}
title={doCombo ? 'Master studyright' : ' '}
byStartYear={false}
/>
</>
)
const displayBreakdown = () => (
<>
{doCombo && (
<BreakdownBarChart data={graduations?.data?.comboTimes?.medians} title="Bachelor + Master studyright" />
)}
<BreakdownBarChart data={timesData?.medians} title={doCombo ? 'Master studyright' : ' '} />
</>
)
const isFetchingOrLoading =
basics.isLoading ||
credits.isLoading ||
@@ -127,37 +157,18 @@ const Overview = ({ studyprogramme, specialGroups, setSpecialGroups, academicYea
/>
</div>
{getDivider('Average graduation times', 'AverageGraduationTimes')}
<Toggle
cypress="GraduationTimeToggle"
firstLabel="Median time"
secondLabel="Mean time"
value={showMeanTime}
setValue={setShowMeanTime}
/>
<div className="section-container-centered">
{/* {graduations?.data?.years.map(year => (
<GaugeChart
cypress={`${year}-AverageGraduationTimes`}
key={year}
year={year}
data={
showMeanTime
? graduations?.data?.graduationMeanTime?.[year]
: graduations?.data?.graduationMedianTime?.[year]
}
graduationAmount={graduations?.data?.graduationAmounts?.[year]}
totalAmount={graduations?.data?.totalAmounts?.[year]}
studyprogramme={studyprogramme}
/>
))} */}
<TimeBarChart
data={
showMeanTime ? graduations?.data?.graduationTimes.means : graduations?.data?.graduationTimes.medians
}
goal={graduations?.data.graduationTimes.goal}
showMeanTime={showMeanTime}
<div className="toggle-container">
<Toggle
cypress="GraduationTimeToggle"
firstLabel="Breakdown"
secondLabel="Median time"
value={showMedian}
setValue={setShowMedian}
/>
</div>
<div className={`section-container${doCombo ? '' : '-centered'}`}>
{showMedian ? displayMedian() : displayBreakdown()}
</div>
{graduations?.data?.programmesBeforeOrAfterGraphStats && (
<>
{getDivider(
@@ -0,0 +1,101 @@
/* eslint-disable react/no-this-in-sfc */
import React from 'react'
import ReactHighcharts from 'react-highcharts'
const BarChart = ({ data, title, byStartYear = false }) => {
const statData = [
{ name: 'On time', color: '#90A959', data: [] },
{ name: 'Max. year overtime', color: '#FEE191', data: [] },
{ name: 'Overtime', color: '#FB6962', data: [] },
]
let categories = []
// eslint-disable-next-line no-restricted-syntax
for (const item of data) {
statData[0].data = [...statData[0].data, item.statistics.onTime]
statData[1].data = [...statData[1].data, item.statistics.yearOver]
statData[2].data = [...statData[2].data, item.statistics.wayOver]
categories = [...categories, item.name]
}
const getHeight = () => {
const t = data.length > 8 ? 80 : 110
return data.length * t + 100
}
const getTooltipText = (seriesName, amount) => {
return `<b>Graduated ${seriesName}</b>: ${amount} ${amount === 1 ? 'student' : 'students'}`
}
const config = {
chart: {
type: 'bar',
height: getHeight(),
margin: [70, 0],
},
title: { text: title },
series: statData,
tooltip: {
backgroundColor: 'white',
fontSize: '25px',
// outside: true,
// eslint-disable-next-line
formatter: function() {
return getTooltipText(this.series.name, this.y)
},
},
xAxis: {
type: 'category',
categories,
title: {
text: byStartYear ? 'Start year' : 'Graduation year',
align: 'high',
offset: 0,
rotation: 0,
y: -10,
},
},
yAxis: {
min: 0,
// max: maxValue,
title: { text: 'Number of students' },
labels: {
overflow: 'justify',
},
allowDecimals: false,
showFirstLabel: false,
},
credits: {
text: 'oodikone | TOSKA',
},
plotOptions: {
bar: {
dataLabels: {
enabled: true,
align: 'right',
color: '#424949',
style: {
textOutline: 'none',
},
formatter() {
// eslint-disable-next-line react/no-this-in-sfc
return this.y !== 0 ? this.y : ''
},
},
},
series: {
pointWidth: data.length > 8 ? 16 : 20,
groupPadding: 0.15,
},
},
}
return (
<div data-cy={`graduation-times-graph-breakdown${title.split(' ')[0]}`}>
<ReactHighcharts config={config} />
</div>
)
}
export default BarChart
@@ -1,7 +1,11 @@
import React, { useEffect, useState } from 'react'
import { connect, useSelector, useDispatch } from 'react-redux'
import { string, func } from 'prop-types'
import { Button, Label, Table, Icon } from 'semantic-ui-react'
import { Button, Label, Table, Icon, Dropdown, Container, Message, Form } from 'semantic-ui-react'
import {
useAddProgressCriteriaCourseMutation,
useAddProgressCriteriaCreditsMutation,
} from 'redux/programmeProgressCriteria'
import { getTextIn } from '../../../common'
import { GetMandatoryCourseLabels } from '../../../redux/mandatoryCourseLabels'
import {
@@ -14,17 +18,35 @@ import sendEvent from '../../../common/sendEvent'
const sendAnalytics = sendEvent.degreeCourses
const DegreeCourses = ({ studyProgramme, setExclusion, removeExclusion }) => {
const DegreeCourses = ({ studyProgramme, criteria, setCriteria, setExclusion, removeExclusion }) => {
const { language } = useLanguage()
const dispatch = useDispatch()
const mandatoryCourses = useSelector(({ populationMandatoryCourses }) => populationMandatoryCourses.data)
const [visible, setVisible] = useState({})
const [modules, setModules] = useState([])
const [creditsLimit1, setCreditsLimit1] = useState('')
const [creditsLimit2, setCreditsLimit2] = useState('')
const [creditsLimit3, setCreditsLimit3] = useState('')
const [addProgressCriteriaCourse, { data: courseData }] = useAddProgressCriteriaCourseMutation()
const [addProgressCriteriaCredits, { data: creditsData }] = useAddProgressCriteriaCreditsMutation()
useEffect(() => {
dispatch(getMandatoryCourses(studyProgramme, true))
}, [])
useEffect(() => {
if (courseData) {
setCriteria(courseData)
}
}, [courseData])
useEffect(() => {
if (creditsData) {
setCriteria(creditsData)
}
}, [creditsData])
useEffect(() => {
if (!mandatoryCourses || !mandatoryCourses.length) return
const modules = {}
@@ -51,6 +73,14 @@ const DegreeCourses = ({ studyProgramme, setExclusion, removeExclusion }) => {
}
}, [modules])
const getYear = criterionYear => {
let year = 'yearOne'
if (criterionYear !== 1) {
year = criterionYear === 2 ? 'yearTwo' : 'yearThree'
}
return year
}
const setExclusionButton = course => (
<Button
onClick={() => {
@@ -72,6 +102,63 @@ const DegreeCourses = ({ studyProgramme, setExclusion, removeExclusion }) => {
</Button>
)
const setCriteriaButton = (course, criterionYear) => (
<Button
onClick={() => {
const year = getYear(criterionYear)
const courses = criteria.courses ? [...criteria.courses[year], course.code] : [course.code]
addProgressCriteriaCourse({ programmeCode: studyProgramme, courses, year: criterionYear })
}}
>
{`Set criterion for year ${criterionYear}`}
</Button>
)
const deleteCriteriaButton = (course, criterionYear) => (
<Button
onClick={() => {
const year = getYear(criterionYear)
const filteredCourses = criteria?.courses[year]?.filter(courseCode => courseCode !== course.code)
addProgressCriteriaCourse({ programmeCode: studyProgramme, courses: filteredCourses, year: criterionYear })
}}
>
{`Remove criterion for year ${criterionYear}`}
</Button>
)
const labelDropdown = course => {
return (
<Dropdown text="Modify labels" className="link item">
<Dropdown.Menu>
{course.visible.visibility ? setExclusionButton(course) : deleteButton(course)}
<Dropdown.Divider />
{criteria?.courses?.yearOne?.includes(course.code)
? deleteCriteriaButton(course, 1)
: setCriteriaButton(course, 1)}
<Dropdown.Divider />
{criteria?.courses?.yearTwo?.includes(course.code)
? deleteCriteriaButton(course, 2)
: setCriteriaButton(course, 2)}
<Dropdown.Divider />
{criteria?.courses?.yearThree?.includes(course.code)
? deleteCriteriaButton(course, 3)
: setCriteriaButton(course, 3)}
</Dropdown.Menu>
</Dropdown>
)
}
const setCreditsLimitCriteria = () => {
const credits = {
year1: creditsLimit1 === '' ? criteria.credits.yearOne : creditsLimit1,
year2: creditsLimit2 === '' ? criteria.credits.yearTwo : creditsLimit2,
year3: creditsLimit3 === '' ? criteria.credits.yearThree : creditsLimit3,
}
addProgressCriteriaCredits({ programmeCode: studyProgramme, credits })
setCreditsLimit1('')
setCreditsLimit2('')
setCreditsLimit3('')
}
const excludeAll = code => {
const module = modules.find(({ module }) => module === code)
setExclusion(
@@ -142,56 +229,111 @@ const DegreeCourses = ({ studyProgramme, setExclusion, removeExclusion }) => {
}
return (
<Table>
<Table.Header>
<Table.Row>
<Table.HeaderCell>Name</Table.HeaderCell>
<Table.HeaderCell>Code</Table.HeaderCell>
<Table.HeaderCell>Label</Table.HeaderCell>
<Table.HeaderCell>Set visibility</Table.HeaderCell>
</Table.Row>
</Table.Header>
<Table.Body>
{modules.map(({ module, courses }) => (
<>
<Table.Row key={module}>
<Table.Cell style={{ cursor: 'pointer' }} onClick={() => toggleVisible(module)}>
<Icon name={visible[module] ? 'angle down' : 'angle right'} />
<b>{courses[0] && courses[0].label_name ? getTextIn(courses[0].label_name, language) : module}</b>
</Table.Cell>
<Table.Cell>{module}</Table.Cell>
<Table.Cell>
<Label
content={calculateModuleVisibility(module)}
color={moduleVisibilityColor(calculateModuleVisibility(module))}
<>
{studyProgramme.includes('KH') && (
<>
<Message style={{ fontSize: '16px' }}>
<Message.Header>Change visibility of degree courses and select criteria for academic years</Message.Header>
<p>
Here you can change visibility of degree courses as and set course and credits criteria. Each academic
year have their own course and credits criteria. The progress of the students by these criteria will be
shown in class statistics view. These statistics will be available at the latest at Friday on 10.2..
</p>
</Message>
<Container>
<h5>Credit criteria</h5>
<Form>
<Form.Group widths="equal">
<Form.Input
type="number"
fluid
label={`First year (12 months) last set: ${criteria?.credits?.yearOne}`}
onChange={e => setCreditsLimit1(e.target.value)}
/>
</Table.Cell>
<Table.Cell>
{calculateModuleVisibility(module) === 'hidden' ? showAllButton(module) : hideAllButton(module)}
</Table.Cell>
</Table.Row>
{visible[module] &&
courses
.sort((a, b) => a.code.localeCompare(b.code))
.map(course => (
<Table.Row key={`${module}/${course.code}`}>
<Table.Cell>{getTextIn(course.name, language)}</Table.Cell>
<Table.Cell>{course.code}</Table.Cell>
<Table.Cell>
<Label
content={course.visible.visibility ? 'visible' : 'hidden'}
color={course.visible.visibility ? 'green' : 'red'}
/>
</Table.Cell>
<Table.Cell>
{course.visible.visibility ? setExclusionButton(course) : deleteButton(course)}
</Table.Cell>
</Table.Row>
))}
</>
))}
</Table.Body>
</Table>
<Form.Input
type="number"
fluid
label={`Second year (24 months) last set: ${criteria?.credits?.yearTwo}`}
onChange={e => setCreditsLimit2(e.target.value)}
/>
<Form.Input
type="number"
fluid
label={`Third year (36 months) last set: ${criteria?.credits?.yearThree}`}
onChange={e => setCreditsLimit3(e.target.value)}
/>
</Form.Group>
<Form.Button content="Save credit changes" onClick={setCreditsLimitCriteria} />
</Form>
</Container>
</>
)}
<Table>
<Table.Header>
<Table.Row key="HeaderRow">
<Table.HeaderCell>Name</Table.HeaderCell>
<Table.HeaderCell>Code</Table.HeaderCell>
<Table.HeaderCell>Visibility Label</Table.HeaderCell>
{studyProgramme.includes('KH') && <Table.HeaderCell>Criterion Labels</Table.HeaderCell>}
<Table.HeaderCell>{studyProgramme.includes('KH') ? 'Set Labels' : 'Set Visibility'}</Table.HeaderCell>
</Table.Row>
</Table.Header>
<Table.Body>
{modules.map(({ module, courses }) => (
<React.Fragment key={`fragment-${module}`}>
<Table.Row key={module}>
<Table.Cell style={{ cursor: 'pointer' }} onClick={() => toggleVisible(module)}>
<Icon name={visible[module] ? 'angle down' : 'angle right'} />
<b>{courses[0] && courses[0].label_name ? getTextIn(courses[0].label_name, language) : module}</b>
</Table.Cell>
<Table.Cell>{module}</Table.Cell>
<Table.Cell>
<Label
content={calculateModuleVisibility(module)}
color={moduleVisibilityColor(calculateModuleVisibility(module))}
/>
</Table.Cell>
{studyProgramme.includes('KH') && <Table.Cell />}
<Table.Cell>
{calculateModuleVisibility(module) === 'hidden' ? showAllButton(module) : hideAllButton(module)}
</Table.Cell>
</Table.Row>
{visible[module] &&
courses
.sort((a, b) => a.code.localeCompare(b.code))
.map(course => (
<Table.Row key={`${module}/${course.code}`}>
<Table.Cell>{getTextIn(course.name, language)}</Table.Cell>
<Table.Cell>{course.code}</Table.Cell>
<Table.Cell>
<Label
content={course.visible.visibility ? 'visible' : 'hidden'}
color={course.visible.visibility ? 'green' : 'red'}
/>
</Table.Cell>
{studyProgramme.includes('KH') && (
<Table.Cell>
{criteria?.courses?.yearOne?.includes(course.code) && <Label content="year 1" color="blue" />}
{criteria?.courses?.yearTwo?.includes(course.code) && <Label content="year 2" color="blue" />}
{criteria?.courses?.yearThree?.includes(course.code) && (
<Label content="year 3" color="blue" />
)}
</Table.Cell>
)}
{studyProgramme.includes('KH') ? (
<Table.Cell>{labelDropdown(course)}</Table.Cell>
) : (
<Table.Cell>
{course.visible.visibility ? setExclusionButton(course) : deleteButton(course)}
</Table.Cell>
)}
</Table.Row>
))}
</React.Fragment>
))}
</Table.Body>
</Table>
</>
)
}
@@ -2,15 +2,24 @@
import React from 'react'
import ReactHighcharts from 'react-highcharts'
const TimeBarChart = ({ data, goal, showMeanTime }) => {
const TimeBarChart = ({ data, goal, title, byStartYear }) => {
if (!data) {
return <></>
}
const maxValue = data.reduce((max, { y }) => {
return y > max ? y : max
}, goal * 2)
const getDataLabel = amount => {
// if (label === 'Start year') {
// return `${amount} graduated (${getPercentage(amount, category)} % of class)`
// }
const getPercentage = (amount, classSize) => {
const percent = Math.round((amount / classSize) * 100 * 10) / 10
return Number.isNaN(percent) ? 0 : percent
}
const getDataLabel = (amount, classSize = undefined) => {
if (byStartYear) {
return `${amount} graduated (${getPercentage(amount, classSize)} % of class)`
}
return `${amount} graduated`
}
@@ -19,17 +28,18 @@ const TimeBarChart = ({ data, goal, showMeanTime }) => {
return data.length * t + 100
}
const getTooltipText = (amount, y, year) => {
const sortingText = `<b>${amount} students graduated in year ${year}</b>`
const timeText = `<br /><p>${sortingText}, <br /><b>${
showMeanTime ? 'mean' : 'median'
} study time: ${y} months</p></b>`
// const statisticsText = `<br /><p>${statistics.onTime} graduated on time</p><br />
// <p>${statistics.yearOver} graduated max year overtime</p>
// <br /><p>${statistics.wayOver} graduated over year late</p>`
const getTooltipText = (amount, y, year, statistics, classSize = undefined) => {
const sortingText = byStartYear
? `<b>From class of ${year}, ${amount}/${classSize} students have graduated</b>`
: `<b>${amount} students graduated in year ${year}</b>`
// return `${timeText}${statisticsText}`
return timeText
const timeText = `<br /><p>${sortingText}, <br /><b>median study time: ${y} months</p></b>`
const statisticsText = `<br /><p>${statistics.onTime} graduated on time</p><br />
<p>${statistics.yearOver} graduated max year overtime</p>
<br /><p>${statistics.wayOver} graduated over year late</p>`
return `${timeText}${statisticsText}`
}
const config = {
@@ -39,13 +49,13 @@ const TimeBarChart = ({ data, goal, showMeanTime }) => {
margin: [70, 0],
height: getHeight(),
},
title: { text: ' ' },
title: { text: title },
tooltip: {
backgroundColor: 'white',
fontSize: '25px',
// eslint-disable-next-line
formatter: function() {
return getTooltipText(this.point.amount, this.y, this.point.name) // , this.point.statistics)
return getTooltipText(this.point.amount, this.y, this.point.name, this.point.statistics, this.point?.classSize)
},
},
plotOptions: {
@@ -70,7 +80,7 @@ const TimeBarChart = ({ data, goal, showMeanTime }) => {
},
// eslint-disable-next-line
formatter: function () {
return getDataLabel(this.point.amount)
return getDataLabel(this.point.amount, this.point?.classSize)
},
},
],
@@ -93,7 +103,7 @@ const TimeBarChart = ({ data, goal, showMeanTime }) => {
xAxis: {
type: 'category',
title: {
text: 'Graduation year',
text: byStartYear ? 'Start year' : 'Graduation year',
align: 'high',
offset: 0,
rotation: 0,
@@ -130,7 +140,7 @@ const TimeBarChart = ({ data, goal, showMeanTime }) => {
}
return (
<div data-cy="graduation-times-graph">
<div data-cy={`graduation-times-graph${title.split(' ')[0]}`}>
<ReactHighcharts config={config} />
</div>
)
@@ -5,7 +5,7 @@ const ReactHighcharts = require('react-highcharts')
const getColors = track => {
if (!track) return ['#e66067', '#f57368', '#fb8c6e', '#fba678', '#dbda7d', '#9ec27c', '#60a866', '#008c59']
if (track.includes('KH'))
if (track.includes('KH') || track === 'MH90_001')
return ['#e66067', '#f57368', '#fb8c6e', '#fba678', '#dbda7d', '#9ec27c', '#60a866', '#008c59']
return ['#f57368', '#fb8c6e', '#fba678', '#dbda7d', '#9ec27c', '#60a866', '#008c59']
}
@@ -1,87 +0,0 @@
import React from 'react'
const ReactHighcharts = require('react-highcharts')
const GaugeChart = ({ cypress, data, year, graduationAmount, totalAmount, studyprogramme }) => {
if (!data || !graduationAmount || !data.length) return null
const thresholdValues = studyprogramme.includes('KH') ? [36, 41] : [24, 27]
const getColor = data => {
if (!studyprogramme.includes('KH') && !studyprogramme.includes('MH')) return ['#1d44a1', '#EBECF0']
if (data <= thresholdValues[0]) return ['#90A959', '#EBECF0']
if (data > thresholdValues[0] && data <= thresholdValues[1]) return ['#FEE191', '#EBECF0']
return ['#FB6962', '#EBECF0']
}
if (!data) return null
const defaultConfig = {
chart: {
plotBackgroundColor: null,
plotBorderWidth: 0,
plotShadow: false,
width: 260,
height: 300,
},
title: {
text: `${data[0][1]} kk`,
align: 'center',
verticalAlign: 'middle',
y: 60,
},
subtitle: {
text: `${year}<br/> n = ${graduationAmount} / ${totalAmount}`,
style: {
fontWeight: 'bold',
fontSize: '15px',
},
},
credits: {
text: '',
},
exporting: {
enabled: false,
},
accessibility: {
point: {
valueSuffix: '%',
},
},
plotOptions: {
pie: {
dataLabels: {
title: '{point.percentage:.1f}%',
distance: -50,
style: {
fontWeight: 'bold',
color: 'white',
},
},
startAngle: -90,
endAngle: 90,
center: ['50%', '75%'],
size: '100%',
colors: getColor(data[0][1]),
},
},
tooltip: {
enabled: false,
},
series: [
{
type: 'pie',
name: '',
innerSize: '40%',
data,
},
],
}
return (
<div data-cy={`Graph-${cypress}`}>
<ReactHighcharts config={defaultConfig} />
</div>
)
}
export default GaugeChart
@@ -30,7 +30,7 @@ const TotalPopulationLink = ({ studytrack, studyprogramme, years }) => {
const months = getMonths(Math.min(...years.map(year => Number(year))))
const href = studytrack
? `/populations?months=${months}&semesters=FALL&semesters=` +
`SPRING&studyRights=%7B"programme"%3A"${studyprogramme}"%2C"studyTrack"%3A"${studytrack}"%7D&year=${years[0].value}&years=${yearsString}`
`SPRING&studyRights=%7B"programme"%3A"${studyprogramme}"%2C"studyTrack"%3A"${studytrack}"%7D&year=All&years=${yearsString}`
: `/populations?months=${months}&semesters=FALL&semesters=` +
`SPRING&studyRights=%7B"programme"%3A"${studyprogramme}"%7D&year=All&years=${yearsString}`
return (
@@ -131,6 +131,7 @@ const sortMainDataByYear = data => {
const copy = [...arrays]
const sortedYear = copy.sort((a, b) => {
if (a[0] === 'Total') return 1
if (b[0] === 'Total') return -1
if (a[0] < b[0]) return 1
if (a[0] > b[0]) return -1
return 0
@@ -187,28 +188,29 @@ const StudytrackDataTable = ({
))}
</Table.Row>
</Table.Header>
<Table.Body>
{singleTrack
? sortedTrackStats.map(row =>
getSingleTrackRow({ row, studyprogramme, code: singleTrack, showPercentages, years, calendarYears })
)
: sortedMainStats?.map(yearlyData =>
yearlyData.map(row =>
getRow({
yearlyData,
row,
studyprogramme,
show,
setShow,
studytracks,
showPercentages,
years,
calendarYears,
})
{(sortedMainStats || sortedTrackStats) && (
<Table.Body>
{singleTrack
? sortedTrackStats.map(row =>
getSingleTrackRow({ row, studyprogramme, code: singleTrack, showPercentages, years, calendarYears })
)
)}
</Table.Body>
: sortedMainStats?.map(yearlyData =>
yearlyData.map(row =>
getRow({
yearlyData,
row,
studyprogramme,
show,
setShow,
studytracks,
showPercentages,
years,
calendarYears,
})
)
)}
</Table.Body>
)}
</Table>
</div>
)

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