Compare commits

...
762 Commits
Author SHA1 Message Date
Laura Sinikallio 3698b8d38b Update test 2022-12-11 19:05:14 +02:00
Laura Sinikallio 850a429165 Graduation times: unify faculty vs. studyprogramme 2022-12-11 18:48:31 +02:00
Miko Keskimäki 9e1c0751d3 Log user's iam groups when login fails 2022-12-09 13:15:07 +02:00
outisa 414495590f Merge branch 'master' of github.com:UniversityOfHelsinkiCS/oodikone 2022-12-08 19:16:49 +02:00
outisa 915128dce6 [Faculty] Add faculty fixes and updates 2022-12-08 19:16:40 +02:00
Laura Sinikallio 8cb8e3c595 Add course code to CSV 2022-12-08 18:43:28 +02:00
Laura Sinikallio 432fd7dbc0 Discount statutory absences from studyprogramme graduation times 2022-12-07 19:28:11 +02:00
outisa b07070dc7f [faculty - programme progress] Fix stats 2022-12-07 18:13:24 +02:00
outisa 2ef6c2a47f [merge] overlaping changes in faculty.js 2022-12-07 18:11:39 +02:00
outisa 67de743838 Fetch studyrights 2022-12-07 18:02:46 +02:00
Laura Sinikallio 37f7e06ba5 Refactor common code location 2022-12-07 15:01:34 +02:00
Laura Sinikallio 7e798821c8 Test commit 2022-12-07 12:00:46 +02:00
Laura Sinikallio 04692463f0 Add pre-commit check for 'only' tests 2022-12-07 11:58:27 +02:00
Laura Sinikallio c3fecddc61 Aaaaand remove 'only' from test 2022-12-07 11:38:29 +02:00
Laura Sinikallio bed3e64e1f Expand tests 2022-12-07 11:37:11 +02:00
Laura Sinikallio 98cb0f5e3e Studyprogramme: display grad times breakdown stats 2022-12-07 10:56:57 +02:00
outisa d062788864 [faculty] Modify laastarifix with transfers 2022-12-07 10:51:39 +02:00
Laura Sinikallio f2b19d5f74 Studyprogramme: display separate Ms and Bc+Ms grad times for master programmes 2022-12-07 10:47:57 +02:00
outisa e624152e3e [Faculty - progress] Refine stats for H90 2022-12-06 13:33:10 +02:00
outisa d296508eb5 Remove statistics until field from tests 2022-12-06 13:30:48 +02:00
outisa 88b00beb5d [faculty] Add laastarifix facultyBasics transfers 2022-12-06 11:55:42 +02:00
outisa a9057b949a [Class stats] Remove adv. settings filter statistics until 2022-12-05 15:29:30 +02:00
Laura Sinikallio d1744d9fb8 More studyprogramme grad times rewriting 2022-12-03 15:36:06 +02:00
outisa 3e83b800d7 [Course statistics] Fix bug 2022-12-02 15:47:23 +02:00
Laura Sinikallio 49891f3779 Programme overview - switch times to bar display 2022-11-27 16:18:13 +02:00
outisa 0aaa745763 [Class statistics] Fix graduation time in csv to show correctly 2022-11-24 19:35:33 +02:00
outisa e4edf31057 [Class statistics] Add phonenumber into excel 2022-11-24 19:32:33 +02:00
outisa 8c8b8ccb1d Add phone number column into student table 2022-11-24 18:23:13 +02:00
outisa dc339896a1 [Updater] Add phone number column to student table 2022-11-24 17:16:36 +02:00
Laura Sinikallio da0f76e2a8 Start transition from gauges to bars in studyprogramme grad times 2022-11-24 16:34:41 +02:00
outisa 0fa9d8a1ca [Open uni] Change icon color value to accepted one 2022-11-24 16:16:41 +02:00
outisa 95066dde6e [Open uni] Betterments for the view 2022-11-24 16:13:20 +02:00
outisa e17ff572bf [Open uni] Create extra columns shown only in csv 2022-11-22 18:04:13 +02:00
outisa 4eb748f76d Fix hook order problem 2022-11-22 13:13:29 +02:00
outisa e6147effdf [Open uni] Allow adding the course codes with different separators 2022-11-22 12:00:39 +02:00
outisa 7e333668c5 Refactor components 2022-11-21 18:27:25 +02:00
outisa 45fb6659a9 [Open uni] Possible fix for 'recursive fn exceeding maximum stack size' 2022-11-21 17:50:53 +02:00
outisa 0f73e5025b [Open uni] Add small modifications 2022-11-18 17:44:06 +02:00
outisa 69f0b4cb75 [Updater] Map unknown marketing allowance as null 2022-11-18 17:18:03 +02:00
Laura Sinikallio 2a96a72151 Fix faculty credit sorting bug 2022-11-16 17:16:52 +02:00
outisa 30c2cfbfa0 [Open uni] Add search into url 2022-11-16 17:15:15 +02:00
outisa e8c3a77ba6 [Open uni] Fix indentation 2022-11-16 14:13:58 +02:00
outisa e15894c2f5 [Course stats] Fix bug faculties not shown in credits gains 2022-11-16 14:12:26 +02:00
outisa 566fb861b0 [Open uni] Add UI betterments 2022-11-15 15:23:40 +02:00
outisa 80873a3227 [Open uni] Add possibility to save, update and delete searches 2022-11-12 17:43:33 +02:00
outisa 809c63ada9 [Open uni] Send usage data to grafana 2022-11-11 12:38:24 +02:00
outisa f0bf56f6c8 [Open uni] Add new columns 2022-11-10 18:01:04 +02:00
outisa e4214ef871 [Updater] Add open uni dissemination of information allowed field for student 2022-11-10 13:28:36 +02:00
outisa 88e6939347 [Open uni] Add possibility to choose dates 2022-11-10 00:16:25 +02:00
outisa dc73aabbd3 [Open uni] Add small adjustments 2022-11-04 12:49:09 +02:00
outisa 1dd41b0b9b Fix cypress tests 2022-11-03 14:42:35 +02:00
Laura Sinikallio 328437ced2 Remove 'only' from tests 2022-11-03 13:42:06 +02:00
Laura Sinikallio f79bd9f2f9 Add faculty graduation times tests 2022-11-03 13:40:01 +02:00
outisa 84bc0434fd Remove role if access no longer exists 2022-11-03 13:26:29 +02:00
outisa 96045d0e17 [Open Uni] Take course codes starting with AY into account 2022-11-03 13:06:56 +02:00
outisa 129c608108 [Navigation bar] Fix typo in role check 2022-11-02 15:45:23 +02:00
outisa d73aa95684 Add new iam to relevant iams 2022-11-02 09:22:45 +02:00
Laura Sinikallio fe9e9d2527 Fix updater looking at ALL term registrations of students-to-update instead of the individual student's registrations 2022-11-01 13:16:17 +02:00
outisa e5a52f3db6 Fix test 2022-11-01 12:37:47 +02:00
outisa 84bb527e5e Modify navigation bar visibility 2022-11-01 11:44:02 +02:00
outisa bd7bf5c712 Cypress please 2022-10-31 19:00:06 +02:00
outisa b6d3106ee9 Fix months calculation 2022-10-31 18:34:06 +02:00
outisa d7bc7d5d2e [faculty] Fix dynamic months limit for master 2022-10-31 18:30:04 +02:00
outisa 8eb3d58658 Change import for uuid 2022-10-31 16:27:58 +02:00
outisa 8c85467bcd [OpenUni] Add studyright filtering betterments 2022-10-31 16:18:02 +02:00
outisa f59f41cc3e Fix (tried at least) fetching courses when list is empty 2022-10-31 15:08:34 +02:00
outisa 678c90cbda [CSV] Change button's place and remove 'not passed' 2022-10-31 14:04:34 +02:00
outisa 7cee60596c Try to fix cypress tests 2022-10-28 18:20:57 +03:00
outisa 979d24d375 Fix some unique key errors 2022-10-28 14:43:24 +03:00
outisa cdf912db9b [Open uni] Add small modifications 2022-10-28 13:11:30 +03:00
outisa 51867d840b [Open uni] Create open uni logic 2022-10-27 22:22:18 +03:00
outisa 51137fa66f Add group for open uni people 2022-10-26 17:40:48 +03:00
outisa a394928e83 [Updater] Add secondary email to mapper to acually get it 2022-10-26 17:39:05 +03:00
Laura Sinikallio c7edc70ccb Replace to-be-deprecated commands 2022-10-25 17:30:36 +03:00
Laura Sinikallio 2aa2c9419c Update to-be-deprecated step 2022-10-25 17:08:28 +03:00
outisa 5b9728dc1d [Faculty] Fix broken link in students stats table 2022-10-21 15:02:20 +03:00
outisa 8c73a32631 Add secondary email to models 2022-10-20 22:00:47 +03:00
outisa 459c214164 [Updater] Add secondary email column for the student 2022-10-20 21:48:08 +03:00
Laura Sinikallio 96235ed242 Update info text 2022-10-20 14:57:28 +03:00
Laura Sinikallio 45ce3a70c9 Class statistics: hide buggy options, improve word choices 2022-10-20 14:42:52 +03:00
Laura Sinikallio c101efb961 Courses of class: fold/unfold all affects all tabs 2022-10-20 14:28:53 +03:00
Laura Sinikallio 0d47ad6cc7 Courses of class: keep module lists open when changing tabs 2022-10-20 13:56:08 +03:00
Luukkainen 9a338b79d5 Merge branch 'master' of github.com:UniversityOfHelsinkiCS/oodikone 2022-10-20 13:43:42 +03:00
Luukkainen 0087226de1 lintignore 2022-10-20 13:43:19 +03:00
Laura Sinikallio 8276359359 Remove duplicate pass/fail tab from courses of class 2022-10-20 12:47:44 +03:00
Laura Sinikallio 53214e822e Slight layout changes to class statistics view 2022-10-19 18:40:51 +03:00
Laura Sinikallio 20c24f2722 Update test 2022-10-19 18:30:08 +03:00
Laura Sinikallio 9b88f6c008 Update tests 2022-10-19 18:03:21 +03:00
Laura Sinikallio c1bab2fb63 Population statistics: move key info to header 2022-10-19 17:42:00 +03:00
outisa e6c23839e3 [Faculty] Change codes to progId 2022-10-18 20:02:42 +03:00
Laura Sinikallio 909448bad1 Add programme ids to before/after tables 2022-10-17 16:14:51 +03:00
Laura Sinikallio 565b6eb11c Restore faculty grad times redis 2022-10-17 15:48:20 +03:00
Laura Sinikallio bb69fcf908 Move studyprogramme id mapping to backend 2022-10-17 15:46:37 +03:00
Laura Sinikallio 7e66e7fafc Grad times: Move programme name mapping to backend 2022-10-17 14:37:22 +03:00
Laura Sinikallio 007644ddfc get programmes for faculty now contains 'letter ids' 2022-10-17 12:41:50 +03:00
Laura Sinikallio 153bacf3d0 Update test 2022-10-17 11:45:51 +03:00
outisa 3a5698011b [Population statistics] Fix course code visibility problem in excel file 2022-10-17 11:04:37 +03:00
Laura Sinikallio 95c37e5e8f Population statistics: switch to using 'class' instead of 'population' 2022-10-17 11:02:08 +03:00
Laura Sinikallio 633c08ee25 Add mention about filter name change 2022-10-12 18:29:41 +03:00
Laura Sinikallio 54e6f4fec9 Rename cherrypicker filter as student number filter 2022-10-12 15:28:54 +03:00
Laura Sinikallio ef2db12e54 Filter out duplicate course instances 2022-10-12 13:46:04 +03:00
Laura Sinikallio b4027560cc Use letter programme codes in faculty graduation times 2022-10-10 16:52:56 +03:00
Miko Keskimäki 6eb4e89199 Update minor package versions 2022-10-10 09:27:35 +03:00
outisa b30def95bc [faculty] Add dynamical change to credit limits 2022-10-07 20:52:12 +03:00
outisa 423c286071 [Cypress] Remove programme filter tests for population statistics view 2022-10-07 15:50:14 +03:00
outisa ddbbaab650 [Population Statistics] Remove Programme filter from the view 2022-10-07 15:26:00 +03:00
outisa 58827652f5 [Population Statistics] Add dynamical month change to Credits gained view 2022-10-07 15:16:59 +03:00
outisa 763f8765c9 [Faculty]Fix typos 2022-10-06 19:43:27 +03:00
outisa 75f83c70dc [Population statistics] Modify gained credits view 2022-10-06 19:40:24 +03:00
Laura Sinikallio d84aaab2af Add IDs for programmes with no jory-IAM 2022-10-06 17:32:34 +03:00
Laura Sinikallio 69a4eb113c Add programme 'letter id' to studyprogramme selector and overview title 2022-10-06 17:19:00 +03:00
Laura Sinikallio 8c5a898309 Switch grad times default view mode 2022-10-06 13:32:31 +03:00
Laura Sinikallio 667a0ef770 Add goal+1y line to grad time graph 2022-10-06 13:19:30 +03:00
outisa 8e97596fc0 [faculty] Fix population link visibility 2022-10-04 13:52:53 +03:00
outisa 3c7259f1cb [Facultt] Modify info text 2022-10-04 12:01:05 +03:00
outisa 17759294fb [Faculty] Add progress tab for faculty public view 2022-10-04 11:05:00 +03:00
outisa d4925669e8 Hide population link for user with no rights 2022-10-03 20:16:07 +03:00
Laura Sinikallio 971e7a3d1b Merge branch 'master' of github.com:UniversityOfHelsinkiCS/oodikone 2022-10-03 15:38:55 +03:00
Laura Sinikallio 86418abd05 Include graduation times to cronjob 2022-10-03 15:24:22 +03:00
outisa 9331609afd Fix another test 2022-09-30 17:31:27 +03:00
outisa 9093d701e4 Fix cypress test 2022-09-30 17:07:55 +03:00
outisa 3ad51b7408 Merge branch 'master' of github.com:UniversityOfHelsinkiCS/oodikone 2022-09-30 16:45:39 +03:00
outisa ebc0cb487a [Faculty] Add progress tab to redis 2022-09-30 16:45:08 +03:00
outisa 2771eeb3d1 [Faculty] Update infoboxes betterment 2022-09-30 16:38:06 +03:00
outisa 4ca31490ae [Faculty] Update infoboxes 2022-09-30 16:36:24 +03:00
Laura Sinikallio 94ab15bd96 Graduation times to Redis 2022-09-30 14:01:09 +03:00
Laura Sinikallio 686cc31e49 Fix test 2022-09-30 10:08:20 +03:00
Laura Sinikallio 6e1976d1e2 Faculty graduations time tab visible for non-admins 2022-09-30 09:55:27 +03:00
outisa 0408a42153 Fix cypress tests 2022-09-29 22:45:21 +03:00
outisa ce2e90c476 [Faculty] Make students stats table more readable 2022-09-29 22:25:00 +03:00
outisa 79eec6e595 Hide % as default in students stats tables 2022-09-29 22:15:47 +03:00
outisa c6812ecc99 [Faculty-Progress tab] Add toggles and hide % as default 2022-09-29 21:45:26 +03:00
Laura Sinikallio 208cf732ac Handle exceptional goal times in faculty 2022-09-29 19:47:49 +03:00
Laura Sinikallio cbd46126cd Set exception goal times for veterinary 2022-09-29 12:46:08 +03:00
Laura Sinikallio ee040806ae Add old programmes to graduation 'only bachelor' filtering, bar text edits 2022-09-29 10:59:48 +03:00
Laura Sinikallio 13d16c3f14 Edit graduations bar text 2022-09-29 10:36:56 +03:00
Laura Sinikallio 29cd40bfbe Clean up inline styling 2022-09-29 09:03:43 +03:00
outisa 27249697bd Fix exporting excel bug? 2022-09-28 21:39:38 +03:00
Laura Sinikallio 3ae80fc123 Gradutations times: form bc + ms combo's class size from bachelors 2022-09-27 16:37:41 +03:00
Laura Sinikallio 7f5a66cc63 Smite a backend monster 2022-09-27 13:37:40 +03:00
Laura Sinikallio a9710ca5ec Merge branch 'master' of github.com:UniversityOfHelsinkiCS/oodikone 2022-09-27 12:53:51 +03:00
Laura Sinikallio ec0239f985 Grad times: sort new programmes before old, tweak chart sizing 2022-09-27 12:53:20 +03:00
outisa a4ceae10e5 [Faculty - pogress tab] Separate masters/bachelor-masters 2022-09-27 12:22:05 +03:00
Laura Sinikallio 0409df0ee4 Graduations chart height set dynamically 2022-09-27 12:05:05 +03:00
outisa b3cc01e9e0 Merge branch 'master' of github.com:UniversityOfHelsinkiCS/oodikone 2022-09-23 15:17:38 +03:00
outisa e7c3c4657d [Faculty] Add improvements to programmes and student poulations -tag 2022-09-23 15:16:58 +03:00
Laura Sinikallio 530254cfd5 Cleaner bar label font, add info 2022-09-23 13:06:54 +03:00
Laura Sinikallio c5ad9309cd Show percentage of graduated, refactor bar label formatting 2022-09-23 12:41:15 +03:00
outisa beaca19d07 [Faculty] Add data fetching for students of the faculty view 2022-09-22 20:10:06 +03:00
outisa 16bfbb4b62 Fix cypress test to correspond changes 2022-09-22 13:40:19 +03:00
outisa 3af18f2408 Merge branch 'master' of github.com:UniversityOfHelsinkiCS/oodikone 2022-09-22 13:22:51 +03:00
outisa 9d08501242 [Faculty/Study Programme] Fix upper credit limits 2022-09-22 13:06:11 +03:00
Laura Sinikallio d8b8e0ef89 Graduation times - show class size 2022-09-21 17:02:30 +03:00
Laura Sinikallio 1626f76e3a Open programme charts update on toggling 2022-09-20 18:28:10 +03:00
Laura Sinikallio d597b9e67f Show how many graduated on time for programmes 2022-09-20 17:58:51 +03:00
Laura Sinikallio e1dbcd0e7e Show how many graduated on time for faculty 2022-09-20 17:33:51 +03:00
outisa 70b33a3ae4 Merge branch 'master' of github.com:UniversityOfHelsinkiCS/oodikone 2022-09-20 16:26:56 +03:00
outisa 4834515b4c Fix studyprogramme update problem 2022-09-20 16:26:07 +03:00
Laura Sinikallio fa0bf87307 Graduation times: show programme names, improve tooltip 2022-09-20 16:22:11 +03:00
outisa 4b9fe9d6a0 Fix cypress test values 2022-09-20 13:59:36 +03:00
outisa a674c4257d Faculty/Study programme unify student progress views 2022-09-20 13:06:05 +03:00
outisa 992d1c13dc [Faculty] Fix faculty progress chart bug for missing data 2022-09-20 09:14:07 +03:00
outisa 7024345088 [Faculty] First betterments for the math 2022-09-19 23:43:58 +03:00
outisa 9560f84395 [Faculty] Add faculty progress for students 2022-09-19 22:53:31 +03:00
Laura Sinikallio 9f9ab3ce09 Faculty - Show programme level graduation times 2022-09-19 19:11:17 +03:00
Laura Sinikallio 4ac29fcfe8 Add faculty grad times info text 2022-09-19 15:17:25 +03:00
Laura Sinikallio 174a3069fe Ordered faculty and studyprogramme lists 2022-09-19 14:31:04 +03:00
Miko Keskimäki fc6b77e32b Log when using iam rights to access view for Grafana 2022-09-16 09:55:21 +03:00
Laura Sinikallio 6cfb89336f Show graduation times by start year 2022-09-15 19:53:40 +03:00
Laura Sinikallio 635409442c Remove old graduation time display code 2022-09-15 11:28:39 +03:00
outisa 2ffd2db995 Merge branch 'master' of github.com:UniversityOfHelsinkiCS/oodikone 2022-09-15 11:08:11 +03:00
outisa 431c2292cb Fix faculty access 2022-09-15 11:02:52 +03:00
Laura Sinikallio 1a4a1a1f1c Redo faculty average graduation times display 2022-09-14 15:30:43 +03:00
outisa fe1b5ac4c6 Merge branch 'master' of github.com:UniversityOfHelsinkiCS/oodikone 2022-09-13 16:08:07 +03:00
outisa 0e9253f698 Update Faculty updater 2022-09-13 16:07:47 +03:00
Miko Keskimäki 11f9b935b4 Log iam rights status via accessLogger for Grafana 2022-09-13 12:53:50 +03:00
outisa a16896e74d [Faculty] Update updater 2022-09-12 13:33:47 +03:00
outisa 814599d6ea [Faculty] Small refactoring and adjusting numbers 2022-09-10 12:18:01 +03:00
outisa 8f2dcac0f9 Fix bug for updating statistics 2022-09-09 18:24:29 +03:00
outisa 240659afc9 [Faculty] Add include/exclude studywork toggle 2022-09-09 15:12:22 +03:00
outisa b9059e2a52 Merge branch 'master' of github.com:UniversityOfHelsinkiCS/oodikone 2022-09-09 15:09:22 +03:00
outisa a1688f7cdd [Faculty] Add infotool text 2022-09-09 14:19:05 +03:00
Laura Sinikallio 2c378bfca2 Patch breaking population statistics course filters ('didn't participate' disabled for now) 2022-09-09 11:54:39 +03:00
outisa aec495b161 [Navigation] Fix issue for faculty tab showing wrongly 2022-09-06 09:58:59 +03:00
outisa 486bc1a194 Merge branch 'master' of github.com:UniversityOfHelsinkiCS/oodikone 2022-09-02 14:19:55 +03:00
outisa 18bcb91d09 Add one more test for faculty view 2022-09-02 14:19:19 +03:00
outisa 5c4d73f555 Add new tests for faculty 2022-09-02 13:21:50 +03:00
Laura Sinikallio 27e8450789 Small clean-ups 2022-09-01 14:40:35 +03:00
Laura Sinikallio 56733f2445 Subtract statutory absences from faculty graduation times 2022-09-01 14:34:41 +03:00
Laura Sinikallio 089b00fcd6 Show graduation times only for levels with data 2022-09-01 11:52:05 +03:00
Laura Sinikallio a709fe70ba Merge branch 'master' of github.com:UniversityOfHelsinkiCS/oodikone 2022-08-31 19:23:14 +03:00
Laura Sinikallio f26e4b4859 Betterments to graduations times 2022-08-31 19:23:06 +03:00
outisa 18a382167b Fix accessrights in navigation bar 2022-08-31 16:25:32 +03:00
outisa b4be0a3e6b [Faculty] Fix accessrights problems 2022-08-31 15:44:41 +03:00
outisa 596ac08ad4 Fix bug related to accessrights 2022-08-31 13:55:40 +03:00
outisa 9c6ad0027a Modify faculty accessrights in navigation bar 2022-08-31 12:05:03 +03:00
outisa 27ce374f35 [faculty] Modify access rights 2022-08-31 11:55:17 +03:00
Laura Sinikallio 7e6f311b62 Faculty - move grad times to new tab 2022-08-31 10:22:43 +03:00
Laura Sinikallio dd77fdff9e Initial pipeline for graduation times, WIP 2022-08-30 17:48:58 +03:00
outisa 0b7adf4888 [Faculty] Add messagebox for incomplete data 2022-08-30 14:12:57 +03:00
outisa 9deee8a384 [Faculty] Change color codes (perhaps) better 2022-08-30 13:58:11 +03:00
outisa 81791381a5 [Faculty] Make betterments for faculty view 2022-08-30 12:45:59 +03:00
outisa 9806639c1b [Faculty] Show total in on-hover tooltip 2022-08-30 12:13:51 +03:00
outisa 5e9a3c4b99 [Faculty] Change others to licentiates (thesis writers) 2022-08-30 12:01:38 +03:00
outisa 12f73f4dee [Faculty] Show numbers with max. 1 decimal 2022-08-30 11:54:04 +03:00
outisa 9d27dfebe5 [Faculty] Make betterments for basic view 2022-08-29 17:15:38 +03:00
outisa 7bebd4c845 Fix updating thesis writers 2022-08-29 17:14:05 +03:00
outisa 43d2e84b32 [Faculty] Add thesis writers to redis 2022-08-29 14:47:25 +03:00
Roni Lindholm ee1cb395a2 Fix custom population crash 2022-08-29 11:45:26 +03:00
Roni Lindholm 4e221d8867 Fix missing students from population of course when no attainment
Enrollments were retrieved by enrollment date instead of semester code
which corresponds to when the course is acually organized.
2022-08-29 11:11:03 +03:00
Roni Lindholm 3aab7b74a3 Fix counting of total enrollments in course stats attempts tab 2022-08-29 10:05:29 +03:00
Roni Lindholm 92651b8bc7 Make course stats search by course code case insenstive 2022-08-29 09:47:30 +03:00
Miko Keskimäki 08b53c34f2 Dependabot updates 2022-08-29 08:52:00 +03:00
Laura Sinikallio 8d6a311936 Add note to update tab 2022-08-27 12:59:05 +03:00
Laura Sinikallio 79b8b92b66 Implement faculty info texts 2022-08-27 12:03:04 +03:00
outisa fa8a07481a [Faculty] Add small adjustment for sorting 2022-08-27 10:45:23 +03:00
outisa f79d6ef0fb [Faculty] Find thesis writers only for new programmes 2022-08-26 21:43:44 +03:00
outisa bb25541202 Disable flaky faculty cypress test 2022-08-26 17:22:21 +03:00
outisa 3530723bd5 [Faculty] Change sort by selection 2022-08-26 16:41:31 +03:00
Laura Sinikallio f2ffaacc6c Info texts for faculty 2022-08-26 16:12:42 +03:00
Laura Sinikallio 14e95fe4df Add update statistics tab to faculty page, refactor programmes tab basis 2022-08-26 15:25:04 +03:00
Laura Sinikallio f69b12a1f9 Add faculty stats refresh to cronjob 2022-08-26 13:13:13 +03:00
Laura Sinikallio e4e527eaeb Imporove faculty filtering 2022-08-26 12:12:53 +03:00
Laura Sinikallio c4c8f6123b Tools for refreshing faculty stats with cronjob (unplugged) 2022-08-25 16:14:03 +03:00
Laura Sinikallio bdb71242a7 Redis edits from previous changes 2022-08-25 14:40:28 +03:00
Laura Sinikallio a3a36fcef1 Put back faculty frontside programme sorting 2022-08-25 14:17:56 +03:00
Laura Sinikallio adbcb1bb88 Faculty toggle for old programmes filters from all stats 2022-08-25 13:51:22 +03:00
outisa 37504f9e97 Fix minor bug for finding thesis writers 2022-08-24 18:29:13 +03:00
outisa 1722bcd7a3 Change order of the navigation bar 2022-08-24 17:10:37 +03:00
outisa a770873da5 [Faculty] Add language options for programme names 2022-08-24 16:45:18 +03:00
Laura Sinikallio a4c3385956 Redis for faculty credits and student info -> speed 2022-08-24 14:04:32 +03:00
Laura Sinikallio 9fe491e7cc Fix error, but better 2022-08-24 13:00:13 +03:00
Laura Sinikallio 42213948b7 Fix redis error 2022-08-24 12:28:10 +03:00
Laura Sinikallio 1014348e5b Redis for faculty programmes 2022-08-24 11:48:25 +03:00
outisa dcecd66e49 [Faculty] Fix sorting bug 2022-08-24 10:33:58 +03:00
Laura Sinikallio 4509e51f66 Refactor file structure, start of redis for faculty 2022-08-24 10:11:38 +03:00
outisa 90841369bb [Faculty] Modify calculation of thesis writers 2022-08-24 09:55:26 +03:00
outisa c3e41c8a60 [Faculty] Add small refactoring 2022-08-24 09:47:39 +03:00
Laura Sinikallio 81ffbf653e Basis for faculty graduation times 2022-08-23 10:20:28 +03:00
Laura Sinikallio b46e30c8fa Switch rest of faculty to new programme search 2022-08-22 15:52:46 +03:00
Miko Keskimäki 3619698744 Fix error with encryption when no selectable students for year exist 2022-08-22 14:21:35 +03:00
Laura Sinikallio 81ee6b8146 Improve faculty programmes retrieval 2022-08-22 14:03:49 +03:00
outisa c9b27fdc64 Merge branch 'master' of github.com:UniversityOfHelsinkiCS/oodikone 2022-08-22 12:55:02 +03:00
outisa 6b50c07ed7 [Faculty] Add toggle for showing only new programmes or all programmes 2022-08-22 12:54:47 +03:00
Roni Lindholm d2670e180c Fix empty study guidance group crash 2022-08-22 11:44:30 +03:00
Roni Lindholm 580e1b4ed0 Minor study guidance group tweaks 2022-08-22 11:08:42 +03:00
Laura Sinikallio e84e487307 Small fix to programme names for faculty 2022-08-22 10:02:08 +03:00
Roni Lindholm d0a54c95a6 Refactor UI for editing of study guidance group metadata
Open editing form to a modal and wrap study guidance table on small
screens.
2022-08-22 09:38:41 +03:00
outisa 18ac10f399 [Faculty] Add sorting by column values (not yet pretty) 2022-08-19 19:20:33 +03:00
Roni Lindholm 263e070b13 Fix open uni credits in teacher leaderboard 2022-08-19 11:17:03 +03:00
Roni Lindholm 8cf9ceac6e Try to fix tests 2022-08-19 08:48:56 +03:00
Roni Lindholm 32005ce23f Add transferred credits and students to programme courses 2022-08-19 08:21:54 +03:00
outisa 111a397811 [Faculty] Make charts to look prettier 2022-08-18 14:14:29 +03:00
outisa 454032368a [Faculty] Fix bug: tooltip not showing on thesis writers chart 2022-08-18 13:52:06 +03:00
Laura Sinikallio 5509cd4b9a Faculty backend tweaks 2022-08-17 16:16:01 +03:00
outisa 346c9f9803 [Faculty] Make betterments in programmelevel graphs 2022-08-17 15:21:33 +03:00
Laura Sinikallio 0a738df3d0 Improvements to faculty's programme search 2022-08-17 15:01:00 +03:00
Miko Keskimäki b67da1c2c4 Enable IAM tests
IAM user added to test db image so should work now
2022-08-17 10:14:30 +03:00
outisa e700586588 [Faculty] Add programmestats thesis writers (missing old ones) 2022-08-16 23:39:43 +03:00
outisa 9bb7c1c6f1 Merge branch 'master' of github.com:UniversityOfHelsinkiCS/oodikone 2022-08-16 18:28:53 +03:00
Miko Keskimäki 7f70968531 Remove 00:00:00 time from general student information csv data 2022-08-16 11:08:40 +03:00
outisa 9ae35e46db [Faculty] Add backend functionality for finding faculty thesis writers 2022-08-16 10:42:35 +03:00
Miko Keskimäki 3d667fe1c6 Update student progress test data to match credits since start of studyright 2022-08-15 14:03:33 +03:00
Miko Keskimäki 62e07ce571 Show only credits gained since start of studyright in study programme progresstable #3661 2022-08-15 13:33:42 +03:00
Miko Keskimäki 38cd587369 Dependabot updates 2022-08-15 11:49:04 +03:00
outisa 77c9801e4c FacultyStats: Add custom sorting 2022-08-12 14:30:25 +03:00
Miko Keskimäki d3f1e101ce Prevent iam users from hiding degree courses 2022-08-12 13:12:41 +03:00
Miko Keskimäki 10a2d2743e Remove links to course statistics when only iam rights 2022-08-12 13:11:07 +03:00
Laura Sinikallio 4d26f42f25 Fix graduations error 2022-08-12 09:27:02 +03:00
Miko Keskimäki e8f3d9f601 Fix error with encryption when no students selected 2022-08-12 08:45:48 +03:00
outisa 6f3af6f135 Merge branch 'master' of github.com:UniversityOfHelsinkiCS/oodikone 2022-08-11 17:02:52 +03:00
outisa 3e08836d4e Faculty: Make study programme level colors to match faculty level ones 2022-08-11 17:00:21 +03:00
Miko Keskimäki efea7cdc9e Encrypt studentnumbers in population's course statistics if iam user 2022-08-11 16:04:27 +03:00
Miko Keskimäki c6232e617f Hide students tab of courses of population from iam users 2022-08-11 16:03:08 +03:00
outisa f360716b8e FacultyStats: Show difference to previous year 2022-08-11 15:02:45 +03:00
Laura Sinikallio 55bf6b0457 General refactoring 2022-08-11 14:29:58 +03:00
Miko Keskimäki 3067b37298 Merge branch 'master' of github.com:UniversityOfHelsinkiCS/oodikone 2022-08-11 13:38:03 +03:00
Miko Keskimäki a19a343feb Make courses of population visible to iam users 2022-08-11 13:36:45 +03:00
Miko Keskimäki edb5ca5422 Fix selectedStudentsByYear not being decrypted 2022-08-11 13:04:52 +03:00
Miko Keskimäki 2eb5f10eee Skip iam tests for now 2022-08-11 12:55:32 +03:00
Miko Keskimäki 7ce269d8be Decrypt student numbers 2022-08-11 12:30:58 +03:00
Miko Keskimäki 42554bba7a Send decryptable student selection to backend 2022-08-11 12:29:54 +03:00
Laura Sinikallio be77832f72 Edit column names 2022-08-11 11:58:19 +03:00
Miko Keskimäki f32d29ca34 Encrypt studentnumbers for iam user instead of hashing
Need to decrypt in backend to make courses of population tab work
2022-08-11 11:48:34 +03:00
Laura Sinikallio bb3652f9c4 Fix missing programme names 2022-08-11 10:51:17 +03:00
Laura Sinikallio a6272bff4f [Faculty] programme names for the rest of the tables 2022-08-10 15:27:20 +03:00
Miko Keskimäki 29f8cf110b Fix type error in IAMConfig 2022-08-10 12:06:57 +03:00
Laura Sinikallio 05ccbd6b16 Merge branch 'master' of github.com:UniversityOfHelsinkiCS/oodikone 2022-08-10 11:41:19 +03:00
outisa 608f2a921e [Faculty] sort programmes alphabetically by default 2022-08-10 10:53:28 +03:00
Miko Keskimäki 7bd40f7030 Align study track with correct programme in student statistics 2022-08-10 10:32:40 +03:00
Laura Sinikallio 0075a5f994 Merge branch 'master' of github.com:UniversityOfHelsinkiCS/oodikone 2022-08-10 09:41:16 +03:00
outisa 06414288f8 Merge branch 'master' of github.com:UniversityOfHelsinkiCS/oodikone 2022-08-10 09:29:02 +03:00
outisa ac9b32408c Disable eslint in tooltip formatter() for 'no-this' 2022-08-09 18:26:39 +03:00
Laura Sinikallio 4ad1e4237c Programme details for rest of current faculty tables 2022-08-09 16:37:14 +03:00
Miko Keskimäki 2f0ebd04d0 Remove transfered studytracks from studyright associations 2022-08-09 15:17:55 +03:00
Miko Keskimäki 40264f28d1 Filter transfered students' studytracks from population stats 2022-08-09 15:17:41 +03:00
outisa 95eaef7f8d Faculty: Change button to 'array' 2022-08-09 14:50:21 +03:00
Laura Sinikallio 7a1fa68c94 Provide programme names for faculty credit table 2022-08-09 13:01:56 +03:00
Laura Sinikallio 1d86557ed4 Merge branch 'master' of github.com:UniversityOfHelsinkiCS/oodikone 2022-08-09 09:07:53 +03:00
outisa d013809645 Facultystatistics: Add years to x-axis (linegraphs) 2022-08-08 17:25:34 +03:00
Laura Sinikallio 32b860bf0b [Faculty] programme-level numbers for graduations 2022-08-08 16:11:19 +03:00
Laura Sinikallio 02ea189fcb Fix table titles 2022-08-08 15:39:13 +03:00
outisa d38b647fc3 FacultyStatistics: Add new table views 2022-08-08 14:25:42 +03:00
Miko Keskimäki 0eb31cc8ea Reload to maybe make iam test user work 2022-08-08 13:52:29 +03:00
Miko Keskimäki 7aef916e70 Simple tests for iam user studyprogramme and population statistics access 2022-08-08 12:30:57 +03:00
Miko Keskimäki fc84ef78fd Create new test user with only IAM access 2022-08-08 12:28:11 +03:00
Laura Sinikallio 7c31425114 Faculty - separate graduations breakdown from students -table 2022-08-08 11:56:19 +03:00
Laura Sinikallio c88ad2201d Refactor faculty basics data structure 2022-08-08 11:07:38 +03:00
Miko Keskimäki e936510754 Update searching by course code #3457 2022-08-08 10:12:55 +03:00
outisa 825e549b49 Fix faculty stats cypress tests 2022-08-08 09:32:50 +03:00
outisa b8b0ffe34b Disable cypress tests for toggle credits for further investigation 2022-08-05 16:52:16 +03:00
outisa e2018d3e4c Modify faculty statistics cypress tests 2022-08-05 15:58:52 +03:00
outisa c3dbc9682e Add cypress tests for toggling CreditsCharts 2022-08-05 15:06:54 +03:00
Miko Keskimäki 54195f0a85 Add babel/core to dev dependencies 2022-08-05 12:50:30 +03:00
Miko Keskimäki 600795ca48 Fix more lint errors 2022-08-05 12:33:56 +03:00
Miko Keskimäki 3b2b04b10d Update eslint, eslintrc and fix errors 2022-08-05 12:08:29 +03:00
Miko Keskimäki 57ddbcaa36 Update stylelint config and fix errors 2022-08-05 12:00:52 +03:00
Miko Keskimäki 1185793e4d Update dev package major versions 2022-08-05 12:00:39 +03:00
outisa d8a5989ba1 FacultyStatistics: Make barchart look better 2022-08-05 10:54:30 +03:00
Miko Keskimäki 7a8ba85071 Run tests when actions updated 2022-08-05 09:32:12 +03:00
Miko Keskimäki 378b886947 Use newer Cypress action 2022-08-05 09:22:43 +03:00
Miko Keskimäki 167fa3b979 Update Cypress paths in test workflow and readme 2022-08-05 08:53:28 +03:00
Miko Keskimäki 7ddcf94b57 Update Cypress and migrate tests and config to v10 2022-08-05 08:48:52 +03:00
outisa f71c406180 InteractiveTAble: Fix key props 2022-08-04 17:44:47 +03:00
outisa c1a94acb92 FacultyStatistics: Add toggleTableView 2022-08-04 17:11:42 +03:00
Miko Keskimäki 1264d4f3b9 Use run-tests action for all Cypress tests 2022-08-04 15:19:43 +03:00
Miko Keskimäki 3ab87a18e3 Test composite action with failing test 2022-08-04 15:07:58 +03:00
Miko Keskimäki 2a68a9cc9b Pass registry username and password to action 2022-08-04 14:49:31 +03:00
Miko Keskimäki 15f86a85d4 Test composite action for custom population tests 2022-08-04 14:37:53 +03:00
Miko Keskimäki 16952cece6 Try moving repeatitive Cypress test job config into composite action 2022-08-04 14:35:24 +03:00
Miko Keskimäki 4c71092e7f Use actionlint npm package instead of broken docker version 2022-08-04 13:43:59 +03:00
Miko Keskimäki d2558a40d8 [skip ci] Remove test comment 2022-08-04 13:10:40 +03:00
Miko Keskimäki c918c5ffc9 Test that workflow activates 2022-08-04 13:02:49 +03:00
Miko Keskimäki 54ecddc46e Run test-oodikone.yaml when sis-db migrations updated #3729 2022-08-04 13:00:01 +03:00
Miko Keskimäki f2e655bb05 Update remaining actions 2022-08-04 12:36:48 +03:00
Miko Keskimäki 5ee3e7310e Update dependabot.yml 2022-08-04 12:30:33 +03:00
Miko Keskimäki 8bdb30bca5 Fix wrong login-action version 2022-08-04 10:54:45 +03:00
Miko Keskimäki bc06295f97 Update github actions versions
See if anything breaks
2022-08-04 10:49:51 +03:00
outisa 522e9f851e Merge branch 'master' of github.com:UniversityOfHelsinkiCS/oodikone 2022-08-04 10:27:45 +03:00
outisa 28e798ff28 FacultyStatistics: Add interactive data view 2022-08-04 10:26:51 +03:00
Miko Keskimäki ca4576e155 Allow usage without oodikone iams if user has iam rights to any programme 2022-08-03 13:58:36 +03:00
Laura Sinikallio 90309143fd Start of programme-level breakdown of faculty stats 2022-08-01 12:42:26 +03:00
Miko KeskimäkiandGitHub 8f4fcb4364 Merge pull request #3632 from UniversityOfHelsinkiCS/dependabot/npm_and_yarn/services/frontend/async-2.6.4
Bump async from 2.6.3 to 2.6.4 in /services/frontend
2022-08-01 10:55:07 +03:00
Miko KeskimäkiandGitHub facd2c15b9 Merge pull request #3538 from UniversityOfHelsinkiCS/dependabot/npm_and_yarn/services/frontend/minimist-1.2.6
Bump minimist from 1.2.5 to 1.2.6 in /services/frontend
2022-08-01 10:54:56 +03:00
Miko KeskimäkiandGitHub 756cadd0e6 Merge pull request #3539 from UniversityOfHelsinkiCS/dependabot/npm_and_yarn/updater/sis-updater-worker/minimist-1.2.6
Bump minimist from 1.2.5 to 1.2.6 in /updater/sis-updater-worker
2022-08-01 10:54:44 +03:00
Miko KeskimäkiandGitHub 842a561f94 Merge pull request #3540 from UniversityOfHelsinkiCS/dependabot/npm_and_yarn/minimist-1.2.6
Bump minimist from 1.2.5 to 1.2.6
2022-08-01 10:54:26 +03:00
Miko KeskimäkiandGitHub 9346b1e245 Merge pull request #3775 from UniversityOfHelsinkiCS/dependabot/npm_and_yarn/moment-2.29.4
Bump moment from 2.29.1 to 2.29.4
2022-08-01 10:54:09 +03:00
Miko KeskimäkiandGitHub b93b430554 Merge pull request #3777 from UniversityOfHelsinkiCS/dependabot/npm_and_yarn/updater/sis-updater-worker/moment-2.29.4
Bump moment from 2.29.1 to 2.29.4 in /updater/sis-updater-worker
2022-08-01 10:53:54 +03:00
Miko KeskimäkiandGitHub a90dd779d5 Merge pull request #3778 from UniversityOfHelsinkiCS/dependabot/npm_and_yarn/updater/sis-updater-scheduler/moment-2.29.4
Bump moment from 2.29.3 to 2.29.4 in /updater/sis-updater-scheduler
2022-08-01 10:53:39 +03:00
Miko KeskimäkiandGitHub dafec6bd6b Merge pull request #3779 from UniversityOfHelsinkiCS/dependabot/npm_and_yarn/services/frontend/moment-2.29.4
Bump moment from 2.29.1 to 2.29.4 in /services/frontend
2022-08-01 10:53:17 +03:00
Miko Keskimäki b27e23e7fb [skip ci] Ignore major semver updates with Dependabot
Too many ignored PRs
2022-08-01 10:16:51 +03:00
Miko Keskimäki 6fff1f15fb Expand Cypress viewport 2022-08-01 09:46:50 +03:00
Miko Keskimäki 85bfc6041e Fix breaking test 2022-08-01 09:46:22 +03:00
Miko Keskimäki eb1a92e96a Update nonbreaking package versions 2022-08-01 09:18:03 +03:00
Miko Keskimäki 8c7a94b476 Npm audit fix 2022-08-01 09:10:56 +03:00
Miko Keskimäki f683d33769 Update all package-locks to lockfile v2 2022-08-01 09:10:53 +03:00
Miko Keskimäki 0c84cbf08f Show search by class to iam users 2022-07-29 13:11:39 +03:00
Miko Keskimäki 358f629285 Hide tabs without data from iam users 2022-07-29 13:10:53 +03:00
Miko Keskimäki af77bc500e Return obfuscated population statistics for iam rights 2022-07-29 13:03:52 +03:00
Miko Keskimäki 224e199931 Hide population statistics from iam users for now
Make visible after a more limited access and obfuscation are working
2022-07-14 12:07:20 +03:00
Miko Keskimäki cfcb28aa58 Show study programme overview to users with correct iam rights
Basic information, Studytracks and Programme courses tabs are visible
2022-07-14 11:09:39 +03:00
Laura Sinikallio 469a2be823 Fix duplicate api calls 2022-07-13 16:20:19 +03:00
Laura Sinikallio 61e0a3b380 Refactor faculty basics services 2022-07-13 15:35:00 +03:00
Laura Sinikallio 5b092f905e Add faculty transfers stats 2022-07-13 15:05:46 +03:00
Laura Sinikallio 01715f4879 Refactor faculty graduates 2022-07-13 14:06:31 +03:00
Laura Sinikallio 6acef8abad Merge branch 'master' of github.com:UniversityOfHelsinkiCS/oodikone 2022-07-13 14:04:59 +03:00
Miko Keskimäki 44743b70ca Show user's iam groups in users tab 2022-07-13 13:04:47 +03:00
Laura Sinikallio fcd637a2fa Fix issue in faculty academic year setting 2022-07-13 11:57:29 +03:00
Laura Sinikallio f5e08acf04 Merge branch 'master' of github.com:UniversityOfHelsinkiCS/oodikone 2022-07-13 11:52:03 +03:00
Laura Sinikallio 04c4ee1b80 Add faculty graduations breakdown 2022-07-13 11:51:57 +03:00
Roni Lindholm d87ee4bd84 Lost test 2022-07-13 11:50:02 +03:00
Roni Lindholm 04758edba0 Better numbers for tests? 2022-07-13 11:14:11 +03:00
Laura Sinikallio 796feea25c Fix faculty graduate count 2022-07-13 11:08:20 +03:00
Roni Lindholm 52c47a214e Unskip test 2022-07-13 10:21:08 +03:00
Laura Sinikallio 92450f634c Merge branch 'master' of github.com:UniversityOfHelsinkiCS/oodikone 2022-07-12 14:33:04 +03:00
Laura Sinikallio fec92c752f Faculty started and graduated stats exist 2022-07-12 14:32:57 +03:00
Roni Lindholm 5d6bbfb4b5 Fix study track filter when student has graduated
Fixes #3536
2022-07-12 14:12:10 +03:00
Miko Keskimäki 3c22bbc3d1 Move getting iam rights into formatUser to work with mock users 2022-07-12 13:56:38 +03:00
Roni Lindholm 5e5edede6f Skip for now 2022-07-12 13:41:23 +03:00
Laura Sinikallio a2a4a05f02 Merge branch 'master' of github.com:UniversityOfHelsinkiCS/oodikone 2022-07-12 13:17:03 +03:00
Laura Sinikallio 4f0f5acb88 Remove duplicates from faculty started stats, work on graduated 2022-07-12 13:16:56 +03:00
Roni Lindholm f00ef92e86 Uncomment correct course stats attemps table test values 2022-07-12 12:41:30 +03:00
Roni Lindholm 38f1872d68 Fix course stats attemps table crash 2022-07-12 12:26:56 +03:00
Miko Keskimäki c314138d89 Merge branch 'master' of github.com:UniversityOfHelsinkiCS/oodikone 2022-07-12 12:25:04 +03:00
Miko Keskimäki b174082623 Include only relevant iams 2022-07-12 12:23:57 +03:00
Miko Keskimäki 38dcf3dcae Save iam groups and get iam based access rights in userService 2022-07-12 12:15:40 +03:00
Miko Keskimäki a872500fb1 Add iam_groups column to user 2022-07-12 12:09:22 +03:00
Roni Lindholm 5a9d8b922c Fix broken obscurification in course stats
Fixes #3785
2022-07-12 10:45:02 +03:00
Miko Keskimäki 9ee09fdbcf Add modified version of IAM logic from Norppa 2022-07-12 10:31:46 +03:00
Laura Sinikallio 8a9e2d2c5c Merge branch 'master' of github.com:UniversityOfHelsinkiCS/oodikone 2022-07-12 10:31:39 +03:00
Laura Sinikallio 1f955889c6 Work on faculty basic stats 2022-07-12 10:31:32 +03:00
Roni Lindholm a565179cb2 Mock current date in filter tests
Fixes #3360, no happy birthday commits anymore :(
2022-07-12 10:23:49 +03:00
Laura Sinikallio 42307f6e56 Merge branch 'master' of github.com:UniversityOfHelsinkiCS/oodikone 2022-07-12 09:47:21 +03:00
Roni Lindholm bfd3119a28 Clean also transfers from and to duplicate study programmes
Fixes #3784
2022-07-12 08:51:53 +03:00
Roni Lindholm 596688f701 Fix duplicate study programmes
Update references from duplicate study programme to corresponding
regular study programme in post update. Fixes #3259.
2022-07-12 08:02:19 +03:00
Laura Sinikallio eef3c3c766 Faculty touch-ups 2022-07-11 11:25:23 +03:00
Laura Sinikallio c1f81ef855 Stabilize most faculty non-major credits 2022-07-08 12:51:56 +03:00
Laura Sinikallio 30ac43e131 Fix faculty selector language picker 2022-07-08 09:41:38 +03:00
Laura Sinikallio 67cb87c932 Unify spelling 2022-07-07 15:36:57 +03:00
Laura Sinikallio f044966725 Merge branch 'master' of github.com:UniversityOfHelsinkiCS/oodikone 2022-07-07 15:34:21 +03:00
Laura Sinikallio 6606755d99 Faculty credit stats redone 2022-07-07 15:31:41 +03:00
Roni Lindholm 9afe77977d Fix language picker in study programme overview 2022-07-07 14:10:42 +03:00
Laura Sinikallio 12aef60d93 Start rewrite of faculty credits 2022-07-07 13:26:41 +03:00
Roni Lindholm 3479c95a6d Fix study programme overview graduation tests 2022-07-07 13:25:42 +03:00
Roni Lindholm a0b3e3f76c Fix study programme overview graduated stats 2022-07-07 13:10:50 +03:00
Laura Sinikallio 107f64d85a Edit faculty test 2022-07-07 10:52:39 +03:00
Roni Lindholm 03327f59c1 Update study programme overview tests 2022-07-07 10:46:53 +03:00
Laura Sinikallio 98d2e52e64 Add faculty tests to workflow better 2022-07-07 10:44:47 +03:00
dependabot[bot]andGitHub 7608f62869 Bump moment from 2.29.1 to 2.29.4 in /services/frontend
Bumps [moment](https://github.com/moment/moment) from 2.29.1 to 2.29.4.
- [Release notes](https://github.com/moment/moment/releases)
- [Changelog](https://github.com/moment/moment/blob/develop/CHANGELOG.md)
- [Commits](https://github.com/moment/moment/compare/2.29.1...2.29.4)

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

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

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

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

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-07-07 07:40:43 +00:00
Laura Sinikallio 9ea473dc68 Add faculty tests to workflow 2022-07-07 10:39:59 +03:00
dependabot[bot]andGitHub 44ee47c34d Bump moment from 2.29.1 to 2.29.4
Bumps [moment](https://github.com/moment/moment) from 2.29.1 to 2.29.4.
- [Release notes](https://github.com/moment/moment/releases)
- [Changelog](https://github.com/moment/moment/blob/develop/CHANGELOG.md)
- [Commits](https://github.com/moment/moment/compare/2.29.1...2.29.4)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-07-07 04:29:55 +00:00
Laura Sinikallio 18e4055588 Basis for faculty student stats 2022-07-06 14:38:05 +03:00
Laura Sinikallio ba819d4074 Merge branch 'master' of github.com:UniversityOfHelsinkiCS/oodikone 2022-07-06 14:27:58 +03:00
Roni Lindholm b9eaf86711 Fix graduations in study programme overview basic info table 2022-07-06 14:22:26 +03:00
Roni Lindholm 9f92bb94b1 Fix transfer and study right start dates when using snapshot date time
Use first snapshot date time when study right starts or education
changes. This was not working previously.
2022-07-06 13:28:41 +03:00
Laura Sinikallio 51cb20c75d Fix refresh crashing faculty page 2022-07-06 10:15:50 +03:00
Laura Sinikallio fe1243f9b8 Add tests for faculty view 2022-07-05 16:55:51 +03:00
Laura Sinikallio b81c7140a8 Refactor faculty state management 2022-07-05 14:58:13 +03:00
Laura Sinikallio b5fb6274dd Refactor faculty credits 2022-07-05 14:20:14 +03:00
Roni Lindholm 4c363b2658 Fix study programme overview caching crash when no data 2022-07-05 12:16:38 +03:00
Laura Sinikallio 881da1c79d Refactor faculty stats routes 2022-07-05 12:14:44 +03:00
Laura Sinikallio e8301077f6 Filter out some faculties 2022-07-05 10:43:49 +03:00
Laura Sinikallio d59c6092b3 Merge branch 'master' of github.com:UniversityOfHelsinkiCS/oodikone 2022-07-05 09:46:20 +03:00
Roni Lindholm 09dfd45cb2 Fix credit distribution graph crash on future population 2022-07-05 09:16:48 +03:00
Roni Lindholm 008e808134 Refactor study programme routes and fix credit stats caching 2022-07-05 09:07:42 +03:00
Laura Sinikallio 370084845d Show credits produced by faculty 2022-07-04 18:49:24 +03:00
Laura Sinikallio de73afef15 Add basis for faculty view elements 2022-07-04 14:47:12 +03:00
Laura Sinikallio d981144bc4 Merge branch 'master' of github.com:UniversityOfHelsinkiCS/oodikone 2022-07-04 14:40:29 +03:00
Roni Lindholm 6e4151bd14 Fix study programme graduation programmes 2022-07-04 14:13:07 +03:00
Roni Lindholm 939fd69e6d Update tests? 2022-07-04 13:45:25 +03:00
Laura Sinikallio 05520f31ec Add faculty selector 2022-07-04 12:47:51 +03:00
Roni Lindholm 2786fbfc0f Fix calculation of non degree and non major credits in study programme
Minor bug fixes.
2022-07-04 12:26:40 +03:00
SNurmivaara 8ec998044b Add barebones faculty page only visible to admins 2022-06-30 09:28:18 +03:00
Laura Sinikallio 29c914069e Merge branch 'master' of github.com:UniversityOfHelsinkiCS/oodikone 2022-06-29 15:18:14 +03:00
Laura Sinikallio 24033598f9 Edit load indicator [custom populations] 2022-06-29 15:17:57 +03:00
Roni Lindholm 94584e9faa Fix missing action in transfer filter 2022-06-29 14:14:59 +03:00
Roni Lindholm 953cf38ffd Display student count filter on study guidance courses of population
Fix #3441 as the filter was hidden but still filtered courses with low
enough count of students.
2022-06-29 13:54:20 +03:00
Laura Sinikallio 0b16c5eab2 Hide future semesters/years from course statistics 2022-06-29 13:22:27 +03:00
Roni Lindholm 58de142232 Use study right valid start date as fallback in study right element
Use old logic and additionally use fallback value when issue
described in #3387 occurs.
2022-06-29 12:54:03 +03:00
Roni Lindholm a88e0c0362 Happy birthday? :feels_birthday_man: 2022-06-29 12:10:01 +03:00
Roni Lindholm 7cb5f86937 Add study plan filter to study guidance groups
Add also possibility to set credit date filter to study start when
filtering credits by study plan.
2022-06-29 09:52:12 +03:00
Roni Lindholm e45e4ec239 Fix study right end date when graduated
Use graduation date as it is or study right end date - 1 day, as the
date range from Sisu is a semi-open interval.
2022-06-21 14:57:14 +03:00
Roni Lindholm 14308236c6 Add missing attainment date to population credits 2022-06-20 12:45:50 +03:00
Roni Lindholm d4254cc60f Fix wrong default start date for population credit graph 2022-06-20 12:19:21 +03:00
Roni Lindholm 4fcb3fcd07 Fix credit graph start on study guidance groups 2022-06-20 09:36:42 +03:00
Roni Lindholm a23bc4c0e3 Fix credit graph start on custom population 2022-06-20 08:58:13 +03:00
Roni Lindholm 8f44b40cff Add all study plan courses to population and show those in credit graph
Courses in population students can not be filtered by study start date
as study plan courses might be attained before study right.
2022-06-20 08:16:52 +03:00
SNurmivaara 0e9dfad793 Add non-degree students to studyprogramme overview tests 2022-06-16 15:06:49 +03:00
SNurmivaara 02e30db67f Add non degree students credits to studyprogramme overview 2022-06-16 15:06:49 +03:00
SNurmivaara d840758123 Unify study programme pages to use same terminology 2022-06-16 15:06:49 +03:00
Roni Lindholm b6fc419e53 Fix study plan updater ignoring attained modules when not graduated
Custom module attainments still ignored...
2022-06-15 14:14:04 +03:00
Roni Lindholm aed379e4c4 Add missing main course code column to backend model 2022-06-10 14:30:27 +03:00
Roni Lindholm 15faebf71b Dummy commit to run migrations for :ano: 2022-06-10 14:05:41 +03:00
Roni Lindholm 4d837e5308 Fix fetching courses of population when no mandatory courses 2022-06-10 12:43:59 +03:00
Roni Lindholm 901899cb76 Hide completion percentage from study right when graduated 2022-06-10 12:41:14 +03:00
Mitja Karhusaari ebbd4dabe9 Use precalculated substitution data in bottlenecksOf 2022-06-09 13:31:30 +03:00
Mitja Karhusaari db77c431f5 Pre-calculate substitution priorities for courses in updater 2022-06-09 12:38:35 +03:00
Roni Lindholm f813b6c1a6 Fix population filtering by study plan credits 2022-06-09 11:04:31 +03:00
Roni Lindholm e1799a2f0a Fix study plan filter when no study plan found 2022-06-08 13:36:08 +03:00
Roni Lindholm d770d629d3 Fix custom course code parsing in study plan updater 2022-06-08 10:29:53 +03:00
Roni Lindholm a42988106d Pls trigger necessary workflows 2022-06-07 15:02:18 +03:00
Roni Lindholm 24f6631fb5 Dummy commit 2022-06-07 14:55:30 +03:00
Roni Lindholm fceff7e673 Fix module and duplicate study plans mayhem in study plan updater
Study plan updater starts gettin more and more :tunkki:
2022-06-06 17:39:58 +03:00
Roni Lindholm 3641e131a7 Use pre calculated study plan credits in populations 2022-06-06 17:38:49 +03:00
Roni Lindholm c6570f6d35 Use pre calculated study plan completion credits 2022-06-06 10:00:55 +03:00
Roni Lindholm ea156482be Calculate study plan completion credits in updater 2022-06-06 09:48:31 +03:00
SNurmivaara c33862bde9 Add tooltip text for non-degree 2022-06-03 14:04:20 +03:00
SNurmivaara e5cb8855f2 "New" banner for programme courses 2022-06-03 14:03:52 +03:00
SNurmivaara 2fc629fc96 Programme course tab order and tooltip
- Reordred programme courses tabs
- Add a tooltip
2022-06-03 14:03:00 +03:00
Roni Lindholm 2924d1147b Draw target line on student credit graph correctly with absences 2022-06-02 13:37:07 +03:00
Roni Lindholm a5d8fdda5d Fix graduations mapping when updating study plans 2022-06-02 12:39:47 +03:00
Roni Lindholm 27837a09e2 Include substituted course unit selections in study plan update 2022-06-02 11:30:26 +03:00
Roni Lindholm b0d80a7e70 Fix study plan parsing from attainments 2022-06-02 08:52:37 +03:00
Roni Lindholm c52c0a3337 Fix course code sanitizing and add back updater dev limit 2022-06-02 08:18:24 +03:00
Roni Lindholm 4682ae4fc9 Refactor study plan composite key and add missing custom selections
Custom course unit selections were not included to study plan courses.
2022-06-01 19:49:59 +03:00
Roni Lindholm 1e9ffe6d1f Display study plan completion always if a study plan exists 2022-06-01 19:49:59 +03:00
SNurmivaara b8522d800f Programme courses visible to basic users
- Study programme -> programme courses tab now visible for basic users
- Tests updated accordingly to stop using admin
2022-05-31 13:20:11 +03:00
Petri Palmu 8854bc0f38 made trend iclude new AY codeless open courses 2022-05-31 13:18:34 +03:00
SNurmivaara acbb19a2bb Gotta test em all 2022-05-31 10:57:52 +03:00
SNurmivaara 6e04c28f0b Add data fields for cypress testing 2022-05-31 10:57:52 +03:00
Petri Palmu 670a276f9a little change to studyprogramme keys 2022-05-31 09:54:04 +03:00
Petri Palmu d5aa9bc70e removed unused segments from studyprogramme 2022-05-31 09:54:04 +03:00
Petri Palmu b1ca01cd47 removed new texts from studyprogramme 2022-05-31 09:54:04 +03:00
Roni Lindholm 1f59e00f54 Fix HOPS updater missing module attainments with empty nodes 2022-05-31 09:17:22 +03:00
Roni Lindholm 4609d156a7 Highlight courses included in HOPS when a study right is selected 2022-05-31 09:15:46 +03:00
Roni Lindholm 1a170be522 Fix courses of population attempts per student when multiple years
Attempts per student is not a percentage.
2022-05-30 12:16:43 +03:00
Roni Lindholm 65022b5894 Add toggle to view course stats by semester or year
Display enrollments always.
2022-05-30 10:50:33 +03:00
Petri Palmu 04e989736b added range attribute in studyprogramme courses 2022-05-27 13:21:59 +03:00
Petri Palmu 303449f2b6 added analytics to he programme courses 2022-05-27 10:20:33 +03:00
Roni Lindholm 27394ea7ef Add more detailed info about semester enrollments to student page
Display also in credit graph the status of semester enrollment.
2022-05-24 15:18:28 +03:00
Petri Palmu 18448389b6 fix sql query problem in other programme students in programme courses 2022-05-24 14:00:12 +03:00
Petri Palmu 5e08eb5753 fix to the programme courses year filter 2022-05-24 12:42:31 +03:00
Petri Palmu a8ac538ca5 separated students and credits behind selector in course programmes 2022-05-24 12:42:31 +03:00
Petri Palmu 74a37b8549 correction to sql queries in programme courses 2022-05-24 12:42:31 +03:00
Petri Palmu 1796678a01 removed transfrred credits from programme courses 2022-05-24 12:42:31 +03:00
Mitja Karhusaari d8173e6cf3 Enable student count filter in courses of population on the programme population page 2022-05-23 13:53:48 +03:00
Mitja Karhusaari e539f42dc0 Fix courses of population updating problem 2022-05-23 13:41:30 +03:00
Mitja Karhusaari 15f782ff23 Send filter usage events to grafana 2022-05-23 12:40:01 +03:00
Mitja Karhusaari 59ce52b138 Fix tests 2022-05-23 10:38:29 +03:00
Roni Lindholm 626d0942b8 Fix wrong enrollments shown in course population
Refactor some SQL mayhem.
2022-05-23 10:13:59 +03:00
Mitja Karhusaari 20d5fb5ca9 Reflect changed default pane in tests 2022-05-23 10:17:01 +03:00
Mitja Karhusaari 1f9cc8451b Set default credit statistics pane to distribution development 2022-05-23 09:59:15 +03:00
Mitja Karhusaari 36db9a8ee2 Add "Graduated" segment to credit distribution development chart 2022-05-23 09:59:15 +03:00
Mitja Karhusaari 0318f5a0f8 Make the semester based division the default in Credit Distribution Development chart 2022-05-23 09:59:15 +03:00
Roni Lindholm d89e16ea5f Fix tests 2022-05-19 15:17:32 +03:00
Roni Lindholm cc42a2040f Fix programme distribution in course population when enrolled no grade 2022-05-19 14:48:57 +03:00
Leo Varis 62da9c574d since start of studyright betterment (#3695) 2022-05-19 14:32:34 +03:00
Roni Lindholm 3f71655748 Fix course population crash when no enrollments 2022-05-19 13:54:03 +03:00
Roni Lindholm c973b672b7 Display only graduation of selected study right when filtering by HOPS 2022-05-19 13:36:52 +03:00
Roni Lindholm 2d588c0be4 Add students with enrollment and no grade to course populations
Fix also minor student page HOPS filtering issues.
2022-05-19 13:07:57 +03:00
Petri Palmu 40a65c2000 betterments to the programme courses route 2022-05-19 08:48:53 +03:00
Mitja Karhusaari 7c604213ff Move HOPS filtering logic into a FilterView filter 2022-05-18 17:42:11 +03:00
Mitja Karhusaari f46fdda4ff Fix SortableTable header sizing 2022-05-18 17:42:11 +03:00
Mitja Karhusaari 5c2bd77ffe Fix column help tooltip positioning 2022-05-18 17:42:11 +03:00
Mitja Karhusaari ad2fb13ef0 Fix email column breaking headers 2022-05-18 17:42:11 +03:00
Petri Palmu 78f167ea24 Merge branch 'studyprograms' 2022-05-18 16:32:15 +03:00
Petri Palmu 15a7ffb300 credits added to the programme courses 2022-05-18 16:31:26 +03:00
SNurmivaara e8754aba72 Fix studyprogram courses of pop to use filter
The state was only updated once, as the condition for data to exist
was always satisfied after the initial load. Added a check for making
sure that the existing data matches with the filtered data.

Fixes #3423
2022-05-18 13:35:48 +03:00
Petri Palmu 1ef1d4028f studyprogramme backend return right object 2022-05-18 09:15:08 +03:00
Mitja Karhusaari 29ccc7a730 Oops wrong workflow 2022-05-17 17:42:04 +03:00
Mitja Karhusaari 7a8c82ae0a Use commit sha for updater sentry version 2022-05-17 17:35:18 +03:00
Mitja Karhusaari 65185e5e31 Add 'daily' job type to update, which refreshes the trends materialized view 2022-05-17 16:16:57 +03:00
Mitja Karhusaari dc66370fd0 Add missing step to updater pipeline 2022-05-17 16:16:57 +03:00
SNurmivaara 7d31b1b2c8 Remove broken assertion from studyprogramme overview tests 2022-05-17 09:35:12 +03:00
Petri Palmu 0c8be55d1d little fix to getAllStudyprogramme courses 2022-05-17 09:13:31 +03:00
Petri Palmu dd54f2474b studyprogramme courses ay and reqular courses unified 2022-05-17 08:58:23 +03:00
Roni Lindholm 2788273d79 Fix student page crash when no study rights 2022-05-17 08:30:50 +03:00
Roni Lindholm 242508153b Implement filtering of credit graph by study plan in student page
Fix also missing credits before study right and missing credit label
in graph.
2022-05-17 08:10:29 +03:00
Mitja Karhusaari 6ef04fa9d7 Allow selecting students by clicking a segment in the credit distribution development graph 2022-05-16 13:45:09 +03:00
Mitja Karhusaari 8e735531a5 Implement cherry-picking filter 2022-05-16 13:45:09 +03:00
SNurmivaara 4862f3e9a3 Fix filterView flexbox bug
Fixes #3677 and probably other visual inconsistencies
https://stackoverflow.com/a/66689926
2022-05-16 12:04:23 +03:00
SNurmivaara 984cf8e3e8 Add student page sisu link test 2022-05-13 10:18:37 +03:00
SNurmivaara 16cec93af9 Add Sisu link to student page
- Return sis_person_id for single student from backend
- Add link to Sisu from student infocard
- Make Sisu links open in new tab
2022-05-13 09:45:08 +03:00
SNurmivaara 8bc71edafb Fix sisu link formatting in generaltab 2022-05-12 15:14:30 +03:00
Roni Lindholm 1c4e502a4b Include substitutions when fetching courses of population 2022-05-12 12:42:00 +03:00
Mitja Karhusaari cbce10d373 How did this happen 2022-05-11 21:48:42 +03:00
Mitja Karhusaari 243a3f566d Add year range selection capability to trends 2022-05-11 20:51:11 +03:00
Petri Palmu 1d6cdb05ec added degree students, non degree students nad other degree students in studyprogramme courses overview 2022-05-11 14:28:03 +03:00
Petri Palmu b50ce20072 bug fix Nan in studyprogramme list 2022-05-11 14:28:03 +03:00
Petri Palmu 9c2ed04462 language options added to stufyprogramme courses 2022-05-11 14:28:03 +03:00
Petri Palmu ced41fd7da changed using sql queries in studyprogramme courses 2022-05-11 14:28:03 +03:00
Petri Palmu b1cc7fd91c studyprogramme own students added to studyprogramme courses 2022-05-11 14:28:03 +03:00
SNurmivaara cdf2809cd8 Remove faulty return statements from trends 2022-05-11 12:45:24 +03:00
SNurmivaara 2eb7ab4254 Add error handling for trends to narrow down on a bug 2022-05-11 11:50:35 +03:00
SNurmivaara a41ab262af Add per person link to Sisu for student info
- Backend and frontend for sis_person_id
- Link icon betterments
- Resolves #3326
2022-05-10 14:28:29 +03:00
SNurmivaara f907b76023 Update study programme courses tests for time management betterments 2022-05-09 12:37:15 +03:00
Petri Palmu 8c9bd4c3e0 study programme couses time management betterments 2022-05-09 12:16:32 +03:00
Petri Palmu 8090823b2b try to fix yearly null error in trends update at night 2022-05-09 12:16:32 +03:00
SNurmivaara 85679662b5 Fix studyprogramme courses test 2022-05-06 09:14:26 +03:00
Roni Lindholm c0f72c93e2 Fix filtering by course in courses of population
Wrong redux store.
2022-05-06 08:41:14 +03:00
SNurmivaara 9b25d47f6f Add tests for studyprogramme courses 2022-05-05 14:31:56 +03:00
SNurmivaara e694123330 Sort studyprogramme courses alphabetically by default 2022-05-05 12:48:47 +03:00
Roni Lindholm fa48c0cc75 Fix student statistics tests 2022-05-05 10:59:14 +03:00
Petri Palmu 540386fed5 programme courses time filtering works with data 2022-05-05 10:28:27 +03:00
Petri Palmu df0ac9bb2b sturdy programme courses time filter bug fixes 2022-05-05 10:28:27 +03:00
Petri Palmu d0bd2ea311 programme courses time filter first version 2022-05-05 10:28:27 +03:00
Petri Palmu 3a243e5c26 programme courses radio styling done 2022-05-05 10:28:27 +03:00
Roni Lindholm 38a6f9c58a Fix off by one in study right end dates 2022-05-05 10:16:50 +03:00
Roni Lindholm 1cce0da021 Fix missing data in populations students courses tab
Use same data as courses of population for courses table.
2022-05-04 15:14:44 +03:00
Roni Lindholm fda6455a85 Fix randomly missing current programme in course population
Add also help text for semesters.
2022-05-02 13:15:35 +03:00
Petri Palmu 488020df23 new test in course statistics 2022-04-29 15:00:38 +03:00
Petri Palmu 65b48361b3 user can see students detailed info if has rights for course provider 2022-04-29 15:00:38 +03:00
Leo Varis 0be8f6f192 Merge branch 'master' of github.com:UniversityOfHelsinkiCS/oodikone 2022-04-27 12:44:11 +03:00
Leo Varis 9888ae4599 fix #3643 2022-04-27 12:44:03 +03:00
Roni Lindholm 8b4198d477 Add count of present semester enrollments to population students table 2022-04-27 11:04:19 +03:00
Roni Lindholm 9bc73d7aaf Add grades to course statistics students table
For each student the best grade is shown.
2022-04-27 09:29:33 +03:00
Leo Varis ea6e77cd6a Merge branch 'master' of github.com:UniversityOfHelsinkiCS/oodikone 2022-04-26 11:54:42 +03:00
Leo Varis ba1350114a fix #3637 2022-04-26 11:54:32 +03:00
Petri Palmu 4a2bfe8362 study programme courses rtk-api connected to backend 2022-04-26 09:53:24 +03:00
Petri Palmu b3cb3b15d5 fix to sentry issue yearly is null 2022-04-26 09:53:24 +03:00
Roni Lindholm bdc862a935 Fix filtering by "no programme" 2022-04-25 17:20:43 +03:00
Leo Varis f63a4fdf20 Merge branch 'master' of github.com:UniversityOfHelsinkiCS/oodikone 2022-04-25 16:49:03 +03:00
Leo Varis 57c53c254a finally fix (#3623) 2022-04-25 16:48:40 +03:00
Petri Palmu 83a15d737c test fix 2022-04-25 16:16:17 +03:00
Petri Palmu 68ee9bd31e Merge branch 'master' of github.com:UniversityOfHelsinkiCS/oodikone 2022-04-25 15:28:41 +03:00
Petri Palmu 1c4b8b3f33 Merge branch 'studyprograms' 2022-04-25 15:22:30 +03:00
Petri Palmu e065a64258 study programme courses moved behind admin status 2022-04-25 15:21:58 +03:00
SNurmivaara af5cb0e160 Do not show a term registration too early fix #3520 2022-04-25 15:06:54 +03:00
dependabot[bot]andGitHub f79aafa6cf Bump minimist from 1.2.5 to 1.2.6 in /services/frontend
Bumps [minimist](https://github.com/substack/minimist) from 1.2.5 to 1.2.6.
- [Release notes](https://github.com/substack/minimist/releases)
- [Commits](https://github.com/substack/minimist/compare/1.2.5...1.2.6)

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

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

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

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

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

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

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-04-25 11:23:22 +00:00
Petri Palmu 074942d4a5 is_open takes into account if attainment has no studyright 2022-04-25 14:21:49 +03:00
Mitja Karhusaari 92e402b0e6 Update years in page titles in trends 2022-04-25 11:59:04 +03:00
Mitja Karhusaari 515456feaf Better handling of Total-rows in sortable table 2022-04-25 11:49:26 +03:00
Mitja Karhusaari 1f3dce0f60 Show Empty only for null and undefined values in column filters 2022-04-25 11:21:13 +03:00
Mitja Karhusaari 5b21c246f8 Lower figure z-index to below that of modal's 2022-04-25 11:20:39 +03:00
Mitja Karhusaari 2be6a004e3 Fix [Object object] headers in export 2022-04-25 11:19:46 +03:00
Leo Varis 27e285c182 fix credit calculation again 2022-04-21 20:38:14 +03:00
SNurmivaara 6aeb78c90d Remove unused logging extras 2022-04-21 13:38:29 +03:00
Petri Palmu f8bc7606e8 studyprogramme courses backend structure with promise all done 2022-04-21 12:57:17 +03:00
SNurmivaara 3968a6407c Add granularity to updater sentry fingerprints 2022-04-21 12:36:54 +03:00
SNurmivaara 6467bdf555 Remove unnecessary conditional from updater sentry 2022-04-21 10:48:24 +03:00
SNurmivaara 677922a2cf Varied error messages for updater 2022-04-21 08:50:12 +03:00
Leo Varis 8e2e760ffc fix bug (#3623) 2022-04-20 23:00:53 +03:00
Mitja Karhusaari c606dd9040 Reduce the precision of the credit date filter to a day
Attempts to address #3535 even though I could not find anything that
triggered the bug
2022-04-20 15:16:50 +03:00
Mitja Karhusaari ea62047af5 Automatically include data up to the latest complete academic year in trends 2022-04-20 14:59:20 +03:00
Mitja Karhusaari 2990c6e903 Group credit columns together 2022-04-20 13:53:00 +03:00
Mitja Karhusaari c273cba4be Halve the credit target limit for the semester-view in credit distribution development 2022-04-20 10:27:07 +03:00
Roni Lindholm bef81b60fd Fix minor bugs reported by Sentry
Add missing return to send email method, fix broken months parameter
in total row of studyprogrammes by staring year.
2022-04-20 08:39:08 +03:00
Petri Palmu e314dd0647 firts route to programme courses made 2022-04-20 08:14:54 +03:00
Roni Lindholm eb87cff81e Better performance for courses of population
By default fetch stats only for mandatory courses and fetch it only
once.
2022-04-18 08:18:05 +03:00
Mitja Karhusaari 473d81eac0 Credit distribution development betterments
- Reversible stack ordering
- Green-Yellow-Red colorscheme from Norppa
- Better tooltips with percentages
- Fixed credit bucket bounds
- Exclude graduated students from statistics for timeslots after graduation
2022-04-13 22:49:17 +03:00
Roni Lindholm bcd8febcc8 Fix selectors in course stats tests 2022-04-13 15:44:24 +03:00
Roni Lindholm 401114e5c6 Try to fix tests
New sort in course stats
2022-04-13 14:43:52 +03:00
Roni Lindholm 49d237ada6 Sort years or semesters by code instead of name in course stats 2022-04-13 14:10:23 +03:00
Saara Satokangas 992eb51393 Show "data is complete"-message only for non-new programmes 2022-04-13 12:49:31 +03:00
Saara Satokangas 3d46f0d753 Update doctoral programmes too every night
And don't use redis for programmes that do not get their data
regularly updated
2022-04-13 12:48:33 +03:00
Saara Satokangas 7b2b760997 Search for licenciate-thesis or doctoral thesis for spec. programmes 2022-04-13 12:48:33 +03:00
Saara Satokangas 8401ba0d55 Add nodata-message 2022-04-13 12:48:33 +03:00
Saara Satokangas 9e3a1adcc7 Separate doctoral programmes into their own table 2022-04-13 12:48:33 +03:00
Roni Lindholm 1dee45c937 Enable tests for updater 2022-04-13 12:27:48 +03:00
Roni Lindholm 4c3f583712 Enrollment should not be required when fetching courses of population 2022-04-13 12:00:30 +03:00
Roni Lindholm d32608a0f2 Add new enrollments tab to courses of population
Include simple total students and enrolled no grade columns
2022-04-13 09:41:30 +03:00
Saara Satokangas c920729d57 Minor improvements 2022-04-12 18:37:16 +03:00
Saara Satokangas dae5c5181f Define correct provider code for doctoral programmes 2022-04-12 18:37:16 +03:00
Saara Satokangas 60ab9a849d Add new categories for credit progress for doctoral programmes 2022-04-12 18:37:16 +03:00
Saara Satokangas 8d6c8d71ef Don't show programmes before or after if they don't exist 2022-04-12 18:37:16 +03:00
Saara Satokangas 478722b1ad Change dates of the start date to match doctoral and spec. programmes 2022-04-12 18:37:16 +03:00
Saara Satokangas 2c1341fac7 Change color to blue for other than MH or KH programmes 2022-04-12 18:37:07 +03:00
Saara Satokangas 76ffc929b0 Improve studyright status infotext 2022-04-12 18:37:07 +03:00
Saara Satokangas 99dfb4ed63 Remove unnecessary year-mapping 2022-04-12 18:37:07 +03:00
Saara Satokangas 1d8db829cb Remove "NEW"-label from places that are not so new anymore 2022-04-12 18:37:07 +03:00
Saara Satokangas 5cb67f7f24 Hide credits-table if no data is found 2022-04-12 18:37:07 +03:00
Leo Varis d6d0824b6c add lääkis to credit calculation 2022-04-12 14:31:01 +03:00
Leo Varis 59b257fb90 fix async bug 2022-04-12 14:23:44 +03:00
Leo Varis d5e175f362 Merge branch 'master' of github.com:UniversityOfHelsinkiCS/oodikone 2022-04-12 14:11:18 +03:00
Leo Varis c2a2331013 fix total credit calculation 2022-04-12 14:11:04 +03:00
Petri Palmu fe99727a6f student statistics show open courses in name correctly 2022-04-12 13:53:51 +03:00
Leo Varis 12c076740a hops student page changes (#3534) 2022-04-12 13:37:57 +03:00
Leo Varis a8b4aa3f1f fix updater hops issue when student has graduated (#3586) 2022-04-12 13:37:34 +03:00
Roni Lindholm f0549eb4ac No tests for updater 2022-04-08 13:25:29 +03:00
Roni Lindholm eebbb2d1db Try to fix updater tests 2022-04-08 12:27:03 +03:00
Petri Palmu 389a0b8512 changed showByYear as default in trends 2022-04-06 21:16:25 +03:00
Petri Palmu 7775ef6c54 fix to population when selevting many years 2022-04-06 21:16:25 +03:00
Saara Satokangas 2e984bdb89 Filter out students without studentnumber 2022-04-06 10:57:23 +03:00
Roni Lindholm ab28becd97 Fix on first attempt and eventually column values in course stats 2022-04-06 09:33:41 +03:00
Saara Satokangas 5a5039318c Betterments to the new studyright-status-filter 2022-04-06 08:17:29 +03:00
Saara Satokangas 433dd15020 Remove last mentions of cancelled 2022-04-05 18:22:41 +03:00
Saara Satokangas a971433273 Add studyright status -filter 2022-04-05 18:16:26 +03:00
Saara Satokangas 314642c1b5 don't skip students with no studentnumber here 2022-04-05 17:16:38 +03:00
SNurmivaara 44b4a43f16 Remove unused sentry init 2022-04-05 16:10:55 +03:00
SNurmivaara f483a564e8 Refactor and consolidate updater logging 2022-04-05 15:57:50 +03:00
SNurmivaara c112c27d59 Add sentry to updater worker 2022-04-05 13:42:56 +03:00
Saara Satokangas fec8346766 Remove canceldate from studyrights in updater 2022-04-05 10:56:03 +03:00
Saara Satokangas 9d152cac10 Remove canceldate from backend studyright-model 2022-04-05 10:31:15 +03:00
Saara Satokangas 63fea99dfd Adjust filters-tests to match the removal of canceldate 2022-04-05 10:30:21 +03:00
Saara Satokangas 46bbd79a88 Include also the cancelled studyrights in the populations tests
The canceldate was used in tests which made the total amount of
CS-students to be 149. Now that populations no longer uses canceldate
and no student gets filtered out based on it, the correct number is 170.
2022-04-05 09:54:34 +03:00
Saara Satokangas 1e65c70210 Remove mentions of canceldate 2022-04-05 09:54:28 +03:00
Saara Satokangas 031d33561c Stop updating canceldate for studyrights 2022-04-05 09:19:07 +03:00
Saara Satokangas 11eab49a0f Skip students with no studentnumber 2022-04-05 08:59:17 +03:00
SNurmivaara 9b2b4bc6d8 Fix updater version number for sentry releases 2022-04-04 17:52:38 +03:00
SNurmivaara 29f94bb94e Add sentry steps to updater workflow 2022-04-04 17:32:44 +03:00
SNurmivaara 0310292531 Add sentry to updater scheduler 2022-04-04 17:32:44 +03:00
Mitja Karhusaari 6c9ac8cd2d Fix ResizeObserver issue in older Firefox versions 2022-04-04 17:28:27 +03:00
Mitja Karhusaari 5afe92d85e Add Distribution Development tab to population credit statistics 2022-04-04 17:08:08 +03:00
Leo Varis e7e19b3f1b make all credits column show acually all credits 2022-03-31 11:30:20 +03:00
Petri Palmu 6df4f8f8da fix ti TKT1 population if many years is selected 2022-03-31 08:34:56 +03:00
Leo Varis b1d6648f7f enable hops stuff again 2022-03-30 15:01:51 +03:00
Leo Varis 80e0668a3c bypass tests 2022-03-30 14:21:35 +03:00
Saara Satokangas 9b9a585057 Include different combinations to studytrack redis-updates 2022-03-30 08:27:22 +03:00
Leo Varis c396f5437a temp fix 2022-03-29 21:35:05 +03:00
Saara Satokangas 5149bb3a62 Change "peruutettu" to "passiivinen" like in Sisu 2022-03-29 15:31:05 +03:00
Saara Satokangas acd4a34048 Fix trends not showing inactive students 2022-03-29 15:31:05 +03:00
Mitja Karhusaari fba8d92c5a Some fixes to course statistics table settings 2022-03-29 15:18:13 +03:00
Mitja Karhusaari a888d171a2 Better handle invalid parameters to RangeSelector 2022-03-29 15:13:07 +03:00
Mitja Karhusaari e4c56e11b7 Fix filter combination tests 2022-03-29 15:03:47 +03:00
Mitja Karhusaari c938d2440d Use RangeSelector for the Age and Credit filters 2022-03-29 13:55:12 +03:00
Mitja Karhusaari a4b32129f1 Separate range selector widget into its own component 2022-03-29 13:55:12 +03:00
Mitja Karhusaari 7cb8d710d7 Better handle non-number input in range widget 2022-03-29 13:55:12 +03:00
Mitja Karhusaari 038ffefa84 Add option for reseting single columns filters 2022-03-29 13:55:12 +03:00
Mitja Karhusaari 2a8176ebe2 Use popper-based Popup for dropdowns so that they can flow outside of the overflow-hidden container 2022-03-29 13:55:12 +03:00
Mitja Karhusaari 2be93ef839 Avoid some renders using use-context-selector 2022-03-29 13:55:12 +03:00
Mitja Karhusaari 319a001c0e Remove dirty workaround 2022-03-29 13:55:12 +03:00
Mitja Karhusaari 831b00ae75 Semester-related RTK Query fixes 2022-03-29 13:55:12 +03:00
Mitja Karhusaari be923fa80d Avoid recreation of contents when fullscreening a figure 2022-03-29 13:55:12 +03:00
Mitja Karhusaari 596ebb66cb Allow mass-cycling filter values by pressing enter on the search field 2022-03-29 13:55:12 +03:00
Mitja Karhusaari 118315385e Use better vertical titles in PopulationStudents courses table and other small improvements 2022-03-29 13:55:12 +03:00
Mitja Karhusaari 192b1eb54b Add curried getTextIn to useLanguage 2022-03-29 13:55:11 +03:00
Mitja Karhusaari ea9a8da78d Use cellProps instead of removed getCellProps 2022-03-29 13:55:11 +03:00
Mitja Karhusaari bcd44aa644 Use SortableTable help text functionalty for the CourseStatistics attempts table 2022-03-29 13:55:11 +03:00
Mitja Karhusaari 6381363722 Fix positioning of column help text and force floating column tool mode for columns having help text 2022-03-29 13:55:11 +03:00
Saara Satokangas b8d075be42 Fix tests 2022-03-29 11:21:16 +03:00
Saara Satokangas b109accac8 Adjust infobox-texts 2022-03-29 11:07:09 +03:00
Saara Satokangas 0a98526f18 Revert changes in populations.js for now
For some reason tests fail after removal of the "CANCELLED"-field,
even though the real data looks the same in local and production
environments.
2022-03-29 11:07:09 +03:00
Saara Satokangas 0f6cf38e73 Remove cancelled-field from overview tests and replace it with inactive 2022-03-29 11:07:09 +03:00
Saara Satokangas 2df92ec068 Remove inactive-field from studyprogramme basics 2022-03-29 11:07:09 +03:00
Saara Satokangas dae7bbb058 Include also expired studyrights to inactive ones 2022-03-29 11:07:09 +03:00
Saara Satokangas ab121b7ec6 Remove inactive-selection from populations 2022-03-29 11:07:09 +03:00
Saara Satokangas a2df1944c6 Remove inactive-selection from advanced filters 2022-03-29 11:07:09 +03:00
Saara Satokangas 62821e2f3b Switch places of median and mean 2022-03-29 11:06:04 +03:00
Saara Satokangas 14bd681260 Unify the way inactives are defined 2022-03-29 11:06:04 +03:00
Saara Satokangas fc123276ff Start using the new inactive-field for populations 2022-03-29 11:06:04 +03:00
Saara Satokangas ed96ae4f95 Start using the new inactive-field in studyprogramme-overview 2022-03-29 11:06:04 +03:00
Leo Varis c0f01fb224 Merge branch 'master' of github.com:UniversityOfHelsinkiCS/oodikone 2022-03-28 16:08:26 +03:00
Leo Varis e50f631495 fix tests 2022-03-28 16:08:15 +03:00
Leo VarisandGitHub 2838099f0a Merge pull request #3549 from UniversityOfHelsinkiCS/studyplans
maybe fix tests
2022-03-28 15:12:43 +03:00
Leo Varis 9452ce08bd maybe fix tests 2022-03-28 15:05:47 +03:00
Leo VarisandGitHub 075385c962 Merge pull request #3547 from UniversityOfHelsinkiCS/studyplans
Studyplans
2022-03-28 14:03:07 +03:00
Saara Satokangas 87e6199093 Flatmap termregistrations 2022-03-28 13:38:53 +03:00
Leo Varis d47d0c22dd change title 2022-03-28 10:24:55 +03:00
Leo Varis 240edec696 revert dev value 2022-03-28 09:59:20 +03:00
Leo Varis 6dcdaae83e fix 2022-03-28 09:57:52 +03:00
Leo Varis 608670d65d add hops to study programme view (#3533) 2022-03-28 09:57:00 +03:00
Leo Varis 50ed56471c mangle hops to sis-db 2022-03-28 09:55:49 +03:00
Leo Varis 1d54d41861 add hops to study programme view (#3533) 2022-03-28 09:54:25 +03:00
Petri Palmu 0904703a57 changed laoding to loading and hope tests works 2022-03-25 18:28:26 +02:00
Roni Lindholm 8a4063486b Fix parsing of failed grades in totals 2022-03-25 12:59:32 +02:00
Matti Luukkainen 86cebeb628 presnet -> present 2022-03-25 12:04:19 +02:00
Matti Luukkainen 6770850822 rapodiff uncomment 2022-03-24 18:51:38 +02:00
Matti Luukkainen 133edbad89 eslint igonre 2022-03-24 17:12:45 +02:00
Petri Palmu e8556d0741 fix to the maxYearsToCreatePopulationFrom 2022-03-24 16:41:22 +02:00
Saara Satokangas b654a247ae Replace "ABSENT" with "NONATTENDING" 2022-03-24 14:15:20 +02:00
Roni Lindholm bba5c7e946 Fix grades on course stats attempts tab
Display all given grades for course.
2022-03-24 11:59:46 +02:00
Mitja Karhusaari 0cbd1f2e16 Work on making the tests happy 2022-03-24 01:41:02 +02:00
Mitja Karhusaari 7f79a95385 Use better slider library 2022-03-23 23:47:23 +02:00
Mitja Karhusaari d6901a40b3 Add hover effect to DateTimeSelector semester buttons 2022-03-23 22:27:32 +02:00
Mitja Karhusaari 46d786f9e5 Use RTK-Query for fetching semesters 2022-03-23 22:18:38 +02:00
Mitja Karhusaari e059ee3f93 Move column header info tooltip functionality to SortableTable so that it does not get overlaid by the column header tool icons 2022-03-23 20:50:44 +02:00
Mitja Karhusaari 4b164b1f2a Fix z-index on fullscreen figures 2022-03-23 19:34:28 +02:00
Mitja Karhusaari c44de1e2b5 Go through tables and tweak column settings 2022-03-23 19:34:23 +02:00
Mitja Karhusaari a3d65ab87b Fix Orientable not responding to prop change 2022-03-23 19:23:53 +02:00
Mitja Karhusaari 79eec49a51 Fix handling of non-number values in range filter 2022-03-23 19:23:52 +02:00
Mitja Karhusaari b15a7a2102 Fix dropdown disappearing on blur due to css cascade 2022-03-23 19:23:52 +02:00
Mitja Karhusaari 99abeae5ad Prevent oscillation between the column header tool states fixed and floating 2022-03-23 19:23:52 +02:00
Mitja Karhusaari 3b65e39e35 Add debouncing to the range slider 2022-03-23 19:23:52 +02:00
Mitja Karhusaari 1af0bd77e9 Fix rendering of the Slider component in Dropdowns 2022-03-23 19:23:52 +02:00
Mitja Karhusaari f51bdfe23c Fix SortableTable value extraction 2022-03-23 19:23:52 +02:00
Mitja Karhusaari 2d50786093 Fix column header tooling alignment 2022-03-23 19:23:52 +02:00
Mitja Karhusaari e8f899a96e Add debouncing to the range filter component 2022-03-23 19:23:52 +02:00
Mitja Karhusaari dd0f8f966d Implement vertical titles & smart positioning of filter and sort icons in SortableTable 2022-03-23 19:23:52 +02:00
Mitja Karhusaari 2d684d1f70 Implement range sider filter for SortableTable 2022-03-23 19:23:52 +02:00
Mitja Karhusaari ec2bbfb497 Use date-filter type for appropriate columns 2022-03-23 19:23:52 +02:00
Mitja Karhusaari a07365c1a6 Split visitors into separate files and implement date-filter type for SortableTable columns 2022-03-23 19:23:52 +02:00
Mitja Karhusaari 5e3605930d Add quick select for semester start and end dates to the datetime picker 2022-03-23 19:23:52 +02:00
Petri Palmu 5ce24443f2 one fix test 2022-03-23 11:30:10 +02:00
Petri Palmu 9e825f9915 more fix to the tests 2022-03-23 10:34:14 +02:00
Petri Palmu 69c7f02dca little correction to the tests 2022-03-23 09:56:38 +02:00
Petri Palmu bf2c3638d3 selection between open and uni courses works if you drill into spesific year 2022-03-23 09:15:05 +02:00
Saara Satokangas d4ccc7a4dc Fix tests
By skipping some acually broken for now
2022-03-22 20:21:31 +02:00
Saara Satokangas 2bcafbabfd Try fixing filtering of students 2022-03-22 20:10:18 +02:00
Saara Satokangas bc5d44f18f Fix studytrack-filter showing relevant studytracks 2022-03-22 18:50:43 +02:00
Saara Satokangas 51b9e23693 Get correct populationlink also for unfolded studytrack 2022-03-22 18:49:39 +02:00
Saara Satokangas 75f0dca9f1 Add notification about all credits, not just studyprogramme credits 2022-03-22 18:49:39 +02:00
Saara Satokangas b802f92420 Change title of a test set 2022-03-22 18:49:39 +02:00
Matti Luukkainen bf54330c12 Merge branch 'master' of github.com:UniversityOfHelsinkiCS/oodikone 2022-03-22 17:40:41 +02:00
Matti Luukkainen aba30641b2 rapodiff for bsc 2022-03-22 17:36:07 +02:00
Saara Satokangas 5dc06a8f95 Fix tests
Include also the total-line
2022-03-22 14:10:03 +02:00
Saara Satokangas 8dad3ef620 Remove unnecessary useState 2022-03-22 13:55:51 +02:00
Saara Satokangas e23774023d Fix total-row population-link
Closes #https://github.com/UniversityOfHelsinkiCS/oodikone/issues/3517
2022-03-22 13:45:54 +02:00
Saara Satokangas abd31bc807 Use the filtered studytracklist also in case where no result
If the population is searched for a studytrack, and there are no
students who has that studytrack as their latest, still use that empty
list. Instead of showing the unfiltered list like before.
2022-03-22 13:45:54 +02:00
Saara Satokangas 120a6fbc5b Take the latest studytrack based on the startdate in the track
Not the enddate of the studyright
2022-03-22 13:45:54 +02:00
Saara Satokangas a6bc58b0fc Include total-row to the current tables and graphs, remove toggle 2022-03-22 13:45:54 +02:00
Roni Lindholm 4baf6d8509 Display enrollments in course stats based on yearcode instead of hack 2022-03-21 15:40:54 +02:00
Roni Lindholm f263913c03 Refine enrollments in course stats
Calculate  stats for normal view and enrollments view.
2022-03-21 15:17:24 +02:00
Saara Satokangas e9f6eb744f Adjust tests to match the new credit progress categories 2022-03-21 09:39:30 +02:00
Saara Satokangas cd6ef17a10 Add categories >= 300 and >= 180 to credit progress charts 2022-03-21 09:18:50 +02:00
Saara Satokangas f6272a0df0 Add active-field to backend studyright-model 2022-03-21 09:01:46 +02:00
Leo Varis 9b385afb36 mangle hops to sis-db 2022-03-17 17:02:12 +02:00
Matti Luukkainen b7a2d67bcb rapo diff script betterments 2022-03-15 17:41:45 +02:00
306 changed files with 71851 additions and 13174 deletions
+2 -1
View File
@@ -14,6 +14,7 @@
"no-console": "error",
"react/no-unescaped-entities": "off",
"no-async-promise-executor": "off",
"prettier/prettier": "error"
"prettier/prettier": "error",
"cypress/no-assigning-return-values": "off"
}
}
+52
View File
@@ -0,0 +1,52 @@
name: 'Run tests'
description: 'Runs Cypress tests for individual spec'
inputs:
username:
required: true
description: 'Toska registry username'
password:
required: true
description: 'Toska registry password'
spec:
required: true
description: 'Path to Cypress file'
runs:
using: composite
steps:
- name: 'Download images'
uses: actions/download-artifact@v3
with:
path: /tmp/
- name: 'Load built images'
shell: bash
run: |
docker load --input /tmp/artifact/backend.tar
docker load --input /tmp/artifact/frontend.tar
- name: 'Login to toska docker registry'
uses: docker/login-action@v2
with:
registry: registry.toska.cs.helsinki.fi
username: ${{ inputs.username }}
password: ${{ inputs.password }}
- name: 'Run services, pull rest of the needed images'
shell: bash
run: docker-compose --file docker-compose.ci.yml up --detach --no-recreate
- name: 'Run cypress'
uses: cypress-io/github-action@v4
with:
browser: chrome
spec: ${{ inputs.spec }}
env:
CYPRESS_baseUrl: http://localhost:1337
- name: 'Upload video if tests fail'
uses: actions/upload-artifact@v3
if: failure()
with:
name: Cypress videos
path: ./cypress/videos/*
- name: 'Print logs on failure'
shell: bash
if: failure()
run: docker-compose --file docker-compose.ci.yml logs
+21
View File
@@ -8,6 +8,10 @@ updates:
labels:
- 'dependencies'
- 'infra'
ignore:
- dependency-name: '*'
update-types: ['version-update:semver-major']
- package-ecosystem: npm
directory: '/updater/sis-updater-scheduler'
schedule:
@@ -16,6 +20,10 @@ updates:
labels:
- 'dependencies'
- 'updater'
ignore:
- dependency-name: '*'
update-types: ['version-update:semver-major']
- package-ecosystem: npm
directory: '/updater/sis-updater-worker'
schedule:
@@ -24,6 +32,10 @@ updates:
labels:
- 'dependencies'
- 'updater'
ignore:
- dependency-name: '*'
update-types: ['version-update:semver-major']
- package-ecosystem: npm
directory: '/services/backend'
schedule:
@@ -32,6 +44,10 @@ updates:
labels:
- 'dependencies'
- 'back'
ignore:
- dependency-name: '*'
update-types: ['version-update:semver-major']
- package-ecosystem: npm
directory: '/services/frontend'
schedule:
@@ -40,8 +56,13 @@ updates:
labels:
- 'dependencies'
- 'front'
ignore:
- dependency-name: '*'
update-types: ['version-update:semver-major']
- package-ecosystem: 'github-actions'
directory: '/'
schedule:
interval: weekly
time: '03:00'
open-pull-requests-limit: 10
+11 -11
View File
@@ -9,10 +9,10 @@ jobs:
name: 'Build and release backend'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: docker/setup-buildx-action@v1
- uses: actions/checkout@v3
- uses: docker/setup-buildx-action@v2
- name: 'Login to DockerHub'
uses: docker/login-action@v1
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
@@ -21,9 +21,9 @@ jobs:
# This steps extracts release version from GITHUB_REF env var
# See: https://github.community/t/how-to-get-just-the-tag-name/16241/10
id: extract_version
run: echo ::set-output name=VERSION::"${GITHUB_REF#refs/tags/}"
run: echo "VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT
- name: 'Build and push'
uses: docker/build-push-action@v2
uses: docker/build-push-action@v3
with:
build-args: |
SENTRY_RELEASE=${{ steps.extract_version.outputs.VERSION }}
@@ -41,24 +41,24 @@ jobs:
name: 'Build and release frontend'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: docker/setup-buildx-action@v1
- uses: actions/checkout@v3
- uses: docker/setup-buildx-action@v2
- name: 'Login to DockerHub'
uses: docker/login-action@v1
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: 'Get current time'
id: get_current_time
run: echo ::set-output name=NOW::"$(date --iso-8601=seconds)"
run: echo "NOW=$(date --iso-8601=seconds)" >> $GITHUB_OUTPUT
- name: 'Extract release version'
# Github context (e.g. github.ref) only has ref formatted as /refs/tags/vX.X.X
# This steps extracts release version from GITHUB_REF env var
# See: https://github.community/t/how-to-get-just-the-tag-name/16241/10
id: extract_version
run: echo ::set-output name=VERSION::"${GITHUB_REF#refs/tags/}"
run: echo "VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT
- name: 'Build and push'
uses: docker/build-push-action@v2
uses: docker/build-push-action@v3
with:
build-args: |
SENTRY_RELEASE=${{ steps.extract_version.outputs.VERSION }}
+40 -16
View File
@@ -14,11 +14,14 @@ jobs:
name: 'Build sis-updater-worker'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: docker/setup-buildx-action@v1
- uses: actions/checkout@v3
- uses: docker/setup-buildx-action@v2
- name: 'Build the image'
uses: docker/build-push-action@v2
uses: docker/build-push-action@v3
with:
build-args: |
SENTRY_RELEASE=${{ github.sha }}
SENTRY_ENVIRONMENT=production
context: updater/sis-updater-worker/
target: production
push: false
@@ -27,7 +30,7 @@ jobs:
cache-from: type=registry,ref=toska/sis-updater-worker:latest
cache-to: type=inline
- name: 'Store built image'
uses: actions/upload-artifact@v2
uses: actions/upload-artifact@v3
with:
path: /tmp/worker.tar
@@ -35,11 +38,14 @@ jobs:
name: 'Build sis-updater-scheduler'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: docker/setup-buildx-action@v1
- uses: actions/checkout@v3
- uses: docker/setup-buildx-action@v2
- name: 'Build the image'
uses: docker/build-push-action@v2
uses: docker/build-push-action@v3
with:
build-args: |
SENTRY_RELEASE=${{ github.sha }}
SENTRY_ENVIRONMENT=production
context: updater/sis-updater-scheduler/
target: production
push: false
@@ -48,7 +54,7 @@ jobs:
cache-from: type=registry,ref=toska/sis-updater-scheduler:latest
cache-to: type=inline
- name: 'Store built image'
uses: actions/upload-artifact@v2
uses: actions/upload-artifact@v3
with:
path: /tmp/scheduler.tar
@@ -59,15 +65,15 @@ jobs:
- build_worker
- build_scheduler
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- name: 'Login to toska docker registry'
uses: docker/login-action@v1
uses: docker/login-action@v2
with:
registry: registry.toska.cs.helsinki.fi
username: ${{ secrets.TOSKAREGISTRY_USERNAME }}
password: ${{ secrets.TOSKAREGISTRY_PASSWORD }}
- name: 'Download built images'
uses: actions/download-artifact@v2
uses: actions/download-artifact@v3
with:
path: /tmp/
- name: 'Load worker image'
@@ -81,6 +87,11 @@ jobs:
env:
SCHEDULE_IMMEDIATE: hourly
EXIT_AFTER_IMMEDIATES: yes
- name: 'Run daily jobs'
run: docker-compose --file docker-compose.ci.yml run sis-updater-scheduler npm start
env:
SCHEDULE_IMMEDIATE: daily
EXIT_AFTER_IMMEDIATES: yes
- name: 'Run weekly jobs'
run: docker-compose --file docker-compose.ci.yml run sis-updater-scheduler npm start
env:
@@ -103,15 +114,15 @@ jobs:
needs:
- test_updater
steps:
- uses: actions/checkout@v2
- uses: docker/setup-buildx-action@v1
- uses: actions/checkout@v3
- uses: docker/setup-buildx-action@v2
- name: 'Login to DockerHub'
uses: docker/login-action@v1
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: 'Build and push scheduler'
uses: docker/build-push-action@v2
uses: docker/build-push-action@v3
with:
cache-from: type=registry,ref=toska/sis-updater-scheduler:latest
cache-to: type=inline
@@ -120,7 +131,7 @@ jobs:
tags: toska/sis-updater-scheduler:latest
target: production
- name: 'Build and push worker'
uses: docker/build-push-action@v2
uses: docker/build-push-action@v3
with:
cache-from: type=registry,ref=toska/sis-updater-worker:latest
cache-to: type=inline
@@ -128,6 +139,19 @@ jobs:
push: true
tags: toska/sis-updater-worker:latest
target: production
- name: 'Create Sentry release'
uses: getsentry/action-release@v1
env:
SENTRY_URL: ${{ secrets.SENTRY_URL }}
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
SENTRY_ORG: toska
SENTRY_PROJECT: updater
with:
environment: production
set_commits: 'skip'
sourcemaps: './build/static/js'
url_prefix: '~/static/js'
version: ${{ github.sha }}
- name: 'Send notification to slack'
uses: UniversityOfHelsinkiCS/slackbot-action@v1.4.2
with:
+79 -239
View File
@@ -6,14 +6,16 @@ on:
- master
paths:
- '.github/workflows/test-oodikone.yaml'
- '.github/actions/**'
- 'cypress/**'
- 'cypress.json'
- 'cypress.config.json'
- 'services/**'
- '!services/backend/src/rapodiff.js'
- 'docker-compose.ci.yml'
- 'docker-compose.test.yml'
- 'nginx.conf'
- 'package*.json'
- 'updater/sis-updater-worker/src/db/migrations/**'
jobs:
# === Static tests / linting ===
@@ -22,8 +24,8 @@ jobs:
name: 'Lint backend'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
- uses: actions/checkout@v3
- uses: actions/setup-node@v3.4.1
with:
node-version: 14
- name: 'Install dependencies'
@@ -35,8 +37,8 @@ jobs:
name: 'Lint frontend'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
- uses: actions/checkout@v3
- uses: actions/setup-node@v3.4.1
with:
node-version: 14
- name: 'Install dependencies'
@@ -51,10 +53,10 @@ jobs:
name: 'Build backend'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: docker/setup-buildx-action@v1
- uses: actions/checkout@v3
- uses: docker/setup-buildx-action@v2
- name: 'Build image and export it'
uses: docker/build-push-action@v2
uses: docker/build-push-action@v3
with:
build-args: |
SENTRY_RELEASE=${{ github.sha }}
@@ -69,7 +71,7 @@ jobs:
target: production
outputs: type=docker,dest=/tmp/backend.tar
- name: 'Upload image for other jobs'
uses: actions/upload-artifact@v2
uses: actions/upload-artifact@v3
with:
path: /tmp/backend.tar
@@ -77,13 +79,13 @@ jobs:
name: 'Build frontend'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: docker/setup-buildx-action@v1
- uses: actions/checkout@v3
- uses: docker/setup-buildx-action@v2
- name: 'Get current time'
id: get_current_time
run: echo ::set-output name=NOW::"$(date --iso-8601=seconds)"
run: echo "NOW=$(date --iso-8601=seconds)" >> $GITHUB_OUTPUT
- name: 'Build image and export it'
uses: docker/build-push-action@v2
uses: docker/build-push-action@v3
with:
build-args: |
SENTRY_RELEASE=${{ github.sha }}
@@ -99,7 +101,7 @@ jobs:
target: production
outputs: type=docker,dest=/tmp/frontend.tar
- name: 'Upload image for other jobs'
uses: actions/upload-artifact@v2
uses: actions/upload-artifact@v3
with:
path: /tmp/frontend.tar
@@ -107,7 +109,7 @@ jobs:
name: 'Build anon sis-db and push to toska hub if new migrations have been added'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- uses: dorny/paths-filter@v2
id: check_migrations
with:
@@ -116,7 +118,7 @@ jobs:
- 'updater/sis-updater-worker/src/db/migrations/**'
- '.github/workflows/test-oodikone.yaml'
- name: 'Login to toska docker registry'
uses: docker/login-action@v1
uses: docker/login-action@v2
with:
registry: registry.toska.cs.helsinki.fi
username: ${{ secrets.TOSKAREGISTRY_USERNAME }}
@@ -134,7 +136,7 @@ jobs:
- build_backend
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- uses: dorny/paths-filter@v2
id: check_migrations
with:
@@ -143,13 +145,13 @@ jobs:
- 'services/backend/src/database/migrations_kone/**'
- '.github/workflows/test-oodikone.yaml'
- name: 'Download images'
uses: actions/download-artifact@v2
uses: actions/download-artifact@v3
with:
path: /tmp/
- name: 'Load built backend image'
run: docker load --input /tmp/artifact/backend.tar
- name: 'Login to toska docker registry'
uses: docker/login-action@v1
uses: docker/login-action@v2
with:
registry: registry.toska.cs.helsinki.fi
username: ${{ secrets.TOSKAREGISTRY_USERNAME }}
@@ -167,7 +169,7 @@ jobs:
- build_backend
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- uses: dorny/paths-filter@v2
id: check_migrations
with:
@@ -176,13 +178,13 @@ jobs:
- 'services/backend/src/database/migrations_user/**'
- '.github/workflows/test-oodikone.yaml'
- name: 'Download images'
uses: actions/download-artifact@v2
uses: actions/download-artifact@v3
with:
path: /tmp/
- name: 'Load built backend image'
run: docker load --input /tmp/artifact/backend.tar
- name: 'Login to toska docker registry'
uses: docker/login-action@v1
uses: docker/login-action@v2
with:
registry: registry.toska.cs.helsinki.fi
username: ${{ secrets.TOSKAREGISTRY_USERNAME }}
@@ -205,39 +207,13 @@ jobs:
- build_and_push_anon_user_db
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: 'Download images'
uses: actions/download-artifact@v2
- uses: actions/checkout@v3
- name: 'Run tests'
uses: ./.github/actions/run-tests
with:
path: /tmp/
- name: 'Load built images'
run: |
docker load --input /tmp/artifact/backend.tar
docker load --input /tmp/artifact/frontend.tar
- name: 'Login to toska docker registry'
uses: docker/login-action@v1
with:
registry: registry.toska.cs.helsinki.fi
username: ${{ secrets.TOSKAREGISTRY_USERNAME }}
password: ${{ secrets.TOSKAREGISTRY_PASSWORD }}
- name: 'Run services, pull rest of the needed images'
run: docker-compose --file docker-compose.ci.yml up --detach --no-recreate
- name: 'Run cypress'
uses: cypress-io/github-action@v2
with:
browser: chrome
spec: cypress/integration/Course_statistics.js
env:
CYPRESS_baseUrl: http://localhost:1337
- name: 'Upload video if tests fail'
uses: actions/upload-artifact@v2
if: failure()
with:
name: Cypress videos
path: ./cypress/videos/*
- name: 'Print logs on failure'
if: failure()
run: docker-compose --file docker-compose.ci.yml logs
spec: cypress/e2e/Course_statistics.js
test_custom_population:
name: 'Test custom population'
@@ -249,39 +225,13 @@ jobs:
- build_and_push_anon_user_db
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: 'Download images'
uses: actions/download-artifact@v2
- uses: actions/checkout@v3
- name: 'Run tests'
uses: ./.github/actions/run-tests
with:
path: /tmp/
- name: 'Load built images'
run: |
docker load --input /tmp/artifact/backend.tar
docker load --input /tmp/artifact/frontend.tar
- name: 'Login to toska docker registry'
uses: docker/login-action@v1
with:
registry: registry.toska.cs.helsinki.fi
username: ${{ secrets.TOSKAREGISTRY_USERNAME }}
password: ${{ secrets.TOSKAREGISTRY_PASSWORD }}
- name: 'Run services, pull rest of the needed images'
run: docker-compose --file docker-compose.ci.yml up --detach --no-recreate
- name: 'Run cypress'
uses: cypress-io/github-action@v2
with:
browser: chrome
spec: cypress/integration/Custom_population.js
env:
CYPRESS_baseUrl: http://localhost:1337
- name: 'Upload video if tests fail'
uses: actions/upload-artifact@v2
if: failure()
with:
name: Cypress videos
path: ./cypress/videos/*
- name: 'Print logs on failure'
if: failure()
run: docker-compose --file docker-compose.ci.yml logs
spec: cypress/e2e/Custom_population.js
test_population_statistics:
name: 'Test population statistics'
@@ -293,39 +243,13 @@ jobs:
- build_and_push_anon_user_db
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: 'Download images'
uses: actions/download-artifact@v2
- uses: actions/checkout@v3
- name: 'Run tests'
uses: ./.github/actions/run-tests
with:
path: /tmp/
- name: 'Load built images'
run: |
docker load --input /tmp/artifact/backend.tar
docker load --input /tmp/artifact/frontend.tar
- name: 'Login to toska docker registry'
uses: docker/login-action@v1
with:
registry: registry.toska.cs.helsinki.fi
username: ${{ secrets.TOSKAREGISTRY_USERNAME }}
password: ${{ secrets.TOSKAREGISTRY_PASSWORD }}
- name: 'Run services, pull rest of the needed images'
run: docker-compose --file docker-compose.ci.yml up --detach --no-recreate
- name: 'Run cypress'
uses: cypress-io/github-action@v2
with:
browser: chrome
spec: cypress/integration/Population_statistics.js
env:
CYPRESS_baseUrl: http://localhost:1337
- name: 'Upload video if tests fail'
uses: actions/upload-artifact@v2
if: failure()
with:
name: Cypress videos
path: ./cypress/videos/*
- name: 'Print logs on failure'
if: failure()
run: docker-compose --file docker-compose.ci.yml logs
spec: cypress/e2e/Population_statistics.js
test_filters:
name: 'Test filters'
@@ -337,39 +261,13 @@ jobs:
- build_and_push_anon_user_db
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: 'Download images'
uses: actions/download-artifact@v2
- uses: actions/checkout@v3
- name: 'Run tests'
uses: ./.github/actions/run-tests
with:
path: /tmp/
- name: 'Load built images'
run: |
docker load --input /tmp/artifact/backend.tar
docker load --input /tmp/artifact/frontend.tar
- name: 'Login to toska docker registry'
uses: docker/login-action@v1
with:
registry: registry.toska.cs.helsinki.fi
username: ${{ secrets.TOSKAREGISTRY_USERNAME }}
password: ${{ secrets.TOSKAREGISTRY_PASSWORD }}
- name: 'Run services, pull rest of the needed images'
run: docker-compose --file docker-compose.ci.yml up --detach --no-recreate
- name: 'Run cypress'
uses: cypress-io/github-action@v2
with:
browser: chrome
spec: cypress/integration/Filters.js
env:
CYPRESS_baseUrl: http://localhost:1337
- name: 'Upload video if tests fail'
uses: actions/upload-artifact@v2
if: failure()
with:
name: Cypress videos
path: ./cypress/videos/*
- name: 'Print logs on failure'
if: failure()
run: docker-compose --file docker-compose.ci.yml logs
spec: cypress/e2e/Filters.js
test_student_statistics:
name: 'Test student statistics'
@@ -381,39 +279,13 @@ jobs:
- build_and_push_anon_user_db
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: 'Download images'
uses: actions/download-artifact@v2
- uses: actions/checkout@v3
- name: 'Run tests'
uses: ./.github/actions/run-tests
with:
path: /tmp/
- name: 'Load built images'
run: |
docker load --input /tmp/artifact/backend.tar
docker load --input /tmp/artifact/frontend.tar
- name: 'Login to toska docker registry'
uses: docker/login-action@v1
with:
registry: registry.toska.cs.helsinki.fi
username: ${{ secrets.TOSKAREGISTRY_USERNAME }}
password: ${{ secrets.TOSKAREGISTRY_PASSWORD }}
- name: 'Run services, pull rest of the needed images'
run: docker-compose --file docker-compose.ci.yml up --detach --no-recreate
- name: 'Run cypress'
uses: cypress-io/github-action@v2
with:
browser: chrome
spec: cypress/integration/Student_statistics.js
env:
CYPRESS_baseUrl: http://localhost:1337
- name: 'Upload video if tests fail'
uses: actions/upload-artifact@v2
if: failure()
with:
name: Cypress videos
path: ./cypress/videos/*
- name: 'Print logs on failure'
if: failure()
run: docker-compose --file docker-compose.ci.yml logs
spec: cypress/e2e/Student_statistics.js
test_studyprogramme_overview:
name: 'Test studyprogramme overview'
@@ -425,39 +297,13 @@ jobs:
- build_and_push_anon_user_db
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: 'Download images'
uses: actions/download-artifact@v2
- uses: actions/checkout@v3
- name: 'Run tests'
uses: ./.github/actions/run-tests
with:
path: /tmp/
- name: 'Load built images'
run: |
docker load --input /tmp/artifact/backend.tar
docker load --input /tmp/artifact/frontend.tar
- name: 'Login to toska docker registry'
uses: docker/login-action@v1
with:
registry: registry.toska.cs.helsinki.fi
username: ${{ secrets.TOSKAREGISTRY_USERNAME }}
password: ${{ secrets.TOSKAREGISTRY_PASSWORD }}
- name: 'Run services, pull rest of the needed images'
run: docker-compose --file docker-compose.ci.yml up --detach --no-recreate
- name: 'Run cypress'
uses: cypress-io/github-action@v2
with:
browser: chrome
spec: cypress/integration/Studyprogramme_overview.js
env:
CYPRESS_baseUrl: http://localhost:1337
- name: 'Upload video if tests fail'
uses: actions/upload-artifact@v2
if: failure()
with:
name: Cypress videos
path: ./cypress/videos/*
- name: 'Print logs on failure'
if: failure()
run: docker-compose --file docker-compose.ci.yml logs
spec: cypress/e2e/Studyprogramme_overview.js
test_teachers_and_users:
name: 'Test teachers and users'
@@ -469,41 +315,33 @@ jobs:
- build_and_push_anon_user_db
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: 'Download images'
uses: actions/download-artifact@v2
- uses: actions/checkout@v3
- name: 'Run tests'
uses: ./.github/actions/run-tests
with:
path: /tmp/
- name: 'Load built images'
run: |
docker load --input /tmp/artifact/backend.tar
docker load --input /tmp/artifact/frontend.tar
- name: 'Login to toska docker registry'
uses: docker/login-action@v1
with:
registry: registry.toska.cs.helsinki.fi
username: ${{ secrets.TOSKAREGISTRY_USERNAME }}
password: ${{ secrets.TOSKAREGISTRY_PASSWORD }}
- name: 'Run services, pull rest of the needed images'
run: docker-compose --file docker-compose.ci.yml up --detach --no-recreate
- name: 'Run cypress'
uses: cypress-io/github-action@v2
with:
browser: chrome
spec: |
cypress/integration/Teachers.js
cypress/integration/Users.js
env:
CYPRESS_baseUrl: http://localhost:1337
- name: 'Upload video if tests fail'
uses: actions/upload-artifact@v2
if: failure()
cypress/e2e/Teachers.js
cypress/e2e/Users.js
test_faculty_statistics:
name: 'Test faculty statistics'
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:
name: Cypress videos
path: ./cypress/videos/*
- name: 'Print logs on failure'
if: failure()
run: docker-compose --file docker-compose.ci.yml logs
username: ${{ secrets.TOSKAREGISTRY_USERNAME }}
password: ${{ secrets.TOSKAREGISTRY_PASSWORD }}
spec: cypress/e2e/Faculty_statistics.js
# === Other tests ===
@@ -517,9 +355,9 @@ jobs:
- build_and_push_anon_user_db
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- name: 'Download images'
uses: actions/download-artifact@v2
uses: actions/download-artifact@v3
with:
path: /tmp/
- name: 'Load built images'
@@ -527,7 +365,7 @@ jobs:
docker load --input /tmp/artifact/backend.tar
docker load --input /tmp/artifact/frontend.tar
- name: 'Login to toska docker registry'
uses: docker/login-action@v1
uses: docker/login-action@v2
with:
registry: registry.toska.cs.helsinki.fi
username: ${{ secrets.TOSKAREGISTRY_USERNAME }}
@@ -560,11 +398,12 @@ jobs:
- test_student_statistics
- test_studyprogramme_overview
- test_teachers_and_users
- test_faculty_statistics
- test_service_exit_statuses
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- name: 'Download images'
uses: actions/download-artifact@v2
uses: actions/download-artifact@v3
with:
path: /tmp/
- name: 'Load built images'
@@ -572,7 +411,7 @@ jobs:
docker load --input /tmp/artifact/backend.tar
docker load --input /tmp/artifact/frontend.tar
- name: 'Login to DockerHub'
uses: docker/login-action@v1
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
@@ -626,10 +465,11 @@ jobs:
- test_student_statistics
- test_studyprogramme_overview
- test_teachers_and_users
- test_faculty_statistics
- test_service_exit_statuses
if: failure()
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- name: 'Send notification to slack'
uses: UniversityOfHelsinkiCS/slackbot-action@v1.4.2
with:
+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
+1 -1
View File
@@ -9,7 +9,7 @@ module.exports = {
'*.css': files => `stylelint --fix ${files.join(' ')}`,
Dockerfile: files => `${dockerCmdBase} hadolint/hadolint:v2.7.0 hadolint ${relativeFilePaths(files)}`,
'*.sh': files => `${dockerCmdBase} koalaman/shellcheck:v0.7.2 ${relativeFilePaths(files)} -x`,
'.github/workflows/*': files => `${dockerCmdBase} rhysd/actionlint:1.6.2 ${relativeFilePaths(files)}`,
'.github/{workflows,actions}/*': files => `npm run actionlint ${relativeFilePaths(files)}`,
'docker-compose*': files => {
const composeFiles = file => {
if (['docker-compose.ci.yml', 'docker-compose.test.yml'].some(f => file.includes(f))) {
+1 -1
View File
@@ -5,5 +5,5 @@
"printWidth": 120,
"tabWidth": 2,
"useTabs": false,
"jsxSingleQuote": false,
"jsxSingleQuote": false
}
+5 -1
View File
@@ -13,6 +13,10 @@
"ignorePseudoClasses": ["global"]
}
],
"prettier/prettier": true
"prettier/prettier": true,
"selector-class-pattern": null,
"custom-property-pattern": null,
"value-no-vendor-prefix": null,
"property-no-vendor-prefix": null
}
}
+1 -1
View File
@@ -108,7 +108,7 @@ There are three types of tests in this project: static tests with eslint/prettie
- Cypress
- can be launched in interactive mode with `npm run cypress open`. `package.json` defines entrypoint `npm run cypress` so you can basically run cypress with any arguments you want
- are defined in cypress -folder and cypress.json
- are defined in cypress -folder and cypress.config.json
- whole cypress test stack takes about 15 to 20 mins to run. Since tests are ran in our Github actions CI pipe, you're encouraged to take advantage of this instead of running all tests locally.
- There are some different user types and cypress commands defined for testing. Take a look at these when debugging tests.
- Jest
+19
View File
@@ -0,0 +1,19 @@
const { defineConfig } = require('cypress')
module.exports = defineConfig({
projectId: 'c3jsph',
defaultCommandTimeout: 30000,
requestTimeout: 30000,
videoCompression: false,
viewportWidth: 1800,
viewportHeight: 1200,
e2e: {
// We've imported your old cypress plugins here.
// You may want to clean this up later by importing these.
setupNodeEvents(on, config) {
return require('./cypress/plugins/index.js')(on, config)
},
baseUrl: 'http://localhost:3000',
specPattern: 'cypress/e2e/**/*.{js,jsx,ts,tsx}',
},
})
-7
View File
@@ -1,7 +0,0 @@
{
"projectId": "c3jsph",
"baseUrl": "http://localhost:3000",
"defaultCommandTimeout": 30000,
"requestTimeout": 30000,
"videoCompression": false
}
@@ -82,8 +82,8 @@ describe('Course Statistics tests', () => {
cy.contains('Search for courses')
cy.get("input[placeholder='Search by a course code']").type(courseCode)
cy.contains('td', courseCode).click()
cy.contains('Separate statistics for Spring and Fall semesters').click()
cy.contains('Fetch statistics').should('be.enabled').click()
cy.get('[data-cy=separateToggle]', { force: true }).click({ force: true })
cy.contains('Syksy 2019')
cy.contains('Syksy 2017')
cy.get('tbody').contains('19')
@@ -155,8 +155,8 @@ describe('Course Statistics tests', () => {
cy.contains('Käyttöjärjestelmät')
cy.contains('TKT20003')
cy.get(':nth-child(3) > :nth-child(1) > div > .item > .level').click()
cy.contains('Population of course Unified with open uni: Käyttöjärjestelmät 2019-2020')
cy.get('tbody > :nth-child(3) > :nth-child(2) .level').click()
cy.contains('Population of course Käyttöjärjestelmät 2019-2020 (open and normal')
cy.contains('TKT20003')
cy.contains('Students (127)').click()
@@ -169,8 +169,8 @@ describe('Course Statistics tests', () => {
cy.contains(/^TKT20001, AYTKT20001$/).click()
cy.contains('Fetch statistics').should('be.enabled').click()
cy.contains('TKT20001, 58131, AYTKT20001 Tietorakenteet ja algoritmit')
cy.get(':nth-child(3) > :nth-child(1) > div > .item > .level').click()
cy.contains('Population of course Unified with open uni: Tietorakenteet ja algoritmit 2018-2019')
cy.get('tbody >:nth-child(3) > :nth-child(2) .level').click()
cy.contains('Population of course Tietorakenteet ja algoritmit 2018-2019 (open and normal)')
cy.contains('Students (204)').click()
cy.contains('td', '010262566').siblings().contains('4')
cy.contains('td', '010674989').siblings().contains('1')
@@ -182,7 +182,7 @@ describe('Course Statistics tests', () => {
cy.contains('Fetch statistics').should('be.enabled').click()
cy.contains('TKT20003, 582219 Käyttöjärjestelmät')
cy.get('[data-cy=unify_radio_reqular]').click()
cy.get(':nth-child(3) > :nth-child(1) > div > .item > .level').click()
cy.get('tbody > :nth-child(3) > :nth-child(2) .level').click()
cy.contains('Population of course Käyttöjärjestelmät 2019-2020')
cy.contains('Language distribution').click()
cy.contains('td', 'finnish').siblings().contains('122')
@@ -204,28 +204,28 @@ describe('Course Statistics tests', () => {
const yearRange = { from: '2000-2001', to: '2020-2021' }
const attemptsTableContents = [
// [time, passed, failed, passrate]
['Total', 511, 486, 25],
['Total', null, 511, 486, 25],
]
const attemptsTableContentsOpen = [
// [time, passed, failed, passrate]
['Total', 175, 175, 0],
['Total', null, 175, 175, 0],
]
const attemptsTableContentsReqular = [
// [time, passed, failed, passrate]
['Total', 312, 298, 14],
['Total', null, 312, 298, 14],
]
const gradesTableContents = [
// [time, attempts, 0, 1, 2, 3, 4, 5, other passed]
['Total', 511, 25, 11, 17, 21, 59, 365, 13],
['2020-2021', 1, 0, 0, 0, 0, 0, 1, 0],
['2019-2020', 54, 4, 3, 1, 3, 4, 39, 0],
['2018-2019', 154, 7, 2, 6, 5, 15, 119, 0],
['2017-2018', 186, 9, 3, 5, 6, 16, 139, 8],
['2016-2017', 71, 2, 1, 2, 5, 13, 48, 0],
['2015-2016', 21, 1, 1, 1, 0, 7, 8, 3],
['Total', null, 511, 25, 11, 17, 21, 59, 365, 13],
['2020-2021', null, 1, 0, 0, 0, 0, 0, 1, 0],
['2019-2020', null, 54, 4, 3, 1, 3, 4, 39, 0],
['2018-2019', null, 154, 7, 2, 6, 5, 15, 119, 0],
['2017-2018', null, 186, 9, 3, 5, 6, 16, 139, 8],
['2016-2017', null, 71, 2, 1, 2, 5, 13, 48, 0],
['2015-2016', null, 21, 1, 1, 1, 0, 7, 8, 3],
]
it('shows stats', () => {
@@ -252,6 +252,7 @@ describe('Course Statistics tests', () => {
.eq(trIndex)
.within(() => {
values.forEach((value, tdIndex) => {
if (value === null) return
cy.get('td').eq(tdIndex).contains(value)
})
})
@@ -265,6 +266,7 @@ describe('Course Statistics tests', () => {
.eq(trIndex)
.within(() => {
values.forEach((value, tdIndex) => {
if (value === null) return
cy.get('td').eq(tdIndex).contains(value)
})
})
@@ -289,6 +291,7 @@ describe('Course Statistics tests', () => {
.eq(trIndex)
.within(() => {
values.forEach((value, tdIndex) => {
if (value === null) return
cy.get('td').eq(tdIndex).contains(value)
})
})
@@ -301,6 +304,7 @@ describe('Course Statistics tests', () => {
.eq(trIndex)
.within(() => {
values.forEach((value, tdIndex) => {
if (value === null) return
cy.get('td').eq(tdIndex).contains(value)
})
})
@@ -309,7 +313,7 @@ describe('Course Statistics tests', () => {
cy.get('[data-cy=unify_radio_unify]').click()
})
it('after changing time range shows same stats', () => {
it('After changing time range shows same stats', () => {
const newYearRange = { from: '2016-2017', to: '2019-2020' }
cy.get("div[name='fromYear']")
.click()
@@ -347,9 +351,16 @@ describe('Course Statistics tests', () => {
cy.get('[data-cy=unify_radio_reqular]').find('input').should('be.disabled')
cy.get('[data-cy=unify_radio_unify]').find('input').should('not.be.disabled')
})
it('Has right to see all the sudents, because course provider is TKT', () => {
cy.visit('coursestatistics?courseCodes=%5B%22TKT10004%22%5D&cs_tab=0&separate=false')
cy.get('tbody > :nth-child(3) > :nth-child(2) .level').click()
cy.contains('Students (154)').click()
cy.contains('015224224')
})
})
it('Some features of Course Statistics are hidden for courseStatistics-users without other rights', () => {
it.skip('Some features of Course Statistics are hidden for courseStatistics-users without other rights', () => {
cy.init('/coursestatistics', 'onlycoursestatistics')
cy.get('[data-cy=navbar-courseStatistics]').click()
cy.get('[data-cy=course-code-input]').type('TKT20003')
@@ -363,13 +374,13 @@ describe('Course Statistics tests', () => {
const attemptsTableContents = [
// [time, passed, failed, passrate]
['Total', 288, 213, 75, '73.96 %'],
// ['Total', 295, 216, 79, '73.22 %'],
['Total', 288, 213, 75, '73.96 %'], //Somehow cypress in CI can't find the number even if it is present in the artifact video
['2020-2021', '5 or less students', 'NA', 'NA', 'NA'],
// ['2020-2021', 5, 2, 3, '40.00 %'],
['2019-2020', 164, 121, 43, '73.78 %'],
['2018-2019', 85, 60, 25, '70.59 %'],
['2017-2018', 39, 32, 7, '82.05 %'],
// ['Total', null, 295, 216, 79, '73.22 %'],
// ['2020-2021', null, 5, 2, 3, '40.00 %'],
['2019-2020', null, 164, 121, 43, '73.78 %'],
['2018-2019', null, 85, 60, 25, '70.59 %'],
['2017-2018', null, 39, 32, 7, '82.05 %'],
]
cy.contains('#CourseStatPanes a.item', 'Attempts').click()
@@ -379,6 +390,7 @@ describe('Course Statistics tests', () => {
.eq(trIndex)
.within(() => {
values.forEach((value, tdIndex) => {
if (value === null) return
cy.get('td').eq(tdIndex).contains(value)
})
})
+223
View File
@@ -0,0 +1,223 @@
/// <reference types="Cypress" />
// Change admin to regular user when feature is ready for general use
describe('Faculty overview', () => {
describe('Faculty can be selected', () => {
it('Faculties are listed and one can be chosen', () => {
cy.init('/faculties', 'admin')
cy.get('[data-cy=select-faculty]').contains('td', 'H10')
cy.contains('td', 'H99').should('not.exist')
cy.contains('td', 'H90').click()
cy.contains('.header', 'Eläinlääketieteellinen tiedekunta')
})
})
describe('Faculty basic information: admin user', () => {
beforeEach(() => {
cy.init('/faculties', 'admin')
cy.contains('td', 'H90').click()
})
it('Credits produced by faculty are shown', () => {
cy.get('[data-cy="Section-CreditsProducedByTheFaculty"]').should('be.visible')
cy.get('[data-cy="Graph-CreditsProducedByTheFaculty"]').should('be.visible')
})
it('Update statistics tab is shown', () => {
cy.get('[data-cy="FacultySegmentContainer"]').should('contain', 'Update statistics')
})
})
describe('Faculty basic information: basic user', () => {
beforeEach(() => {
cy.init('/faculties')
cy.contains('td', 'H80').click()
})
it('Basic information tab show all graphs and tables', () => {
cy.get('[data-cy="Table-CreditsProducedByTheFaculty"]').should('be.visible')
cy.get('[data-cy="Graph-CreditsProducedByTheFaculty"]').should('be.visible')
cy.get('[data-cy="Table-ThesisWritersOfTheFaculty"]').should('be.visible')
cy.get('[data-cy="Graph-ThesisWritersOfTheFaculty"]').should('be.visible')
cy.get('[data-cy="Table-StudentsOfTheFaculty"]').should('be.visible')
cy.get('[data-cy="Graph-StudentsOfTheFaculty"]').should('be.visible')
cy.get('[data-cy="Table-GraduatedOfTheFaculty"]').should('be.visible')
cy.get('[data-cy="Graph-GraduatedOfTheFaculty"]').should('be.visible')
})
it('Correct tabs are shown', () => {
cy.get('[data-cy="FacultySegmentContainer"]').should('contain', 'Basic information')
cy.get('[data-cy="FacultySegmentContainer"]').should('contain', 'Graduation times')
cy.get('[data-cy="FacultySegmentContainer"]').should('contain', 'Programmes and student populations')
cy.get('[data-cy="FacultySegmentContainer"]').should('not.contain', 'Update statistics')
})
it('Toggle years works', () => {
cy.get('[data-cy="Table-CreditsProducedByTheFaculty"]').should('contain', '2022')
cy.get('[data-cy="Table-ThesisWritersOfTheFaculty"]').should('contain', '2022')
cy.get('[data-cy="Table-StudentsOfTheFaculty"]').should('contain', '2022')
cy.get('[data-cy="Table-GraduatedOfTheFaculty"]').should('contain', '2022')
cy.get('[data-cy="YearToggle"]').click()
cy.get('[data-cy="Table-CreditsProducedByTheFaculty"]').should('contain', '2022 - 2023')
cy.get('[data-cy="Table-ThesisWritersOfTheFaculty"]').should('contain', '2022 - 2023')
cy.get('[data-cy="Table-StudentsOfTheFaculty"]').should('contain', '2022 - 2023')
cy.get('[data-cy="Table-GraduatedOfTheFaculty"]').should('contain', '2022 - 2023')
})
it('Toggle programmes works', () => {
cy.get('[data-cy="FacultyProgrammesShownInfo"]').should('not.exist')
cy.get('[data-cy="ProgrammeToggle"]').click()
cy.get('[data-cy="FacultyProgrammesShownInfo"]').should('be.visible')
})
it('Students of the faculty infobox works', () => {
cy.get('[data-cy="StudentsOfTheFaculty-info-content"]').should('not.exist')
cy.get('[data-cy="StudentsOfTheFaculty-open-info"]').click()
cy.get('[data-cy="StudentsOfTheFaculty-info-content"]').should('be.visible')
cy.get('[data-cy="StudentsOfTheFaculty-close-info"]').click()
cy.get('[data-cy="StudentsOfTheFaculty-info-content"]').should('not.exist')
})
})
describe('Study programme information: users with access', () => {
beforeEach(() => {
cy.init('/faculties')
cy.contains('td', 'H80').click()
})
it('Study programme credit information is not visible in the beginning', () => {
cy.get('table[data-cy="Table-CreditsProducedByTheFaculty"]').should('be.visible')
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-0"]').should('not.be.visible')
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-1"]').should('not.be.visible')
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-2"]').should('not.be.visible')
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-3"]').should('not.be.visible')
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-4"]').should('not.be.visible')
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-5"]').should('not.be.visible')
})
it('Study programme credit information can be toggled', () => {
cy.get('[data-cy="Table-CreditsProducedByTheFaculty"]').should('be.visible')
// I tried to fix this without cy.wait() method without effort. Sorting the programmes
// or something else causes rerendering of the view and yield to failing tests.
// eslint-disable-next-line cypress/no-unnecessary-waiting
cy.wait(10000)
cy.get('[data-cy="Button-Show-CreditsProducedByTheFaculty-0"]').click()
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-0"]').should('be.visible')
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-1"]').should('not.be.visible')
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-2"]').should('not.be.visible')
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-3"]').should('not.be.visible')
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-4"]').should('not.be.visible')
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-5"]').should('not.be.visible')
cy.get('[data-cy="Button-Show-CreditsProducedByTheFaculty-3"]').click()
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-0"]').should('be.visible')
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-1"]').should('not.be.visible')
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-2"]').should('not.be.visible')
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-3"]').should('be.visible')
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-4"]').should('not.be.visible')
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-5"]').should('not.be.visible')
cy.get('[data-cy="Button-Hide-CreditsProducedByTheFaculty-0"]').click()
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-0"]').should('not.be.visible')
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-1"]').should('not.be.visible')
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-2"]').should('not.be.visible')
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-3"]').should('be.visible')
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-4"]').should('not.be.visible')
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-5"]').should('not.be.visible')
})
it('Graph stays open when sorted', () => {
cy.get('[data-cy="Table-CreditsProducedByTheFaculty"]').should('be.visible')
// eslint-disable-next-line cypress/no-unnecessary-waiting
cy.wait(10000)
cy.get('[data-cy="Button-Show-CreditsProducedByTheFaculty-0"]').click()
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-0"]').should('be.visible')
cy.get('[data-cy="Menu-CreditsProducedByTheFaculty-Total"]').click()
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-0"]').should('be.visible')
})
})
describe('Average graduation times', () => {
beforeEach(() => {
cy.init('/faculties')
cy.contains('td', 'H60').click()
cy.contains('Graduation times').click()
})
it('User can view graduation graphs', () => {
cy.get('[data-cy="Section-AverageGraduationTimes"]').should('be.visible')
cy.get('[data-cy="Section-bachelor"]').should('be.visible')
cy.get('[data-cy="Section-master"]').should('be.visible')
cy.get('[data-cy="Section-master"]').within(() => {
cy.contains('Graduation year').should('be.visible')
cy.contains('.message', "Click a bar to view that year's programme level breakdown").should('be.visible')
})
})
it('Graphs display data', () => {
cy.get('[data-cy="Section-bachelor"]').within(() => {
cy.get('div[class="faculty-graph"]')
cy.contains('1 graduated').should('have.length', 1)
cy.contains('1 graduated').trigger('mouseover')
cy.contains('1 students graduated in year 2019')
cy.contains('median study time: 40 months')
cy.contains('0 graduated on time')
cy.contains('1 graduated max year overtime')
cy.contains('1 graduated').click()
cy.contains('Year 2019 by graduation year')
cy.get('div[class="programmes-graph"]').should('be.visible')
cy.get('div[class="programmes-graph"]').within(() => {
cy.contains('EDUK')
cy.contains('1 graduated').trigger('mouseover')
cy.contains('Kasvatustieteiden kandiohjelma')
cy.contains('KH60_001')
})
})
cy.get('[data-cy="Section-master"]').within(() => {
cy.get('div[class="faculty-graph"]')
cy.contains('1 graduated').should('have.length', 1)
cy.contains('1 graduated').trigger('mouseover')
cy.contains('1 students graduated in year 2020')
cy.contains('median study time: 25 months')
cy.contains('0 graduated over year late')
cy.contains('1 graduated max year overtime')
cy.contains('1 graduated').click()
cy.contains('Year 2020 by graduation year')
cy.get('div[class="programmes-graph"]').should('be.visible')
cy.get('div[class="programmes-graph"]').within(() => {
cy.contains('EDUM')
cy.contains('1 graduated').trigger('mouseover')
cy.contains('Kasvatustieteiden maisteriohjelma')
cy.contains('MH60_001')
})
})
})
it('Graduation times grouping and time types can be toggled', () => {
cy.get('[data-cy="GroupByToggle"]').click()
cy.get('[data-cy="Section-bachelor"]').should('not.exist')
cy.get('[data-cy="Section-master"]').should('be.visible')
cy.get('[data-cy="Section-master"]').within(() => {
cy.get('div[class="faculty-graph"]')
cy.contains('1 graduated (100 % of class)').should('have.length', 1)
cy.contains('1 graduated').trigger('mouseover')
cy.contains('From class of 2018, 1/undefined students have graduated')
})
cy.get('[data-cy="GraduationTimeToggle"]').click()
cy.get('[data-cy="Section-master"]').within(() => {
cy.contains('1 graduated (100 % of class)').click()
cy.get('div[class="programmes-graph"]').should('be.visible')
cy.get('div[class="programmes-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')
})
})
})
})
})
@@ -2,6 +2,8 @@
// const baseUrl = Cypress.config().baseUrl
const MOCKED_DATE = new Date(2022, 6, 12).getTime()
const checkFilteringResult = studentCount => {
cy.contains(`Students (${studentCount})`)
}
@@ -17,6 +19,28 @@ const clearSingleDropdownSelection = dataCyAttribute => {
cy.cs(dataCyAttribute).find('i.clear').click({ force: true })
}
const testRangeFilter = (parentEl, min, max, expected) => {
cy.cs(parentEl)
.cs('range-selector-min')
.find('input')
.invoke('val')
.then(initialMin => {
cy.cs(parentEl)
.cs('range-selector-max')
.find('input')
.invoke('val')
.then(initialMax => {
cy.cs(parentEl).cs('range-selector-min').find('input').type(`{selectall}{backspace}${min}`).blur()
cy.cs(parentEl).cs('range-selector-max').find('input').type(`{selectall}{backspace}${max}`).blur()
checkFilteringResult(expected)
cy.cs(parentEl).cs('range-selector-min').find('input').type(`{selectall}{backspace}${initialMin}`).blur()
cy.cs(parentEl).cs('range-selector-max').find('input').type(`{selectall}{backspace}${initialMax}`).blur()
})
})
}
// Helper tool to create pre and post steps for each filter step. Created to avoid copypasting clicking and checking
// to every it-function. Reason behind using test function wrapper is that Cypresses internal beforeEach / afterEach
// functions don't take any parameters and using global object for matching test step name seemed overcomplicated.
@@ -47,7 +71,7 @@ const createRunTestStepWithPreAndPostPartsFunction = amountWithoutFiltering => {
describe('Population Statistics', () => {
const pathToCSBach2018 =
'/populations?months=36&semesters=FALL&semesters=SPRING&studyRights={%22programme%22%3A%22KH50_005%22}&tag&year=2018'
const defaultAmountOfStudents = 148
const defaultAmountOfStudents = 159
const runTestStepWithPreAndPostParts = createRunTestStepWithPreAndPostPartsFunction(defaultAmountOfStudents)
before(() => {
@@ -63,22 +87,10 @@ describe('Population Statistics', () => {
})
})
it('Programme defaults to "Active Study Right" mode', () => {
runTestStepWithPreAndPostParts('Programme', () => {
cy.cs('Programme-filter-mode-selector').children().eq(0).should('contain', 'Active Study Right')
})
})
it('Programme filter works', () => {
runTestStepWithPreAndPostParts('Programme', () => {
const card = cy.cs('Programme-filter-card')
const programmeDropdown = card.cs('Programme-filter-dropdown').selectFromDropdown(0)
checkFilteringResult(5)
programmeDropdown.get('i.delete').click()
})
})
it('Study track filter works', () => {
// The test does not work since it tries to filter by "Ravitsemustiede"-studytrack when checking
// CS Bachelor students. Now that the irrelevant studytracks have been removed, the test fails.
// So fix this
it.skip('Study track filter works', () => {
runTestStepWithPreAndPostParts('StudyTrack', () => {
const card = cy.cs('StudyTrack-filter-card')
const programmeDropdown = card.cs('StudyTrack-filter-dropdown').selectFromDropdown(0)
@@ -115,37 +127,30 @@ describe('Population Statistics', () => {
runTestStepWithPreAndPostParts('EnrollmentStatus', () => {
cy.cs('enrollmentStatusFilter-status').selectFromDropdown(0)
cy.cs('enrollmentStatusFilter-semesters').selectFromDropdown(0)
checkFilteringResult(137)
checkFilteringResult(148)
clearSemanticUIMultipleDropDownSelection('enrollmentStatusFilter-semesters')
})
})
it('Credit filter works', () => {
runTestStepWithPreAndPostParts('CreditsEarned', () => {
cy.cs('credit-filter-min').type('50')
cy.cs('credit-filter-max').type('150')
checkFilteringResult(116)
cy.cs('credit-filter-min').clear()
cy.cs('credit-filter-max').clear()
testRangeFilter('CreditsEarned-filter-card', 50, 150, 118)
})
})
it('Age filter works', () => {
cy.clock(MOCKED_DATE, ['Date'])
runTestStepWithPreAndPostParts('Age', () => {
cy.cs('ageFilter-min').type('20')
cy.cs('ageFilter-max').type('40')
checkFilteringResult(40)
cy.cs('ageFilter-min').find('input').clear()
cy.cs('ageFilter-max').find('input').clear()
testRangeFilter('Age-filter-card', 20, 40, 47)
})
})
it('Gender filter works', () => {
runTestStepWithPreAndPostParts('Gender', () => {
cy.cs('genderFilter-dropdown').selectFromDropdown(0)
checkFilteringResult(36)
checkFilteringResult(42)
cy.cs('genderFilter-dropdown').selectFromDropdown(1)
checkFilteringResult(112)
checkFilteringResult(117)
cy.cs('genderFilter-dropdown').selectFromDropdown(2)
checkFilteringResult(0)
cy.cs('genderFilter-dropdown').selectFromDropdown(3)
@@ -160,7 +165,7 @@ describe('Population Statistics', () => {
checkFilteringResult(1)
clearSemanticUIMultipleDropDownSelection('startYearAtUni-dropdown')
cy.cs('startYearAtUni-dropdown').selectFromDropdown(7)
checkFilteringResult(141)
checkFilteringResult(152)
clearSemanticUIMultipleDropDownSelection('startYearAtUni-dropdown')
})
})
@@ -196,15 +201,12 @@ describe('Population Statistics', () => {
})
it('Filter combinations work', () => {
cy.clock(MOCKED_DATE, ['Date'])
runTestStepWithPreAndPostParts('GraduatedFromProgramme', () => {
runTestStepWithPreAndPostParts('Age', () => {
const getCard = () => cy.cs('GraduatedFromProgramme-filter-card')
getCard().cs('option-graduated-true').click()
cy.cs('ageFilter-min').type('20')
cy.cs('ageFilter-max').type('30')
checkFilteringResult(11)
cy.cs('ageFilter-min').find('input').clear()
cy.cs('ageFilter-max').find('input').clear()
testRangeFilter('Age-filter-card', 20, 30, 1)
getCard().cs('option-all').click()
})
})
@@ -213,7 +215,7 @@ describe('Population Statistics', () => {
describe('Course Statistics', () => {
const pathToDSAndAlgoSpring2019 =
'/coursepopulation?coursecodes=%5B%22TKT20001%22%2C%2258131%22%5D&from=138&separate=true&to=138&years=Kev%C3%A4t%202019'
'/coursepopulation?coursecodes=%5B%22TKT20001%22%2C%2258131%22%5D&from=138&separate=true&to=138&years=Kev%C3%A4t%202019&unifyCourses=unifyStats'
const defaultAmountOfStudents = 118
const runTestStepWithPreAndPostParts = createRunTestStepWithPreAndPostPartsFunction(defaultAmountOfStudents)
@@ -247,7 +249,10 @@ describe('Course Statistics', () => {
})
})
it('Study track filter works', () => {
// The test does not work since it tries to filter by "Ravitsemustiede"-studytrack when checking
// CS Bachelor students. Now that the irrelevant studytracks have been removed, the test fails.
// So fix this
it.skip('Study track filter works', () => {
runTestStepWithPreAndPostParts('StudyTrack', () => {
const card = cy.cs('StudyTrack-filter-card')
const programmeDropdown = card.cs('StudyTrack-filter-dropdown').selectFromDropdown(0)
@@ -257,12 +262,9 @@ describe('Course Statistics', () => {
})
it('Age filter works', () => {
cy.clock(MOCKED_DATE, ['Date'])
runTestStepWithPreAndPostParts('Age', () => {
cy.cs('ageFilter-min').type('20')
cy.cs('ageFilter-max').type('40')
checkFilteringResult(31)
cy.cs('ageFilter-min').find('input').clear()
cy.cs('ageFilter-max').find('input').clear()
testRangeFilter('Age-filter-card', 20, 40, 33)
})
})
@@ -292,14 +294,11 @@ describe('Course Statistics', () => {
})
it('Filter combinations work', () => {
cy.clock(MOCKED_DATE, ['Date'])
runTestStepWithPreAndPostParts('Grade', () => {
runTestStepWithPreAndPostParts('Age', () => {
cy.cs('gradeFilter-3').click()
cy.cs('ageFilter-min').type('20')
cy.cs('ageFilter-max').type('30')
checkFilteringResult(1)
cy.cs('ageFilter-min').find('input').clear()
cy.cs('ageFilter-max').find('input').clear()
testRangeFilter('Age-filter-card', 20, 30, 1)
cy.cs('gradeFilter-3').click()
})
})
@@ -322,12 +321,9 @@ describe('Custom Population Statistics', () => {
})
it('Age filter works', () => {
cy.clock(MOCKED_DATE, ['Date'])
runTestStepWithPreAndPostParts('Age', () => {
cy.cs('ageFilter-min').type('20')
cy.cs('ageFilter-max').type('30')
checkFilteringResult(1)
cy.cs('ageFilter-min').find('input').clear()
cy.cs('ageFilter-max').find('input').clear()
testRangeFilter('Age-filter-card', 20, 30, 1)
})
})
@@ -1,17 +1,18 @@
/// <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')
}
// 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 =
@@ -24,8 +25,8 @@ describe('Population Statistics tests', () => {
})
it('Population statistics search form is usable', () => {
cy.contains('See population').should('be.disabled')
cy.contains('Search for population')
cy.contains('See class').should('be.disabled')
cy.contains('Search for class')
cy.contains('Class of')
.parent()
.within(() => {
@@ -47,7 +48,7 @@ describe('Population Statistics tests', () => {
.children()
.contains('Tietojenkäsittelytieteen maisteriohjelma')
.click()
cy.contains('See population').should('be.enabled')
cy.contains('See class').should('be.enabled')
})
it('Searching for population really shows population', () => {
@@ -57,30 +58,32 @@ describe('Population Statistics tests', () => {
.children()
.contains('Tietojenkäsittelytieteen kandiohjelma')
.click()
cy.contains('See population').click()
cy.contains('Population statistics')
cy.contains('See class').click()
cy.contains('class size')
cy.contains('Tietojenkäsittelytieteen kandiohjelma')
})
it('Population statistics is usable on general level', () => {
cy.visit(pathToCSBach2017)
cy.cs('filtered-students')
setPopStatsUntil('toukokuu 2020')
cy.get('.card').within(() => {
// 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('Sample size: 149 students')
cy.contains('class size 170 students')
})
cy.get('.card').within(() => {
cy.contains('Excludes exchange students')
cy.contains("Excludes students who haven't enrolled present nor absent")
cy.contains('Excludes students with non-degree study right')
cy.contains('Excludes students who have transferred out of this programme')
})
cy.cs('filtered-students')
cy.contains('Courses of population').click()
cy.contains('Courses of class').click()
cy.intercept('/api/v3/courseyearlystats**').as('coursePage')
// eslint-disable-next-line cypress/no-unnecessary-waiting
cy.wait(15000) // a bit hacky way, wait until ui is ready
cy.wait(25000) // a bit hacky way, wait until ui is ready
cy.get('[data-cy=toggle-group-module-TKT1]').click()
cy.contains('td', 'TKT10002').siblings().find('i.level.up').click()
cy.wait('@coursePage')
@@ -90,7 +93,7 @@ describe('Population Statistics tests', () => {
it("Empty 'tags' tab has a link to the page where tags can be created", () => {
cy.visit(pathToCSBach2017)
cy.contains('Students (149)').click()
cy.contains('Students (170)').click()
cy.get('[data-cy=student-table-tabs]').contains('Tags').click()
cy.contains('No tags defined. You can define them here.').find('a').click()
cy.contains('Tags').click()
@@ -102,12 +105,12 @@ describe('Population Statistics tests', () => {
it.skip('Advanced settings work', () => {
cy.visit(pathToCSBach2017)
cy.get('[data-cy=advanced-toggle]').click()
cy.contains('Statistics until')
// only spring
cy.cs('toggle-fall').click()
cy.contains('Fetch population').click()
cy.contains('Fetch class').click()
// moar waiting hack
// eslint-disable-next-line cypress/no-unnecessary-waiting
cy.wait(10000)
cy.contains('No statistics found for the given query.')
@@ -117,15 +120,14 @@ describe('Population Statistics tests', () => {
cy.cs('toggle-fall').click()
cy.cs('toggle-spring').click()
cy.contains('Fetch population').click()
cy.contains('Fetch class').click()
cy.contains('Credit accumulation (for 149 students)')
cy.contains('Credit accumulation (for 170 students)')
// spring + fall and include cancelled
// spring + fall
cy.get('[data-cy=advanced-toggle]').click()
cy.cs('toggle-spring').click()
cy.cs('toggle-cancelled').click()
cy.contains('Fetch population').click()
cy.contains('Fetch class').click()
cy.contains('Credit accumulation (for 170 students)')
})
@@ -133,14 +135,14 @@ describe('Population Statistics tests', () => {
it('Credit Statistics, Credits Gained tab works', () => {
cy.selectStudyProgramme('Tietojenkäsittelytieteen kandiohjelma')
cy.contains('Credit statistics').click()
cy.get("[data-cy='credits-gained-main-table']").should('contain', 'All students of the population')
const months = Math.ceil(moment.duration(moment().diff(moment('2017-08-1'))).asMonths())
cy.contains('Credits Gained').click()
cy.get("[data-cy='credits-gained-main-table']").should('contain', 'All students of the class')
const today = moment().endOf('month')
const months = Math.round(moment.duration(moment(today).diff(moment('2017-08-01'))).asMonths())
const limits = [1, ..._.range(1, 4).map(p => Math.ceil(months * ((p * 15) / 12))), null]
const ranges = _.range(1, limits.length).map(i => _.slice(limits, i - 1, i + 1))
cy.get('.credits-gained-table').should('contain', '(n=149)')
cy.get('.credits-gained-table').should('contain', '(n=170)')
for (const [start, end] of ranges) {
let value = 'credits'
@@ -159,7 +161,7 @@ describe('Population Statistics tests', () => {
cy.get("[data-cy='credits-gained-table-body'] td:nth-child(3)").then($els => {
const sum = [...$els].map($el => parseInt($el.innerText)).reduce((a, b) => a + b, 0)
expect(sum).to.equal(149)
expect(sum).to.equal(170)
})
cy.get("[data-cy='credits-gained-table-Ei valintatapaa']").should('not.exist')
@@ -170,6 +172,7 @@ describe('Population Statistics tests', () => {
cy.cs('filtered-students')
cy.contains('Credit statistics')
cy.get("[data-cy='credit-statistics']").click()
cy.contains('Credits Gained').click()
cy.get('.credits-gained-divider').click()
cy.get("[data-cy='credits-gained-table-Avoin väylä']").should('exist')
cy.get("[data-cy='credits-gained-table-Yhteispisteet']").should('exist')
@@ -182,10 +185,10 @@ describe('Population Statistics tests', () => {
cy.contains('Credit statistics').click({ force: true })
cy.get("[data-cy='credit-stats-tab'] > .menu > :nth-child(2)").click()
cy.get("[data-cy='credit-stats-table-name-header']").should('contain', 'Statistic for n = 149 Students')
cy.get("[data-cy='credit-stats-mean']").should('contain', '123.89')
cy.get("[data-cy='credit-stats-stdev']").should('contain', '61.32')
cy.get("[data-cy='credit-stats-min']").should('contain', '11')
cy.get("[data-cy='credit-stats-table-name-header']").should('contain', 'Statistic for n = 170 Students')
cy.get("[data-cy='credit-stats-mean']").should('contain', '114.74')
cy.get("[data-cy='credit-stats-stdev']").should('contain', '65.53')
cy.get("[data-cy='credit-stats-min']").should('contain', '5')
cy.get("[data-cy='credit-stats-max']").should('contain', '314')
})
})
@@ -198,7 +201,7 @@ describe('Population Statistics tests', () => {
const existing = '010113437'
const nonExisting = '66666666'
cy.visit(pathToCSBach2017)
cy.contains('Students (149)').click()
cy.contains('Students (170)').click()
cy.contains(existing)
cy.contains(nonExisting).should('not.exist')
cy.contains('button', 'Check studentnumbers').click()
@@ -215,4 +218,31 @@ describe('Population Statistics tests', () => {
})
})
})
describe('when using IAM user', () => {
beforeEach(() => {
cy.init(pathToCSBach2017, 'onlyiamrights')
})
it('Population statistics is visible', () => {
cy.reload()
cy.contains('Tietojenkäsittelytieteen kandiohjelma')
cy.contains('class size 170 students')
cy.get('.card').within(() => {
cy.contains('Excludes exchange students')
cy.contains('Excludes students with non-degree study right')
cy.contains('Excludes students who have transferred out of this programme')
})
cy.contains('Credit accumulation (for 170 students)')
cy.contains('Credit statistics')
})
it('Students tab is not available', () => {
cy.contains('Students (170)').should('not.exist')
// TODO: somehow check that obfuscation works
})
})
})
@@ -11,6 +11,7 @@ describe('Student Statistics tests', () => {
firstnames: 'Matti Jaakko',
lastname: 'Nieminen',
studentnumber: '010654019',
sis_person_id: 'hy-hlo-84830887',
}
it('Student statistics search form is usable', () => {
@@ -39,7 +40,7 @@ describe('Student Statistics tests', () => {
cy.get('.prompt').type(student.studentnumber)
cy.contains('td a', student.studentnumber).click()
cy.contains('Started: 01.08.2017')
cy.contains('Tietojenkäsittelytieteen kandiohjelma (01.08.2017 - 01.08.2025')
cy.contains('Tietojenkäsittelytieteen kandiohjelma (01.08.2017 - 31.07.2025')
cy.contains(student.lastname).should('not.exist')
cy.contains(student.firstnames).should('not.exist')
@@ -72,6 +73,14 @@ describe('Student Statistics tests', () => {
cy.contains('Introduction to Machine Learning')
})
it('Has correct Sisu link', () => {
cy.get('.prompt').type(student.studentnumber)
cy.contains('td a', student.studentnumber).click()
cy.get('[data-cy=sisulink] > a')
.should('have.attr', 'href')
.and('include', 'https://sis-helsinki.funidata.fi/tutor/role/staff/student/hy-hlo-84830887/basic/basic-info')
})
it('Searching with bad inputs doesnt yield results', () => {
cy.get('.prompt').type('SWAG LITTINEN')
cy.contains('Student number').should('not.exist')
@@ -84,8 +93,8 @@ describe('Student Statistics tests', () => {
cy.get('.prompt').type(student.studentnumber)
cy.contains('td a', student.studentnumber).click()
cy.get('i.level.up.alternate.icon').eq(0).click()
cy.contains('Population statistics')
cy.contains('Tietojenkäsittelytieteen kandiohjelma')
cy.contains('class size')
})
})
@@ -25,12 +25,14 @@ describe('Studyprogramme overview', () => {
describe('Basic information -view works for basic user', () => {
beforeEach(() => {
cy.init('/study-programme')
cy.viewport(1536, 960)
// eslint-disable-next-line cypress/no-unnecessary-waiting
cy.wait(10000)
cy.contains('a', 'Tietojenkäsittelytieteen kandiohjelma').click({ force: true })
// cy.viewport(1536, 960)
})
// If the backend breaks for one of the sections, the section header is not rendered and this will fail
it('Basic information -tab loads', () => {
cy.contains('a', 'Tietojenkäsittelytieteen kandiohjelma').click()
cy.get('[data-cy=Section-StudentsOfTheStudyprogramme]')
cy.get('[data-cy=Section-CreditsProducedByTheStudyprogramme]')
cy.get('[data-cy=Section-GraduatedAndThesisWritersOfTheProgramme]')
@@ -39,45 +41,42 @@ describe('Studyprogramme overview', () => {
})
it('Basic information contains correct students', () => {
cy.contains('a', 'Tietojenkäsittelytieteen kandiohjelma').click()
const years = getEmptyYears()
const tableContents = [
// [Year, Started, Graduated, Cancelled, Transferred Away, Transferred to]
...years.map(year => [year, 0, 0, 0, 0, 0]),
[2021, 0, 0, 0, 1, 0],
[2020, 12, 36, 16, 3, 2],
[2019, 87, 17, 14, 0, 1],
[2018, 161, 1, 3, 0, 0],
[2017, 171, 0, 0, 0, 0],
// [Year, Started, Graduated, Transferred Away, Transferred to]
...years.map(year => [year, 0, 0, 0, 0]),
[2021, 0, 0, 1, 0],
[2020, 12, 35, 3, 2],
[2019, 87, 17, 0, 1],
[2018, 161, 1, 0, 0],
[2017, 171, 0, 0, 0],
]
cy.checkTableStats(tableContents, 'StudentsOfTheStudyprogramme')
})
it('Basic information contains correct credits', () => {
cy.contains('a', 'Tietojenkäsittelytieteen kandiohjelma').click()
const years = getEmptyYears()
const tableContents = [
// [Year, Total, Major credits, Non-major credits, Transferred credits]
...years.map(year => [year, 0, 0, 0, 0]),
[2021, 0, 0, 0, 0],
[2020, 4928, 548, 4213, 167],
[2019, 7737, 1644, 5744, 349],
[2018, 6730, 1820, 4638, 272],
[2017, 2900, 788, 1724, 388],
// [Year, Total, Major credits, Non-major credits, Non-degree credits, Transferred credits]
...years.map(year => [year, 0, 0, 0, 0, 0]),
[2021, 0, 0, 0, 0, 0],
[2020, 5860, 5591, 102, 0, 167],
[2019, 8696, 7737, 577, 33, 349],
[2018, 7818, 6584, 852, 110, 272],
[2017, 3085, 2492, 153, 52, 388],
]
cy.checkTableStats(tableContents, 'CreditsProducedByTheStudyprogramme')
})
it('Basic information contains correct thesis writers and graduates', () => {
cy.contains('a', 'Tietojenkäsittelytieteen kandiohjelma').click()
const years = getEmptyYears()
const tableContents = [
// [Year, Graduated, Wrote thesis]
...years.map(year => [year, 0, 0]),
[2021, 0, 0],
[2020, 36, 35],
[2020, 35, 35],
[2019, 17, 25],
[2018, 1, 5],
[2017, 0, 0],
@@ -87,7 +86,6 @@ describe('Studyprogramme overview', () => {
})
it.skip('Special studyrights can be excluded and basic data changes accordingly', () => {
cy.contains('a', 'Tietojenkäsittelytieteen maisteriohjelma').click()
cy.get('[data-cy=StudentToggle]').click()
const years = getEmptyYears()
const studentTableContents = [
@@ -129,39 +127,39 @@ describe('Studyprogramme overview', () => {
})
it('Year can be changed to academic year, and data changes accordingly', () => {
cy.contains('a', 'Tietojenkäsittelytieteen maisteriohjelma').click()
cy.get('[data-cy=YearToggle]').click()
// eslint-disable-next-line cypress/no-unnecessary-waiting
cy.get('[data-cy=YearToggle]').click({ force: true })
const isAcademicYear = true
const years = getEmptyYears(isAcademicYear)
const studentTableContents = [
// [Year, Started, Graduated, Cancelled, Transferred away, Transferred to]
...years.map(year => [year, 0, 0, 0, 0, 0]),
['2021 - 2022', 0, 0, 0, 0, 0],
['2020 - 2021', 5, 0, 1, 0, 0],
['2019 - 2020', 27, 0, 1, 0, 0],
['2018 - 2019', 4, 0, 0, 0, 0],
['2017 - 2018', 0, 0, 0, 0, 0],
// [Year, Started, Graduated, Transferred away, Transferred to]
...years.map(year => [year, 0, 0, 0, 0]),
['2021 - 2022', 0, 0, 0, 0],
['2020 - 2021', 12, 6, 3, 2],
['2019 - 2020', 86, 41, 1, 0],
['2018 - 2019', 162, 5, 0, 1],
['2017 - 2018', 171, 1, 0, 0],
]
// eslint-disable-next-line cypress/no-unnecessary-waiting
cy.wait(10000)
cy.checkTableStats(studentTableContents, 'StudentsOfTheStudyprogramme')
const creditTableContents = [
// [Year, Total, Major students credits, Non-major students credits, Transferred credits]
...years.map(year => [year, 0, 0, 0, 0]),
['2021 - 2022', 0, 0, 0, 0],
['2020 - 2021', 0, 0, 0, 0],
['2019 - 2020', 338.5, 0, 331, 7.5],
['2018 - 2019', 173, 0, 173, 0],
['2017 - 2018', 42, 0, 42, 0],
// [Year, Total, Major students credits, Non-major students credits, Non-degree credits, Transferred credits]
...years.map(year => [year, 0, 0, 0, 0, 0]),
['2021 - 2022', 0, 0, 0, 0, 0],
['2020 - 2021', 566, 486, 20, 0, 60],
['2019 - 2020', 8595, 8271, 106, 0, 218],
['2018 - 2019', 9992, 8855, 781, 53, 303],
['2017 - 2018', 6055, 4792, 777, 142, 344],
]
cy.checkTableStats(creditTableContents, 'CreditsProducedByTheStudyprogramme')
//cy.wait(10000)
cy.get('[data-cy=YearToggle]').click()
})
it('Basic information graphs render', () => {
cy.contains('a', 'Tietojenkäsittelytieteen kandiohjelma').click()
cy.get('[data-cy=Graph-StudentsOfTheStudyprogramme')
.should('contain', 'Started')
.should('contain', 'Graduated')
@@ -171,8 +169,8 @@ describe('Studyprogramme overview', () => {
.should('contain', 'Major students credits')
.should('contain', 'Non-major students credits')
.should('contain', 'Transferred credits')
.should('contain', 788)
.should('contain', 1820)
.should('contain', 852)
.should('contain', 577)
cy.get('[data-cy=Graph-GraduatedAndThesisWritersOfTheProgramme')
.should('contain', 'Graduated students')
@@ -181,13 +179,16 @@ describe('Studyprogramme overview', () => {
.should('contain', 5)
.should('contain', 17)
.should('contain', 25)
.should('contain', 36)
.should('contain', 35)
cy.get('[data-cy=Graph-2019-AverageGraduationTimes]')
.should('contain', '2019')
.should('contain', 'n = 17')
.should('contain', '22 kk')
cy.get('[data-cy=graduation-times-graph]').within(() => {
cy.contains('Graduation year')
cy.contains('2019')
cy.contains('17 graduated').trigger('mouseover')
cy.contains('study time: 23 months')
cy.contains('17 graduated on time')
cy.contains('0 graduated max year overtime')
})
cy.get('[data-cy=Graph-ProgrammesBeforeOrAfter')
.should('contain', 'Tietojenkäsittelytieteen maisteriohjelma')
@@ -199,139 +200,79 @@ describe('Studyprogramme overview', () => {
})
})
describe('Graduation times of master programmes', () => {
it('are split into two graphs', () => {
cy.init('/study-programme', 'admin')
// eslint-disable-next-line cypress/no-unnecessary-waiting
cy.wait(5000)
cy.contains('a', 'Kasvatustieteiden maisteriohjelma').click({ force: true })
cy.get('[data-cy=graduation-times-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('Basic information -view works for basic user', () => {
describe('Studytrack overview works for basic user', () => {
beforeEach(() => {
cy.init('/study-programme')
cy.contains('a', 'Tietojenkäsittelytieteen kandiohjelma').click()
cy.get('.attached').contains('Studytracks and student populations').click()
})
// If the backend breaks for one of the sections, the section header is not rendered and this will fail
it('Studytracks and student populations -tab loads', () => {
cy.contains('a', 'Tietojenkäsittelytieteen kandiohjelma').click()
cy.get('.attached').contains('Studytracks and student populations').click()
cy.get('[data-cy=Section-StudytrackOverview]')
cy.get('[data-cy=Section-StudytrackProgress]')
cy.get('[data-cy=Section-AverageGraduationTimes]')
})
it('Students of the studyprogramme are shown correctly', () => {
cy.contains('a', 'Tietojenkäsittelytieteen kandiohjelma').click()
cy.get('.attached').contains('Studytracks and student populations').click()
const tableContents = [
// [Year, All, Started studying, Currently enrolled, Absent, Cancelled, Graduated, Men, Women, Finnish]
[
'2020 - 2021',
12,
'100.0 %',
10,
'83.3 %',
0,
'0 %',
0,
'0 %',
0,
'0 %',
0,
'0 %',
10,
'83.3 %',
2,
'16.7 %',
12,
'100.0 %',
],
[
'2019 - 2020',
86,
'100.0 %',
86,
'100.0 %',
0,
'0 %',
0,
'0 %',
1,
'1.2 %',
1,
'1.2 %',
65,
'75.6 %',
21,
'24.4 %',
86,
'100.0 %',
],
[
'2018 - 2019',
162,
'100.0 %',
161,
'99.4 %',
0,
'0 %',
0,
'0 %',
11,
'6.8 %',
11,
'6.8 %',
118,
'72.8 %',
44,
'27.2 %',
161,
'99.4 %',
],
[
'2017 - 2018',
171,
'100.0 %',
171,
'100.0 %',
0,
'0 %',
0,
'0 %',
21,
'12.3 %',
42,
'24.6 %',
135,
'78.9 %',
36,
'21.1 %',
166,
'97.1 %',
],
['2020 - 2021', 12, 10, 0, 0, 0, 0, 10, 2, 12],
['2019 - 2020', 86, 86, 0, 0, 1, 1, 65, 21, 86],
['2018 - 2019', 162, 161, 0, 0, 0, 11, 118, 44, 161],
['2017 - 2018', 171, 171, 0, 0, 4, 42, 135, 36, 166],
['Total', 431, 431, 0, 0, 5, 54, 328, 103, 425],
]
cy.checkTableStats(tableContents, 'StudytrackOverview')
})
it('Student progress data is shown correctly', () => {
cy.contains('a', 'Tietojenkäsittelytieteen kandiohjelma').click()
cy.get('.attached').contains('Studytracks and student populations').click()
const tableContents = [
['2020 - 2021', 12, 5, 2, 2, 0, 2, 1],
['2019 - 2020', 86, 13, 15, 32, 11, 2, 13],
['2018 - 2019', 162, 7, 16, 35, 33, 27, 44],
['2017 - 2018', 171, 10, 22, 18, 22, 24, 75],
['2020 - 2021', 12, 12, 0, 0, 0, 0, 0, 0, 0],
['2019 - 2020', 86, 11, 15, 35, 20, 5, 0, 0, 0],
['2018 - 2019', 162, 3, 9, 28, 50, 33, 25, 11, 3],
['2017 - 2018', 171, 7, 13, 22, 32, 18, 23, 32, 24],
['Total', 431, 16, 35, 65, 103, 75, 56, 50, 31],
]
cy.checkTableStats(tableContents, 'StudytrackProgress')
})
it('Studytrack overview graphs render', () => {
cy.contains('a', 'Tietojenkäsittelytieteen kandiohjelma').click()
cy.get('.attached').contains('Studytracks and student populations').click()
cy.get('[data-cy=Graph-StudytrackProgress]')
.should('contain', 'Less than 30 credits')
.should('contain', 'Less than 15 credits')
.should('contain', '30-59 credits')
.should('contain', 'More than 150 credits')
.should('contain', '5.8%')
.should('contain', '4.3%')
.should('contain', '15.1%')
.should('contain', '180 or more credits')
.should('contain', '4.1%') // The percentage for less than 15 credits in 2017-2018, to check that the graph renders
.should('contain', '5.6%') // The percentage for 15-29 credits in 2018-2019
.should('contain', '12.8%') // The percentage for less than 15 credits in 2019-2020
cy.get('[data-cy=Graph-2018-AverageGraduationTimes]')
.should('contain', '2018 - 2019')
@@ -340,16 +281,92 @@ describe('Studyprogramme overview', () => {
})
})
/* Programme courses -tests*/
describe('Programme courses works for basic user', () => {
beforeEach(() => {
cy.init('/study-programme')
cy.contains('a', 'Tietojenkäsittelytieteen kandiohjelma').click()
cy.get('.attached').contains('Programme courses').click()
})
it('content loads', () => {
cy.get('[data-cy=CoursesYearFilter]')
cy.get('[data-cy=CourseTabs]')
})
it('time range selection works', () => {
cy.get('[data-cy=fromYear]').click().contains('2018').click()
cy.get('[data-cy=toYear').click().contains('2019').click()
cy.get('[data-cy=CoursesSortableTable]').within(() => {
cy.get('tr').eq(1).contains('684')
})
})
it('calendar year -> academic year toggle works', () => {
// eslint-disable-next-line cypress/no-unnecessary-waiting
cy.wait(10000)
cy.get('[data-cy=fromYear]').click().contains('2018').click()
cy.get('[data-cy=toYear').click().contains('2019').click()
// eslint-disable-next-line cypress/no-unnecessary-waiting
cy.wait(20000)
cy.get('[data-cy=CoursesSortableTable]').within(() => {
cy.get('tr').eq(1).contains('684')
})
// eslint-disable-next-line cypress/no-unnecessary-waiting
cy.wait(20000)
cy.get('[data-cy=calendarAcademicYearToggle]').first().click()
// eslint-disable-next-line cypress/no-unnecessary-waiting
cy.wait(20000)
cy.get('[data-cy=CoursesSortableTable]').within(() => {
cy.get('tr').eq(1).contains('772')
})
})
it('contains correct courses in alphabetical order', () => {
cy.get('[data-cy=CoursesSortableTable]').within(() => {
cy.get('tr').eq(1).contains('Aineopintojen harjoitustyö: Tietokantasovellus')
cy.get('tr').eq(-1).contains('Äidinkielinen viestintä')
})
})
it('different sorting options work', () => {
cy.get('[data-cy=CoursesSortableTable]').within(() => {
// Test sorting by different columns
cy.get('th').eq(0).click()
cy.get('tr').eq(2).contains('Tietojenkäsittelytieteen kisälliopetus: Java-ohjelmointi')
cy.get('th').eq(1).click()
cy.get('tr').eq(1).contains('Äidinkielinen viestintä')
cy.get('th').eq(2).click()
cy.get('tr').eq(1).contains('Tietorakenteet ja algoritmit')
})
})
it('show credits -> show students toggle works', () => {
cy.get('[data-cy=CoursesSortableTable]').within(() => {
cy.get('tr').eq(1).contains('1068')
})
cy.get('[data-cy=creditsStudentsToggle]').first().click()
cy.get('[data-cy=CoursesSortableTable]').within(() => {
cy.get('tr').eq(1).contains('267')
})
})
})
/* Tag-tests*/
describe('Basic information -view works for basic user', () => {
beforeEach(() => {
cy.init('/study-programme')
cy.contains('a', 'Tietojenkäsittelytieteen kandiohjelma').click()
cy.get('.attached').contains('Tags').click()
})
it('can create and delete tags for population', () => {
const name = `tag-${new Date().getTime()}`
cy.contains('a', 'Tietojenkäsittelytieteen kandiohjelma').click()
cy.get('.attached').contains('Tags').click()
cy.get('.tagNameSelectInput > .ui > input').type(name)
cy.get('.yearSelectInput').type('2018')
cy.contains('Create new tag').click()
@@ -360,8 +377,6 @@ describe('Studyprogramme overview', () => {
it('can create personal tags', () => {
const name = `tag-${new Date().getTime()}`
cy.contains('a', 'Tietojenkäsittelytieteen kandiohjelma').click()
cy.get('.attached').contains('Tags').click()
cy.get('.tagNameSelectInput > .ui > input').type(name)
cy.get('.yearSelectInput').type('2018')
@@ -375,9 +390,6 @@ describe('Studyprogramme overview', () => {
const name = `tag-${new Date().getTime()}`
const student = '010113437'
cy.contains('a', 'Tietojenkäsittelytieteen kandiohjelma').click()
cy.get('.attached').contains('Tags').click()
cy.get('.tagNameSelectInput > .ui > input').type(name)
cy.get('.yearSelectInput').type('2018')
cy.contains('Create new tag').click()
@@ -398,7 +410,8 @@ describe('Studyprogramme overview', () => {
cy.go('back')
cy.go('back')
// eslint-disable-next-line cypress/no-unnecessary-waiting
cy.wait(10000)
deleteTag(name)
cy.contains('Student statistics').click()
@@ -407,4 +420,42 @@ describe('Studyprogramme overview', () => {
cy.contains(name).should('not.exist')
})
})
describe('IAM user', () => {
beforeEach(() => {
cy.init('/study-programme', 'onlyiamrights')
})
it('can access programme and correct tabs are visible', () => {
cy.reload()
cy.contains('a', 'Tietojenkäsittelytieteen kandiohjelma').click()
cy.contains('Basic information')
cy.contains('Studytracks and student populations')
cy.contains('Update statistics').should('not.exist')
cy.contains('Degree Courses').should('not.exist')
})
it('can access basic information', () => {
cy.contains('a', 'Tietojenkäsittelytieteen kandiohjelma').click()
cy.contains('Basic information').click()
cy.get('[data-cy=Section-StudentsOfTheStudyprogramme]')
cy.get('[data-cy=Section-CreditsProducedByTheStudyprogramme]')
cy.get('[data-cy=Section-GraduatedAndThesisWritersOfTheProgramme]')
cy.get('[data-cy=Section-ProgrammesBeforeOrAfter]')
cy.get('[data-cy=Section-AverageGraduationTimes]')
})
it('can access studytracks', () => {
cy.contains('a', 'Tietojenkäsittelytieteen kandiohjelma').click()
cy.get('.attached').contains('Studytracks and student populations').click()
cy.get('[data-cy=Section-StudytrackOverview]')
cy.get('[data-cy=Section-StudytrackProgress]')
cy.get('[data-cy=Section-AverageGraduationTimes]')
})
})
})
+18 -2
View File
@@ -43,7 +43,23 @@ const norightsUserHeaders = {
hypersonsisuid: 'hy-hlo-0000000',
}
const userHeaders = [adminUserHeaders, basicUserHeaders, onlycoursestatisticsUserHeaders, norightsUserHeaders]
const onlyIamRightsUserHeaders = {
uid: 'onlyiamrights',
displayname: 'Only IAM rights user',
'shib-session-id': 'mock-cypress-session',
hygroupcn: 'hy-employees;hy-mltdk-tkt-jory',
edupersonaffiliation: 'member',
mail: 'grp-toska+mockonlyiamrightsuser@helsinki.fi',
hypersonsisuid: 'hy-hlo-1111111',
}
const userHeaders = [
adminUserHeaders,
basicUserHeaders,
onlycoursestatisticsUserHeaders,
norightsUserHeaders,
onlyIamRightsUserHeaders,
]
/**
Set up headers to login, set up correct user (admin / basic / etc.) and open given path.
@@ -102,7 +118,7 @@ Cypress.Commands.add('selectStudyProgramme', name => {
cy.cs('navbar-studyProgramme').click()
cy.cs('navbar-class').click()
cy.cs('select-study-programme').click().children().contains(name).click()
cy.contains('See population').click()
cy.contains('See class').click()
})
Cypress.Commands.add('checkTableStats', (correctStats, tableName) => {
@@ -14,3 +14,13 @@
// ***********************************************************
import './commands'
// This error is benign and seems to be only present in Cypress
const resizeObserverLoopErrRe = /^[^(ResizeObserver loop limit exceeded)]/
Cypress.on('uncaught:exception', err => {
/* returning false here prevents Cypress from failing the test */
if (resizeObserverLoopErrRe.test(err.message)) {
return false
}
})
+9150 -2728
View File
File diff suppressed because it is too large Load Diff
+17 -14
View File
@@ -6,6 +6,7 @@
"concurrently": "concurrently",
"eslint": "eslint",
"stylelint": "stylelint",
"actionlint": "node-actionlint",
"prettier": "prettier",
"cli": "./cli.sh",
"oodikone": "./run.sh oodikone anon up --detach",
@@ -28,31 +29,33 @@
},
"homepage": "https://github.com/UniversityOfHelsinkiCS/oodikone#readme",
"devDependencies": {
"@babel/eslint-parser": "^7.17.0",
"@babel/core": "^7.19.3",
"@babel/eslint-parser": "^7.19.1",
"@babel/plugin-syntax-jsx": "^7.16.7",
"concurrently": "^6.5.1",
"cypress": "^8.7.0",
"eslint": "^7.32.0",
"eslint-config-airbnb": "^18.2.1",
"concurrently": "^7.4.0",
"cypress": "^10.9.0",
"eslint": "^8.25.0",
"eslint-config-airbnb": "^19.0.4",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-babel": "^5.3.1",
"eslint-plugin-cypress": "^2.12.1",
"eslint-plugin-import": "^2.25.4",
"eslint-plugin-jsx-a11y": "^6.5.1",
"eslint-plugin-prefer-object-spread": "^1.2.1",
"eslint-plugin-prettier": "^3.4.1",
"eslint-plugin-react": "^7.29.3",
"eslint-plugin-prettier": "^4.2.1",
"eslint-plugin-react": "^7.31.9",
"eslint-plugin-react-hooks": "^4.3.0",
"husky": "^7.0.4",
"lint-staged": "^11.2.6",
"husky": "^8.0.1",
"lint-staged": "^13.0.3",
"node-actionlint": "^1.2.2",
"prettier": "^2.5.1",
"stylelint": "^13.13.1",
"stylelint-config-prettier": "^8.0.2",
"stylelint-config-standard": "^22.0.0",
"stylelint-prettier": "^1.2.0"
"stylelint": "^14.13.0",
"stylelint-config-prettier": "^9.0.3",
"stylelint-config-standard": "^26.0.0",
"stylelint-prettier": "^2.0.0"
},
"dependencies": {
"lodash": "^4.17.21",
"moment": "^2.29.1"
"moment": "^2.29.4"
}
}
+438
View File
@@ -0,0 +1,438 @@
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
@@ -0,0 +1,19 @@
const mapToDegreeCode = organisationCode => {
if (!organisationCode) return ''
if (organisationCode.length < 7) return ''
const doctoral = organisationCode[0] === 'T'
if (doctoral) {
return organisationCode
}
const [start, end] = organisationCode.split('-')
if (end && end.length < 3) return ''
if (start.length < 2) return ''
const masters = end[0] === 'M'
const code = `${masters ? 'M' : 'K'}H${start.substr(0, 2)}_${end.substr(-3)}`
return code
}
module.exports = {
mapToDegreeCode,
}
File diff suppressed because it is too large Load Diff
+139
View File
@@ -0,0 +1,139 @@
// Map programme codes to "letter indentifier"
const codes = {
KH10_001: 'tuk',
MH10_001: 'tum',
KH20_001: 'on',
MH20_001: 'otm',
MH20_002: 'ibl',
MH20_003: 'ggl',
KH30_001: 'psyk',
MH30_004: 'psym',
KH30_002: 'logk',
MH30_005: 'logm',
MH30_001: 'll',
MH30_002: 'tmed',
MH30_003: 'hll',
KH40_001: 'filk',
KH40_002: 'ttk',
KH40_003: 'kik',
KH40_004: 'kok',
KH40_005: 'kuka',
KH40_006: 'hisk',
MH40_001: 'ttm',
MH40_002: 'kim',
MH40_003: 'eng',
MH40_004: 'rus',
MH40_005: 'lingdig',
MH40_006: 'tra',
MH40_007: 'suku',
MH40_008: 'nor',
MH40_009: 'kir',
MH40_010: 'kuma',
MH40_011: 'ice',
MH40_012: 'alku',
MH40_014: 'spt',
MH40_015: 'hism',
KH50_001: 'mat',
KH50_002: 'fys',
KH50_003: 'kemk',
KH50_004: 'mfkk',
KH50_005: 'tkt',
KH50_006: 'geok',
KH50_007: 'maa',
KH50_008: 'bsc',
MH50_001: 'mast',
MH50_002: 'lsi',
MH50_003: 'tcm',
MH50_004: 'paras',
MH50_005: 'matres',
MH50_006: 'atm',
MH50_007: 'kem',
MH50_008: 'mfkm',
MH50_009: 'csm',
MH50_010: 'data',
MH50_011: 'geom',
MH50_012: 'geog',
MH50_013: 'usp',
KH55_001: 'farm',
MH55_001: 'prov',
MH55_002: 'mpharm',
KH57_001: 'bio',
KH57_002: 'mole',
KH57_003: 'env',
MH57_001: 'eeb',
MH57_002: 'ips',
MH57_003: 'gmb',
MH57_004: 'neuro',
MH57_005: 'ecgs',
KH60_001: 'eduk',
MH60_001: 'edum',
MH60_002: 'ce',
KH70_001: 'pvk',
KH70_002: 'yk',
KH70_003: 'sosk',
KH70_004: 'ecok',
MH70_001: 'film',
MH70_002: 'pvm',
MH70_003: 'gpc',
MH70_004: 'ym',
MH70_005: 'cos',
MH70_006: 'ens',
MH70_007: 'msv',
MH70_008: 'sosm',
MH70_009: 'econ',
MH70_011: 'sote',
KH74_001: 'ksv',
KH80_001: 'maatk',
KH80_002: 'metsak',
KH80_003: 'etk',
KH80_004: 'yet',
MH80_001: 'agri',
MH80_002: 'agere',
MH80_003: 'for',
MH80_004: 'food',
MH80_005: 'hnfb',
MH80_006: 'ekm',
MH80_007: 'mmb',
KH90_001: 'elk',
MH90_001: 'ell',
T920101: 'dptheol',
T920102: 'dplaw',
T920111: 'philartsoc',
T920103: 'dphistcult',
T920104: 'dplang',
T920105: 'sky',
T920106: 'dpsocs',
T920107: 'pyam',
T920108: 'dpe',
T920109: 'seduce',
T920110: 'clic',
T921101: 'dpbm',
T921102: 'klto',
T921103: 'docpop',
T921104: 'findos',
T921105: 'dpdr',
T921106: 'ils',
T921107: 'bandm',
T921108: 'cvm',
T921109: 'dphub',
T922101: 'luova',
T922102: 'dpps',
T922103: 'denvi',
T922104: 'agforee',
T922105: 'mbdp',
T922106: 'foodhealth',
T923101: 'papu',
T923102: 'geodoc',
T923103: 'atm',
T923104: 'chems',
T923105: 'domast',
T923106: 'matrena',
T923107: 'docs',
// manually added programs with no IAM, either abolished, or special co-operation programmes
MH50_014: 'ech',
MH40_013: 'mkk',
MH70_010: 'imes',
}
module.exports = { codes }
+5312 -1353
View File
File diff suppressed because it is too large Load Diff
+5 -5
View File
@@ -15,20 +15,20 @@
"bluebird": "^3.7.2",
"cors": "^2.8.5",
"cron": "^1.8.2",
"express": "^4.17.3",
"express": "^4.18.2",
"express-async-errors": "^3.1.1",
"lodash": "^4.17.21",
"lru-cache": "^5.1.1",
"moment": "^2.29.1",
"moment-timezone": "^0.5.34",
"moment-timezone": "^0.5.37",
"morgan": "^1.10.0",
"pg": "^8.7.3",
"pg": "^8.8.0",
"redis": "^3.1.2",
"sequelize": "^6.17.0",
"sequelize": "^6.24.0",
"umzug": "^2.3.0",
"unfuck-utf8-headers-middleware": "^1.0.1",
"uuid": "^3.4.0",
"winston": "^3.6.0"
"winston": "^3.8.2"
},
"devDependencies": {
"nodemon": "^1.19.4"
@@ -0,0 +1,26 @@
const { BIGINT, STRING, DATE, ARRAY } = require('sequelize')
module.exports = {
up: async queryInterface => {
await queryInterface.createTable('open_uni_population_searches', {
id: {
type: BIGINT,
primaryKey: true,
autoIncrement: true,
},
user_id: {
type: BIGINT,
},
name: {
type: STRING,
allowNull: false,
},
course_codes: {
type: ARRAY(STRING),
},
created_at: DATE,
updated_at: DATE,
})
},
down: () => {},
}
@@ -0,0 +1,14 @@
const { ARRAY, STRING } = require('sequelize')
module.exports = {
up: async queryInterface => {
await queryInterface.addColumn('users', 'iam_groups', {
type: ARRAY(STRING),
allowNull: false,
defaultValue: [],
})
},
down: async queryInterface => {
await queryInterface.removeColumn('users', 'iam_groups')
},
}
@@ -0,0 +1,17 @@
module.exports = {
up: queryInterface => {
return queryInterface.bulkInsert('access_groups', [
{
group_code: 'facultyStatistics',
group_info: 'grants access to faculty statistics',
},
])
},
down: queryInterface => {
return queryInterface.bulkDelete('access_groups', [
{
group_code: 'facultyStatistics',
},
])
},
}
@@ -0,0 +1,17 @@
module.exports = {
up: queryInterface => {
return queryInterface.bulkInsert('access_groups', [
{
group_code: 'openUniSearch',
group_info: 'grants access to custom open uni course searches',
},
])
},
down: queryInterface => {
return queryInterface.bulkDelete('access_groups', [
{
group_code: 'openUniSearch',
},
])
},
}
+27 -48
View File
@@ -1,17 +1,12 @@
const { CronJob } = require('cron')
const moment = require('moment')
const {
refreshProtoC,
refreshStatus,
refreshStatusGraduated,
refreshUber,
refreshProtoCProgramme,
getStartYears,
} = require('./services/trends')
const { refreshStatus, refreshStatusGraduated, refreshUber, getStartYears } = require('./services/trends')
const { refreshAssociationsInRedis } = require('./services/studyrights')
const { getAllProgrammes } = require('./services/studyrights')
const { updateBasicView, updateStudytrackView } = require('./services/studyprogrammeUpdates')
const { findAndSaveTeachers } = require('./services/topteachers')
const { faculties } = require('./services/organisations')
const { updateFacultyOverview, updateFacultyProgressOverview } = require('./services/faculty/facultyUpdates')
const { isProduction } = require('./conf-backend')
const { getCurrentSemester } = require('./services/semesters')
const logger = require('./util/logger')
@@ -23,9 +18,29 @@ const refreshStudyrightAssociations = async () => {
logger.info('Studyright associations refreshed!')
}
const refreshFaculties = async () => {
logger.info('Refreshing faculty overview statistics for all faculties')
const facultyList = (await faculties()).filter(f => !['Y', 'H99', 'Y01', 'H92', 'H930'].includes(f.code))
let ready = 0
for (const faculty of facultyList) {
try {
await updateFacultyOverview(faculty.code, 'ALL')
await updateFacultyProgressOverview(faculty.code)
ready += 1
} catch (e) {
logger.error({ message: `Failed to update stats for faculty ${faculty?.code} with type ALL`, meta: e })
}
logger.info(`${ready}/${facultyList.length} faculties done`)
}
logger.info('Faculty stats refreshed!')
}
const refreshNewOverviews = async () => {
logger.info('Refreshing studyprogramme and studytrack overview statistics for all programmes')
const codes = (await getAllProgrammes()).map(p => p.code).filter(code => code.includes('KH') || code.includes('MH'))
const codes = (await getAllProgrammes())
.map(p => p.code)
.filter(code => code.includes('KH') || code.includes('MH') || /^(T)[0-9]{6}$/.test(code))
let ready = 0
for (const code of codes) {
try {
@@ -46,19 +61,6 @@ const refreshTeacherLeaderboard = async () => {
await findAndSaveTeachers(currentSemestersYearCode - 1, currentSemestersYearCode)
}
const refreshProtoCtoRedis = async () => {
logger.info('Refreshing CDS ProtoC')
const defaultQuery = { include_old_attainments: 'false', exclude_non_enrolled: 'false' }
const onlyOld = { include_old_attainments: 'true', exclude_non_enrolled: 'false' }
const onlyEnr = { include_old_attainments: 'false', exclude_non_enrolled: 'true' }
const bothToggles = { include_old_attainments: 'true', exclude_non_enrolled: 'true' }
await refreshProtoC(defaultQuery)
await refreshProtoC(onlyOld)
await refreshProtoC(onlyEnr)
await refreshProtoC(bothToggles)
logger.info('Refreshing CDS ProtoC doned')
}
const refreshStatusToRedis = async () => {
const unixMillis = moment().valueOf()
const date = new Date(Number(unixMillis))
@@ -95,30 +97,6 @@ const refreshUberToRedis = async () => {
logger.info(`Refreshing CDS Uber data doned`)
}
const refreshProtoCProgrammeToRedis = async () => {
const codes = (await getAllProgrammes()).map(p => p.code).filter(code => code.includes('KH'))
for (const code of codes) {
logger.info(`Refreshing ProtoCProgramme code ${code}`)
const defaultQuery = { include_old_attainments: 'false', exclude_non_enrolled: 'false', code }
const onlyOld = { include_old_attainments: 'true', exclude_non_enrolled: 'false', code }
const onlyEnr = { include_old_attainments: 'false', exclude_non_enrolled: 'true', code }
const bothToggles = { include_old_attainments: 'true', exclude_non_enrolled: 'true', code }
const queries = [defaultQuery, onlyOld, onlyEnr, bothToggles]
for (const query of queries) {
try {
await refreshProtoCProgramme(query)
} catch (e) {
logger.error({
message: `Error when refreshing ProtocProgramme code ${code}, query ${Object.keys({ query })[0]}`,
meta: e,
}) // object.keys is hax to print query variable name
}
}
logger.info(`Refreshing ProtoCProgramme code ${code} doned`)
}
logger.info(`Refreshing protocprogramme doned`)
}
const refreshStatistics = async () => {
const statfuncs = [refreshStudyrightAssociations, refreshTeacherLeaderboard]
logger.info('Refreshing statistics')
@@ -129,7 +107,7 @@ const refreshStatistics = async () => {
}
const refreshTrends = async () => {
const trendfuncs = [refreshProtoCtoRedis, refreshStatusToRedis, refreshUberToRedis, refreshProtoCProgrammeToRedis]
const trendfuncs = [refreshStatusToRedis, refreshUberToRedis]
logger.info('Refreshing trends')
for (const func of trendfuncs) {
await func()
@@ -142,7 +120,7 @@ const startCron = () => {
logger.info('Cronjob for refreshing stats started: runs at 3am every day.')
// refresh 3am every day
schedule('0 3 * * *', async () => {
for (const func of [refreshStatistics, refreshTrends, refreshNewOverviews]) {
for (const func of [refreshStatistics, refreshTrends, refreshNewOverviews, refreshFaculties]) {
await func()
}
})
@@ -153,4 +131,5 @@ module.exports = {
startCron,
refreshStatistics,
refreshTrends,
refreshFaculties,
}
@@ -27,11 +27,16 @@ const accessLogger = morgan((tokens, req, res) => {
'ms',
].join(' ')
const usingIamRights = user.iamRights.some(programmeCode => tokens['url'](req, res).includes(programmeCode))
const onlyIamRights = !user.isAdmin && user.rights.length === 0
logger.info(message, {
// don't log student list which might be huge
..._.omit(meta, ['studentsUserCanAccess']),
// pass this as a custom field so we can filter by it in graylog
isUsageStats: true,
// needed for Grafana IAM users panel
isIamRights: onlyIamRights || usingIamRights,
})
})
@@ -3,10 +3,14 @@ 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) ?? []
const hasRequiredIamGroup = iamGroups => _.intersection(iamGroups, requiredGroup).length > 0
const hasRequiredIamGroup = (iamGroups, iamRights) =>
_.intersection(iamGroups, requiredGroup).length > 0 || iamRights.length > 0
const currentUserMiddleware = async (req, _res, next) => {
const {
@@ -24,9 +28,12 @@ const currentUserMiddleware = async (req, _res, next) => {
throw new ApplicationError('Not enough data in request headers', 403, { logoutUrl })
}
const iamGroups = parseIamGroups(hygroupcn)
const iamGroups = parseIamGroups(hygroupcn).filter(iam => relevantIAMs.includes(iam))
if (!hasRequiredIamGroup(iamGroups)) {
const iamRights = Object.keys(await getOrganizationAccess({ iamGroups }))
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 })
}
+4
View File
@@ -50,6 +50,10 @@ Course.init(
course_unit_type: {
type: STRING,
},
mainCourseCode: {
field: 'main_course_code',
type: STRING,
},
},
{
underscored: false,
+5
View File
@@ -10,6 +10,7 @@ const CreditType = require('./creditType')
const Credit = require('./credit')
const CreditTeacher = require('./creditTeacher')
const ElementDetail = require('./elementDetail')
const Studyplan = require('./studyplan')
const StudyrightExtent = require('./studyrightExtent')
const Studyright = require('./studyright')
const StudyrightElement = require('./studyrightElement')
@@ -63,6 +64,9 @@ Teacher.belongsToMany(Credit, { through: CreditTeacher, foreignKey: 'teacher_id'
Organization.hasMany(Studyright, { foreignKey: 'facultyCode', sourceKey: 'code' })
Studyright.belongsTo(Organization, { foreignKey: 'facultyCode', targetKey: 'code' })
Studyplan.belongsTo(Student, { foreignKey: 'studentnumber', targetKey: 'studentnumber' })
Student.hasMany(Studyplan, { foreignKey: 'studentnumber', sourceKey: 'studentnumber' })
Studyright.belongsTo(Student, { foreignKey: 'studentStudentnumber', targetKey: 'studentnumber' })
Student.hasMany(Studyright, { foreignKey: 'studentStudentnumber', sourceKey: 'studentnumber' })
@@ -121,6 +125,7 @@ module.exports = {
Credit,
CreditTeacher,
ElementDetail,
Studyplan,
StudyrightExtent,
Studyright,
StudyrightElement,
@@ -121,6 +121,28 @@ const CustomPopulationSearch = sequelizeKone.define('custom_population_searches'
},
})
const OpenUniPopulationSearch = sequelizeKone.define(
'open_uni_population_searches',
{
id: {
primaryKey: true,
type: Sequelize.BIGINT,
autoIncrement: true,
},
userId: {
type: Sequelize.BIGINT,
},
name: {
type: Sequelize.STRING,
allowNull: false,
},
courseCodes: {
type: Sequelize.ARRAY(Sequelize.STRING),
},
},
{ underscored: true, timestamps: true }
)
const StudyGuidanceGroupTag = sequelizeKone.define(
'study_guidance_group_tags',
{
@@ -162,6 +184,7 @@ module.exports = {
ThesisTypeEnums,
MandatoryCourseLabels,
CustomPopulationSearch,
OpenUniPopulationSearch,
sequelizeKone,
ExcludedCourse,
StudyGuidanceGroupTag,
@@ -76,6 +76,11 @@ const User = sequelizeUser.define(
email: { type: Sequelize.STRING },
language: { type: Sequelize.STRING },
sisu_person_id: { type: Sequelize.STRING },
iam_groups: {
type: Sequelize.ARRAY(Sequelize.STRING),
allowNull: false,
defaultValue: [],
},
last_login: { type: Sequelize.DATE },
},
{
+4 -1
View File
@@ -1,4 +1,4 @@
const { Model, STRING, DATE, INTEGER } = require('sequelize')
const { Model, STRING, DATE, INTEGER, BOOLEAN } = require('sequelize')
const { dbConnections } = require('../database/connection')
class Student extends Model {}
@@ -16,7 +16,9 @@ Student.init(
creditcount: { type: INTEGER },
dateofuniversityenrollment: { type: DATE },
email: { type: STRING },
secondary_email: { type: STRING },
national_student_number: { type: STRING },
phone_number: { type: STRING },
country_fi: { type: STRING },
country_sv: { type: STRING },
country_en: { type: STRING },
@@ -25,6 +27,7 @@ Student.init(
home_country_en: { type: STRING },
gender_code: { type: INTEGER },
sis_person_id: { type: STRING },
dissemination_info_allowed: { type: BOOLEAN },
createdAt: {
type: DATE,
},
+48
View File
@@ -0,0 +1,48 @@
const { Model, DATE, ARRAY, STRING, INTEGER } = require('sequelize')
const { dbConnections } = require('../database/connection')
class Studyplan extends Model {}
Studyplan.init(
{
id: {
primaryKey: true,
type: STRING,
allowNull: false,
},
studentnumber: {
type: STRING,
references: {
model: 'student',
key: 'studentnumber',
},
onUpdate: 'cascade',
onDelete: 'cascade',
allowNull: false,
},
programme_code: {
type: STRING,
allowNull: false,
},
included_courses: {
type: ARRAY(STRING),
},
completed_credits: {
type: INTEGER,
},
createdAt: {
type: DATE,
},
updatedAt: {
type: DATE,
},
},
{
underscored: false,
sequelize: dbConnections.sequelize,
modelName: 'studyplan',
tableName: 'studyplan',
}
)
module.exports = Studyplan
+3 -3
View File
@@ -9,9 +9,6 @@ Studyright.init(
primaryKey: true,
type: STRING,
},
canceldate: {
type: DATE,
},
startdate: {
type: DATE,
},
@@ -27,6 +24,9 @@ Studyright.init(
graduated: {
type: INTEGER,
},
active: {
type: INTEGER,
},
// irtisanomisperuste
studentStudentnumber: {
type: STRING,
+328 -16
View File
@@ -1,18 +1,69 @@
/* eslint no-console: 0 */
// depends on nodeproxy running in importer.cs.helsinki.fi, and env RAPO_NODEPROXY (see importer for a proper value)
/*
depends on nodeproxy running in importer.cs.helsinki.fi, and env RAPO_NODEPROXY (see importer for a proper value)
needs also acces to importer db api, uses env IMPORTER_API
known differences in APIs
- rapo does not show students that have graduated (it however takes some days for rapo to notice a graduation)
- rapo does not handle the starting year of transfers properly (shows in old staring population)
- rapo show some students incorrectly passive, see https://github.com/UniversityOfHelsinkiCS/oodikone/issues/3521
*/
const axios = require('axios').default
const _ = require('lodash')
const { optimizedStatisticsOf } = require('./services/populations')
const { Student, Transfer, StudyrightElement, Studyright } = require('./models')
const token = process.env.RAPO_NODEPROXY
const programme_diff = async (programme, facultyCode, year) => {
const getFromRapo = async (urlStart, urlEnd) => {
const { data: rapoStuffLasna } = await axios.get(urlStart)
const { data: rapoStuffLasnaMinus } = await axios.get(urlEnd)
const rapoStudents = _.difference(
rapoStuffLasna.map(s => s.opiskelijanumero),
rapoStuffLasnaMinus.map(s => s.opiskelijanumero)
)
return rapoStudents
}
const known_problems_in_oodikone = {
KH30_001: ['013896191'],
KH40_003: ['013200457'],
KH55_001: ['013195834', '015119708', '015138336'],
}
const known_problems_in_rapo = {
KH55_001: ['015160142', '015135575', '015119708', '015138336'],
}
const getFromOodikone = async (programme, year) => {
const isNotAmongKnownProblems = student => {
const hasProblems = known_problems_in_oodikone[programme] ? known_problems_in_oodikone[programme] : []
return !hasProblems.includes(student.studentNumber)
}
const theStudyrights = student => {
const theOne = s => s.studyright_elements.find(e => e.code === programme)
// in some rare cases student may have more than one studyright for same programme
return student.studyrights.filter(theOne)
}
const notGraduated = student => {
const graduation = theStudyrights(student).filter(r => r.graduated === 1)
return graduation.length === 0
}
const notTransferred = student => {
const transfer = student.transfers.find(t => t.targetcode === programme)
return transfer === undefined
}
const months = 12
const studyrightStartDate = `${year}-08-01`
const oodikoneQuery = {
semesters: ['FALL', 'SPRING'],
@@ -20,32 +71,293 @@ const programme_diff = async (programme, facultyCode, year) => {
studyRights: { programme },
year,
}
const rapoUrl = presence =>
const oodikoneStuff = (await optimizedStatisticsOf(oodikoneQuery)).students
return oodikoneStuff
.filter(notGraduated)
.filter(notTransferred)
.filter(isNotAmongKnownProblems)
.map(s => s.studentNumber)
}
const isAcuallyActive = async student => {
const currentSemester = 144
const tokenImporterApi = process.env.IMPORTER_API
const url = `https://importer.cs.helsinki.fi/api/importer/students/${student}/rapo_semester_enrollments?token=${tokenImporterApi}`
const { data } = await axios.get(url)
const terms = data.reduce((all, s) => all.concat(s.terms), []).filter(t => t.semester_code === currentSemester)
const active = terms.filter(t => t.type !== 'NEGLECTED')
const negl = terms.filter(t => t.type === 'NEGLECTED')
return active.length > 0 && negl.length > 0
}
const hasTransferred = async (student, programme) => {
const data = await Student.findByPk(student, {
include: [
{
model: Transfer,
},
],
})
const transferTo = data.transfers.find(t => t.targetcode === programme)
return transferTo !== undefined
}
const graduatedVeryRecently = async (student, programme) => {
const data = await Student.findByPk(student, {
include: [
{
model: Studyright,
include: [
{
model: StudyrightElement,
required: true,
where: {
code: programme,
},
},
],
where: {
graduated: 1,
},
},
],
})
if (!data || data.studyrights.length === 0) {
return false
}
const graduated = new Date(data.studyrights[0].enddate)
const weekAgo = new Date(new Date().getTime() - 7 * 24 * 60 * 60 * 1000)
return graduated > weekAgo
}
const programme_diff_year = async (programme, facultyCode, year, brief = false) => {
const oodikoneStudents = await getFromOodikone(programme, year)
const studyrightStartDate = `${year}-08-01`
const studyrightEndDate = `${year + 1}-08-01`
const rapoUrlStart = presence =>
encodeURI(
`https://importer.cs.helsinki.fi/test/rapo/${facultyCode}/${programme}/${studyrightStartDate}/${presence}?token=${token}`
)
const rapoUrlEnd = presence =>
encodeURI(
`https://importer.cs.helsinki.fi/test/rapo/${facultyCode}/${programme}/${studyrightEndDate}/${presence}?token=${token}`
)
const oodikoneStuff = await optimizedStatisticsOf(oodikoneQuery)
const oodikoneStudents = oodikoneStuff.students.map(s => s.studentNumber)
const rapoStudentsLasna = await getFromRapo(rapoUrlStart('Läsnä'), rapoUrlEnd('Läsnä'))
const rapoStudentsPoissa = await getFromRapo(rapoUrlStart('Poissa'), rapoUrlEnd('Poissa'))
const rapoStudentPoissaLaki = await getFromRapo(
rapoUrlStart('Poissa ei kuluta opintoaikaa'),
rapoUrlEnd('Poissa ei kuluta opintoaikaa')
)
const { data: rapoStuffLasna } = await axios.get(rapoUrl('Läsnä'))
const { data: rapoStuffPoissa } = await axios.get(rapoUrl('Poissa'))
const isNotAmongKnownProblems = student => {
const hasProblems = known_problems_in_rapo[programme] ? known_problems_in_rapo[programme] : []
return !hasProblems.includes(student.studentNumber)
}
console.log(rapoStuffLasna.length, rapoStuffPoissa.length)
const rapoStudents = rapoStuffLasna.map(s => s.opiskelijanumero).concat(rapoStuffPoissa.map(s => s.opiskelijanumero))
console.log(rapoStudents.length, oodikoneStudents.length)
const rapoStudents = rapoStudentsLasna
.concat(rapoStudentsPoissa)
.concat(rapoStudentPoissaLaki)
.filter(isNotAmongKnownProblems)
const onlyInOodikone = _.difference(oodikoneStudents, rapoStudents)
const onlyInRapo = _.difference(rapoStudents, oodikoneStudents)
console.log('Only in Oodikone:', onlyInOodikone)
console.log('Only in Rapo:', onlyInRapo)
const wrongMissingInRapo = []
for (let student of onlyInOodikone) {
const status = await isAcuallyActive(student)
if (status) {
wrongMissingInRapo.push(student)
}
}
// these are in wrong RAPO-population
const transferred = []
for (let student of onlyInRapo) {
const isTransfer = await hasTransferred(student, programme)
if (isTransfer) {
transferred.push(student)
} else {
const isRecentGraduation = await graduatedVeryRecently(student, programme)
if (isRecentGraduation) {
transferred.push(student)
}
}
}
const checkOodikoneOnly = _.difference(onlyInOodikone, wrongMissingInRapo)
const checkRapoOnly = _.difference(onlyInRapo, transferred)
if (!brief) {
console.log('vuosi', year)
console.log('')
console.log('oodikone', oodikoneStudents.length, ' transferred', transferred.length)
console.log('rapo', rapoStudents.length, ' activeness wrong rapo', wrongMissingInRapo.length)
console.log('only rapo', checkRapoOnly.length)
console.log('only oodikone', checkOodikoneOnly.length)
console.log('')
if (checkRapoOnly.length > 0) {
console.log('Only in Rapo:')
console.log(checkRapoOnly.join('\n'))
}
if (checkOodikoneOnly.length > 0) {
console.log('Only in Oodikone:')
console.log(checkOodikoneOnly.join('\n'))
}
}
if (checkRapoOnly.length + checkOodikoneOnly.length) {
console.log('******************************', year)
if (checkRapoOnly.length > 0) {
console.log('Only in Rapo:')
console.log(checkRapoOnly.join('\n'))
}
if (checkOodikoneOnly.length > 0) {
console.log('Only in Oodikone:')
console.log(checkOodikoneOnly.join('\n'))
}
}
if (!brief) {
console.log('')
}
}
const programme_diff = async (programme, faculty, brief) => {
console.log()
console.log(programme)
console.log('--------', '\n')
await programme_diff_year(programme, faculty, 2017, brief)
await programme_diff_year(programme, faculty, 2018, brief)
await programme_diff_year(programme, faculty, 2019, brief)
await programme_diff_year(programme, faculty, 2020, brief)
await programme_diff_year(programme, faculty, 2021, brief)
}
const faculty_diff = async (faculty, programmes, brief) => {
console.log('\n')
console.log(faculty)
console.log('==')
for (let programme of programmes) {
await programme_diff(programme, faculty, brief)
}
}
// eslint-disable-next-line no-unused-vars
const msc_programme_diff_year = async (programme, facultyCode, year, brief = false) => {
const oodikoneStudents = await getFromOodikone(programme, year)
const studyrightStartDate = `${year}-08-01`
const studyrightEndDate = `${year + 1}-08-01`
const rapoUrlStart = presence =>
encodeURI(
`https://importer.cs.helsinki.fi/test/rapo/${facultyCode}/${programme}/${studyrightStartDate}/${presence}?token=${token}`
)
const rapoUrlEnd = presence =>
encodeURI(
`https://importer.cs.helsinki.fi/test/rapo/${facultyCode}/${programme}/${studyrightEndDate}/${presence}?token=${token}`
)
const rapoStudentsLasna = await getFromRapo(rapoUrlStart('Läsnä'), rapoUrlEnd('Läsnä'))
const rapoStudentsPoissa = await getFromRapo(rapoUrlStart('Poissa'), rapoUrlEnd('Poissa'))
const rapoStudentPoissaLaki = await getFromRapo(
rapoUrlStart('Poissa ei kuluta opintoaikaa'),
rapoUrlEnd('Poissa ei kuluta opintoaikaa')
)
const rapoStudents = rapoStudentsLasna.concat(rapoStudentsPoissa).concat(rapoStudentPoissaLaki)
const onlyInOodikone = _.difference(oodikoneStudents, rapoStudents)
const onlyInRapo = _.difference(rapoStudents, oodikoneStudents)
const wrongInRapo = []
for (let student of onlyInOodikone) {
const status = await isAcuallyActive(student)
if (status) {
//wrongInRapo.push(student)
}
}
// in wrong RAPO population
const transferred = []
for (let student of onlyInRapo) {
const status = await hasTransferred(student, programme)
if (status) {
transferred.push(student)
}
}
const checkOodikoneOnly = _.difference(onlyInOodikone, wrongInRapo)
const checkRapoOnly = _.difference(onlyInRapo, transferred)
const both = _.intersection(oodikoneStudents, rapoStudents)
console.log('vuosi', year)
if (!brief) {
console.log('')
console.log('oodikone', oodikoneStudents.length, ' transferred', transferred.length)
console.log('rapo', rapoStudents.length, ' activeness wrong rapo', wrongInRapo.length)
console.log('both', both.length)
console.log('only rapo', checkRapoOnly.length)
console.log('only oodikone', checkOodikoneOnly.length)
console.log('')
if (checkRapoOnly.length > 0) {
console.log('Only in Rapo:')
console.log(checkRapoOnly.join('\n'))
}
if (checkOodikoneOnly.length > 0) {
console.log('Only in Oodikone:')
console.log(checkOodikoneOnly.join('\n'))
}
}
if (checkRapoOnly.length + checkOodikoneOnly.length) {
console.log('******************************************************')
}
if (!brief) {
console.log('')
}
}
const bsc_priogrammes_of_faculties = {
H10: ['KH10_001'],
H20: ['KH20_001'],
H30: ['KH30_001', 'KH30_002'],
H40: ['KH40_001', 'KH40_002', 'KH40_003', 'KH40_004', 'KH40_005', 'KH40_006'],
H50: ['KH50_001', 'KH50_002', 'KH50_003', 'KH50_004', 'KH50_005', 'KH50_006', 'KH50_007', 'KH50_008'],
H55: ['KH55_001'],
H60: ['KH60_001'],
H70: ['KH70_001', 'KH70_002', 'KH70_003', 'KH70_004'],
H74: ['KH74_001'],
H80: ['KH80_001', 'KH80_002', 'KH80_003', 'KH80_004'],
H90: ['KH90_001'],
}
const main = async () => {
await programme_diff('KH50_005', 'H50', 2021)
for (let faculty of Object.keys(bsc_priogrammes_of_faculties)) {
await faculty_diff(faculty, bsc_priogrammes_of_faculties[faculty], true)
}
//await programme_diff_year('KH55_001', 'H55', 2021, true)
//await programme_diff('KH60_001', 'H60', true)
}
main().then(() => process.exit(0))
+3 -1
View File
@@ -30,6 +30,7 @@ const trends = require('./routes/trends')
const programmeModules = require('./routes/programmeModules')
const studyGuidanceGroups = require('./routes/studyGuidanceGroups')
const studyProgramme = require('./routes/studyProgramme')
const customOpenUniSearch = require('./routes/customOpenUniSearch')
const initializeSentry = require('./util/sentry')
const errorMiddleware = require('./middleware/errorMiddleware')
@@ -57,7 +58,8 @@ module.exports = (app, url) => {
app.use(url, tags)
app.use(url, programmeModules)
app.use(url, studyProgramme)
app.use(`${url}/faculties`, faculties)
app.use(`${url}/openunisearch`, auth.roles(['openUniSearch']), customOpenUniSearch)
app.use(`${url}/faculties`, auth.roles(['facultyStatistics']), faculties)
app.use(`${url}/updater`, auth.roles(['admin']), updater)
app.use(`${url}/teachers`, auth.roles(['teachers']), teachers)
app.use(`${url}/users`, auth.roles(['admin']), users)
+8 -3
View File
@@ -1,6 +1,7 @@
const router = require('express').Router()
const crypto = require('crypto')
const Course = require('../services/courses')
const { faculties } = require('../services/organisations')
const { validateParamLength } = require('../util')
router.get('/v2/coursesmulti', async (req, res) => {
@@ -28,10 +29,9 @@ router.get('/v3/courseyearlystats', async (req, res) => {
return res.status(403).json({ error: 'No programmes so no access to course stats' })
}
const { codes, separate: sep, unifyOpenUniCourses: unify } = req.query
const { codes, separate: sep } = req.query
const separate = !sep ? false : JSON.parse(sep)
const unifyOpenUniCourses = !unify ? false : JSON.parse(unify)
if (!codes) {
res.status(422).send('Missing required query parameters')
} else {
@@ -39,9 +39,14 @@ router.get('/v3/courseyearlystats', async (req, res) => {
// and users with rights to any specific study programmes
const anonymize = allowedRoles !== 'admin' && rights.length < 1
const anonymizationSalt = anonymize ? crypto.randomBytes(12).toString('hex') : null
const results = await Course.courseYearlyStats(codes, separate, unifyOpenUniCourses, anonymizationSalt)
const results = await Course.courseYearlyStats(codes, separate, anonymizationSalt)
res.json(results)
}
})
router.get('/faculties-only', async (_req, res) => {
const facultyList = await faculties()
res.json(facultyList)
})
module.exports = router
@@ -0,0 +1,74 @@
const router = require('express').Router()
var moment = require('moment-timezone')
const { getCustomOpenUniCourses } = require('../services/openUni/openUniStats')
const {
getOpenUniSearches,
createNewSearch,
deleteSearch,
updateSearch,
} = require('../services/openUni/openUniManageSearches')
router.get('/', async (req, res) => {
const courseCodes = JSON.parse(req.query?.courselist) || []
const startdate = req.query?.startdate ? req.query?.startdate : moment('01-08-2017 00:00:00', 'DD-MM-YYYY')
const enddate = req.query?.enddate ? req.query?.enddate : moment().endOf('day')
if (!Array.isArray(courseCodes)) return res.status(400).json({ error: 'Courses must be of type array' })
const customOpenUniSearches = await getCustomOpenUniCourses(courseCodes, startdate, enddate)
return res.json(customOpenUniSearches)
})
router.get('/searches', async (req, res) => {
const userId = req.user.id
const foundSearches = await getOpenUniSearches(userId)
return res.json(foundSearches)
})
router.post('/searches', async (req, res) => {
const courseCodes = req.body?.courselist || []
const userId = req.user.id
const name = req.body?.name
if (!name) return res.status(400).json({ error: 'Name missing' })
if (courseCodes && !Array.isArray(courseCodes))
return res.status(400).json({ error: 'Course codes must be type of array' })
const createdSearch = await createNewSearch(userId, name, courseCodes)
if (!createdSearch) return res.status(400).json({ error: '' })
return res.status(201).json({
id: createdSearch.id,
userId: createdSearch.userId,
name: createdSearch.name,
courseList: createdSearch.courseCodes,
updatedAt: createdSearch.updatedAt,
})
})
router.put('/searches/:id', async (req, res) => {
const id = req.params?.id
const courseCodes = req.body?.courselist || []
const userId = req.user.id
if (!id || !userId) return res.status(422).end()
const updatedSearch = await updateSearch(userId, id, courseCodes)
if (!updatedSearch) return res.status(404).json({ error: `Open uni search could not be found` })
return res.json({
id: updatedSearch.id,
userId: updatedSearch.userId,
name: updatedSearch.name,
courseList: updatedSearch.courseCodes,
updatedAt: updatedSearch.updatedAt,
})
})
router.delete('/searches/:id', async (req, res) => {
const id = req.params?.id
const userId = req.user.id
if (!id || !userId) return res.status(422).end()
const deletedSearch = await deleteSearch(userId, id)
if (!deletedSearch) return res.status(404).json({ error: `Open uni search could not be found` })
return res.json(id)
})
module.exports = router
+198 -1
View File
@@ -1,9 +1,206 @@
const router = require('express').Router()
const { faculties } = require('../services/organisations')
const { combineFacultyBasics } = require('../services/faculty/facultyBasics')
const { combineFacultyCredits } = require('../services/faculty/facultyCredits')
const { findFacultyProgrammeCodes } = require('../services/faculty/faculty')
const { combineFacultyThesisWriters } = require('../services/faculty/facultyThesisWriters')
const { countGraduationTimes } = require('../services/faculty/facultyGraduationTimes')
const { updateFacultyOverview, updateFacultyProgressOverview } = require('../services/faculty/facultyUpdates')
const { combineFacultyStudentProgress } = require('../services/faculty/facultyStudentProgress')
const { combineFacultyStudents } = require('../services/faculty/facultyStudents')
const {
getFacultyProgrammes,
setFacultyProgrammes,
getBasicStats,
setBasicStats,
getCreditStats,
setCreditStats,
getThesisWritersStats,
setThesisWritersStats,
getGraduationStats,
setGraduationStats,
getFacultyStudentStats,
setFacultyStudentStats,
getFacultyProgressStats,
setFacultyProgressStats,
} = require('../services/faculty/facultyService')
const logger = require('../util/logger')
// Faculty uses a lot of tools designed for Study programme.
// Some of them have been copied here and slightly edited for faculty purpose.
const getProgrammes = async (code, programmeFilter) => {
const programmes = await getFacultyProgrammes(code, programmeFilter)
if (programmes) return programmes
let updatedProgrammes = await findFacultyProgrammeCodes(code, programmeFilter)
if (updatedProgrammes) updatedProgrammes = await setFacultyProgrammes(code, updatedProgrammes, programmeFilter)
return updatedProgrammes
}
router.get('/', async (req, res) => {
const facultyList = await faculties()
const ignore = ['Y', 'H99', 'Y01', 'H92', 'H930']
const facultyList = (await faculties()).filter(f => !ignore.includes(f.code))
facultyList.sort((a, b) => (a.name.fi > b.name.fi ? 1 : -1))
res.json(facultyList)
})
router.get('/:id/basicstats', async (req, res) => {
const code = req.params.id
const yearType = req.query?.year_type
const programmeFilter = req.query?.programme_filter
const specialGroups = req.query?.special_groups
if (!code) return res.status(422).end()
const data = await getBasicStats(code, yearType, programmeFilter, specialGroups)
if (data) return res.json(data)
const wantedProgrammes = await getProgrammes(code, programmeFilter)
if (!wantedProgrammes) return res.status(422).end()
//all programmes are required for correct sorting of transfers
let allProgrammeCodes = []
if (programmeFilter === 'NEW_STUDY_PROGRAMMES') {
const allProgs = await getProgrammes(code, 'ALL_PROGRAMMES')
allProgs?.data.forEach(prog => allProgrammeCodes.push(prog.code))
} else {
wantedProgrammes?.data.forEach(prog => allProgrammeCodes.push(prog.code))
}
let updatedStats = await combineFacultyBasics(
code,
wantedProgrammes.data,
yearType,
allProgrammeCodes,
programmeFilter,
specialGroups
)
if (updatedStats) {
updatedStats = await setBasicStats(updatedStats, yearType, programmeFilter, specialGroups)
}
return res.json(updatedStats)
})
router.get('/:id/creditstats', async (req, res) => {
const code = req.params.id
const yearType = req.query?.year_type
const specialGroups = req.query?.special_groups
const programmeFilter = req.query?.programme_filter
if (!code) return res.status(422).end()
const data = await getCreditStats(code, yearType, programmeFilter, specialGroups)
if (data) return res.json(data)
const programmes = await getProgrammes(code, programmeFilter)
if (!programmes) return res.status(422).end()
// list of all programmes is also needed in classification of credits
const allProgrammes = programmeFilter === 'ALL_PROGRAMMES' ? programmes : await getProgrammes(code, 'ALL_PROGRAMMES')
let updatedStats = await combineFacultyCredits(code, programmes.data, allProgrammes.data, yearType, specialGroups)
if (updatedStats) {
updatedStats = await setCreditStats(updatedStats, yearType, programmeFilter, specialGroups)
}
return res.json(updatedStats)
})
router.get('/:id/thesisstats', async (req, res) => {
const code = req.params.id
const yearType = req.query?.year_type
const specialGroups = req.query?.special_groups
const programmeFilter = req.query?.programme_filter
if (!code) return res.status(422).end()
const data = await getThesisWritersStats(code, yearType, programmeFilter, specialGroups)
if (data) return res.json(data)
const programmes = await getProgrammes(code, programmeFilter)
if (!programmes) return res.status(422).end()
let updateStats = await combineFacultyThesisWriters(code, programmes.data, yearType, specialGroups)
if (updateStats) {
updateStats = await setThesisWritersStats(updateStats, yearType, programmeFilter, specialGroups)
}
return res.json(updateStats)
})
router.get('/:id/graduationtimes', async (req, res) => {
const code = req.params.id
const programmeFilter = req.query?.programme_filter
if (!code) return res.status(422).end()
const data = await getGraduationStats(code, programmeFilter)
if (data) {
return res.json(data)
}
let updatedStats = await countGraduationTimes(code, programmeFilter)
if (updatedStats) {
updatedStats = await setGraduationStats(updatedStats, programmeFilter)
}
return res.json(updatedStats)
})
router.get('/:id/progressstats', async (req, res) => {
const code = req.params.id
const programmeFilter = req.query.programme_filter
const specialGroups = req.query?.special_groups
const graduated = req.query?.graduated
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)
if (updateStats) {
updateStats = await setFacultyProgressStats(updateStats, specialGroups, graduated)
}
return res.json(updateStats)
})
router.get('/:id/studentstats', async (req, res) => {
const code = req.params.id
const programmeFilter = req.query.programme_filter
const specialGroups = req.query?.special_groups
const graduated = req.query?.graduated
if (!code) return res.status(422).end()
const data = await getFacultyStudentStats(code, specialGroups, graduated)
if (data) return res.json(data)
const programmes = await getProgrammes(code, programmeFilter)
if (!programmes) return res.status(422).end()
let updateStats = await combineFacultyStudents(code, programmes.data, specialGroups, graduated)
if (updateStats) {
updateStats = await setFacultyStudentStats(updateStats, specialGroups, graduated)
}
return res.json(updateStats)
})
router.get('/:id/update_basicview', async (req, res) => {
const code = req.params.id
const statsType = req.query?.stats_type
if (code) {
let result = null
try {
result = await updateFacultyOverview(code, statsType)
} catch (e) {
logger.error(`Failed to update faculty ${code} basic tab stats for ${statsType}: ${e}`)
}
return res.json(result)
}
return res.status(422).end()
})
router.get('/:id/update_progressview', async (req, res) => {
const code = req.params.id
if (code) {
let result = null
try {
result = await updateFacultyProgressOverview(code)
} catch (e) {
logger.error(`Failed to update faculty ${code} progress tab stats: ${e}`)
}
return res.json(result)
}
return res.status(422).end()
})
module.exports = router
+106 -24
View File
@@ -8,6 +8,10 @@ const StudyrightService = require('../services/studyrights')
const TagService = require('../services/tags')
const CourseService = require('../services/courses')
const StatMergeService = require('../services/statMerger')
const { mapToProviders } = require('../util/utils')
const { encrypt, decrypt } = require('../services/encrypt')
const { mapCodesToIds } = require('../services/studyprogrammeHelpers')
const { ApplicationError } = require('../util/customErrors')
const filterPersonalTags = (population, userId) => {
@@ -36,16 +40,26 @@ router.post('/v2/populationstatistics/courses', async (req, res) => {
req.body.months = 12
}
const encrypted = req.body.selectedStudents[0]?.encryptedData
if (req.body.years) {
const upperYearBound = new Date().getFullYear() + 1
const multicoursestatPromises = Promise.all(
req.body.years.map(year => {
if (req.body.selectedStudentsByYear) {
req.body.selectedStudents = req.body.selectedStudentsByYear[year]
req.body.selectedStudents = encrypted
? req.body.selectedStudents
.filter(
({ encryptedData }) =>
req.body.selectedStudentsByYear[year] &&
req.body.selectedStudentsByYear[year].includes(encryptedData)
)
.map(decrypt)
: req.body.selectedStudentsByYear[year]
}
const newMonths = (upperYearBound - Number(year)) * 12
const query = { ...req.body, year, months: newMonths }
const coursestatistics = Population.bottlenecksOf(query)
const coursestatistics = Population.bottlenecksOf(query, null, encrypted)
return coursestatistics
})
)
@@ -58,7 +72,9 @@ router.post('/v2/populationstatistics/courses', async (req, res) => {
res.json(result)
} else {
const result = await Population.bottlenecksOf(req.body)
if (encrypted) req.body.selectedStudents = req.body.selectedStudents.map(decrypt)
const result = await Population.bottlenecksOf(req.body, null, encrypted)
if (result.error) {
Sentry.captureException(new Error(result.error))
@@ -66,6 +82,20 @@ router.post('/v2/populationstatistics/courses', async (req, res) => {
return
}
//These cause problems in front (where they were originally calculated)
// Do them here for the moment
for (const course of result.coursestatistics) {
// course.students.notParticipated = {}
course.students.didNotPass = {}
for (const student of Object.keys(course.students.all)) {
if (
!Object.keys(course.students.passed).includes(student) &&
!Object.keys(course.students.retryPassed).includes(student)
)
course.students.didNotPass[student] = true
}
}
res.json(result)
}
})
@@ -141,7 +171,7 @@ router.post('/v2/populationstatistics/coursesbytag', async (req, res) => {
router.post('/v2/populationstatistics/coursesbystudentnumberlist', async (req, res) => {
const {
user: { isAdmin, studentsUserCanAccess },
body: { studentnumberlist: studentnumbersInReq },
body: { studentnumberlist: studentnumbersInReq, studentCountLimit, ...query },
} = req
if (!studentnumbersInReq) {
@@ -152,10 +182,11 @@ router.post('/v2/populationstatistics/coursesbystudentnumberlist', async (req, r
const result = await Population.bottlenecksOf(
{
year: 1900,
studyRights: [],
semesters: ['FALL', 'SPRING'],
months: 10000,
year: query?.year ?? 1900,
studyRights: query?.studyRights ?? [],
semesters: query?.semesters ?? ['FALL', 'SPRING'],
months: query?.months ?? 10000,
studentCountLimit,
},
studentnumberlist
)
@@ -177,13 +208,13 @@ router.get('/v3/populationstatistics', async (req, res) => {
return
}
let studyRights = null
try {
studyRights = JSON.parse(studyRightsJSON)
const {
user: { rights, isAdmin },
} = req
studyRights = JSON.parse(studyRightsJSON)
const {
user: { rights, iamRights, isAdmin },
} = req
if (!isAdmin && !rights.includes(studyRights.programme)) {
try {
if (!isAdmin && !rights.includes(studyRights.programme) && !iamRights.includes(studyRights.programme)) {
res.status(403).json([])
return
}
@@ -229,6 +260,28 @@ router.get('/v3/populationstatistics', async (req, res) => {
return
}
// Obfuscate if user has only iam rights
if (!isAdmin && !rights.includes(studyRights.programme)) {
result.students = result.students.map(student => {
const { iv, encryptedData } = encrypt(student.studentNumber)
const obfuscatedBirthDate = new Date(new Date(student.birthdate).setMonth(0, 0)) // only birth year for age distribution
return {
...student,
firstnames: '',
lastname: '',
phone_number: '',
started: null,
iv,
studentNumber: encryptedData,
name: '',
email: '',
tags: [],
birthdate: obfuscatedBirthDate,
obfuscated: true,
}
})
}
res.json(filterPersonalTags(result, user.id))
}
})
@@ -274,9 +327,9 @@ router.get('/v3/populationstatisticsbytag', async (req, res) => {
})
router.get('/v3/populationstatisticsbycourse', async (req, res) => {
const { coursecodes, from, to, separate: sep } = req.query
const { coursecodes, from, to, separate: sep, unifyCourses } = req.query
const {
user: { id, isAdmin, studentsUserCanAccess: allStudentsUserCanAccess },
user: { id, isAdmin, studentsUserCanAccess: allStudentsUserCanAccess, rights },
} = req
const separate = sep ? JSON.parse(sep) : false
@@ -293,7 +346,13 @@ router.get('/v3/populationstatisticsbycourse', async (req, res) => {
}
const semesters = ['FALL', 'SPRING']
const studentnumbers = await Student.findByCourseAndSemesters(JSON.parse(coursecodes), from, to, separate)
const studentnumbers = await Student.findByCourseAndSemesters(
JSON.parse(coursecodes),
from,
to,
separate,
unifyCourses
)
const result = await Population.optimizedStatisticsOf(
{
// Useless, because studentnumbers are already filtered above by from & to.
@@ -305,10 +364,15 @@ router.get('/v3/populationstatisticsbycourse', async (req, res) => {
},
studentnumbers
)
let courseproviders = []
if (!isAdmin) {
courseproviders = await CourseService.getCourseProvidersForCourses(JSON.parse(coursecodes))
}
const rightsMappedToProviders = mapToProviders(rights)
const found = courseproviders.some(r => rightsMappedToProviders.includes(r))
const studentsUserCanAccess = isAdmin
? new Set(studentnumbers)
: new Set(_.intersection(studentnumbers, allStudentsUserCanAccess))
const studentsUserCanAccess =
isAdmin || found ? new Set(studentnumbers) : new Set(_.intersection(studentnumbers, allStudentsUserCanAccess))
const randomHash = crypto.randomBytes(12).toString('hex')
const obfuscateStudent = ({ studyrights, studentNumber, courses, gender_code }) => ({
@@ -318,6 +382,7 @@ router.get('/v3/populationstatisticsbycourse', async (req, res) => {
studentNumber: crypto.createHash('md5').update(`${studentNumber}${randomHash}`).digest('hex'),
firstnames: '',
lastname: '',
phone_number: '',
name: '',
email: '',
started: null,
@@ -363,13 +428,15 @@ router.post('/v3/populationstatisticsbystudentnumbers', async (req, res) => {
router.get('/v3/populationstatistics/studyprogrammes', async (req, res) => {
const {
user: { rights, roles },
user: { rights, iamRights, roles },
} = req
if (roles?.includes('admin')) {
const studyrights = await StudyrightService.getAssociations()
mapCodesToIds(studyrights.programmes)
res.json(studyrights)
} else {
const studyrights = await StudyrightService.getFilteredAssociations(rights)
const studyrights = await StudyrightService.getFilteredAssociations(rights.concat(iamRights))
mapCodesToIds(studyrights.programmes)
res.json(studyrights)
}
})
@@ -381,8 +448,23 @@ router.get('/v3/populationstatistics/studyprogrammes/unfiltered', async (req, re
router.get('/v3/populationstatistics/maxYearsToCreatePopulationFrom', async (req, res) => {
const { courseCodes } = req.query
const maxYearsToCreatePopulationFrom = await CourseService.maxYearsToCreatePopulationFrom(JSON.parse(courseCodes))
return res.json(maxYearsToCreatePopulationFrom)
const maxYearsToCreatePopulationFromOpen = await CourseService.maxYearsToCreatePopulationFrom(
JSON.parse(courseCodes),
'openStats'
)
const maxYearsToCreatePopulationFromUni = await CourseService.maxYearsToCreatePopulationFrom(
JSON.parse(courseCodes),
'reqularStats'
)
const maxYearsToCreatePopulationFromBoth = await CourseService.maxYearsToCreatePopulationFrom(
JSON.parse(courseCodes),
'unifyStats'
)
return res.json({
openCourses: maxYearsToCreatePopulationFromOpen,
uniCourses: maxYearsToCreatePopulationFromUni,
unifyCourses: maxYearsToCreatePopulationFromBoth,
})
})
module.exports = router
+67 -100
View File
@@ -3,6 +3,7 @@ const { getBasicStatsForStudytrack } = require('../services/studyprogrammeBasics
const { getCreditStatsForStudytrack } = require('../services/studyprogrammeCredits')
const { getGraduationStatsForStudytrack } = require('../services/studyprogrammeGraduations')
const { getStudytrackStatsForStudyprogramme } = require('../services/studytrackStats')
const { getStudyprogrammeCoursesForStudytrack } = require('../services/studyprogrammeCourses')
const {
getBasicStats,
setBasicStats,
@@ -21,31 +22,20 @@ router.get('/v2/studyprogrammes/:id/basicstats', async (req, res) => {
const yearType = req.query?.year_type
const specialGroups = req.query?.special_groups
if (code) {
let data = null
try {
data = await getBasicStats(code, yearType, specialGroups)
} catch (e) {
logger.error(`Failed to get code ${code} basic stats: ${e}`)
}
if (!data) {
try {
const result = await getBasicStatsForStudytrack({
studyprogramme: req.params.id,
settings: {
isAcademicYear: yearType === 'ACADEMIC_YEAR',
includeAllSpecials: specialGroups === 'SPECIAL_INCLUDED',
},
})
data = await setBasicStats(result, yearType, specialGroups)
} catch (e) {
logger.error(`Failed to update code ${code} basic stats: ${e}`)
}
}
return res.json(data)
} else {
res.status(422).end()
}
if (!code) return res.status(422).end()
const data = await getBasicStats(code, yearType, specialGroups)
if (data) return res.json(data)
const updated = await getBasicStatsForStudytrack({
studyprogramme: req.params.id,
settings: {
isAcademicYear: yearType === 'ACADEMIC_YEAR',
includeAllSpecials: specialGroups === 'SPECIAL_INCLUDED',
},
})
if (updated) await setBasicStats(updated, yearType, specialGroups)
return res.json(updated)
})
router.get('/v2/studyprogrammes/:id/creditstats', async (req, res) => {
@@ -53,31 +43,19 @@ router.get('/v2/studyprogrammes/:id/creditstats', async (req, res) => {
const yearType = req.query?.year_type
const specialGroups = req.query?.special_groups
if (code) {
let data = null
try {
data = await getCreditStats(code, yearType, specialGroups)
} catch (e) {
logger.error(`Failed to get code ${code} credit stats`)
}
if (!data) {
try {
const result = await getCreditStatsForStudytrack({
studyprogramme: req.params.id,
settings: {
isAcademicYear: yearType === 'ACADEMIC_YEAR',
includeAllSpecials: specialGroups === 'SPECIAL_INCLUDED',
},
})
data = await setCreditStats(result, yearType, specialGroups)
} catch (e) {
logger.error(`Failed to update code ${code} credit stats`)
}
}
return res.json(data)
} else {
res.status(422).end()
}
if (!code) return res.status(422).end()
const data = await getCreditStats(code, yearType, specialGroups)
if (data) return res.json(data)
const updatedStats = await getCreditStatsForStudytrack({
studyprogramme: req.params.id,
settings: {
isAcademicYear: yearType === 'ACADEMIC_YEAR',
includeAllSpecials: specialGroups === 'SPECIAL_INCLUDED',
},
})
if (updatedStats) await setCreditStats(updatedStats, yearType, specialGroups)
return res.json(updatedStats)
})
router.get('/v2/studyprogrammes/:id/graduationstats', async (req, res) => {
@@ -85,30 +63,32 @@ router.get('/v2/studyprogrammes/:id/graduationstats', async (req, res) => {
const yearType = req.query?.year_type
const specialGroups = req.query?.special_groups
if (code) {
let data = null
try {
data = await getGraduationStats(code, yearType, specialGroups)
} catch (e) {
logger.error(`Failed to get code ${code} graduation stats`)
}
if (!data) {
try {
const result = await getGraduationStatsForStudytrack({
studyprogramme: req.params.id,
settings: {
isAcademicYear: yearType === 'ACADEMIC_YEAR',
includeAllSpecials: specialGroups === 'SPECIAL_INCLUDED',
},
})
data = await setGraduationStats(result, yearType, specialGroups)
} catch (e) {
logger.error(`Failed to update code ${code} graduation stats`)
}
}
if (!code) return res.status(422).end()
const data = await getGraduationStats(code, yearType, specialGroups)
if (data) return res.json(data)
const updatedStats = await getGraduationStatsForStudytrack({
studyprogramme: req.params.id,
settings: {
isAcademicYear: yearType === 'ACADEMIC_YEAR',
includeAllSpecials: specialGroups === 'SPECIAL_INCLUDED',
},
})
if (updatedStats) await setGraduationStats(updatedStats, yearType, specialGroups)
return res.json(updatedStats)
})
router.get('/v2/studyprogrammes/:id/coursestats', async (req, res) => {
const code = req.params.id
const showByYear = req.query.academicyear
const date = new Date()
date.setHours(23, 59, 59, 999)
try {
const data = await getStudyprogrammeCoursesForStudytrack(date.getTime(), code, showByYear)
return res.json(data)
} else {
res.status(422).end()
} catch (e) {
logger.error(`Failed to get code ${code} programme courses stats: ${e}`)
}
})
@@ -116,34 +96,21 @@ router.get('/v2/studyprogrammes/:id/studytrackstats', async (req, res) => {
const code = req.params.id
const graduated = req.query?.graduated
const specialGroups = req.query?.special_groups
const yearsCombined = req.query?.years_combined
if (code) {
let data = null
try {
data = await getStudytrackStats(code, graduated, specialGroups, yearsCombined)
} catch (e) {
logger.error(`Failed to get code ${code} studytrack stats: ${e}`)
}
if (!data) {
try {
const result = await getStudytrackStatsForStudyprogramme({
studyprogramme: code,
settings: {
graduated: graduated === 'GRADUATED_INCLUDED',
specialGroups: specialGroups === 'SPECIAL_INCLUDED',
yearsCombined: yearsCombined === 'YEARS_COMBINED',
},
})
data = await setStudytrackStats(result, graduated, specialGroups, yearsCombined)
} catch (e) {
logger.error(`Failed to update code ${code} studytrack stats: ${e}`)
}
}
return res.json(data)
} else {
res.status(422).end()
}
if (!code) return res.status(422).end()
const data = await getStudytrackStats(code, graduated, specialGroups)
if (data) return res.json(data)
const updated = await getStudytrackStatsForStudyprogramme({
studyprogramme: code,
settings: {
graduated: graduated === 'GRADUATED_INCLUDED',
specialGroups: specialGroups === 'SPECIAL_INCLUDED',
},
})
if (updated) await setStudytrackStats(updated, graduated, specialGroups)
return res.json(updated)
})
router.get('/v2/studyprogrammes/:id/update_basicview', async (req, res) => {
@@ -1,14 +1,17 @@
const { redisClient } = require('../services/redis')
const moment = require('moment')
// Only new bachelor, masters and doctoral programmes get their data updated in redis every night, use redis for them
const isUpdatedNewProgramme = code => code.includes('KH') || code.includes('MH') || /^(T)[0-9]{6}$/.test(code)
const createRedisKeyForBasicStats = (id, yearType, specialGroups) => `BASIC_STATS_${id}_${yearType}_${specialGroups}`
const createRedisKeyForCreditStats = (id, yearType, specialGroups) => `CREDIT_STATS_${id}_${yearType}_${specialGroups}`
const createRedisKeyForGraduationStats = (id, yearType, specialGroups) =>
`GRADUATION_STATS_${id}_${yearType}_${specialGroups}`
const createRedisKeyForStudytrackStats = (id, graduated, specialGroups, yearsCombined) =>
`STUDYTRACK_STATS_${id}_${graduated}_${specialGroups}_${yearsCombined}`
const createRedisKeyForStudytrackStats = (id, graduated, specialGroups) =>
`STUDYTRACK_STATS_${id}_${graduated}_${specialGroups}`
const getBasicStats = async (id, yearType, specialGroups) => {
if (!isUpdatedNewProgramme(id)) return null
const redisKey = createRedisKeyForBasicStats(id, yearType, specialGroups)
const dataFromRedis = await redisClient.getAsync(redisKey)
if (!dataFromRedis) return null
@@ -23,12 +26,14 @@ const setBasicStats = async (data, yearType, specialGroups) => {
status: 'DONE',
lastUpdated: moment().format(),
}
if (!isUpdatedNewProgramme(id)) return dataToRedis
const setOperationStatus = await redisClient.setAsync(redisKey, JSON.stringify(dataToRedis))
if (setOperationStatus !== 'OK') return null
return dataToRedis
}
const getCreditStats = async (id, yearType, specialGroups) => {
if (!isUpdatedNewProgramme(id)) return null
const redisKey = createRedisKeyForCreditStats(id, yearType, specialGroups)
const dataFromRedis = await redisClient.getAsync(redisKey)
if (!dataFromRedis) return null
@@ -43,12 +48,14 @@ const setCreditStats = async (data, yearType, specialGroups) => {
status: 'DONE',
lastUpdated: moment().format(),
}
if (!isUpdatedNewProgramme(id)) return dataToRedis
const setOperationStatus = await redisClient.setAsync(redisKey, JSON.stringify(dataToRedis))
if (setOperationStatus !== 'OK') return null
return dataToRedis
}
const getGraduationStats = async (id, yearType, specialGroups) => {
if (!isUpdatedNewProgramme(id)) return null
const redisKey = createRedisKeyForGraduationStats(id, yearType, specialGroups)
const dataFromRedis = await redisClient.getAsync(redisKey)
if (!dataFromRedis) return null
@@ -63,26 +70,29 @@ const setGraduationStats = async (data, yearType, specialGroups) => {
status: 'DONE',
lastUpdated: moment().format(),
}
if (!isUpdatedNewProgramme(id)) return dataToRedis
const setOperationStatus = await redisClient.setAsync(redisKey, JSON.stringify(dataToRedis))
if (setOperationStatus !== 'OK') return null
return dataToRedis
}
const getStudytrackStats = async (id, graduated, specialGroups, yearsCombined) => {
const redisKey = createRedisKeyForStudytrackStats(id, graduated, specialGroups, yearsCombined)
const getStudytrackStats = async (id, graduated, specialGroups) => {
if (!isUpdatedNewProgramme(id)) return null
const redisKey = createRedisKeyForStudytrackStats(id, graduated, specialGroups)
const dataFromRedis = await redisClient.getAsync(redisKey)
if (!dataFromRedis) return null
return JSON.parse(dataFromRedis)
}
const setStudytrackStats = async (data, graduated, specialGroups, yearsCombined) => {
const setStudytrackStats = async (data, graduated, specialGroups) => {
const { id } = data
const redisKey = createRedisKeyForStudytrackStats(id, graduated, specialGroups, yearsCombined)
const redisKey = createRedisKeyForStudytrackStats(id, graduated, specialGroups)
const dataToRedis = {
...data,
status: 'DONE',
lastUpdated: moment().format(),
}
if (!isUpdatedNewProgramme(id)) return dataToRedis
const setOperationStatus = await redisClient.setAsync(redisKey, JSON.stringify(dataToRedis))
if (setOperationStatus !== 'OK') return null
return dataToRedis
@@ -40,18 +40,22 @@ class CourseStatsCounter {
triedOfPopulation: undefined,
passingSemesters: this.initializePassingSemesters(),
}
this.enrollments = {
enrolledNoGrade: new Set(),
semesters: this.initializePassingSemesters({}),
}
this.grades = {}
}
initializePassingSemesters() {
initializePassingSemesters(initialValue = 0) {
const passingSemesters = {
BEFORE: 0,
LATER: 0,
BEFORE: initialValue,
LATER: initialValue,
}
for (let i = 0; i < 7; i++) {
passingSemesters[fall[i]] = 0
passingSemesters[spring[i]] = 0
passingSemesters[fall[i]] = initialValue
passingSemesters[spring[i]] = initialValue
}
return passingSemesters
@@ -91,6 +95,13 @@ class CourseStatsCounter {
}
}
markEnrollment(studentnumber, state, semester) {
if (!this.enrollments[state]) this.enrollments[state] = new Set()
this.enrollments[state].add(studentnumber)
if (!this.enrollments.semesters[semester][state]) this.enrollments.semesters[semester][state] = new Set()
this.enrollments.semesters[semester][state].add(studentnumber)
}
failedBefore(studentnumber) {
return this.students.failed[studentnumber] !== undefined
}
@@ -173,6 +184,10 @@ class CourseStatsCounter {
getFinalStats() {
const stats = this.stats
const students = this.students
const allStudents = Object.keys(students.all)
const totalEnrolledNoGrade = [...(this.enrollments.ENROLLED || [])].filter(
student => !allStudents.includes(student)
).length
stats.students = lengthOf(students.all)
stats.passed = lengthOf(students.passed)
@@ -185,6 +200,9 @@ class CourseStatsCounter {
stats.triedOfPopulation = percentageOf(stats.students, this.studentsInTotal)
stats.perStudent = stats.attempts / (stats.passed + stats.failed)
stats.passingSemestersCumulative = this.getPassingSemestersCumulative()
stats.totalStudents = stats.students + totalEnrolledNoGrade
stats.totalEnrolledNoGrade = totalEnrolledNoGrade
stats.percentageWithEnrollments = percentageOf(stats.passed, stats.totalStudents)
return {
stats,
@@ -54,18 +54,11 @@ class CourseYearlyStatsCounter {
passedEventually: [],
neverPassed: [],
},
grades: {},
studentnumbers: [],
},
enrollments: [],
enrollmentsByState: {
ENROLLED: 0,
NOT_ENROLLED: 0,
REJECTED: 0,
CONFIRMED: 0,
ABORTED_BY_STUDENT: 0,
ABORTED_BY_TEACHER: 0,
PROCESSING: 0,
},
allEnrollments: [],
yearcode,
}
}
@@ -138,19 +131,21 @@ class CourseYearlyStatsCounter {
this.initGroup(groupcode, groupname, coursecode, yearcode)
}
this.markCreditToAttempts(studentnumber, passed, grade, groupcode)
this.markBestEffortGrade(studentnumber, passed, grade, groupcode)
}
markEnrollmentToGroup(studentnumber, state, enrollment_date_time, groupcode, groupname, coursecode, yearcode) {
if (!this.groups[groupcode]) this.initGroup(groupcode, groupname, coursecode, yearcode)
this.groups[groupcode].enrollmentsByState[state] += 1 // not used?
const enrollment = { studentnumber, state, enrollment_date_time }
this.groups[groupcode].allEnrollments.push(enrollment)
const oldEnrollment = this.groups[groupcode].enrollments.find(e => e.studentnumber === studentnumber)
if (!oldEnrollment) return this.groups[groupcode].enrollments.push({ studentnumber, state, enrollment_date_time })
if (oldEnrollment.state === 'ENROLLED' || oldEnrollment.state === 'CONFIRMED') return
if (state !== 'ENROLLED' || state !== 'CONFIRMED') return
this.groups[groupcode].enrollments = this.groups[groupcode].enrollments
.filter(e => e.studentnumber !== studentnumber)
.concat([{ studentnumber, state, enrollment_date_time }])
.concat([enrollment])
}
markCreditToAttempts(studentnumber, passed, grade, groupcode) {
@@ -167,6 +162,19 @@ class CourseYearlyStatsCounter {
}
}
markBestEffortGrade(studentnumber, passed, grade, groupcode) {
const current = this.groups[groupcode].students.grades[studentnumber]
if (!current) {
this.groups[groupcode].students.grades[studentnumber] = { grade, passed }
return
}
if (current.passed && !passed) return
if (current.passed && Number(current.grade) >= Number(grade)) return
this.groups[groupcode].students.grades[studentnumber] = { grade, passed }
}
markCreditToStudentCategories(studentnumber, passed, attainment_date, groupcode) {
if (!this.students.has(studentnumber)) {
if (passed) {
@@ -232,8 +240,17 @@ class CourseYearlyStatsCounter {
}
const groupStatistics = Object.values(this.groups).map(({ ...rest }) => {
const { students } = rest
const grades = {}
Object.keys(students.grades).forEach(student => {
const { grade, passed } = students.grades[student]
const parsed = passed ? grade : '0'
if (!grades[parsed]) grades[parsed] = []
grades[parsed].push(student)
})
const normalStats = {
...rest,
students: { ...students, grades },
}
if (anonymizationSalt && normalStats.students.studentnumbers.length < 6) {
+48 -10
View File
@@ -108,6 +108,11 @@ const creditsForCourses = async (codes, anonymizationSalt, unification) => {
{
model: Semester,
attributes: ['semestercode', 'name', 'yearcode', 'yearname'],
where: {
startdate: {
[Op.lte]: new Date(),
},
},
},
],
where: {
@@ -173,6 +178,11 @@ const enrollmentsForCourses = async (codes, anonymizationSalt, unification) => {
{
model: Semester,
attributes: ['semestercode', 'name', 'yearcode', 'yearname'],
where: {
startdate: {
[Op.lte]: new Date(),
},
},
},
],
where: {
@@ -183,11 +193,10 @@ const enrollmentsForCourses = async (codes, anonymizationSalt, unification) => {
[Op.ne]: null,
},
enrollment_date_time: { [Op.gte]: new Date('2021-05-31') },
state: ['ENROLLED', 'CONFIRMED', 'REJECTED', 'ABORTED_BY_STUDENT', 'ABORTED_BY_TEACHER'],
state: ['ENROLLED', 'CONFIRMED'],
[Op.or]: [{ is_open }, { is_open: null }],
},
})
return enrollments.map(enrollment => parseEnrollment(enrollment, anonymizationSalt))
}
@@ -328,7 +337,17 @@ const yearlyStatsOfNew = async (coursecode, separate, unification, anonymization
}
}
const maxYearsToCreatePopulationFrom = async coursecodes => {
const maxYearsToCreatePopulationFrom = async (coursecodes, unifyCourses) => {
let is_open = false
if (unifyCourses === 'openStats') is_open = true
if (unifyCourses === 'unifyStats') {
is_open = {
[Op.in]: [false, true],
}
}
const maxAttainmentDate = new Date(
Math.max(
...(
@@ -343,6 +362,7 @@ const maxYearsToCreatePopulationFrom = async coursecodes => {
).map(c => new Date(c.max_attainment_date).getTime())
)
)
const attainmentThreshold = new Date(maxAttainmentDate.getFullYear(), 0, 1)
attainmentThreshold.setFullYear(attainmentThreshold.getFullYear() - 6)
@@ -354,6 +374,7 @@ const maxYearsToCreatePopulationFrom = async coursecodes => {
attainment_date: {
[Op.gt]: attainmentThreshold,
},
is_open,
},
order: [['attainment_date', 'ASC']],
})
@@ -377,14 +398,12 @@ const maxYearsToCreatePopulationFrom = async coursecodes => {
return maxYearsToCreatePopulationFrom
}
const courseYearlyStats = async (coursecodes, separate, unifyOpenUniCourses, anonymizationSalt) => {
const [unify, reqular, open] = ['unify', 'reqular', 'open']
const courseYearlyStats = async (coursecodes, separate, anonymizationSalt) => {
const statsReqular = await Promise.all(
coursecodes.map(async code => {
const unifyStats = await yearlyStatsOfNew(code, separate, unify, anonymizationSalt)
const reqularStats = await yearlyStatsOfNew(code, separate, reqular, anonymizationSalt)
const openStats = await yearlyStatsOfNew(code, separate, open, anonymizationSalt)
const unifyStats = await yearlyStatsOfNew(code, separate, 'unify', anonymizationSalt)
const reqularStats = await yearlyStatsOfNew(code, separate, 'reqular', anonymizationSalt)
const openStats = await yearlyStatsOfNew(code, separate, 'open', anonymizationSalt)
return { unifyStats, reqularStats, openStats }
})
@@ -430,7 +449,8 @@ const codeLikeTerm = code =>
? undefined
: {
code: {
[Op.iLike]: `%${code.trim()}%`,
// Starts with code or has AY/A in front of the code
[Op.iRegexp]: `^(AY|ay|A|a)?${code}`,
},
}
@@ -476,6 +496,23 @@ const byNameAndOrCodeLike = async (name, code) => {
return { courses }
}
const getCourseProvidersForCourses = async codes => {
return (
await Organization.findAll({
attributes: ['code'],
include: {
model: Course,
where: {
code: {
[Op.in]: codes,
},
},
},
raw: true,
})
).map(({ code }) => code)
}
module.exports = {
byCode,
byName,
@@ -488,4 +525,5 @@ module.exports = {
unifyOpenUniversity,
allCodeAlternatives,
findOneByCode,
getCourseProvidersForCourses,
}
+28
View File
@@ -0,0 +1,28 @@
const { randomBytes, createCipheriv, createDecipheriv } = require('crypto')
const algorithm = 'aes-256-cbc'
const key = randomBytes(32)
const iv = randomBytes(16)
const encrypt = text => {
const cipher = createCipheriv(algorithm, Buffer.from(key), iv)
let encrypted = cipher.update(text)
encrypted = Buffer.concat([encrypted, cipher.final()])
return {
iv: iv.toString('hex'),
encryptedData: encrypted.toString('hex'),
}
}
const decrypt = text => {
const iv = Buffer.from(text.iv, 'hex')
const encryptedText = Buffer.from(text.encryptedData, 'hex')
const decipher = createDecipheriv(algorithm, Buffer.from(key), iv)
let decrypted = decipher.update(encryptedText)
decrypted = Buffer.concat([decrypted, decipher.final()])
return decrypted.toString()
}
module.exports = { encrypt, decrypt }
@@ -0,0 +1,351 @@
const Sequelize = require('sequelize')
const { Op } = Sequelize
const {
ElementDetail,
Organization,
ProgrammeModule,
Studyright,
StudyrightElement,
Student,
Transfer,
Credit,
Course,
} = require('../../models')
const {
facultyFormatStudyright,
facultyFormatProgramme,
formatFacultyTransfer,
formatFacultyThesisWriter,
formatOrganization,
isNewProgramme,
mapCodesToIds,
} = require('./facultyHelpers')
const getTransferredToAndAway = async (programmeCodes, allProgrammeCodes, since) => {
const awayTransfers = (await transferredAway(programmeCodes, allProgrammeCodes, since)).map(t => t.studyrightid)
const toTransfers = (await transferredTo(programmeCodes, allProgrammeCodes, since)).map(t => t.studyrightid)
const transferredToOrAway = [...toTransfers, ...awayTransfers]
return transferredToOrAway
}
const getTransferredInside = async (programmeCodes, allProgrammeCodes, since) => {
const transfersInside = (await transferredInsideFaculty(programmeCodes, allProgrammeCodes, since)).map(
t => t.studyrightid
)
return transfersInside
}
const startedStudyrights = async (faculty, since, studyRightWhere) =>
(
await Studyright.findAll({
include: [
{
model: Student,
attributes: ['studentnumber'],
required: true,
},
{
model: StudyrightElement,
required: true,
include: {
model: ElementDetail,
required: true,
},
},
],
where: {
faculty_code: faculty,
studystartdate: {
[Op.gte]: since,
},
student_studentnumber: { [Op.not]: null },
...studyRightWhere,
},
})
).map(facultyFormatStudyright)
const graduatedStudyrights = async (faculty, since, studyrightWhere) =>
(
await Studyright.findAll({
include: [
{
model: Student,
attributes: ['studentnumber'],
required: true,
},
{
model: StudyrightElement,
required: true,
include: {
model: ElementDetail,
required: true,
},
},
],
where: {
faculty_code: faculty,
enddate: {
[Op.gte]: since,
},
graduated: 1,
student_studentnumber: { [Op.not]: null },
...studyrightWhere,
},
})
).map(facultyFormatStudyright)
const studyrightsByRightStartYear = async (faculty, since, graduated = 1) =>
(
await Studyright.findAll({
include: [
{
model: Student,
attributes: ['studentnumber'],
required: true,
},
{
model: StudyrightElement,
required: true,
include: {
model: ElementDetail,
required: true,
},
},
],
where: {
faculty_code: faculty,
startdate: {
[Op.gte]: since,
},
graduated: graduated,
student_studentnumber: { [Op.not]: null },
},
})
).map(facultyFormatStudyright)
const getStudyRightsByExtent = async (faculty, academicYearStart, academicYearEnd, code, extents, graduated) =>
(
await Studyright.findAll({
include: [
{
model: Student,
attributes: ['studentnumber'],
required: true,
},
{
model: StudyrightElement,
required: true,
where: {
startdate: {
[Op.between]: [academicYearStart, academicYearEnd],
},
code: code,
},
include: {
model: ElementDetail,
required: true,
},
},
],
where: {
faculty_code: faculty,
extentcode: {
[Op.in]: extents,
},
graduated: {
[Op.in]: graduated,
},
student_studentnumber: { [Op.not]: null },
},
})
).map(facultyFormatStudyright)
const hasMasterRight = async id => {
return await Studyright.findOne({
where: {
studyrightid: id,
extentcode: 2,
},
})
}
const transferredInsideFaculty = async (programmes, allProgrammeCodes, since) =>
(
await Transfer.findAll({
where: {
transferdate: {
[Op.gte]: since,
},
sourcecode: allProgrammeCodes,
targetcode: programmes,
},
})
).map(formatFacultyTransfer)
const transferredAway = async (programmes, allProgrammeCodes, since) =>
(
await Transfer.findAll({
where: {
transferdate: {
[Op.gte]: since,
},
sourcecode: programmes,
targetcode: {
[Op.notIn]: allProgrammeCodes,
},
},
})
).map(formatFacultyTransfer)
const transferredTo = async (programmes, allProgrammeCodes, since) =>
(
await Transfer.findAll({
where: {
transferdate: {
[Op.gte]: since,
},
sourcecode: {
[Op.notIn]: allProgrammeCodes,
},
targetcode: programmes,
},
})
).map(formatFacultyTransfer)
const degreeProgrammesOfFaculty = async facultyCode =>
(
await ProgrammeModule.findAll({
attributes: ['code', 'name'],
include: {
model: Organization,
where: {
code: {
[Op.startsWith]: facultyCode,
},
},
},
})
).map(facultyFormatProgramme)
const facultyOgranizationId = async faculty => {
return await Organization.findOne({
attributes: ['id'],
where: {
code: faculty,
},
})
}
const getChildOrganizations = async facultyId =>
(
await Organization.findAll({
attributes: ['id', 'name', 'code', 'parent_id'],
where: {
parent_id: facultyId,
},
})
).map(formatOrganization)
const thesisWriters = async (provider, since, thesisTypes, students) =>
(
await Credit.findAll({
attributes: ['id', 'course_code', 'attainment_date', 'student_studentnumber'],
include: {
model: Course,
attributes: ['course_unit_type'],
required: true,
where: {
course_unit_type: {
[Op.in]: thesisTypes,
},
},
include: {
model: Organization,
attributes: [],
required: true,
where: {
code: provider,
},
},
},
where: {
credittypecode: 4,
isStudyModule: {
[Op.not]: true,
},
attainment_date: {
[Op.gte]: since,
},
student_studentnumber: students.length > 0 ? students : { [Op.not]: null },
},
})
).map(formatFacultyThesisWriter)
// Some programme modules are not directly associated to a faculty (organization).
// Some have intermediate organizations, such as department, so the connection must be digged up
const findFacultyProgrammeCodes = async (faculty, programmeFilter) => {
let allProgrammes = []
let allProgrammeCodes = []
const directAssociationProgrammes = await degreeProgrammesOfFaculty(faculty)
allProgrammes = allProgrammes.concat(directAssociationProgrammes)
allProgrammeCodes = allProgrammeCodes.concat(directAssociationProgrammes.map(prog => prog.code))
// find faculty's organization id and its direct child organizations
const { id } = await facultyOgranizationId(faculty)
const facultyChildOrganizations = await getChildOrganizations(id)
// get programme modules that have a faculty child as organization(_id)
for (const org of facultyChildOrganizations) {
const childAssociationProgrammes = await degreeProgrammesOfFaculty(org.code)
if (childAssociationProgrammes.length > 0) {
childAssociationProgrammes.forEach(prog => {
if (!(prog.code in allProgrammeCodes)) {
allProgrammes = allProgrammes.concat([prog])
allProgrammeCodes = allProgrammeCodes.concat([prog.code])
}
})
} else {
// dig deeper
const grandChildren = await getChildOrganizations(org.id)
if (grandChildren.length > 0) {
for (const gcOrg of grandChildren) {
const associatedProgrammes = await degreeProgrammesOfFaculty(gcOrg.code)
if (associatedProgrammes.length > 0) {
associatedProgrammes.forEach(prog => {
if (!(prog.code in allProgrammeCodes)) {
allProgrammes = allProgrammes.concat([prog])
allProgrammeCodes = allProgrammeCodes.concat([prog.code])
}
})
}
}
}
}
}
if (programmeFilter === 'NEW_STUDY_PROGRAMMES') {
allProgrammes = allProgrammes.filter(prog => isNewProgramme(prog.code))
}
mapCodesToIds(allProgrammes)
return allProgrammes
}
module.exports = {
startedStudyrights,
graduatedStudyrights,
studyrightsByRightStartYear,
hasMasterRight,
transferredInsideFaculty,
transferredAway,
transferredTo,
degreeProgrammesOfFaculty,
thesisWriters,
findFacultyProgrammeCodes,
facultyOgranizationId,
getTransferredToAndAway,
getTransferredInside,
getStudyRightsByExtent,
}
@@ -0,0 +1,472 @@
const { indexOf } = require('lodash')
const moment = require('moment')
const {
startedStudyrights,
graduatedStudyrights,
transferredInsideFaculty,
transferredAway,
transferredTo,
getTransferredToAndAway,
getTransferredInside,
studyrightsByRightStartYear,
} = require('./faculty')
const { getStatsBasis, getYearsArray, defineYear } = require('../studyprogrammeHelpers')
const { findRightProgramme, isNewProgramme, checkTransfers, getExtentFilter } = require('./facultyHelpers')
const { codes } = require('../../../config/programmeCodes')
const filterDuplicateStudyrights = studyrights => {
// bachelor+master students have two studyrights (separated by two last digits in studyrightid)
// choose only the earlier started one, so we don't count start of masters as starting in faculty
let rightsToCount = {}
studyrights.forEach(right => {
const id = right.studyrightid.slice(0, -2)
const start = new Date(right.studystartdate)
if (id in rightsToCount) {
if (new Date(rightsToCount[id].studystartdate) > start) {
rightsToCount[id] = right
}
} else {
rightsToCount[id] = right
}
})
return Object.values(rightsToCount)
}
const addProgramme = async (
stats,
programme,
tableStats,
transferYear,
index,
allBasics,
facultyProgrammes,
includeAllSpecials
) => {
let { progId, name, code } = facultyProgrammes.filter(prog => prog.code === programme)[0]
if (!(progId in stats)) {
stats[progId] = {}
if (includeAllSpecials) {
Object.keys(tableStats).forEach(year => (stats[progId][year] = [0, 0, 0]))
} else {
Object.keys(tableStats).forEach(year => (stats[progId][year] = [0]))
}
}
stats[progId][transferYear][index] += 1
if (!(progId in allBasics.programmeNames)) {
allBasics.programmeNames[progId] = { ...name, code: code }
}
}
const getFacultyStarters = async (
faculty,
transfersToOrAwayStudyrights,
insideTransfersStudyrights,
since,
isAcademicYear,
yearsArray,
graphStats,
tableStats,
counts,
allBasics,
programmeData,
programmeFilter,
includeAllSpecials
) => {
const startedGraphStats = [...graphStats]
const startedTableStats = { ...tableStats }
const studyrightWhere = getExtentFilter(includeAllSpecials)
const studyrights = await startedStudyrights(faculty, since, studyrightWhere)
let filteredStudyrights = filterDuplicateStudyrights(studyrights)
const keys = Object.keys(codes)
const programmeTableStats = {}
if (!includeAllSpecials) {
filteredStudyrights = filteredStudyrights.filter(
s => !checkTransfers(s, insideTransfersStudyrights, transfersToOrAwayStudyrights)
)
}
filteredStudyrights.forEach(({ studystartdate, studyrightElements }) => {
const { programme, programmeName } = findRightProgramme(studyrightElements, 'started')
let programmeId = programme
if (keys.includes(programme)) {
programmeId = codes[programme].toUpperCase()
}
if (programmeFilter === 'ALL_PROGRAMMES' || isNewProgramme(programme)) {
const startYear = defineYear(studystartdate, isAcademicYear)
startedGraphStats[indexOf(yearsArray, startYear)] += 1
startedTableStats[startYear] += 1
if (!(programmeId in programmeTableStats)) {
programmeTableStats[programmeId] = { ...tableStats }
}
programmeTableStats[programmeId][startYear] += 1
if (!(programmeId in allBasics.programmeNames)) {
allBasics.programmeNames[programmeId] = { ...programmeName, code: programme }
}
}
})
allBasics.studentInfo.graphStats.push({ name: 'Started studying', data: startedGraphStats })
programmeData['started'] = programmeTableStats
Object.keys(startedTableStats).forEach(year => {
counts[year] = [startedTableStats[year]]
})
}
const getFacultyGraduates = async (
faculty,
transfersToOrAwayStudyrights,
insideTransfersStudyrights,
since,
isAcademicYear,
yearsArray,
graphStats,
tableStats,
allBasics,
counts,
countsGraduations,
programmeData,
programmeFilter,
includeAllSpecials
) => {
const graduatedGraphStats = [[...graphStats], [...graphStats], [...graphStats], [...graphStats], [...graphStats]]
const graduatedTableStats = {}
const keys = Object.keys(codes)
let studyrightWhere = getExtentFilter(includeAllSpecials)
let graduatedRights = await graduatedStudyrights(faculty, since, studyrightWhere)
const programmeTableStats = {}
Object.keys(tableStats).forEach(year => (graduatedTableStats[year] = [0, 0, 0, 0, 0]))
if (!includeAllSpecials) {
graduatedRights = graduatedRights.filter(
s => !checkTransfers(s, insideTransfersStudyrights, transfersToOrAwayStudyrights)
)
}
graduatedRights.forEach(({ enddate, extentcode, studyrightElements }) => {
const { programme, programmeName } = findRightProgramme(studyrightElements, 'graduated')
let programmeId = programme
if (keys.includes(programme)) {
programmeId = codes[programme].toUpperCase()
}
if (programmeFilter === 'ALL_PROGRAMMES' || isNewProgramme(programme)) {
const endYear = defineYear(enddate, isAcademicYear)
graduatedGraphStats[0][indexOf(yearsArray, endYear)] += 1
graduatedTableStats[endYear][0] += 1
if (!(programmeId in programmeTableStats)) {
programmeTableStats[programmeId] = {}
Object.keys(tableStats).forEach(year => (programmeTableStats[programmeId][year] = [0, 0, 0, 0, 0]))
}
programmeTableStats[programmeId][endYear][0] += 1
if (extentcode === 1) {
graduatedGraphStats[1][indexOf(yearsArray, endYear)] += 1
graduatedTableStats[endYear][1] += 1
programmeTableStats[programmeId][endYear][1] += 1
} else if (extentcode === 2) {
graduatedGraphStats[2][indexOf(yearsArray, endYear)] += 1
graduatedTableStats[endYear][2] += 1
programmeTableStats[programmeId][endYear][2] += 1
} else if (extentcode === 4) {
graduatedGraphStats[3][indexOf(yearsArray, endYear)] += 1
graduatedTableStats[endYear][3] += 1
programmeTableStats[programmeId][endYear][3] += 1
} else {
graduatedGraphStats[4][indexOf(yearsArray, endYear)] += 1
graduatedTableStats[endYear][4] += 1
programmeTableStats[programmeId][endYear][4] += 1
}
if (!(programmeId in allBasics.programmeNames)) {
allBasics.programmeNames[programmeId] = { code: programme, ...programmeName }
}
}
})
allBasics.studentInfo.graphStats.push({ name: 'Graduated', data: graduatedGraphStats[0] })
allBasics.graduationInfo.graphStats.push({ name: 'Bachelors', data: graduatedGraphStats[1] })
allBasics.graduationInfo.graphStats.push({ name: 'Masters', data: graduatedGraphStats[2] })
allBasics.graduationInfo.graphStats.push({ name: 'Doctors', data: graduatedGraphStats[3] })
allBasics.graduationInfo.graphStats.push({ name: 'Others', data: graduatedGraphStats[4] })
programmeData['graduated'] = programmeTableStats
Object.keys(graduatedTableStats).forEach(year => {
counts[year] = counts[year].concat(graduatedTableStats[year][0])
countsGraduations[year] = graduatedTableStats[year]
})
}
// If includeAllSpecial false, insidetransfers should contain only students started after 1.8.2017.
const getInsideTransfers = async (programmeCodes, allProgrammeCodes, since, includeAllSpecials, faculty) => {
if (includeAllSpecials) {
return await transferredInsideFaculty(programmeCodes, allProgrammeCodes, since)
}
const insiders = await transferredInsideFaculty(programmeCodes, allProgrammeCodes, since)
const studyrights = (await studyrightsByRightStartYear(faculty, moment('2017-08-01', 'YYYY-MM-DD'))).map(
sr => sr.studyrightid
)
const filteredTransfers = insiders.filter(tr => studyrights.includes(tr.studyrightid))
return filteredTransfers
}
const getFacultyTransfers = async (
programmes,
programmeCodes,
allProgrammeCodes,
since,
isAcademicYear,
yearsArray,
graphStats,
tableStats,
allBasics,
counts,
programmeData,
includeAllSpecials,
faculty
) => {
const insideTransfers = await getInsideTransfers(
programmeCodes,
allProgrammeCodes,
since,
includeAllSpecials,
faculty
)
let transferGraphStats = []
let transferTableStats = {}
if (includeAllSpecials) {
transferGraphStats = [[...graphStats], [...graphStats], [...graphStats]]
Object.keys(tableStats).forEach(year => (transferTableStats[year] = [0, 0, 0]))
} else {
transferGraphStats = [[...graphStats]]
Object.keys(tableStats).forEach(year => (transferTableStats[year] = [0]))
}
const programmeTableStats = {}
// inside transfer is counted towards target
for (const { transferdate, targetcode } of insideTransfers) {
const transferYear = defineYear(transferdate, isAcademicYear)
transferGraphStats[0][indexOf(yearsArray, transferYear)] += 1
if (isAcademicYear || !(transferYear > moment().year())) {
transferTableStats[transferYear][0] += 1
await addProgramme(
programmeTableStats,
targetcode,
tableStats,
transferYear,
0,
allBasics,
programmes,
includeAllSpecials
)
}
}
if (includeAllSpecials) {
const awayTransfers = await transferredAway(programmeCodes, allProgrammeCodes, since)
const toTransfers = await transferredTo(programmeCodes, allProgrammeCodes, since)
for (const { transferdate, sourcecode } of awayTransfers) {
const transferYear = defineYear(transferdate, isAcademicYear)
if (isAcademicYear || !(transferYear > moment().year())) {
transferGraphStats[1][indexOf(yearsArray, transferYear)] += 1
transferTableStats[transferYear][1] += 1
await addProgramme(
programmeTableStats,
sourcecode,
tableStats,
transferYear,
1,
allBasics,
programmes,
includeAllSpecials
)
}
}
for (const { transferdate, targetcode } of toTransfers) {
const transferYear = defineYear(transferdate, isAcademicYear)
if (isAcademicYear || !(transferYear > moment().year())) {
transferGraphStats[2][indexOf(yearsArray, transferYear)] += 1
transferTableStats[transferYear][2] += 1
await addProgramme(
programmeTableStats,
targetcode,
tableStats,
transferYear,
2,
allBasics,
programmes,
includeAllSpecials
)
}
}
}
allBasics.studentInfo.graphStats.push({ name: 'Transferred inside', data: transferGraphStats[0] })
if (includeAllSpecials) {
allBasics.studentInfo.graphStats.push({ name: 'Transferred away', data: transferGraphStats[1] })
allBasics.studentInfo.graphStats.push({ name: 'Transferred to', data: transferGraphStats[2] })
}
programmeData['transferred'] = programmeTableStats
Object.keys(transferTableStats).forEach(year => {
counts[year] = counts[year].concat(transferTableStats[year])
})
}
const combineFacultyBasics = async (faculty, programmes, yearType, allProgrammeCodes, programmeFilter, special) => {
let counts = {}
let countsGraduations = {}
let years = []
let programmeData = {}
const isAcademicYear = yearType === 'ACADEMIC_YEAR'
const includeAllSpecials = special === 'SPECIAL_INCLUDED'
const since = isAcademicYear ? new Date('2017-08-01') : new Date('2017-01-01')
const yearsArray = getYearsArray(since.getFullYear(), isAcademicYear)
const { graphStats, tableStats } = getStatsBasis(yearsArray)
Object.keys(tableStats).forEach(year => years.push(year))
const wantedProgrammeCodes = programmes.map(prog => prog.code)
let allBasics = {
id: faculty,
years: [],
programmeNames: {},
studentInfo: {
tableStats: [],
graphStats: [],
titles: includeAllSpecials
? ['', 'Started studying', 'Graduated', 'Transferred inside', 'Transferred away', 'Transferred to']
: ['', 'Started studying', 'Graduated', 'Transferred inside'],
programmeTableStats: {},
},
graduationInfo: {
tableStats: [],
graphStats: [],
titles: ['', 'All', 'Bachelors', 'Masters', 'Doctors', 'Others'],
programmeTableStats: {},
},
}
const transfersToAwayStudyrights = await getTransferredToAndAway(wantedProgrammeCodes, allProgrammeCodes, since)
const transferInsideStudyrights = await getTransferredInside(wantedProgrammeCodes, allProgrammeCodes, since)
// Started studying in faculty
await getFacultyStarters(
faculty,
transfersToAwayStudyrights,
transferInsideStudyrights,
since,
isAcademicYear,
yearsArray,
graphStats,
tableStats,
counts,
allBasics,
programmeData,
programmeFilter,
includeAllSpecials
)
// Graduated in faculty
await getFacultyGraduates(
faculty,
transfersToAwayStudyrights,
transferInsideStudyrights,
since,
isAcademicYear,
yearsArray,
graphStats,
tableStats,
allBasics,
counts,
countsGraduations,
programmeData,
programmeFilter,
includeAllSpecials
)
// Transfers
await getFacultyTransfers(
programmes,
wantedProgrammeCodes,
allProgrammeCodes,
since,
isAcademicYear,
yearsArray,
graphStats,
tableStats,
allBasics,
counts,
programmeData,
includeAllSpecials,
faculty
)
// combine tableStats from all categories
allBasics.years = years
years.forEach(year => {
allBasics.studentInfo.tableStats.push([year, ...counts[year]])
allBasics.graduationInfo.tableStats.push([year, ...countsGraduations[year]])
})
allBasics.studentInfo.tableStats.reverse()
allBasics.graduationInfo.tableStats.reverse()
//combine programme level tablestats
// all programmes are not present in all data types, check all found programmes and patch missing details with 0
let allCodes = Object.keys(programmeData.started).concat(
Object.keys(programmeData.graduated),
Object.keys(programmeData.transferred)
)
allCodes = [...new Set(allCodes)]
let studentInfo = {}
let graduationInfo = {}
allCodes.forEach(code => {
if (!(code in studentInfo)) studentInfo[code] = {}
for (const year of years) {
// started
if (code in programmeData.started) {
studentInfo[code][year] = [year, programmeData.started[code][year]]
} else {
studentInfo[code][year] = [year, 0]
}
// graduated total
if (code in programmeData.graduated) {
studentInfo[code][year] = studentInfo[code][year].concat([programmeData.graduated[code][year][0]])
if (!(code in graduationInfo)) graduationInfo[code] = {}
graduationInfo[code][year] = [year, ...programmeData.graduated[code][year]]
} else {
studentInfo[code][year] = studentInfo[code][year].concat([0])
}
// trasferred
if (code in programmeData.transferred) {
studentInfo[code][year] = studentInfo[code][year].concat(programmeData.transferred[code][year])
} else {
if (includeAllSpecials) {
studentInfo[code][year] = studentInfo[code][year].concat([0, 0, 0])
} else {
studentInfo[code][year] = studentInfo[code][year].concat([0])
}
}
}
})
Object.keys(studentInfo).forEach(prog => {
allBasics.studentInfo.programmeTableStats[prog] = [...Object.values(studentInfo[prog])].reverse()
})
Object.keys(graduationInfo).forEach(prog => {
allBasics.graduationInfo.programmeTableStats[prog] = Object.values(graduationInfo[prog]).reverse()
})
return allBasics
}
module.exports = { combineFacultyBasics }
@@ -0,0 +1,275 @@
const { indexOf } = require('lodash')
const { mapToProviders } = require('../../util/utils')
const {
getStatsBasis,
defineYear,
isMajorStudentCredit,
isNonMajorCredit,
getYearsArray,
getStartDate,
isSpecialGroupCredit,
} = require('../studyprogrammeHelpers')
const {
getStudyRights,
getCreditsForStudyProgramme,
allTransfers,
getCourseCodesForStudyProgramme,
} = require('../studyprogramme')
const isFacultyNonMajorCredit = (studyrights, attainment_date, facultyProgrammes) => {
let right = ''
studyrights.forEach(studyright => {
if (studyright) {
if (!studyright.graduated && new Date(attainment_date) >= new Date(studyright.studystartdate)) {
if (!right) right = studyright.code
else if (right && !facultyProgrammes.includes(right) && facultyProgrammes.includes(studyright.code))
right = studyright.code
} else {
if (
new Date(attainment_date) >= new Date(studyright.studystartdate) &&
new Date(attainment_date) <= new Date(studyright.enddate)
) {
if (!right) right = studyright.code
else if (right && !facultyProgrammes.includes(right) && facultyProgrammes.includes(studyright.code))
right = studyright.code
}
}
}
})
return facultyProgrammes.includes(right)
}
// Fetches all the credits for the studyprogramme and divides them into major-students, non-major faculty students,
// non-major other faculty students adn non-degree student credits
// Division is done on the basis that whether the student had a primary studyright to the programme on the attainment_date
const getFacultyRegularCreditStats = async ({
studyprogramme,
since,
years,
isAcademicYear,
includeAllSpecials,
facultyProgrammes,
}) => {
const providercode = mapToProviders([studyprogramme])[0]
const courses = await getCourseCodesForStudyProgramme(providercode)
const credits = await getCreditsForStudyProgramme(courses, since)
const students = [...new Set(credits.map(({ student_studentnumber }) => student_studentnumber))]
let studyrights = await getStudyRights(students)
const transfers = (await allTransfers(studyprogramme, since)).map(t => t.studyrightid).filter(s => s !== 'undefined')
if (!includeAllSpecials) {
studyrights = studyrights.filter(s => !transfers.includes(s.studyrightid))
}
let majors = getStatsBasis(years)
let allNonMajors = getStatsBasis(years)
let facultyNonMajors = getStatsBasis(years)
let otherNonMajors = getStatsBasis(years)
let nonDegree = getStatsBasis(years)
credits.forEach(({ student_studentnumber, attainment_date, credits }) => {
const studentStudyrights = studyrights.filter(studyright => studyright.studentnumber === student_studentnumber)
const attainmentYear = defineYear(attainment_date, isAcademicYear)
if (!includeAllSpecials && isSpecialGroupCredit(studentStudyrights, attainment_date, transfers)) {
return
}
if (isMajorStudentCredit(studentStudyrights, attainment_date, studyprogramme)) {
majors.graphStats[indexOf(years, attainmentYear)] += credits || 0
majors.tableStats[attainmentYear] += credits || 0
} else if (isNonMajorCredit(studentStudyrights, attainment_date)) {
// for comparison
allNonMajors.graphStats[indexOf(years, attainmentYear)] += credits || 0
allNonMajors.tableStats[attainmentYear] += credits || 0
if (isFacultyNonMajorCredit(studentStudyrights, attainment_date, facultyProgrammes)) {
facultyNonMajors.graphStats[indexOf(years, attainmentYear)] += credits || 0
facultyNonMajors.tableStats[attainmentYear] += credits || 0
} else {
otherNonMajors.graphStats[indexOf(years, attainmentYear)] += credits || 0
otherNonMajors.tableStats[attainmentYear] += credits || 0
}
} else {
nonDegree.graphStats[indexOf(years, attainmentYear)] += credits || 0
nonDegree.tableStats[attainmentYear] += credits || 0
}
})
if (
majors.graphStats.every(year => year === 0) &&
facultyNonMajors.graphStats.every(year => year === 0) &&
otherNonMajors.graphStats.every(year => year === 0) &&
nonDegree.graphStats.every(year => year === 0)
) {
return {
majors: { graphStats: [], tableStats: {} },
facultyNonMajors: { graphStats: [], tableStats: {} },
otherNonMajors: { graphStats: [], tableStats: {} },
nonDegree: { graphStats: [], tablestats: {} },
}
}
return { majors, facultyNonMajors, otherNonMajors, nonDegree }
}
// Fetches all credits for the studytrack and combines them into statistics
// These are for the faculty view which differs from study programme overview
// (no transfers, non majors split to in- and outside-faculty)
const getFacultyCreditStatsForStudytrack = async ({ studyprogramme, facultyProgrammes, settings }) => {
const { isAcademicYear, includeAllSpecials } = settings
const since = getStartDate(studyprogramme, isAcademicYear)
const years = getYearsArray(since.getFullYear(), isAcademicYear)
const queryParameters = { studyprogramme, since, years, isAcademicYear, includeAllSpecials, facultyProgrammes }
const { majors, facultyNonMajors, otherNonMajors, nonDegree } = await getFacultyRegularCreditStats(queryParameters)
const reversedYears = getYearsArray(since.getFullYear(), isAcademicYear).reverse()
const dataFound = [majors, facultyNonMajors, otherNonMajors].some(d => d.graphStats.length) // , transferred
if (!dataFound) return null
const tableStats = reversedYears.map(year =>
includeAllSpecials
? [
year,
majors.tableStats[year] +
facultyNonMajors.tableStats[year] +
otherNonMajors.tableStats[year] +
nonDegree.tableStats[year],
majors.tableStats[year],
facultyNonMajors.tableStats[year],
otherNonMajors.tableStats[year],
nonDegree.tableStats[year],
]
: [year, majors.tableStats[year], majors.tableStats[year]]
)
const graphStats = includeAllSpecials
? [
{
name: 'Major students credits',
data: majors.graphStats,
},
{
name: 'Non-major faculty students credits',
data: facultyNonMajors.graphStats,
},
{
name: 'Non-major other faculty students credits 2',
data: otherNonMajors.graphStats,
},
{
name: 'Non-degree credits',
data: nonDegree.graphStats,
},
]
: [
{
name: 'Major students credits',
data: majors.graphStats,
},
]
return {
id: studyprogramme,
years,
tableStats,
graphStats,
}
}
const getProgrammeCredits = async (code, yearType, specialGroups, facultyProgrammes) => {
const updatedStats = await getFacultyCreditStatsForStudytrack({
studyprogramme: code,
facultyProgrammes,
settings: {
isAcademicYear: yearType === 'ACADEMIC_YEAR',
includeAllSpecials: specialGroups === 'SPECIAL_INCLUDED',
},
})
return updatedStats
}
const combineFacultyCredits = async (faculty, programmes, allProgrammes, yearType, specialGroups) => {
let counts = {}
let years = []
const titles =
specialGroups === 'SPECIAL_INCLUDED'
? [
'',
'Total',
'Major students credits',
'Non-major faculty students credits',
'Non-major other faculty students credits',
'Non-degree student credits',
]
: ['', 'Total', 'Major students credits']
let allCredits = {
id: faculty,
years: [],
tableStats: [],
graphStats: [],
programmeTableStats: {},
programmeNames: {},
titles,
}
const progCodes = allProgrammes.map(p => p.code)
for (const prog of programmes) {
const data = await getProgrammeCredits(prog.code, yearType, specialGroups, progCodes)
if (data) {
allCredits.programmeNames[prog.progId] = { code: prog.code, ...prog.name }
allCredits.programmeTableStats[prog.progId] = data.tableStats
if (!allCredits.lastUpdated || new Date(data.lastUpdated) > new Date(allCredits.lastUpdated))
allCredits.lastUpdated = data.lastUpdated
data.tableStats.forEach(row => {
if (!(row[0] in counts)) {
counts[row[0]] = row.slice(1)
years.push(row[0])
} else {
counts[row[0]] = row.slice(1).map((value, i) => {
return counts[row[0]][i] + value
})
}
})
}
}
// save table stats and graph stats
years.forEach(year => {
allCredits.tableStats.push([year, ...counts[year]])
})
years.sort()
allCredits.years = years
let majors = []
let facultyNonMajor = []
let otherNonMajor = []
let nonDegree = []
years.forEach(year => {
majors.push(counts[year][1])
facultyNonMajor.push(counts[year][2])
otherNonMajor.push(counts[year][3])
nonDegree.push(counts[year][4])
})
allCredits.graphStats =
specialGroups === 'SPECIAL_INCLUDED'
? [
{ name: 'Major students credits', data: majors },
{ name: 'Non-major faculty students credits', data: facultyNonMajor },
{ name: 'Non-major other faculty students credits', data: otherNonMajor },
{ name: 'Non-degree credits', data: nonDegree },
]
: [{ name: 'Major students credits', data: majors }]
return allCredits
}
module.exports = { combineFacultyCredits, getFacultyCreditStatsForStudytrack }
@@ -0,0 +1,365 @@
const moment = require('moment')
const { graduatedStudyrights, studyrightsByRightStartYear, hasMasterRight } = require('./faculty')
const { findRightProgramme, isNewProgramme } = require('./facultyHelpers')
const { getYearsArray, getYearsObject, getMean, getMedian, defineYear } = require('../studyprogrammeHelpers')
const { codes } = require('../../../config/programmeCodes')
const { bachelorStudyright, countTimeCategories, getStatutoryAbsences } = require('../graduationHelpers')
const sortProgrammes = data => {
const check = name => {
if (isNaN(name[0])) return -1
return 1
}
data.sort((a, b) => {
if (check(a.name) === check(b.name)) return a.name.localeCompare(b.name)
return check(a.name) - check(b.name)
})
}
const getSortedProgIds = progs => {
let programmes = []
for (const prog of progs) {
if (Object.keys(codes).includes(prog)) {
programmes = [...programmes, { name: codes[prog].toUpperCase(), code: prog }]
} else {
programmes = [...programmes, { name: prog, code: prog }]
}
}
sortProgrammes(programmes)
return programmes
}
const findBachelorStartdate = async id => {
const studyright = await bachelorStudyright(id)
if (studyright) return studyright.startdate
return null
}
const getProgrammeObjectBasis = (years, levels, emptyObject = true) => {
return levels.reduce(
(acc, level) => ({ ...acc, [level]: years.reduce((acc, year) => ({ ...acc, [year]: emptyObject ? {} : 0 }), {}) }),
{}
)
}
const hasMS = async (programme, elements, studyrightid) => {
// Only bachelor studyright: socCom, kasvatustiede - varhaiskasvatus track, farmasia - applied only to Bc = farmaseutti,
if (['KH74_001', '620050-ba', '620030-ba', '520091-ba'].includes(programme)) {
return false
}
if (programme === 'KH60_001') {
return !elements.some(el => ['EDUK-VO', '0371', '620050-ba', '620030-ba'].includes(el.dataValues.code))
}
if (programme === 'KH55_001') {
const result = await hasMasterRight(studyrightid.replace(/-1$/, '-2'))
if (!result) {
return false
}
}
return true
}
const addGraduation = async (
extentcode,
startdate,
studyrightid,
enddate,
studentnumber,
graduationAmounts,
graduationTimes,
year,
programmes,
programme
) => {
let actualStartdate = startdate
let level = null
if (programme === 'MH30_001' || programme === 'MH30_003') {
level = 'bcMsCombo'
} else if (extentcode === 1) {
level = 'bachelor'
} else if (extentcode === 2) {
if (studyrightid.slice(-2) === '-2') {
level = 'bcMsCombo'
actualStartdate = await findBachelorStartdate(studyrightid.replace(/-2$/, '-1'))
} else {
level = 'master'
}
} else if (extentcode === 4) {
level = 'doctor'
} else if (extentcode === 3) {
level = 'licentiate'
} else {
return
}
const absoluteTimeToGraduation = moment(enddate).diff(moment(actualStartdate), 'months')
const statutoryAbsences = await getStatutoryAbsences(studentnumber, actualStartdate, enddate)
const timeToGraduation = absoluteTimeToGraduation - statutoryAbsences
graduationAmounts[level][year] += 1
graduationTimes[level][year] = [...graduationTimes[level][year], timeToGraduation]
if (!(programme in programmes[level][year]))
programmes[level][year][programme] = { graduationTimes: [], graduationAmounts: 0 }
programmes[level][year][programme].graduationAmounts += 1
programmes[level][year][programme].graduationTimes = [
...programmes[level][year][programme].graduationTimes,
timeToGraduation,
]
}
const getClassSizes = async (faculty, since, classSizes, programmeFilter, years) => {
const studyrights = await studyrightsByRightStartYear(faculty, since, [0, 1])
// get all received studyrights for each year
// a transferred student is counted into new programmes class size i.e.
// students who received studyright on year X and graduated/will be graduating from programme Y
// if we only counted those who started fresh in the new programme we could get a bigger number of graduates
// than the class size, as the graduatation time count only looks at a degree as whole studyright
for (const right of studyrights) {
const { startdate, studyrightid, extentcode, studyrightElements } = right
const { programme } = findRightProgramme(studyrightElements, 'graduated')
if (programmeFilter === 'NEW_STUDY_PROGRAMMES' && !isNewProgramme(programme)) continue
let actualStartdate = startdate
let level = null
if (programme === 'MH30_001' || programme === 'MH30_003') {
level = 'bcMsCombo'
} else if (extentcode === 1) {
level = 'bachelor'
} else if (extentcode === 2) {
if (studyrightid.slice(-2) === '-2') {
level = 'bcMsCombo'
actualStartdate = await findBachelorStartdate(studyrightid.replace(/-2$/, '-1'))
} else {
level = 'master'
}
} else if (extentcode === 4) {
level = 'doctor'
} else if (extentcode === 3) {
level = 'licentiate'
} else {
continue
}
const startYear = defineYear(actualStartdate, false)
// On faculty level, count started bachelor rights to BcMsCombo as well --> useful class size even if master studies have yet not started
// On programme level BcMsCombo sizes are iffy as many students haven't started masters yet --> no Ms programme element to count
if (level !== 'bcMsCombo') {
classSizes[level][startYear] += 1
if (level === 'bachelor' && (await hasMS(programme, studyrightElements, studyrightid))) {
classSizes['bcMsCombo'][startYear] += 1
}
}
if (!(programme in classSizes.programmes)) {
if (extentcode === 2) {
classSizes.programmes[programme] = {}
classSizes.programmes[programme]['bcMsCombo'] = getYearsObject({ years, emptyArrays: false })
classSizes.programmes[programme]['master'] = getYearsObject({ years, emptyArrays: false })
} else {
classSizes.programmes[programme] = getYearsObject({ years, emptyArrays: false })
}
}
if (extentcode === 2) {
classSizes.programmes[programme][level][startYear] += 1
} else {
classSizes.programmes[programme][startYear] += 1
}
}
}
const count = async (faculty, since, years, yearsList, levels, programmeFilter, programmeNames, goals, mode) => {
let graduationAmounts = {}
let graduationTimes = {}
let programmes = getProgrammeObjectBasis(yearsList, levels)
let data = {
means: {},
medians: {},
programmes: {
medians: getProgrammeObjectBasis(yearsList, levels),
means: getProgrammeObjectBasis(yearsList, levels),
},
}
levels.forEach(level => {
graduationAmounts[level] = getYearsObject({ years: yearsList })
graduationTimes[level] = getYearsObject({ years: yearsList, emptyArrays: true })
data.medians[level] = []
data.means[level] = []
})
let studyrights = null
if (mode === 'gradYear') {
studyrights = await graduatedStudyrights(faculty, since)
} else {
studyrights = await studyrightsByRightStartYear(faculty, since)
}
for (const right of studyrights) {
const { enddate, startdate, studyrightid, extentcode, studyrightElements, studentnumber } = right
const { programme, programmeName } = findRightProgramme(studyrightElements, 'graduated')
if (programmeFilter === 'NEW_STUDY_PROGRAMMES' && !isNewProgramme(programme)) continue
if (!(programme in programmeNames)) {
programmeNames[programme] = programmeName
}
let year = null
if (mode === 'gradYear') {
year = defineYear(enddate, false)
} else {
year = defineYear(startdate, false)
}
await addGraduation(
extentcode,
startdate,
studyrightid,
enddate,
studentnumber,
graduationAmounts,
graduationTimes,
year,
programmes,
programme
)
}
years.forEach(year => {
levels.forEach(level => {
const median = getMedian(graduationTimes[level][year])
const 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)) {
goal += goals.exceptions[prog.code]
}
const progStatistics = countTimeCategories(programmes[level][year][prog.code].graduationTimes, goal)
Object.keys(progStatistics).forEach(key => (statistics[key] += progStatistics[key]))
data.programmes.medians[level][year].programmes = [
...data.programmes.medians[level][year].programmes,
prog.code,
]
data.programmes.medians[level][year].data = [
...data.programmes.medians[level][year].data,
{
y: progMedian,
amount: programmes[level][year][prog.code].graduationAmounts,
statistics: progStatistics,
name: prog.name,
code: prog.code,
},
]
data.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 },
]
})
})
return data
}
const countGraduationTimes = async (faculty, programmeFilter) => {
const isAcademicYear = false // = yearType === 'ACADEMIC_YEAR'
const since = isAcademicYear ? new Date('2017-08-01') : new Date('2017-01-01')
const yearsList = getYearsArray(since.getFullYear(), isAcademicYear)
const levels = ['bachelor', 'bcMsCombo', 'master', 'doctor', 'licentiate']
const programmeNames = {}
const years = [...yearsList].reverse()
const goals = {
bachelor: 36,
bcMsCombo: 60,
master: 24,
doctor: 48,
licentiate: 78,
exceptions: {
MH30_004: 6, //months more
'420420-ma': 6,
MH30_001: 12,
'320011-ma': 12,
'320001-ma': 12,
MH30_003: 12,
'320002-ma': 12,
},
}
if (faculty === 'H90') {
goals.bcMsCombo += 12
}
// We count studyrights (vs. studyright_elements)
// This way we get the whole time for a degree, even if the student was transferred to a new programme
// E.g. started 8/2016 in old Bc, transferred to new 10/2020, graduated from new 1/2021 --> total 53 months (not 3)
const byGradYear = await count(
faculty,
since,
years,
yearsList,
levels,
programmeFilter,
programmeNames,
goals,
'gradYear'
)
const byStartYear = await count(
faculty,
since,
years,
yearsList,
levels,
programmeFilter,
programmeNames,
goals,
'startYear'
)
const classSizes = getProgrammeObjectBasis(yearsList, levels, false)
classSizes['programmes'] = {}
await getClassSizes(faculty, since, classSizes, programmeFilter, years)
return { id: faculty, goals, byGradYear, byStartYear, programmeNames, classSizes }
}
module.exports = { countGraduationTimes }
@@ -0,0 +1,145 @@
const Sequelize = require('sequelize')
const { Op } = Sequelize
const { codes } = require('../../../config/programmeCodes')
const findRightProgramme = (studyrightElements, mode) => {
let programme = ''
let programmeName = ''
let studyRightElement = null
if (studyrightElements) {
if (mode === 'started') {
studyRightElement = studyrightElements
.filter(sre => sre.element_detail.type === 20)
.sort((a, b) => new Date(a.startdate) - new Date(b.startdate))[0]
// this way round counts to old programmes, other way around to new programmes
} else {
// graduated
studyRightElement = studyrightElements
.filter(sre => sre.element_detail.type === 20)
.sort((a, b) => new Date(b.startdate) - new Date(a.startdate))[0]
}
if (studyRightElement) {
programme = studyRightElement.code
programmeName = studyRightElement.element_detail.name
}
}
return { programme, programmeName }
}
const facultyFormatStudyright = studyright => {
const {
studyrightid,
studystartdate,
enddate,
givendate,
graduated,
active,
prioritycode,
extentcode,
student,
studyright_elements,
startdate,
} = studyright
return {
studyrightid,
studystartdate,
startdate,
enddate,
givendate,
graduated,
active,
prioritycode,
extentcode,
studentnumber: student.studentnumber,
studyrightElements: studyright_elements,
}
}
const formatFacultyTransfer = transfer => {
const { sourcecode, targetcode, transferdate, studyrightid } = transfer
return {
sourcecode,
targetcode,
transferdate,
studyrightid,
}
}
const facultyFormatProgramme = programme => {
const { code, name } = programme
return { code, name }
}
const formatFacultyThesisWriter = credit => {
const { course_code, credits, attainment_date, student_studentnumber, course } = credit
return {
course_code,
credits,
attainment_date,
student_studentnumber,
courseUnitType: course.course_unit_type,
}
}
const formatOrganization = org => {
const { id, name, code, parent_id } = org
return { id, name, code, parentId: parent_id }
}
const newProgrammes = [/^KH/, /^MH/, /^T/, /^LI/, /^K-/, /^FI/, /^00901$/, /^00910$/]
const isNewProgramme = code => {
for (let i = 0; i < newProgrammes.length; i++) {
if (newProgrammes[i].test(code)) {
return true
}
}
return false
}
const checkTransfers = (s, insideTransfersStudyrights, transfersToOrAwayStudyrights) => {
if (transfersToOrAwayStudyrights.includes(s.studyrightid)) return true
if (insideTransfersStudyrights.includes(s.studyrightid) && new Date(s.studystartdate) < new Date('2017-08-01'))
return true
return false
}
const getExtentFilter = includeAllSpecials => {
const filteredExtents = [16] // always filter out secondary subject students
if (!includeAllSpecials) {
filteredExtents.push(7, 34, 22, 99, 14, 13, 9)
}
let studyrightWhere = {
extentcode: {
[Op.notIn]: filteredExtents,
},
}
return studyrightWhere
}
const mapCodesToIds = data => {
// Add programme id e.g. TKT
const keys = Object.keys(codes)
for (const prog of data) {
if (keys.includes(prog.code)) {
prog.progId = codes[prog.code].toUpperCase()
} else {
prog.progId = prog.code
}
}
}
module.exports = {
findRightProgramme,
facultyFormatStudyright,
facultyFormatProgramme,
formatFacultyTransfer,
formatFacultyThesisWriter,
formatOrganization,
isNewProgramme,
checkTransfers,
getExtentFilter,
mapCodesToIds,
}
@@ -0,0 +1,171 @@
const { redisClient } = require('../redis')
const moment = require('moment')
const createRedisKeyForFacultyProgrammes = (id, programmeFilter) => `FACULTY_PROGRAMMES_${id}_${programmeFilter}`
const createRedisKeyForBasicStats = (id, yearType, programmeFilter, specialGroups) =>
`FACULTY_BASIC_STATS_${id}_${yearType}_${programmeFilter}_${specialGroups}`
const createRedisKeyForCreditStats = (id, yearType, programmeFilter, specialGroups) =>
`FACULTY_CREDIT_STATS_${id}_${yearType}_${programmeFilter}_${specialGroups}`
const createRedisKeyForThesiswriters = (id, yearType, programmeFilter, specialGroups) =>
`FACULTY_THESIS_WRITERS_STATS_${id}_${yearType}_${programmeFilter}_${specialGroups}`
const createRedisKeyForGraduationTimeStats = (id, programmeFilter) =>
`FACULTY_GRADUATION_TIME_STATS_${id}_${programmeFilter}`
const createRediskeyForFacultyProgress = (id, special_groups, graduated) =>
`FACULTY_PROGRESS_STATS_${id}_${special_groups}_${graduated}`
const createRediskeyForFacultyStudents = (id, special_groups, graduated) =>
`FACULTY_STUDENTS_STATS_${id}_${special_groups}_${graduated}`
const setFacultyProgrammes = async (id, data, programmeFilter) => {
const redisKey = createRedisKeyForFacultyProgrammes(id, programmeFilter)
const dataToRedis = {
data: data,
status: 'DONE',
lastUpdated: moment().format(),
}
const setOperationStatus = await redisClient.setAsync(redisKey, JSON.stringify(dataToRedis))
if (setOperationStatus !== 'OK') return null
return dataToRedis
}
const getFacultyProgrammes = async (id, programmeFilter) => {
const redisKey = createRedisKeyForFacultyProgrammes(id, programmeFilter)
const dataFromRedis = await redisClient.getAsync(redisKey)
if (!dataFromRedis) return null
return JSON.parse(dataFromRedis)
}
const setBasicStats = async (data, yearType, programmeFilter, specialGroups) => {
const { id } = data
const redisKey = createRedisKeyForBasicStats(id, yearType, programmeFilter, specialGroups)
const dataToRedis = {
...data,
status: 'DONE',
lastUpdated: moment().format(),
}
const setOperationStatus = await redisClient.setAsync(redisKey, JSON.stringify(dataToRedis))
if (setOperationStatus !== 'OK') return null
return dataToRedis
}
const getBasicStats = async (id, yearType, programmeFilter, specialGroups) => {
const redisKey = createRedisKeyForBasicStats(id, yearType, programmeFilter, specialGroups)
const dataFromRedis = await redisClient.getAsync(redisKey)
if (!dataFromRedis) return null
return JSON.parse(dataFromRedis)
}
const setCreditStats = async (data, yearType, programmeFilter, specialGroups) => {
const { id } = data
const redisKey = createRedisKeyForCreditStats(id, yearType, programmeFilter, specialGroups)
const dataToRedis = {
...data,
status: 'DONE',
lastUpdated: moment().format(),
}
const setOperationStatus = await redisClient.setAsync(redisKey, JSON.stringify(dataToRedis))
if (setOperationStatus !== 'OK') return null
return dataToRedis
}
const getCreditStats = async (id, yearType, programmeFilter, specialGroups) => {
const redisKey = createRedisKeyForCreditStats(id, yearType, programmeFilter, specialGroups)
const dataFromRedis = await redisClient.getAsync(redisKey)
if (!dataFromRedis) return null
return JSON.parse(dataFromRedis)
}
const setThesisWritersStats = async (data, yearType, programmeFilter, specialGroups) => {
const { id } = data
const redisKey = createRedisKeyForThesiswriters(id, yearType, programmeFilter, specialGroups)
const dataToRedis = {
...data,
status: 'DONE',
lastUpdated: moment().format(),
}
const setOperationStatus = await redisClient.setAsync(redisKey, JSON.stringify(dataToRedis))
if (setOperationStatus !== 'OK') return null
return dataToRedis
}
const getThesisWritersStats = async (id, yearType, programmeFilter, specialGroups) => {
const redisKey = createRedisKeyForThesiswriters(id, yearType, programmeFilter, specialGroups)
const dataFromRedis = await redisClient.getAsync(redisKey)
if (!dataFromRedis) return null
return JSON.parse(dataFromRedis)
}
const setGraduationStats = async (data, programmeFilter) => {
const { id } = data
const redisKey = createRedisKeyForGraduationTimeStats(id, programmeFilter)
const dataToRedis = {
...data,
status: 'DONE',
lastUpdated: moment().format(),
}
const setOperationStatus = await redisClient.setAsync(redisKey, JSON.stringify(dataToRedis))
if (setOperationStatus !== 'OK') return null
return dataToRedis
}
const getGraduationStats = async (id, programmeFilter) => {
const redisKey = createRedisKeyForGraduationTimeStats(id, programmeFilter)
const dataFromRedis = await redisClient.getAsync(redisKey)
if (!dataFromRedis) return null
return JSON.parse(dataFromRedis)
}
const getFacultyProgressStats = async (id, specialGroups, graduated) => {
const redisKey = createRediskeyForFacultyProgress(id, specialGroups, graduated)
const dataFromRedis = await redisClient.getAsync(redisKey)
if (!dataFromRedis) return null
return JSON.parse(dataFromRedis)
}
const setFacultyProgressStats = async (data, specialGroups, graduated) => {
const { id } = data
const redisKey = createRediskeyForFacultyProgress(id, specialGroups, graduated)
const dataToRedis = {
...data,
status: 'DONE',
lastUpdated: moment().format(),
}
const setOperationStatus = await redisClient.setAsync(redisKey, JSON.stringify(dataToRedis))
if (setOperationStatus !== 'OK') return null
return dataToRedis
}
const getFacultyStudentStats = async (id, specialGroups, graduated) => {
const redisKey = createRediskeyForFacultyStudents(id, specialGroups, graduated)
const dataFromRedis = await redisClient.getAsync(redisKey)
if (!dataFromRedis) return null
return JSON.parse(dataFromRedis)
}
const setFacultyStudentStats = async (data, specialGroups, graduated) => {
const { id } = data
const redisKey = createRediskeyForFacultyStudents(id, specialGroups, graduated)
const dataToRedis = {
...data,
status: 'DONE',
lastUpdated: moment().format(),
}
const setOperationStatus = await redisClient.setAsync(redisKey, JSON.stringify(dataToRedis))
if (setOperationStatus !== 'OK') return null
return dataToRedis
}
module.exports = {
setFacultyProgrammes,
getFacultyProgrammes,
setBasicStats,
getBasicStats,
setCreditStats,
getCreditStats,
setThesisWritersStats,
getThesisWritersStats,
setGraduationStats,
getGraduationStats,
getFacultyProgressStats,
setFacultyProgressStats,
getFacultyStudentStats,
setFacultyStudentStats,
}
@@ -0,0 +1,328 @@
const { indexOf } = require('lodash')
const moment = require('moment')
const { getAcademicYearDates } = require('../../util/semester')
const {
getCreditThresholds,
getBachelorCreditGraphStats,
getMasterCreditGraphStats,
getDoctoralCreditGraphStats,
getYearsArray,
tableTitles,
getOnlyMasterCreditGraphStats,
getOnlyMasterThresholds,
getVetenaryCreditGraphStats,
} = require('../studyprogrammeHelpers')
const { studytrackStudents } = require('../studyprogramme')
const { getStudyRightsByExtent, transferredAway, transferredTo, transferredInsideFaculty } = require('./faculty')
const getStudentData = (
startDate,
students,
thresholdKeys,
thresholdAmounts,
limits,
limitKeys,
prog,
earlyStudyRights = []
) => {
let data = {}
let programmeData = {}
let start = startDate
thresholdKeys.forEach(t => (data[t] = 0))
limitKeys.forEach(t => (programmeData[t] = 0))
students.forEach(({ studentnumber, credits }) => {
if (earlyStudyRights.length > 0) {
const bcStudyright = earlyStudyRights.filter(sr => sr.studentnumber === studentnumber)
start = bcStudyright.length > 0 ? bcStudyright[0].startdate : startDate
}
const creditcount = credits
.filter(credit => moment(credit.attainment_date).isSameOrAfter(start))
.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[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[5]] += creditcount >= thresholdAmounts[4] && creditcount < thresholdAmounts[5] ? 1 : 0
data[thresholdKeys[6]] += creditcount >= thresholdAmounts[5] ? 1 : 0
} else {
data[thresholdKeys[5]] += creditcount >= thresholdAmounts[4] ? 1 : 0
}
if (['KH', 'MH'].includes(prog)) {
programmeData[limitKeys[0]] += creditcount <= limits[5][0] ? 1 : 0
programmeData[limitKeys[1]] += limits[4][0] <= creditcount && limits[4][1] > creditcount ? 1 : 0
programmeData[limitKeys[2]] += limits[3][0] <= creditcount && limits[3][1] > creditcount ? 1 : 0
programmeData[limitKeys[3]] += limits[2][0] <= creditcount && limits[2][1] > creditcount ? 1 : 0
programmeData[limitKeys[4]] += limits[1][0] <= creditcount && limits[1][1] > creditcount ? 1 : 0
programmeData[limitKeys[5]] += creditcount >= limits[0][0] ? 1 : 0
}
})
return { data, programmeData }
}
const createLimits = (months, creditsToAdd) => {
return [
[Math.ceil(months * (60 / 12)) + creditsToAdd, null],
[Math.ceil(months * (45 / 12)) + creditsToAdd, Math.ceil(months * (60 / 12)) + creditsToAdd],
[Math.ceil(months * (30 / 12)) + creditsToAdd, Math.ceil(months * (45 / 12)) + creditsToAdd],
[Math.ceil(months * (15 / 12)) + creditsToAdd, Math.ceil(months * (30 / 12)) + creditsToAdd],
[creditsToAdd + 1, Math.ceil(months * (15 / 12)) + creditsToAdd],
[creditsToAdd, null],
]
}
const createYearlyTitles = (start, limitList) => {
return [
`${start} Credits`,
`${limitList[4][0]} ≤ Credits < ${limitList[4][1]}`,
`${limitList[3][0]} ≤ Credits < ${limitList[3][1]}`,
`${limitList[2][0]} ≤ Credits < ${limitList[2][1]}`,
`${limitList[1][0]} ≤ Credits < ${limitList[1][1]}`,
`${limitList[0][0]} ≤ Credits`,
]
}
const filterOutTransfers = async (studyrights, programmes, allProgrammeCodes, since) => {
const transferredIn = (await transferredTo(programmes, allProgrammeCodes, since)).map(tr => tr.studyrightid)
const transferredOut = (await transferredAway(programmes, allProgrammeCodes, since)).map(tr => tr.studyrightid)
const transferredInside = (await transferredInsideFaculty(programmes, allProgrammeCodes, since)).map(
tr => tr.studyrightid
)
const filteredStudyrights = studyrights.filter(
sr =>
!transferredIn.includes(sr.studyrightid) &&
!transferredOut.includes(sr.studyrightid) &&
!(transferredInside.includes(sr.studyrightid) && moment(sr.startdate).isBefore('2017-08-01', 'YYYY-MM-DD'))
)
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 graduationStatus = [0]
if (includeGraduated) {
graduationStatus.push(1)
}
const yearsArray = getYearsArray(since.getFullYear(), isAcademicYear, includeYearsCombined)
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: programmes.reduce(
(obj, dataItem) => ({ ...obj, [dataItem.progId]: { code: dataItem.code, ...dataItem.name } }),
{}
),
bachelorsProgrammeStats: {},
bcMsProgrammeStats: {},
mastersProgrammeStats: {},
doctoralProgrammeStats: {},
}
const reversedYears = [...yearsArray].reverse()
reversedYears.forEach(year => {
progressStats.bachelorsTableStats[indexOf(reversedYears, year)] = [year, 0, 0, 0, 0, 0, 0, 0, 0, 0]
progressStats.bcMsTableStats[indexOf(reversedYears, year)] = [year, 0, 0, 0, 0, 0, 0, 0, 0]
progressStats.mastersTableStats[indexOf(reversedYears, year)] = [year, 0, 0, 0, 0, 0, 0, 0]
progressStats.doctoralTableStats[indexOf(reversedYears, year)] = [year, 0, 0, 0, 0, 0, 0, 0, 0]
})
const limitKeys = ['zero', 't5', 't4', 't3', 't2', 't1']
let bachelorlimits = []
let masterlimits = []
let bcmslimits = []
for (const year of reversedYears) {
const { startDate, endDate } = getAcademicYearDates(year, since)
const lastDayOfMonth = moment().endOf('month')
let months = Math.round(moment.duration(moment(lastDayOfMonth).diff(moment(startDate))).asMonths())
if (months >= 36) {
bachelorlimits = createLimits(36, 0)
} else {
bachelorlimits = createLimits(months, 0)
}
if (months >= 24) {
masterlimits = createLimits(24, 0)
} else {
masterlimits = createLimits(months, 0)
}
if (months >= 72 && faculty === 'H90') {
bcmslimits = createLimits(72, 180)
} else if (months >= 60) {
bcmslimits = createLimits(60, 180)
} else {
bcmslimits = createLimits(months, 180)
}
progressStats.yearlyBachelorTitles = [...progressStats.yearlyBachelorTitles, createYearlyTitles(0, bachelorlimits)]
progressStats.yearlyMasterTitles = [...progressStats.yearlyMasterTitles, createYearlyTitles(0, masterlimits)]
progressStats.yearlyBcMsTitles = [...progressStats.yearlyBcMsTitles, createYearlyTitles(180, bcmslimits)]
const programmeCodes = programmes.map(prog => prog.code)
for (const { progId, code } of programmes) {
let extentcodes = []
if (includeAllSpecials) {
extentcodes = [7, 9, 34, 22, 99, 14, 13]
}
let { creditThresholdKeys, creditThresholdAmounts } = getCreditThresholds(code)
if (!creditThresholdKeys) return
if (code.includes('KH')) {
extentcodes.push(1)
let studyrights = await getStudyRightsByExtent(faculty, startDate, endDate, code, extentcodes, graduationStatus)
if (!includeAllSpecials) {
studyrights = await filterOutTransfers(studyrights, programmeCodes, allProgrammeCodes, since)
}
const all = studyrights
.filter(sr => sr.studyrightElements.some(element => element.code === code))
.map(sr => sr.studentnumber)
const students = await studytrackStudents(all)
const { data: studentData, programmeData } = getStudentData(
startDate,
students,
creditThresholdKeys,
creditThresholdAmounts,
bachelorlimits,
limitKeys,
'KH'
)
if (!(progId in progressStats.bachelorsProgrammeStats)) {
progressStats.bachelorsProgrammeStats[progId] = new Array(reversedYears.length - 1)
}
progressStats.bachelorsTableStats[indexOf(reversedYears, year)][1] += all.length || 0
if (year !== 'Total') {
progressStats.bachelorsProgrammeStats[progId][indexOf(reversedYears, year)] = limitKeys.map(
key => programmeData[key]
)
}
for (const key of Object.keys(studentData)) {
progressStats.bachelorsTableStats[indexOf(reversedYears, year)][indexOf(creditThresholdKeys, key) + 2] +=
studentData[key] || 0
progressStats.bachelorsGraphStats[key].data[indexOf(yearsArray, year)] += studentData[key] || 0
}
} else if (code.includes('MH')) {
extentcodes.push(2)
let all = await getStudyRightsByExtent(faculty, startDate, endDate, code, extentcodes, graduationStatus)
if (!includeAllSpecials) {
all = await filterOutTransfers(all, programmeCodes, allProgrammeCodes, since)
}
const allMsStudents = all.filter(sr => sr.studyrightid.slice(-2) !== '-2').map(sr => sr.studentnumber)
const allBcMsStudents = all.filter(sr => sr.studyrightid.slice(-2) === '-2').map(sr => sr.studentnumber)
const studentsWithBachelor = all
.filter(sr => sr.studyrightid.slice(-2) === '-2')
.map(sr => ({ studentnumber: sr.studentnumber, startdate: sr.startdate }))
const bcMsStudents = await studytrackStudents(allBcMsStudents)
const msStudents = await studytrackStudents(allMsStudents)
progressStats.mastersTableStats[indexOf(reversedYears, year)][1] += allMsStudents.length || 0
progressStats.bcMsTableStats[indexOf(reversedYears, year)][1] += allBcMsStudents.length || 0
const { data: bcMsStudentdata, programmeData: bcMsProgrammedata } = getStudentData(
startDate,
bcMsStudents,
creditThresholdKeys,
creditThresholdAmounts,
bcmslimits,
limitKeys,
'MH',
studentsWithBachelor
)
const { msOnlyCreditThresholdKeys, msOnlyCreditThresholdAmount } = getOnlyMasterThresholds()
const { data: msStudentdata, programmeData: msProgrammedata } = getStudentData(
startDate,
msStudents,
msOnlyCreditThresholdKeys,
msOnlyCreditThresholdAmount,
masterlimits,
limitKeys,
'MH'
)
if (!(progId in progressStats.mastersProgrammeStats)) {
progressStats.mastersProgrammeStats[progId] = new Array(reversedYears.length - 1)
}
if (!(progId in progressStats.bcMsProgrammeStats)) {
progressStats.bcMsProgrammeStats[progId] = new Array(reversedYears.length - 1)
}
if (year !== 'Total') {
progressStats.mastersProgrammeStats[progId][indexOf(reversedYears, year)] = limitKeys.map(
key => msProgrammedata[key]
)
progressStats.bcMsProgrammeStats[progId][indexOf(reversedYears, year)] = limitKeys.map(
key => bcMsProgrammedata[key]
)
}
for (const key of Object.keys(msStudentdata)) {
progressStats.mastersTableStats[indexOf(reversedYears, year)][indexOf(msOnlyCreditThresholdKeys, key) + 2] +=
msStudentdata[key] || 0
progressStats.mastersGraphStats[key].data[indexOf(yearsArray, year)] += msStudentdata[key] || 0
}
for (const key of Object.keys(bcMsStudentdata)) {
progressStats.bcMsTableStats[indexOf(reversedYears, year)][indexOf(creditThresholdKeys, key) + 2] +=
bcMsStudentdata[key] || 0
progressStats.bcMsGraphStats[key].data[indexOf(yearsArray, year)] += bcMsStudentdata[key] || 0
}
} else {
extentcodes.push(4)
let studyrights = await getStudyRightsByExtent(faculty, startDate, endDate, code, extentcodes, graduationStatus)
if (!includeAllSpecials) {
studyrights = await filterOutTransfers(studyrights, programmeCodes, allProgrammeCodes, since)
}
const all = studyrights
.filter(sr => sr.studyrightElements.some(element => element.code === code))
.map(sr => sr.studentnumber)
const students = await studytrackStudents(all)
const { data } = getStudentData(startDate, students, creditThresholdKeys, creditThresholdAmounts, [], [], 'T')
progressStats.doctoralTableStats[indexOf(reversedYears, year)][1] += all.length || 0
if (!(progId in progressStats.doctoralProgrammeStats)) {
progressStats.doctoralProgrammeStats[progId] = new Array(reversedYears.length - 1)
}
if (year !== 'Total') {
progressStats.doctoralProgrammeStats[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.doctoralGraphStats[key].data[indexOf(yearsArray, year)] += data[key] || 0
}
}
}
}
return progressStats
}
module.exports = { combineFacultyStudentProgress }
@@ -0,0 +1,160 @@
const {
getStartDate,
getYearsArray,
getPercentage,
getYearsObject,
getCorrectStudentnumbers,
tableTitles,
} = require('../studyprogrammeHelpers')
const {
studytrackStudents,
allStudyrights,
startedStudyrights,
inactiveStudyrights,
enrolledStudents,
absentStudents,
graduatedStudyRights,
} = require('../studyprogramme')
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 = {
total: 0,
allStarted: 0,
allEnrolled: 0,
allAbsent: 0,
allInactive: 0,
allGraduated: 0,
allFinnish: 0,
allFemale: 0,
allMale: 0,
}
for (const { progId, code } of programmes) {
const studentnumbers = await getCorrectStudentnumbers({
codes: [code],
startDate,
endDate,
includeAllSpecials,
includeGraduated,
})
// 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)
// Count stats for the programme
if (!(progId in programmeTableStats)) {
programmeTableStats[progId] = years.reduce((resultObj, yearKey) => {
return { ...resultObj, [yearKey]: [] }
}, {})
}
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),
]
totals.total += all.length
totals.allStarted += started.length
totals.allEnrolled += enrolled.length
totals.allAbsent += absent.length
totals.allInactive += inactive.length
totals.allGraduated += graduated.length
totals.allFemale += studentData.female
totals.allMale += studentData.male
totals.allFinnish += studentData.finnish
}
return totals
}
// Combines all the data for faculty students table
const combineFacultyStudents = async (code, programmes, 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 settings = { includeAllSpecials, includeGraduated }
let facultyTableStats = getYearsObject({ years, emptyArrays: true })
let programmeTableStats = {}
for (const year of years.reverse()) {
const queryParams = { programmes, since, settings, year, years, programmeTableStats }
const totals = await getFacultyDataForYear(queryParams)
facultyTableStats[year] = [
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),
]
}
const studentsData = {
id: code,
years: years,
facultyTableStats: facultyTableStats,
programmeStats: programmeTableStats,
titles: tableTitles['studytracks'],
programmeNames: programmes.reduce(
(obj, dataItem) => ({ ...obj, [dataItem.progId]: { code: dataItem.code, ...dataItem.name } }),
{}
),
}
return studentsData
}
module.exports = { combineFacultyStudents }
@@ -0,0 +1,134 @@
const { indexOf } = require('lodash')
const { mapToProviders } = require('../../util/utils')
const { thesisWriters } = require('./faculty')
const {
getStatsBasis,
getYearsArray,
defineYear,
getCorrectStudentnumbers,
alltimeEndDate,
alltimeStartDate,
} = require('../studyprogrammeHelpers')
const getFacultyThesisWriters = async ({ since, years, isAcademicYear, facultyProgrammes, includeAllSpecials }) => {
const thesisTypes = [
'urn:code:course-unit-type:bachelors-thesis',
'urn:code:course-unit-type:masters-thesis',
'urn:code:course-unit-type:doctors-thesis',
'urn:code:course-unit-type:licentiate-thesis',
]
let bachelors = getStatsBasis(years)
let masters = getStatsBasis(years)
let doctors = getStatsBasis(years)
let licentiates = getStatsBasis(years)
let programmeCounts = {}
let programmeNames = {}
for (const { progId, code, name } of facultyProgrammes) {
const provider = mapToProviders([code])[0]
const students = await getCorrectStudentnumbers({
codes: [code],
startDate: alltimeStartDate,
endDate: alltimeEndDate,
includeAllSpecials,
})
const thesisCourseCodes = await thesisWriters(provider, since, thesisTypes, students)
thesisCourseCodes?.forEach(({ attainment_date, courseUnitType }) => {
const thesisYear = defineYear(attainment_date, isAcademicYear)
if (!(progId in programmeCounts)) {
programmeCounts[progId] = {}
Object.keys(bachelors.tableStats).forEach(year => (programmeCounts[progId][year] = [0, 0, 0, 0, 0]))
programmeNames[progId] = { ...name, code: code }
}
programmeCounts[progId][thesisYear][0] += 1
if (courseUnitType === thesisTypes[0]) {
bachelors.graphStats[indexOf(years, thesisYear)] += 1
bachelors.tableStats[thesisYear] += 1
programmeCounts[progId][thesisYear][1] += 1
} else if (courseUnitType === thesisTypes[1]) {
masters.graphStats[indexOf(years, thesisYear)] += 1
masters.tableStats[thesisYear] += 1
programmeCounts[progId][thesisYear][2] += 1
} 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 }
}
const getFacultyThesisWritersForStudyTrack = async (
allThesisWriters,
facultyProgrammes,
isAcademicYear,
includeAllSpecials
) => {
const since = isAcademicYear ? new Date('2017-08-01') : new Date('2017-01-01')
const years = getYearsArray(since.getFullYear(), isAcademicYear)
const queryParameters = { since, years, isAcademicYear, facultyProgrammes, includeAllSpecials }
const { bachelors, masters, doctors, licentiates, programmeCounts, programmeNames } = await getFacultyThesisWriters(
queryParameters
)
allThesisWriters.years = years.map(year => year.toString())
const reversedYears = years.reverse()
allThesisWriters.tableStats = reversedYears.map(year => [
year,
bachelors.tableStats[year] + masters.tableStats[year] + doctors.tableStats[year] + licentiates.tableStats[year],
bachelors.tableStats[year],
masters.tableStats[year],
doctors.tableStats[year],
licentiates.tableStats[year],
])
allThesisWriters.graphStats.push({ name: 'Bachelors', data: bachelors.graphStats })
allThesisWriters.graphStats.push({ name: 'Masters', data: masters.graphStats })
allThesisWriters.graphStats.push({ name: 'Doctors', data: doctors.graphStats })
allThesisWriters.graphStats.push({ name: 'Licentiates', data: licentiates.graphStats })
const programmes = programmeNames ? Object.keys(programmeNames) : facultyProgrammes.map(programme => programme.code)
programmes.forEach(programmeId => {
reversedYears.forEach(year => {
if (programmeCounts && programmeId in programmeCounts) {
if (!(programmeId in allThesisWriters.programmeTableStats)) {
allThesisWriters.programmeTableStats[programmeId] = []
}
allThesisWriters.programmeTableStats[programmeId].push([year, ...programmeCounts[programmeId][year]])
}
})
})
allThesisWriters.programmeNames = programmeNames
}
const combineFacultyThesisWriters = async (faculty, facultyProgrammes, yearType, specialGroups) => {
let allThesisWriters = {
id: faculty,
years: [],
tableStats: [],
graphStats: [],
programmeTableStats: {},
titles: ['', 'All', 'Bachelors', 'Masters', 'Doctors', 'Licentiates'],
programmeNames: {},
status: 'Done',
lastUpdated: '',
}
const isAcademicYear = yearType === 'ACADEMIC_YEAR'
const includeAllSpecials = specialGroups === 'SPECIAL_INCLUDED'
await getFacultyThesisWritersForStudyTrack(allThesisWriters, facultyProgrammes, isAcademicYear, includeAllSpecials)
return allThesisWriters
}
module.exports = { combineFacultyThesisWriters }
@@ -0,0 +1,188 @@
const logger = require('../../util/logger')
const { findFacultyProgrammeCodes } = require('./faculty')
const { combineFacultyBasics } = require('./facultyBasics')
const { combineFacultyCredits } = require('./facultyCredits')
const { combineFacultyThesisWriters } = require('./facultyThesisWriters')
const { countGraduationTimes } = require('./facultyGraduationTimes')
const {
setFacultyProgrammes,
setBasicStats,
setCreditStats,
setThesisWritersStats,
setGraduationStats,
setFacultyProgressStats,
setFacultyStudentStats,
} = require('./facultyService')
const { combineFacultyStudentProgress } = require('./facultyStudentProgress')
const { combineFacultyStudents } = require('./facultyStudents')
const updateFacultyOverview = async (faculty, statsType) => {
const calendarNewSpecial = {
yearType: 'CALENDAR_YEAR',
programmeFilter: 'NEW_STUDY_PROGRAMMES',
specialGroups: 'SPECIAL_INCLUDED',
}
const calendarNewSpecialOut = {
yearType: 'CALENDAR_YEAR',
programmeFilter: 'NEW_STUDY_PROGRAMMES',
specialGroups: 'SPECIAL_EXCLUDED',
}
const calendarAllSpecial = {
yearType: 'CALENDAR_YEAR',
programmeFilter: 'ALL_PROGRAMMES',
specialGroups: 'SPECIAL_INCLUDED',
}
const calendarAllSpecialOut = {
yearType: 'CALENDAR_YEAR',
programmeFilter: 'ALL_PROGRAMMES',
specialGroups: 'SPECIAL_EXCLUDED',
}
const academicNewSpecial = {
yearType: 'ACADEMIC_YEAR',
programmeFilter: 'NEW_STUDY_PROGRAMMES',
specialGroups: 'SPECIAL_INCLUDED',
}
const academicAllSpecial = {
yearType: 'ACADEMIC_YEAR',
programmeFilter: 'ALL_PROGRAMMES',
specialGroups: 'SPECIAL_INCLUDED',
}
const academicNewSpecialOut = {
yearType: 'ACADEMIC_YEAR',
programmeFilter: 'NEW_STUDY_PROGRAMMES',
specialGroups: 'SPECIAL_EXCLUDED',
}
const academicAllSpecialOut = {
yearType: 'ACADEMIC_YEAR',
programmeFilter: 'ALL_PROGRAMMES',
specialGroups: 'SPECIAL_EXCLUDED',
}
const options = [
calendarNewSpecial,
calendarAllSpecial,
academicNewSpecial,
academicAllSpecial,
calendarNewSpecialOut,
calendarAllSpecialOut,
academicNewSpecialOut,
academicAllSpecialOut,
]
let allProgrammes = []
let newProgrammes = []
let allProgrammeCodes = []
try {
const all = await findFacultyProgrammeCodes(faculty, 'ALL_PROGRAMMES')
const onlyNew = await findFacultyProgrammeCodes(faculty, 'NEW_STUDY_PROGRAMMES')
allProgrammes = await setFacultyProgrammes(faculty, all, 'ALL_PROGRAMMES')
newProgrammes = await setFacultyProgrammes(faculty, onlyNew, 'NEW_STUDY_PROGRAMMES')
allProgrammes?.data.forEach(prog => allProgrammeCodes.push(prog.code))
} catch (e) {
logger.error(e)
}
for (const option of options) {
const { yearType, programmeFilter, specialGroups } = option
try {
if (statsType === 'ALL' || statsType === 'STUDENT') {
const updatedStudentInfo = await combineFacultyBasics(
faculty,
programmeFilter === 'ALL_PROGRAMMES' ? allProgrammes.data : newProgrammes.data,
yearType,
allProgrammeCodes,
programmeFilter,
specialGroups
)
await setBasicStats(updatedStudentInfo, option.yearType, option.programmeFilter, option.specialGroups)
}
if (statsType === 'ALL' || statsType === 'CREDITS') {
const updatedCredits = await combineFacultyCredits(
faculty,
programmeFilter === 'ALL_PROGRAMMES' ? allProgrammes.data : newProgrammes.data,
yearType,
specialGroups
)
await setCreditStats(updatedCredits, yearType, programmeFilter, specialGroups)
}
if (statsType === 'ALL' || statsType === 'THESIS') {
const updateThesisWriters = await combineFacultyThesisWriters(
faculty,
programmeFilter === 'ALL_PROGRAMMES' ? allProgrammes.data : newProgrammes.data,
yearType,
specialGroups
)
await setThesisWritersStats(updateThesisWriters, yearType, programmeFilter, specialGroups)
}
} catch (e) {
logger.error(e)
}
}
try {
const updatedTimesAll = await countGraduationTimes(faculty, 'ALL_PROGRAMMES')
await setGraduationStats(updatedTimesAll, 'ALL_PROGRAMMES')
const updatedTimesNew = await countGraduationTimes(faculty, 'NEW_STUDY_PROGRAMMES')
await setGraduationStats(updatedTimesNew, 'NEW_STUDY_PROGRAMMES')
} catch (e) {
logger.error(e)
}
return 'OK'
}
const updateFacultyProgressOverview = async faculty => {
const specialGraduated = {
specialGroups: 'SPECIAL_INCLUDED',
graduated: 'GRADUATED_INCLUDED',
}
const specialNotGraduated = {
specialGroups: 'SPECIAL_INCLUDED',
graduated: 'GRADUATED_EXCLUDED',
}
const notSpecialGraduated = {
specialGroups: 'SPECIAL_EXCLUDED',
graduated: 'GRADUATED_INCLUDED',
}
const notSpecialNotGraduated = {
specialGroups: 'SPECIAL_EXCLUDED',
graduated: 'GRADUATED_EXCLUDED',
}
const options = [specialGraduated, specialNotGraduated, notSpecialGraduated, notSpecialNotGraduated]
let newProgrammes = []
try {
const onlyNew = await findFacultyProgrammeCodes(faculty, 'NEW_STUDY_PROGRAMMES')
newProgrammes = await setFacultyProgrammes(faculty, onlyNew, 'NEW_STUDY_PROGRAMMES')
} catch (e) {
logger.error(e)
}
for (const option of options) {
const { specialGroups, graduated } = option
try {
const updateFacultyStudentStats = await combineFacultyStudents(
faculty,
newProgrammes.data,
specialGroups,
graduated
)
await setFacultyStudentStats(updateFacultyStudentStats, specialGroups, graduated)
const updateFacultyProgressStats = await combineFacultyStudentProgress(
faculty,
newProgrammes.data,
specialGroups,
graduated
)
await setFacultyProgressStats(updateFacultyProgressStats, specialGroups, graduated)
} catch (e) {
logger.error(e)
}
}
return 'OK'
}
module.exports = { updateFacultyOverview, updateFacultyProgressOverview }
@@ -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,
}
+1 -1
View File
@@ -22,7 +22,7 @@ const baseSettings = {
const sendEmail = async (options = {}) => {
if (!isProduction) throw new ApplicationError('Email sending is disabled in development mode.')
if (!pateToken) throw new ApplicationError('Email sending failed because pate token is missing.')
await pateClient.post('/', options)
return await pateClient.post('/', options)
}
const sendFeedbackToToska = async ({ feedbackContent, user }) => {
@@ -0,0 +1,47 @@
const mapOpenCredits = credit => {
const { course_code, attainment_date, student_studentnumber, grade } = credit
return {
course_code,
attainment_date,
student_studentnumber,
grade,
}
}
const mapStundentInfo = student => {
const { email, secondary_email, studentnumber, dissemination_info_allowed } = student
return {
studentnumber,
email,
secondary_email,
dissemination_info_allowed,
}
}
const mapOpenEnrollments = enrollment => {
const { studentnumber, course_code, state, enrollment_date_time } = enrollment
return {
enrollmentStudentnumber: studentnumber,
course_code,
state,
enrollment_date_time,
}
}
const mapStudyRights = studyright => {
const { enddate, startdate, student } = studyright
return {
startdate,
enddate,
studyrightStudentnumber: student.studentnumber,
}
}
const mapCourseInfo = course => {
const { code, name } = course
return {
label: code,
name,
}
}
module.exports = { mapOpenCredits, mapOpenEnrollments, mapStundentInfo, mapStudyRights, mapCourseInfo }
@@ -0,0 +1,65 @@
const logger = require('../../util/logger')
const {
getOpenUniSearchesByUser,
createOpenUniPopulationSearch,
updateOpenUniPopulationSearch,
deleteOpenUniSearch,
} = require('./openUniSearches')
const getOpenUniSearches = async userId => {
const savedSearches = await getOpenUniSearchesByUser(userId)
if (savedSearches === undefined) return []
const foundSearches = savedSearches.reduce(
(res, search) => [
...res,
{
id: search.id,
userId: search.userId,
name: search.name,
courseList: search.courseCodes,
updatedAt: search.updatedAt,
},
],
[]
)
return foundSearches
}
const createNewSearch = async (userId, name, courseCodes) => {
const savedSearches = await getOpenUniSearchesByUser(userId)
if (savedSearches !== undefined) {
const searchNames = savedSearches.map(search => search.name)
if (searchNames.includes(name)) return null
}
try {
const created = await createOpenUniPopulationSearch(userId, name, courseCodes)
if (!created) return null
return created
} catch (e) {
logger.error(`Couldn't create new open uni search instance: ${e}`)
return null
}
}
const deleteSearch = async (userId, id) => {
try {
const deleted = await deleteOpenUniSearch(userId, id)
if (!deleted) return null
return deleted
} catch (e) {
logger.error(`Couldn't delete open uni search instance: ${e}`)
return null
}
}
const updateSearch = async (userId, id, courseCodes) => {
try {
const updated = await updateOpenUniPopulationSearch(userId, id, courseCodes)
if (!updated) return null
return updated
} catch (e) {
logger.error(`Couldn't update open uni search instance: ${e}`)
}
}
module.exports = { getOpenUniSearches, createNewSearch, deleteSearch, updateSearch }
@@ -0,0 +1,135 @@
const { Credit, Enrollment, Studyright, Student, Course } = require('../../models')
const { OpenUniPopulationSearch } = require('../../models/models_kone')
const {
mapOpenCredits,
mapOpenEnrollments,
mapStundentInfo,
mapStudyRights,
mapCourseInfo,
} = require('./openUniHelpers')
const { Op } = require('sequelize')
// 1. iteration: time is hardcoded. Check hyväksytty grades
const getCredits = async (courseCodes, startdate) =>
(
await Credit.findAll({
attributes: ['course_code', 'student_studentnumber', 'grade', 'attainment_date'],
where: {
course_code: {
[Op.in]: courseCodes,
},
is_open: true,
attainment_date: {
[Op.gte]: startdate,
},
},
})
).map(mapOpenCredits)
const getStudentInfo = async students =>
(
await Student.findAll({
where: {
studentnumber: {
[Op.in]: students,
},
},
})
).map(mapStundentInfo)
const getEnrollments = async (courseCodes, startdate, enddate) =>
(
await Enrollment.findAll({
where: {
course_code: {
[Op.in]: courseCodes,
},
is_open: true,
enrollment_date_time: {
[Op.and]: {
[Op.lte]: enddate,
[Op.gte]: startdate,
},
},
},
})
).map(mapOpenEnrollments)
const getCourseNames = async courseCodes =>
(
await Course.findAll({
attributes: ['code', 'name'],
where: {
code: {
[Op.in]: courseCodes,
},
},
})
).map(mapCourseInfo)
const getStudyRights = async students =>
(
await Studyright.findAll({
include: [
{
model: Student,
attributes: ['studentnumber'],
required: true,
},
],
where: {
student_studentnumber: {
[Op.in]: students.length > 0 ? students : { [Op.not]: null },
},
},
})
).map(mapStudyRights)
const getOpenUniSearchesByUser = async userId => {
return await OpenUniPopulationSearch.findAll({
where: {
userId,
},
})
}
const createOpenUniPopulationSearch = async (userId, name, courseCodes) => {
return await OpenUniPopulationSearch.create({
userId,
name,
courseCodes,
})
}
const updateOpenUniPopulationSearch = async (userId, id, courseCodes) => {
const searchToUpdate = await OpenUniPopulationSearch.findOne({
where: {
userId: userId,
id: id,
},
})
if (!searchToUpdate) return null
return await searchToUpdate.update({ courseCodes })
}
const deleteOpenUniSearch = async (userId, id) => {
return await OpenUniPopulationSearch.destroy({
where: {
userId: userId,
id: id,
},
})
}
module.exports = {
getCredits,
getEnrollments,
getStudyRights,
getStudentInfo,
getCourseNames,
getOpenUniSearchesByUser,
createOpenUniPopulationSearch,
updateOpenUniPopulationSearch,
deleteOpenUniSearch,
}
@@ -0,0 +1,108 @@
const moment = require('moment')
const { getCredits, getStudyRights, getEnrollments, getStudentInfo, getCourseNames } = require('./openUniSearches')
const uniq = objects => [...new Set(objects)]
const calulateTotalsForStudent = async (studentStats, studentnumber) => {
Object.keys(studentStats[studentnumber].courseInfo).forEach(course => {
if (studentStats[studentnumber].courseInfo[course].status.passed) {
studentStats[studentnumber].totals.passed += 1
} else if (studentStats[studentnumber].courseInfo[course].status.failed) {
studentStats[studentnumber].totals.failed += 1
} else if (studentStats[studentnumber].courseInfo[course].status.unfinished) {
studentStats[studentnumber].totals.unfinished += 1
}
})
}
const getCustomOpenUniCourses = async (courseCodes, startdate, enddate) => {
const ayCourseCodes = courseCodes.map(courseCode => 'AY' + courseCode)
const allCourseCodes = courseCodes.concat(ayCourseCodes)
const allCredits = await getCredits(allCourseCodes, startdate)
const allEnrollments = await getEnrollments(allCourseCodes, startdate, enddate)
const students = uniq(allEnrollments.map(enrollment => enrollment.enrollmentStudentnumber))
const courses = await getCourseNames(courseCodes)
const passedGrades = ['1', '2', '3', '4', '5', 'Hyv.', 'hyv.', 'HT', 'TT']
const failedGrades = ['Hyl.', 'HYL', '0']
const allStudyrights = await getStudyRights(students)
const studentInfo = await getStudentInfo(students)
// Filter out current studyrights:
// Case 1: Both startdate and enddate are outside of the given interval
// Case 2: Startdate is inside of the given interval and enddate is outside
// Case 3: Startdate is before the interval start and the enddate is within the interval
const studentsWithCurrentStudyRight = allStudyrights
.filter(
right =>
moment(right.startdate).isBetween(startdate, moment()) ||
(moment(right.startdate).isSameOrBefore(startdate) && moment(right.enddate).isSameOrAfter(moment())) ||
moment(right.enddate).isSameOrBefore(moment())
)
.map(right => right.studyrightStudentnumber)
const uniqueStudentsWithCurrentStudyRight = uniq(studentsWithCurrentStudyRight)
const studentStats = {}
const getEmptyCourseInfo = () => {
return {
status: {
passed: null,
failed: null,
unfinished: null,
},
}
}
for (const { studentnumber, email, secondary_email, dissemination_info_allowed } of studentInfo) {
// Check if the student has existing studyright: if yes, then stop here
if (!uniqueStudentsWithCurrentStudyRight.includes(studentnumber)) {
if (!(studentnumber in studentStats)) {
studentStats[studentnumber] = {
courseInfo: courseCodes.reduce((acc, code) => ({ ...acc, [code]: getEmptyCourseInfo() }), {}),
email: email,
secondaryEmail: secondary_email,
disseminationInfoAllowed: dissemination_info_allowed,
totals: { passed: 0, failed: 0, unfinished: 0 },
}
}
for (const { course_code, attainment_date, student_studentnumber, grade } of allCredits) {
if (student_studentnumber === studentnumber) {
const courseCode = course_code.replace('AY', '')
if (
passedGrades.includes(grade) &&
(!studentStats[studentnumber].courseInfo[courseCode].status.passed ||
moment(studentStats[studentnumber].courseInfo[courseCode].status.passed).isBefore(attainment_date, 'day'))
) {
studentStats[studentnumber].courseInfo[courseCode].status.passed = attainment_date
} else if (
failedGrades.includes(grade) &&
!studentStats[studentnumber].courseInfo[courseCode].status.passed &&
(!studentStats[studentnumber].courseInfo[courseCode].status.failed ||
moment(studentStats[studentnumber].courseInfo[courseCode].status.failed).isBefore(attainment_date, 'day'))
) {
studentStats[studentnumber].courseInfo[courseCode].status.failed = attainment_date
}
}
}
for (const { enrollmentStudentnumber, course_code, enrollment_date_time } of allEnrollments) {
if (enrollmentStudentnumber === studentnumber) {
const courseCode = course_code.replace('AY', '')
if (
!studentStats[studentnumber].courseInfo[courseCode].status.passed &&
!studentStats[studentnumber].courseInfo[courseCode].status.failed &&
(!studentStats[studentnumber].courseInfo[courseCode].status.unfinished ||
moment(studentStats[studentnumber].courseInfo[courseCode].status.unfinished).isBefore(
enrollment_date_time,
'day'
))
) {
studentStats[studentnumber].courseInfo[courseCode].status.unfinished = enrollment_date_time
}
}
}
await calulateTotalsForStudent(studentStats, studentnumber)
}
}
const openUniStats = { students: studentStats, courses: courses }
return openUniStats
}
module.exports = { getCustomOpenUniCourses }
+200 -164
View File
@@ -1,8 +1,7 @@
const { Op } = require('sequelize')
const Sequelize = require('sequelize')
const { Op } = Sequelize
const moment = require('moment')
const { sortBy } = require('lodash')
const { sortMainCode } = require('../util/utils')
const { getCurrentSemester } = require('../services/semesters')
const { sortBy, keyBy } = require('lodash')
const {
Student,
@@ -11,11 +10,12 @@ const {
Studyright,
StudyrightExtent,
ElementDetail,
CourseType,
Studyplan,
SemesterEnrollment,
Semester,
Transfer,
StudyrightElement,
Enrollment,
} = require('../models')
const {
dbConnections: { sequelize },
@@ -24,6 +24,7 @@ const { Tag, TagStudent } = require('../models/models_kone')
const { CourseStatsCounter } = require('./course_stats_counter')
const { getPassingSemester, semesterEnd, semesterStart } = require('../util/semester')
const { getAllProgrammes } = require('./studyrights')
const { encrypt } = require('../services/encrypt')
const enrolmentDates = () => {
const query = 'SELECT DISTINCT s.dateOfUniversityEnrollment as date FROM Student s'
@@ -47,7 +48,9 @@ const formatStudentForPopulationStatistics = (
creditcount,
abbreviatedname,
email,
phone_number,
studyrights,
studyplans,
semester_enrollments,
transfers,
updatedAt,
@@ -59,10 +62,11 @@ const formatStudentForPopulationStatistics = (
tags,
option,
birthdate,
sis_person_id,
},
enrollments,
credits,
startDate,
endDate,
startDateMoment,
endDateMoment
) => {
@@ -94,11 +98,13 @@ const formatStudentForPopulationStatistics = (
studentNumber: studentnumber,
credits: creditcount || 0,
courses: credits[studentnumber] ? credits[studentnumber].map(toCourse) : [],
enrollments: enrollments[studentnumber],
name: abbreviatedname,
transfers: transfers || [],
gender_code,
gender: { gender_en, gender_fi, gender_sv },
email,
phoneNumber: phone_number,
semesterenrollments: semester_enrollments || [],
updatedAt: updatedAt || createdAt,
tags: tags || [],
@@ -106,6 +112,8 @@ const formatStudentForPopulationStatistics = (
starting: moment(started).isBetween(startDateMoment, endDateMoment, null, '[]'),
option,
birthdate,
studyplans,
sis_person_id,
}
}
@@ -141,10 +149,24 @@ const getStudentsIncludeCoursesBetween = async (studentnumbers, startDate, endDa
if (tag) studentnumbers = studentnumbersWithTag
const attainmentDateFrom = tag ? moment(startDate).year(tag.year) : startDate
const studyPlans = await Studyplan.findAll({
where: { studentnumber: studentnumbers },
attributes: ['included_courses'],
raw: true,
})
const studyPlanCourses = Array.from(new Set([...studyPlans.map(plan => plan.included_courses)].flat()))
const creditsOfStudentOther = {
attainment_date: {
[Op.between]: [attainmentDateFrom, endDate],
},
[Op.or]: [
{
attainment_date: {
[Op.between]: [attainmentDateFrom, endDate],
},
},
{
course_code: studyPlanCourses,
},
],
student_studentnumber: {
[Op.in]: studentnumbers,
},
@@ -163,7 +185,7 @@ const getStudentsIncludeCoursesBetween = async (studentnumbers, startDate, endDa
},
{
course_code: {
[Op.in]: ['375063', '339101'],
[Op.in]: ['375063', '339101'].concat(studyPlanCourses),
},
},
],
@@ -174,8 +196,9 @@ const getStudentsIncludeCoursesBetween = async (studentnumbers, startDate, endDa
: creditsOfStudentOther
if (studentnumbers.length === 0)
return { students: [], credits: [], extents: [], semesters: [], elementdetails: [], courses: [] }
const [courses, students, credits, extents, semesters, elementdetails] = await Promise.all([
return { students: [], enrollments: [], credits: [], extents: [], semesters: [], elementdetails: [], courses: [] }
const [courses, enrollments, students, credits, extents, semesters, elementdetails] = await Promise.all([
Course.findAll({
attributes: [sequelize.literal('DISTINCT ON("code") code'), 'name', 'coursetypecode'],
include: [
@@ -187,6 +210,19 @@ const getStudentsIncludeCoursesBetween = async (studentnumbers, startDate, endDa
],
raw: true,
}),
Enrollment.findAll({
attributes: ['course_code', 'state', 'enrollment_date_time', 'studentnumber', 'semestercode'],
where: {
enrollment_date_time: {
[Op.between]: [attainmentDateFrom, endDate],
},
studentnumber: {
[Op.in]: studentnumbers,
},
state: ['ENROLLED', 'CONFIRMED'],
},
raw: true,
}),
Student.findAll({
attributes: [
'firstnames',
@@ -197,9 +233,11 @@ const getStudentsIncludeCoursesBetween = async (studentnumbers, startDate, endDa
'creditcount',
'abbreviatedname',
'email',
'phone_number',
'updatedAt',
'gender_code',
'birthdate',
'sis_person_id',
/* 'gender_fi',
'gender_sv',
'gender_en' */
@@ -218,7 +256,7 @@ const getStudentsIncludeCoursesBetween = async (studentnumbers, startDate, endDa
'enddate',
'extentcode',
'graduated',
'canceldate',
'active',
'prioritycode',
'faculty_code',
'studystartdate',
@@ -251,6 +289,11 @@ const getStudentsIncludeCoursesBetween = async (studentnumbers, startDate, endDa
},
},
},
{
model: Studyplan,
attributes: ['included_courses', 'programme_code', 'completed_credits'],
separate: true,
},
],
where: {
studentnumber: {
@@ -331,7 +374,7 @@ EXISTS (SELECT 1 FROM studyright_elements WHERE studentnumber IN (:studentnumber
student.tags = studentNumberToTags[student.studentnumber] || []
})
return { students, credits, extents, semesters, elementdetails, courses }
return { students, enrollments, credits, extents, semesters, elementdetails, courses }
}
const count = (column, count, distinct = false) => {
@@ -362,7 +405,6 @@ const studentnumbersWithAllStudyrightElements = async (
startDate,
endDate,
exchangeStudents,
cancelledStudents,
nondegreeStudents,
transferredOutStudents,
tag,
@@ -374,7 +416,7 @@ const studentnumbersWithAllStudyrightElements = async (
// db startdate is formatted to utc so need to change it when querying
const formattedStartDate = new Date(moment.tz(startDate, 'Europe/Helsinki').format()).toUTCString()
const filteredExtents = [10] // always filter out secondary subject students
const filteredExtents = [16] // always filter out secondary subject students
let studyrightWhere = {
extentcode: {
[Op.notIn]: filteredExtents,
@@ -384,10 +426,7 @@ const studentnumbersWithAllStudyrightElements = async (
filteredExtents.push(7, 34)
}
if (!nondegreeStudents) {
filteredExtents.push(33, 99, 14, 13)
}
if (!cancelledStudents) {
studyrightWhere.canceldate = null
filteredExtents.push(22, 99, 14, 13)
}
let studentWhere = {}
@@ -449,44 +488,6 @@ const studentnumbersWithAllStudyrightElements = async (
let studentnumbers = [...new Set(students.map(s => s.student_studentnumber))]
// Oodi canceldates don't match SIS studyright statuses, so
// - filter out all students who haven't enrolled for semester
// - filter out all students who have enrolled for both semesters but whose studyright has ended
// - don't use above filtering to graduated students
// eslint-disable-next-line no-constant-condition
if (false && !cancelledStudents) {
const { semestercode } = await getCurrentSemester()
const enrolments = await SemesterEnrollment.findAll({
where: {
studentnumber: {
[Op.in]: studentnumbers,
},
semestercode,
enrollment_date: {
[Op.not]: null,
},
enrollmenttype: {
[Op.not]: 3,
},
},
})
const studentsByStudentnumber = students.reduce((res, curr) => {
res[curr.student_studentnumber] = curr
return res
}, {})
const today = new Date()
const graduated = [...new Set(students.filter(s => s.graduated).map(s => s.student_studentnumber))]
const enrolledStudentnumbers = enrolments
.map(e => e.studentnumber)
.filter(s => new Date(studentsByStudentnumber[s].enddate) >= today)
studentnumbers = [...new Set(graduated.concat(enrolledStudentnumbers))]
}
// bit hacky solution, but this is used to filter out studentnumbers who have since changed studytracks
const rights = await Studyright.findAll({
attributes: ['studyrightid'],
@@ -532,13 +533,17 @@ const studentnumbersWithAllStudyrightElements = async (
return acc
}, {})
// Take the newest studytrack primarily by latest starting date in the track, secondarily by the latest enddate
const filteredStudentnumbers = studentnumbers.filter(studentnumber => {
const newestStudytrack = sortBy(formattedStudytracks[studentnumber], ['enddate', 'startdate']).reverse()[0]
const newestStudytrack = sortBy(formattedStudytracks[studentnumber], ['startdate', 'enddate']).reverse()[0]
if (!newestStudytrack) return false
return studyRights.includes(newestStudytrack.code)
})
let studentnumberlist = filteredStudentnumbers.length > 0 ? filteredStudentnumbers : studentnumbers
// Use the filtered list, if the search includes studytracks
// Then the studyrights length is > 1, which means that there is [studyright, studytrack].
// When searching only for studyprogramme, there is [studyright]
let studentnumberlist = studyRights.length > 1 ? filteredStudentnumbers : studentnumbers
// fetch students that have transferred out of the programme and filter out these studentnumbers
if (!transferredOutStudents) {
@@ -631,13 +636,11 @@ const parseQueryParams = query => {
? `${moment(year, 'YYYY').add(1, 'years').format('YYYY')}-${semesterEnd[semesters.find(s => s === 'SPRING')]}`
: `${year}-${semesterEnd[semesters.find(s => s === 'FALL')]}`
const exchangeStudents = studentStatuses && studentStatuses.includes('EXCHANGE')
const cancelledStudents = studentStatuses && studentStatuses.includes('CANCELLED')
const nondegreeStudents = studentStatuses && studentStatuses.includes('NONDEGREE')
const transferredStudents = studentStatuses && studentStatuses.includes('TRANSFERRED')
return {
exchangeStudents,
cancelledStudents,
nondegreeStudents,
transferredStudents,
// if someone passes something falsy like null as the studyright, remove it so it doesn't break
@@ -742,7 +745,7 @@ const getOptionsForStudents = async (students, code, level) => {
}
const formatStudentsForApi = async (
{ students, credits, extents, semesters, elementdetails, courses },
{ students, enrollments, credits, extents, semesters, elementdetails, courses },
startDate,
endDate,
{ studyRights },
@@ -763,6 +766,11 @@ const formatStudentsForApi = async (
acc[e.student_studentnumber].push(e)
return acc
}, {})
enrollments = enrollments.reduce((acc, e) => {
acc[e.studentnumber] = acc[e.studentnumber] || []
acc[e.studentnumber].push(e)
return acc
}, {})
const result = students.reduce(
(stats, student) => {
student.transfers.forEach(transfer => {
@@ -776,7 +784,7 @@ const formatStudentsForApi = async (
if (student.studentnumber in optionData) student.option = optionData[student.studentnumber]
else student.option = null
stats.students.push(
formatStudentForPopulationStatistics(student, credits, startDate, endDate, startDateMoment, endDateMoment)
formatStudentForPopulationStatistics(student, enrollments, credits, startDate, startDateMoment, endDateMoment)
)
return stats
},
@@ -839,22 +847,13 @@ const optimizedStatisticsOf = async (query, studentnumberlist) => {
if (
formattedQueryParams.studentStatuses &&
!formattedQueryParams.studentStatuses.every(
status => status === 'CANCELLED' || status === 'EXCHANGE' || status === 'NONDEGREE' || status === 'TRANSFERRED'
status => status === 'EXCHANGE' || status === 'NONDEGREE' || status === 'TRANSFERRED'
)
) {
return { error: 'Student status should be either CANCELLED or EXCHANGE or NONDEGREE or TRANSFERRED' }
return { error: 'Student status should be either EXCHANGE or NONDEGREE or TRANSFERRED' }
}
const {
studyRights,
startDate,
months,
endDate,
exchangeStudents,
cancelledStudents,
nondegreeStudents,
transferredStudents,
tag,
} = parseQueryParams(formattedQueryParams)
const { studyRights, startDate, months, endDate, exchangeStudents, nondegreeStudents, transferredStudents, tag } =
parseQueryParams(formattedQueryParams)
// db startdate is formatted to utc so need to change it when querying
const formattedStartDate = new Date(moment.tz(startDate, 'Europe/Helsinki').format()).toUTCString()
@@ -866,7 +865,6 @@ const optimizedStatisticsOf = async (query, studentnumberlist) => {
formattedStartDate,
endDate,
exchangeStudents,
cancelledStudents,
nondegreeStudents,
transferredStudents
)
@@ -880,48 +878,89 @@ const optimizedStatisticsOf = async (query, studentnumberlist) => {
optionData = await getOptionsForStudents(studentnumbers, code, 'BSC')
}
const { students, credits, extents, semesters, elementdetails, courses } = await getStudentsIncludeCoursesBetween(
studentnumbers,
startDate,
dateMonthsFromNow(startDate, months),
studyRights,
tag
)
const { students, enrollments, credits, extents, semesters, elementdetails, courses } =
await getStudentsIncludeCoursesBetween(
studentnumbers,
startDate,
dateMonthsFromNow(startDate, months),
studyRights,
tag
)
const formattedStudents = await formatStudentsForApi(
{ students, credits, extents, semesters, elementdetails, courses },
{ students, enrollments, credits, extents, semesters, elementdetails, courses },
startDate,
endDate,
formattedQueryParams,
optionData
)
return formattedStudents
}
const findCourses = async (studentnumbers, beforeDate) => {
const res = await Course.findAll({
attributes: ['code', 'name', 'coursetypecode', 'substitutions'],
include: [
{
required: true,
model: Credit,
attributes: ['grade', 'student_studentnumber', 'credittypecode', 'attainment_date', 'course_code'],
where: {
student_studentnumber: {
[Op.in]: studentnumbers,
},
attainment_date: {
[Op.lt]: beforeDate,
},
},
const getSubstitutions = async codes => {
const courses = await Course.findAll({ where: { code: codes }, attributes: ['code', 'substitutions'], raw: true })
return [...new Set(courses.map(({ code, substitutions }) => [code, ...substitutions]).flat())]
}
const findCourses = async (studentnumbers, beforeDate, courses = [], studentCountLimit = 0) => {
const courseCodes = courses.length > 0 ? await getSubstitutions(courses) : ['DUMMY']
const res = await sequelize.query(
`
SELECT DISTINCT ON (course.id)
course.code,
course.name,
course.coursetypecode,
course.substitutions,
course.main_course_code,
course_types.name AS course_type,
credit.data AS credits,
enrollment.data AS enrollments
FROM course
INNER JOIN (
SELECT
course_code,
ARRAY_AGG(JSON_BUILD_OBJECT(
'grade', grade,
'student_studentnumber', student_studentnumber,
'attainment_date', attainment_date,
'credittypecode', credittypecode,
'course_code', course_code
)) AS data
FROM credit
WHERE student_studentnumber IN (:studentnumbers) AND attainment_date < :beforeDate
GROUP BY credit.course_code
HAVING COUNT(DISTINCT credit.student_studentnumber) >= :studentCountLimit
) credit ON credit.course_code = course.code
INNER JOIN course_types ON course_types.coursetypecode = course.coursetypecode
LEFT JOIN (
SELECT
course_id,
ARRAY_AGG(JSON_BUILD_OBJECT(
'studentnumber', studentnumber,
'state', state,
'enrollment_date_time', enrollment_date_time
)) AS data
FROM enrollment
WHERE enrollment.studentnumber IN (:studentnumbers) AND enrollment.enrollment_date_time < :beforeDate
GROUP BY enrollment.course_id
) enrollment ON enrollment.course_id = course.id
WHERE :skipCourseCodeFilter OR course.code IN (:courseCodes)
-- GROUP BY 1, 2, 3, 4, 5, 6
`,
{
replacements: {
studentnumbers: studentnumbers.length > 0 ? studentnumbers : ['DUMMY'],
beforeDate,
studentCountLimit,
courseCodes,
skipCourseCodeFilter: courses.length === 0,
},
{
attributes: ['coursetypecode', 'name'],
model: CourseType,
required: true,
},
],
})
type: sequelize.QueryTypes.SELECT,
}
)
return res
}
@@ -937,37 +976,9 @@ const parseCreditInfo = credit => ({
date: credit.attainment_date,
})
const findMainCourse = async code => {
const course = await Course.findOne({
where: { code: code },
})
let result = []
if (course.substitutions) {
const sortedCourses = sortMainCode([...course.substitutions, code])
result = await Course.findOne({
attributes: ['code', 'name'],
where: { code: sortedCourses[0] },
})
} else {
return course
}
return result
}
const bottlenecksOf = async (query, studentnumberlist) => {
const bottlenecksOf = async (query, studentnumberlist, encryptdata = false) => {
const isValidRequest = async (query, params) => {
const {
studyRights,
startDate,
endDate,
exchangeStudents,
cancelledStudents,
nondegreeStudents,
transferredStudents,
} = params
const { studyRights, startDate, endDate, exchangeStudents, nondegreeStudents, transferredStudents } = params
if (!query.semesters.every(semester => semester === 'FALL' || semester === 'SPRING')) {
return { error: 'Semester should be either SPRING OR FALL' }
@@ -975,10 +986,10 @@ const bottlenecksOf = async (query, studentnumberlist) => {
if (
query.studentStatuses &&
!query.studentStatuses.every(
status => status === 'CANCELLED' || status === 'EXCHANGE' || status === 'NONDEGREE' || status === 'TRANSFERRED'
status => status === 'EXCHANGE' || status === 'NONDEGREE' || status === 'TRANSFERRED'
)
) {
return { error: 'Student status should be either CANCELLED or EXCHANGE or NONDEGREE or TRANSFERRED' }
return { error: 'Student status should be either EXCHANGE or NONDEGREE or TRANSFERRED' }
}
if (query.selectedStudents) {
const allStudents = await studentnumbersWithAllStudyrightElements(
@@ -986,7 +997,6 @@ const bottlenecksOf = async (query, studentnumberlist) => {
startDate,
endDate,
exchangeStudents,
cancelledStudents,
nondegreeStudents,
transferredStudents,
query.tag
@@ -1000,19 +1010,10 @@ const bottlenecksOf = async (query, studentnumberlist) => {
return null
}
const getStudentsAndCourses = async (selectedStudents, studentnumberlist) => {
const getStudentsAndCourses = async (selectedStudents, studentnumberlist, courseCodes) => {
if (!studentnumberlist) {
const {
months,
studyRights,
startDate,
endDate,
exchangeStudents,
cancelledStudents,
nondegreeStudents,
transferredStudents,
tag,
} = params
const { months, studyRights, startDate, endDate, exchangeStudents, nondegreeStudents, transferredStudents, tag } =
params
const studentnumbers =
selectedStudents ||
(await studentnumbersWithAllStudyrightElements(
@@ -1020,7 +1021,6 @@ const bottlenecksOf = async (query, studentnumberlist) => {
startDate,
endDate,
exchangeStudents,
cancelledStudents,
nondegreeStudents,
transferredStudents,
tag
@@ -1031,20 +1031,44 @@ const bottlenecksOf = async (query, studentnumberlist) => {
return numbers
}, {})
const courses = await findCourses(studentnumbers, dateMonthsFromNow(startDate, months))
const courses = await findCourses(
studentnumbers,
dateMonthsFromNow(startDate, months),
courseCodes,
query.studentCountLimit
)
return [allstudents, courses]
} else {
const { months, startDate } = params
const beforeDate = months && startDate ? dateMonthsFromNow(startDate, months) : new Date()
const allstudents = studentnumberlist.reduce((numbers, num) => {
numbers[num] = true
return numbers
}, {})
const courses = await findCourses(studentnumberlist, new Date())
const courses = await findCourses(studentnumberlist, beforeDate, courseCodes, query.studentCountLimit)
return [allstudents, courses]
}
}
const encryptStudentnumbers = bottlenecks => {
for (const course in bottlenecks.coursestatistics) {
const encryptedStudentStats = {}
for (const data in bottlenecks.coursestatistics[course].students) {
encryptedStudentStats[data] = {}
const studentnumbers = Object.keys(bottlenecks.coursestatistics[course].students[data])
studentnumbers.forEach(studentnumber => {
encryptedStudentStats[data][encrypt(studentnumber).encryptedData] =
bottlenecks.coursestatistics[course].students[data][studentnumber]
})
}
bottlenecks.coursestatistics[course].students = encryptedStudentStats
}
}
const params = parseQueryParams(query)
const [[allstudents, courses], error] = await Promise.all([
getStudentsAndCourses(query.selectedStudents, studentnumberlist),
getStudentsAndCourses(query.selectedStudents, studentnumberlist, query.courses),
isValidRequest(query, params),
])
if (error) return error
@@ -1058,17 +1082,19 @@ const bottlenecksOf = async (query, studentnumberlist) => {
const startYear = parseInt(query.year, 10)
const allstudentslength = Object.keys(allstudents).length
const coursesByCode = keyBy(courses, 'code')
for (const course of courses) {
let { course_type } = course
let maincourse = course
const sortedMaincourse = await findMainCourse(course.code)
if (sortedMaincourse) maincourse = sortedMaincourse
/* if (isOpenUniCourseCode) {
const nonOpen = unifyOpenUniversity(course.code)
const response = await findOneByCode(nonOpen)
*/
if (course.main_course_code && course.main_course_code !== course.code) {
let newmain = coursesByCode[course.main_course_code]
if (newmain) {
maincourse = newmain
}
}
if (!stats[maincourse.code]) {
stats[maincourse.code] = new CourseStatsCounter(maincourse.code, maincourse.name, allstudentslength)
@@ -1085,12 +1111,22 @@ const bottlenecksOf = async (query, studentnumberlist) => {
const semester = getPassingSemester(startYear, date)
coursestats.markCredit(studentnumber, grade, passingGrade, failingGrade, improvedGrade, semester)
})
if (course.enrollments) {
course.enrollments.forEach(({ studentnumber, state, enrollment_date_time }) => {
const semester = getPassingSemester(startYear, enrollment_date_time)
coursestats.markEnrollment(studentnumber, state, semester)
})
}
stats[maincourse.code] = coursestats
}
bottlenecks.coursestatistics = Object.values(stats).map(coursestatistics => coursestatistics.getFinalStats())
bottlenecks.allStudents = allstudentslength
if (encryptdata) encryptStudentnumbers(bottlenecks)
return bottlenecks
}
@@ -38,8 +38,14 @@ const byProgrammeCode = async code => {
},
})
// filter out possible duplicates
const filtered = result.filter(
(course, index, array) =>
array.findIndex(c => c.id == course.id && c.code == course.code && c.label_code == course.label_code) == index
)
// this labels the modules to match the old system in frontend
const labeled = result.map(module => {
const labeled = filtered.map(module => {
const label = {
id: module.label_name.fi,
label: `${module.label_code}\n${module.label_name.fi}`,
+1 -1
View File
@@ -26,7 +26,7 @@ const populationCourseStatsMerger = multiyearstats => {
stats.coursestatistics[index].stats.retryPassed += c.stats.retryPassed
stats.coursestatistics[index].stats.perStudent =
(stats.coursestatistics[index].stats.attempts / stats.coursestatistics[index].stats.students) * 100
stats.coursestatistics[index].stats.attempts / stats.coursestatistics[index].stats.students
stats.coursestatistics[index].stats.percentage =
(stats.coursestatistics[index].stats.passed / stats.coursestatistics[index].stats.students) * 100
+87 -21
View File
@@ -12,6 +12,7 @@ const {
ElementDetail,
SemesterEnrollment,
Semester,
Studyplan,
} = require('../models')
const { TagStudent, Tag } = require('../models/models_kone')
const Op = Sequelize.Op
@@ -57,6 +58,10 @@ const byId = async id => {
{
model: SemesterEnrollment,
},
{
model: Studyplan,
attributes: ['included_courses', 'programme_code', 'completed_credits'],
},
],
}),
TagStudent.findAll({
@@ -99,33 +104,79 @@ const byId = async id => {
return student
}
const findByCourseAndSemesters = async (coursecodes, from, to, separate) =>
(
const getUnifyStatus = unifyCourses => {
switch (unifyCourses) {
case 'unifyStats':
return [true, false]
case 'openStats':
return [true]
case 'reqularStats':
return [false]
default:
return [true, false]
}
}
const findByCourseAndSemesters = async (coursecodes, from, to, separate, unifyCourses = 'unifyStats') => {
const { startdate, semestercode: fromSemester } = await Semester.findOne({
where: {
[separate ? 'semestercode' : 'yearcode']: from,
},
order: ['semestercode'],
limit: 1,
raw: true,
})
const { enddate, semestercode: toSemester } = await Semester.findOne({
where: {
[separate ? 'semestercode' : 'yearcode']: to,
},
order: [['semestercode', 'DESC']],
limit: 1,
raw: true,
})
return (
await sequelize.query(
`
SELECT
studentnumber
FROM student
INNER JOIN credit ON
student.studentnumber=credit.student_studentnumber
FROM
student s
WHERE
course_code IN (:coursecodes) AND
attainment_date
BETWEEN
(select startdate FROM semesters where ${
separate ? 'semestercode' : 'yearcode'
}=:minYearCode ORDER BY semestercode LIMIT 1) AND
(select enddate FROM semesters where ${
separate ? 'semestercode' : 'yearcode'
}=:maxYearCode ORDER BY semestercode DESC LIMIT 1);
EXISTS (
SELECT
1
FROM
credit c
WHERE
c.student_studentnumber = s.studentnumber
AND c.course_code IN (:coursecodes)
AND c.is_open IN (:isOpen)
AND c.attainment_date BETWEEN '${startdate.toISOString()}' AND '${enddate.toISOString()}'
)
OR EXISTS (
SELECT
1
FROM
enrollment e
WHERE
e.studentnumber = s.studentnumber
AND e.course_code IN (:coursecodes)
AND e.semestercode BETWEEN ${fromSemester} AND ${toSemester}
AND e.enrollment_date_time >= '2021-05-31'
AND e.state IN ('ENROLLED', 'CONFIRMED')
);
`,
{
replacements: { coursecodes, minYearCode: from, maxYearCode: to },
replacements: { coursecodes, minYearCode: from, maxYearCode: to, isOpen: getUnifyStatus(unifyCourses) },
type: sequelize.QueryTypes.SELECT,
raw: true,
}
)
).map(st => st.studentnumber)
}
const findByTag = async tag => {
return (
@@ -140,7 +191,7 @@ const findByTag = async tag => {
).map(st => st.studentnumber)
}
const formatStudent = ({
const formatStudent = async ({
firstnames,
lastname,
studentnumber,
@@ -153,11 +204,13 @@ const formatStudent = ({
studyrights,
semester_enrollments,
transfers,
studyplans,
updatedAt,
createdAt,
tags,
sis_person_id,
}) => {
const toCourse = ({ id, grade, credits, credittypecode, attainment_date, course, isStudyModule }) => {
const toCourse = ({ id, grade, credits, credittypecode, is_open, attainment_date, course, isStudyModule }) => {
try {
course = course.get()
} catch (e) {
@@ -183,18 +236,21 @@ const formatStudent = ({
credits,
credittypecode,
isStudyModuleCredit: isStudyModule,
isOpenCourse: is_open,
}
}
studyrights = studyrights || []
semester_enrollments = semester_enrollments || []
studyplans = studyplans || []
const semesterenrollments = semester_enrollments.map(
({ semestercode, enrollmenttype, name, yearname, startYear }) => ({
({ semestercode, enrollmenttype, name, yearname, startYear, statutory_absence: statutoryAbsence }) => ({
yearname,
name,
startYear,
semestercode,
enrollmenttype,
statutoryAbsence,
})
)
@@ -211,6 +267,13 @@ const formatStudent = ({
.map(toCourse)
.filter(c => c.course.name !== 'missing')
const hopsCourses = studyplans.map(sp => sp.included_courses).flat()
const allCourses = await sequelize.query(
'SELECT DISTINCT ON(course_code) course_code, credits FROM credit WHERE course_code IN (:hopsCourses)',
{ replacements: { hopsCourses: hopsCourses.concat('id-to-fill-array') } }
)
return {
firstnames,
lastname,
@@ -225,14 +288,17 @@ const formatStudent = ({
email,
semesterenrollments,
updatedAt: updatedAt || createdAt,
studyplans,
allCourses: allCourses[0],
tags,
sis_person_id: sis_person_id,
}
}
const withId = async id => {
try {
const result = await byId(id)
return formatStudent(result)
return await formatStudent(result)
} catch (e) {
logger.error({ message: 'error when fetching single student', meta: e })
return {
@@ -285,7 +351,7 @@ const bySearchTerm = async searchterm => {
[Op.or]: [nameLike(terms), studentnumberLike(terms)],
},
})
return matches.map(formatStudent)
return await Promise.all(matches.map(async s => await formatStudent(s)))
}
const bySearchTermAndStudentNumbers = async (searchterm, studentnumbers) => {
@@ -300,7 +366,7 @@ const bySearchTermAndStudentNumbers = async (searchterm, studentnumbers) => {
},
},
})
return matches.map(formatStudent)
return await Promise.all(matches.map(async s => await formatStudent(s)))
}
const filterStudentnumbersByAccessrights = async (studentnumbers, codes) =>
+336 -17
View File
@@ -1,5 +1,10 @@
const sequelize = require('sequelize')
const { Op } = sequelize
const _ = require('lodash')
const Sequelize = require('sequelize')
const {
dbConnections: { sequelize },
} = require('../database/connection')
const moment = require('moment')
const { Op } = Sequelize
const {
Credit,
Course,
@@ -26,6 +31,17 @@ const sinceDate = since => {
const studytrackStudents = async studentnumbers =>
(
await Student.findAll({
include: {
model: Credit,
separate: true,
attributes: ['credits', 'attainment_date'],
where: {
[Op.or]: [{ isStudyModule: null }, { isStudyModule: false }],
credittypecode: {
[Op.in]: [4, 9],
},
},
},
where: {
studentnumber: {
[Op.in]: studentnumbers,
@@ -203,6 +219,40 @@ const graduatedStudyRights = async (studytrack, since, studentnumbers) =>
})
).map(formatStudyright)
const inactiveStudyrights = async (studytrack, studentnumbers) => {
const now = moment(new Date())
const inactiveOrExpired = (
await Studyright.findAll({
include: [
{
model: StudyrightElement,
required: true,
include: {
model: ElementDetail,
required: true,
where: {
code: studytrack,
},
},
},
{
model: Student,
attributes: ['studentnumber'],
required: true,
},
],
where: {
student_studentnumber: whereStudents(studentnumbers),
graduated: 0,
},
})
)
.filter(s => s.active === 0 || (s.enddate && moment(s.enddate).isBefore(now)))
.map(formatStudyright)
return inactiveOrExpired
}
const followingStudyrights = async (since, programmes, studentnumbers) =>
(
await Studyright.findAll({
@@ -218,7 +268,7 @@ const followingStudyrights = async (since, programmes, studentnumbers) =>
include: [
{
model: ElementDetail,
attributes: ['name'],
attributes: ['name', 'code', 'type'],
},
],
attributes: ['code'],
@@ -254,7 +304,7 @@ const previousStudyrights = async (programmes, studentnumbers) =>
include: [
{
model: ElementDetail,
attributes: ['name'],
attributes: ['name', 'code', 'type'],
},
],
attributes: ['code'],
@@ -282,8 +332,6 @@ const transfersAway = async (studytrack, since) =>
},
sourcecode: studytrack,
},
distinct: true,
col: 'studentnumber',
})
).map(formatTransfer)
@@ -296,8 +344,6 @@ const transfersTo = async (studytrack, since) =>
},
targetcode: studytrack,
},
distinct: true,
col: 'studentnumber',
})
).map(formatTransfer)
@@ -330,8 +376,34 @@ const getProgrammesStudyrights = async studyprogramme =>
})
).map(formatStudyright)
const getCreditsForStudyProgramme = async (provider, since) =>
await Credit.findAll({
const getStudyRights = async students =>
(
await Studyright.findAll({
attributes: ['studyrightid', 'studystartdate', 'enddate', 'graduated', 'prioritycode', 'extentcode'],
where: {
studentStudentnumber: students,
},
include: [
{
model: StudyrightElement,
include: {
model: ElementDetail,
where: {
type: 20,
},
},
},
{
model: Student,
attributes: ['studentnumber'],
required: true,
},
],
})
).map(formatStudyright)
const getCreditsForStudyProgramme = async (codes, since) => {
const res = await Credit.findAll({
attributes: ['id', 'course_code', 'credits', 'attainment_date', 'student_studentnumber'],
include: {
model: Course,
@@ -339,13 +411,7 @@ const getCreditsForStudyProgramme = async (provider, since) =>
required: true,
where: {
is_study_module: false,
},
include: {
model: Organization,
required: true,
where: {
code: provider,
},
code: codes,
},
},
where: {
@@ -360,6 +426,232 @@ const getCreditsForStudyProgramme = async (provider, since) =>
},
},
})
return res
}
const getCourseCodesForStudyProgramme = async provider => {
const coursesByProvider = await Course.findAll({
attributes: ['code', 'substitutions'],
where: {
is_study_module: false,
},
include: {
model: Organization,
required: true,
where: {
code: provider,
},
},
})
const coursesWithOpenUniSubstitutions = coursesByProvider.map(({ code, substitutions }) => {
if (!substitutions || !substitutions.length) return [code]
const alternatives = [`AY-${code}`, `AY${code}`, `A-${code}`]
return [code].concat(substitutions.filter(sub => alternatives.includes(sub)))
})
return coursesWithOpenUniSubstitutions.flat()
}
const getAllProgrammeCourses = async providerCode => {
const res = await Course.findAll({
attributes: ['code', 'substitutions'],
include: [
{
model: Organization,
attributes: [],
required: true,
where: {
code: providerCode,
},
through: {
attributes: [],
},
},
],
raw: true,
})
return res
}
const getStudentsForProgrammeCourses = async (from, to, programmeCourses) => {
try {
const res = await sequelize.query(
`
WITH Dist AS (
SELECT DISTINCT cr.student_studentnumber AS student, cr.credits AS credits,
co.code AS code, co.name AS course_name FROM credit cr
INNER JOIN course co ON cr.course_code = co.code
WHERE cr.attainment_date BETWEEN :from AND :to
AND cr.course_code IN (:programmeCourses)
AND (cr."isStudyModule" = false OR cr."isStudyModule" IS NULL)
AND cr.credittypecode = 4
)
SELECT COUNT(student) AS total_students, SUM(credits) AS total_credits, code, course_name
FROM Dist
GROUP BY dist.code, course_name;
`,
{
type: sequelize.QueryTypes.SELECT,
replacements: { from, to, programmeCourses },
}
)
return res.map(course => ({
code: course.code,
name: course.course_name,
year: course.year,
totalAllStudents: parseInt(course.total_students),
totalAllcredits: parseInt(course.total_credits),
type: 'total',
}))
} catch (e) {
// eslint-disable-next-line no-console
console.log(e)
}
}
const getOwnStudentsForProgrammeCourses = async (from, to, programmeCourses, studyprogramme) => {
const res = await sequelize.query(
`
WITH Dist AS (
SELECT DISTINCT cr.student_studentnumber AS student, cr.credits AS credits,
co.code AS code, co.name AS course_name FROM credit cr
INNER JOIN studyright_elements se ON se.studentnumber = cr.student_studentnumber
INNER JOIN course co ON cr.course_code = co.code
INNER JOIN course_providers cp ON cp.coursecode = co.id
INNER JOIN organization o ON o.id = cp.organizationcode
WHERE cr.attainment_date BETWEEN :from AND :to
AND cr.course_code IN (:programmeCourses)
AND (cr."isStudyModule" = false OR cr."isStudyModule" IS NULL)
AND cr.credittypecode = 4
AND (se.code = :studyprogramme AND cr.attainment_date BETWEEN se.startdate AND se.enddate)
)
SELECT COUNT(student) AS total_students, SUM(credits) AS total_credits, code, course_name
FROM Dist
GROUP BY dist.code, course_name;
`,
{
type: sequelize.QueryTypes.SELECT,
replacements: { from, to, programmeCourses, studyprogramme },
}
)
return res.map(course => {
// console.log('course: ', course)
const res = {
code: course.code,
name: course.course_name,
totalProgrammeStudents: parseInt(course.total_students),
totalProgrammeCredits: parseInt(course.total_credits),
type: 'ownProgramme',
}
return res
})
}
const getOtherStudentsForProgrammeCourses = async (from, to, programmeCourses, studyprogramme) => {
const res = await sequelize.query(
`
WITH Dist AS (
SELECT DISTINCT cr.student_studentnumber AS student, cr.credits AS credits,
co.code AS code, co.name AS course_name FROM credit cr
INNER JOIN studyright_elements se ON se.studentnumber = cr.student_studentnumber
INNER JOIN course co ON cr.course_code = co.code
WHERE cr.attainment_date BETWEEN :from AND :to
AND cr.course_code IN (:programmeCourses)
AND (cr."isStudyModule" = false OR cr."isStudyModule" IS NULL)
AND cr.credittypecode = 4
AND cr.student_studentnumber IN
(
SELECT student_studentnumber FROM studyright_elements
WHERE studyright_elements.studentnumber = cr.student_studentnumber
AND (studyright_elements.code != :studyprogramme AND cr.attainment_date BETWEEN studyright_elements.startdate AND studyright_elements.enddate)
AND cr.student_studentnumber NOT IN
(
SELECT studentnumber FROM studyright_elements
WHERE studyright_elements.studentnumber = cr.student_studentnumber
AND (studyright_elements.code = :studyprogramme AND cr.attainment_date BETWEEN studyright_elements.startdate AND studyright_elements.enddate))
)
)
SELECT COUNT(student) AS total_students, SUM(credits) AS total_credits, code, course_name
FROM Dist
GROUP BY dist.code, course_name;
`,
{
type: sequelize.QueryTypes.SELECT,
replacements: { from, to, programmeCourses, studyprogramme },
}
)
return res.map(course => ({
code: course.code,
name: course.course_name,
totalOtherProgrammeStudents: parseInt(course.total_students),
totalOtherProgrammeCredits: parseInt(course.total_credits),
type: 'otherProgramme',
}))
}
const getTransferStudentsForProgrammeCourses = async (from, to, programmeCourses) => {
const res = await sequelize.query(
`
WITH Dist AS (
SELECT DISTINCT cr.student_studentnumber AS student, cr.credits AS credits,
co.code AS code, co.name AS course_name FROM credit cr
INNER JOIN course co ON cr.course_code = co.code
WHERE cr.attainment_date BETWEEN :from AND :to
AND (cr."isStudyModule" = false OR cr."isStudyModule" IS NULL)
AND cr.course_code IN (:programmeCourses)
AND cr.credittypecode = 9
)
SELECT COUNT(student) AS total_students, SUM(credits) AS total_credits, code, course_name
FROM Dist
GROUP BY dist.code, course_name;
`,
{
type: sequelize.QueryTypes.SELECT,
replacements: { from, to, programmeCourses },
}
)
return res.map(course => ({
code: course.code,
name: course.course_name,
totalTransferStudents: parseInt(course.total_students),
totalTransferCredits: parseInt(course.total_credits),
type: 'transfer',
}))
}
const getStudentsWithoutStudyrightForProgrammeCourses = async (from, to, programmeCourses) => {
const res = await sequelize.query(
`
WITH Dist AS (
SELECT DISTINCT cr.student_studentnumber AS student, cr.credits AS credits,
co.code AS code, co.name AS course_name FROM credit cr
INNER JOIN course co ON cr.course_code = co.code
WHERE cr.attainment_date BETWEEN :from AND :to
AND (cr."isStudyModule" = false OR cr."isStudyModule" IS NULL)
AND cr.course_code IN (:programmeCourses)
AND cr.credittypecode = 4
AND cr.student_studentnumber NOT IN
(
SELECT student_studentnumber FROM studyright_elements
WHERE studyright_elements.studentnumber = cr.student_studentnumber
AND cr.attainment_date BETWEEN studyright_elements.startdate AND studyright_elements.enddate)
)
SELECT COUNT(student) AS total_students, SUM(credits) AS total_credits, code, course_name
FROM Dist
GROUP BY dist.code, course_name;
`,
{
type: sequelize.QueryTypes.SELECT,
replacements: { from, to, programmeCourses },
}
)
return res.map(course => ({
code: course.code,
name: course.course_name,
totalWithoutStudyrightStudents: parseInt(course.total_students),
totalWithoutStudyrightCredits: parseInt(course.total_credits),
type: 'noStudyright',
}))
}
const getTransferredCredits = async (provider, since) =>
await Credit.findAll({
@@ -423,6 +715,23 @@ const getThesisCredits = async (provider, since, thesisType, studentnumbers) =>
},
})
const getCurrentStudyYearStartDate = _.memoize(
async unixMillis =>
new Date(
(
await sequelize.query(
`
SELECT startdate FROM SEMESTERS s WHERE yearcode = (SELECT yearcode FROM SEMESTERS WHERE startdate < :a ORDER BY startdate DESC LIMIT 1) ORDER BY startdate LIMIT 1;
`,
{
type: sequelize.QueryTypes.SELECT,
replacements: { a: new Date(unixMillis) },
}
)
)[0].startdate
)
)
module.exports = {
studytrackStudents,
enrolledStudents,
@@ -430,6 +739,7 @@ module.exports = {
allStudyrights,
startedStudyrights,
graduatedStudyRights,
inactiveStudyrights,
previousStudyrights,
followingStudyrights,
transfersAway,
@@ -439,4 +749,13 @@ module.exports = {
getCreditsForStudyProgramme,
getTransferredCredits,
getThesisCredits,
getStudentsForProgrammeCourses,
getCurrentStudyYearStartDate,
getOwnStudentsForProgrammeCourses,
getStudentsWithoutStudyrightForProgrammeCourses,
getOtherStudentsForProgrammeCourses,
getAllProgrammeCourses,
getStudyRights,
getCourseCodesForStudyProgramme,
getTransferStudentsForProgrammeCourses,
}
@@ -6,14 +6,10 @@ const {
getYearsArray,
getStartDate,
tableTitles,
alltimeStartDate,
alltimeEndDate,
} = require('./studyprogrammeHelpers')
const {
allStudyrights,
graduatedStudyRights,
startedStudyrights,
transfersAway,
transfersTo,
} = require('./studyprogramme')
const { graduatedStudyRights, startedStudyrights, transfersAway, transfersTo } = require('./studyprogramme')
const { getYearStartAndEndDates } = require('../util/semester')
const getStartedStats = async ({ studyprogramme, since, years, isAcademicYear, includeAllSpecials }) => {
@@ -43,36 +39,23 @@ const getStartedStats = async ({ studyprogramme, since, years, isAcademicYear, i
const getGraduatedStats = async ({ studyprogramme, since, years, isAcademicYear, includeAllSpecials }) => {
const { graphStats, tableStats } = getStatsBasis(years)
await Promise.all(
years.map(async year => {
const { startDate, endDate } = getYearStartAndEndDates(year, isAcademicYear)
const studentnumbersOfTheYear = await getCorrectStudentnumbers({
codes: [studyprogramme],
startDate,
endDate,
includeAllSpecials,
})
const studentnumbersOfTheYear = await getCorrectStudentnumbers({
codes: [studyprogramme],
startDate: alltimeStartDate,
endDate: alltimeEndDate,
includeAllSpecials,
})
const studyrights = await graduatedStudyRights(studyprogramme, since, studentnumbersOfTheYear)
studyrights.forEach(({ enddate }) => {
const graduationYear = defineYear(enddate, isAcademicYear)
graphStats[indexOf(years, graduationYear)] += 1
tableStats[graduationYear] += 1
})
})
)
return { graphStats, tableStats }
}
const getCancelledStats = async ({ studyprogramme, years, isAcademicYear }) => {
const studyrights = await allStudyrights(studyprogramme)
const { graphStats, tableStats } = getStatsBasis(years)
studyrights.forEach(({ canceldate }) => {
if (canceldate !== null && canceldate !== undefined) {
const cancelYear = defineYear(canceldate, isAcademicYear)
graphStats[indexOf(years, cancelYear)] += 1
tableStats[cancelYear] += 1
const studyrights = await graduatedStudyRights(studyprogramme, since, studentnumbersOfTheYear)
studyrights.forEach(({ enddate, studyrightElements }) => {
// Check that the study right element ending to graduation belong to study programme
const elements = studyrightElements.filter(
sre => new Date(sre.enddate).toDateString() === new Date(enddate).toDateString()
)
if (elements.length) {
const graduationYear = defineYear(enddate, isAcademicYear)
graphStats[indexOf(years, graduationYear)] += 1
tableStats[graduationYear] += 1
}
})
@@ -113,7 +96,6 @@ const getBasicStatsForStudytrack = async ({ studyprogramme, settings }) => {
const queryParameters = { studyprogramme, since, years, isAcademicYear, includeAllSpecials }
const started = await getStartedStats(queryParameters)
const graduated = await getGraduatedStats(queryParameters)
const cancelled = await getCancelledStats(queryParameters)
const transferredAway = await getTransferredAwayStats(queryParameters)
const transferredTo = await getTransferredToStats(queryParameters)
@@ -125,7 +107,6 @@ const getBasicStatsForStudytrack = async ({ studyprogramme, settings }) => {
year,
started.tableStats[year],
graduated.tableStats[year],
cancelled.tableStats[year],
transferredAway.tableStats[year],
transferredTo.tableStats[year],
]
@@ -145,10 +126,6 @@ const getBasicStatsForStudytrack = async ({ studyprogramme, settings }) => {
name: 'Graduated',
data: graduated.graphStats,
},
{
name: 'Cancelled',
data: cancelled.graphStats,
},
{
name: 'Transferred away',
data: transferredAway.graphStats,
@@ -174,5 +151,6 @@ const getBasicStatsForStudytrack = async ({ studyprogramme, settings }) => {
}
module.exports = {
getGraduatedStats,
getBasicStatsForStudytrack,
}
@@ -0,0 +1,231 @@
const _ = require('lodash')
const {
getStudentsForProgrammeCourses,
getCurrentStudyYearStartDate,
getOwnStudentsForProgrammeCourses,
getStudentsWithoutStudyrightForProgrammeCourses,
getTransferStudentsForProgrammeCourses,
getOtherStudentsForProgrammeCourses,
getAllProgrammeCourses,
} = require('./studyprogramme')
const { mapToProviders } = require('../util/utils')
const getCurrentYearStartDate = () => {
return new Date(new Date().getFullYear(), 0, 1)
}
const isOpenUniCourseCode = code => code.match(/^AY?(.+?)(?:en|fi|sv)?$/)
const getAllStudyprogrammeCourses = async studyprogramme => {
const providerCode = mapToProviders([studyprogramme])[0]
const normalCourses = await getAllProgrammeCourses(providerCode)
return normalCourses.reduce((acc, curr) => {
acc.push(curr.code)
if (curr.substitutions && curr.substitutions.includes('AY' + curr.code)) {
acc.push('AY' + curr.code)
}
return acc
}, [])
}
const makeYearlyPromises = (years, academicYear, type, programmeCourses, studyprogramme) => {
return years.map(
year =>
new Promise(async res => {
const from = academicYear == 'true' ? new Date(year, 7, 1, 0, 0, 0) : new Date(year, 0, 1, 0, 0, 0)
const to = academicYear == 'true' ? new Date(year + 1, 6, 31, 23, 59, 59) : new Date(year, 11, 31, 23, 59, 59)
let result = null
switch (type) {
case 'allStudents':
result = await getStudentsForProgrammeCourses(from, to, programmeCourses)
break
case 'ownStudents':
result = await getOwnStudentsForProgrammeCourses(from, to, programmeCourses, studyprogramme)
break
case 'withoutStudyright':
result = await getStudentsWithoutStudyrightForProgrammeCourses(from, to, programmeCourses)
break
case 'otherStudents':
result = await getOtherStudentsForProgrammeCourses(from, to, programmeCourses, studyprogramme)
break
case 'transfer':
result = await getTransferStudentsForProgrammeCourses(from, to, programmeCourses)
break
default:
result = await getStudentsForProgrammeCourses(from, to, programmeCourses)
}
res(
result.map(c => {
c['year'] = year
return c
})
)
})
)
}
const getStudyprogrammeCoursesForStudytrack = async (unixMillis, studyprogramme, academicYear) => {
const startDate = academicYear == 'true' ? await getCurrentStudyYearStartDate(unixMillis) : getCurrentYearStartDate()
const startYear = startDate.getFullYear()
const yearRange = _.range(2017, startYear + 1)
const programmeCourses = await getAllStudyprogrammeCourses(studyprogramme)
const yearlyStudentByCoursePromises = makeYearlyPromises(yearRange, academicYear, 'allStudents', programmeCourses)
const yearlyProgrammeStudentsPromises = makeYearlyPromises(
yearRange,
academicYear,
'ownStudents',
programmeCourses,
studyprogramme
)
const yearlyStudentsWithoutStudyrightPromises = makeYearlyPromises(
yearRange,
academicYear,
'withoutStudyright',
programmeCourses
)
const yearlyOtherProgrammeStudentsPromises = makeYearlyPromises(
yearRange,
academicYear,
'otherStudents',
programmeCourses,
studyprogramme
)
const yearlyTransferStudentsPromises = makeYearlyPromises(
yearRange,
academicYear,
'transfer',
programmeCourses,
studyprogramme
)
const [
yearlyStudentByCourse,
yearlyProgrammeStudents,
yearlyStudentsWithoutStudyright,
yearlyOtherProgrammeStudents,
yearlyTransferStudents,
] = await Promise.all([
Promise.all(yearlyStudentByCoursePromises),
Promise.all(yearlyProgrammeStudentsPromises),
Promise.all(yearlyStudentsWithoutStudyrightPromises),
Promise.all(yearlyOtherProgrammeStudentsPromises),
Promise.all(yearlyTransferStudentsPromises),
])
let maxYear = 0
const allCourses = [
...yearlyStudentByCourse.flat(),
...yearlyProgrammeStudents.flat(),
...yearlyStudentsWithoutStudyright.flat(),
...yearlyOtherProgrammeStudents.flat(),
...yearlyTransferStudents.flat(),
].reduce((acc, curr) => {
if (curr.year > maxYear) maxYear = curr.year
if (!acc[curr.code]) {
acc[curr.code] = {
code: curr.code,
name: curr.name,
years: {},
}
}
if (!acc[curr.code]['years'][curr.year]) {
acc[curr.code]['years'][curr.year] = {
totalAllStudents: 0,
totalAllCredits: 0,
totalProgrammeStudents: 0,
totalProgrammeCredits: 0,
totalOtherProgrammeStudents: 0,
totalOtherProgrammeCredits: 0,
totalWithoutStudyrightStudents: 0,
totalWithoutStudyrightCredits: 0,
totalTransferStudents: 0,
totalTransferCredits: 0,
}
}
switch (curr.type) {
case 'total':
acc[curr.code]['years'][curr.year]['totalAllStudents'] += curr.totalAllStudents
acc[curr.code]['years'][curr.year]['totalAllCredits'] += curr.totalAllcredits
break
case 'ownProgramme':
acc[curr.code]['years'][curr.year]['totalProgrammeStudents'] += curr.totalProgrammeStudents
acc[curr.code]['years'][curr.year]['totalProgrammeCredits'] += curr.totalProgrammeCredits
break
case 'otherProgramme':
acc[curr.code]['years'][curr.year]['totalOtherProgrammeStudents'] += curr.totalOtherProgrammeStudents
acc[curr.code]['years'][curr.year]['totalOtherProgrammeCredits'] += curr.totalOtherProgrammeCredits
break
case 'noStudyright':
acc[curr.code]['years'][curr.year]['totalWithoutStudyrightStudents'] += curr.totalWithoutStudyrightStudents
acc[curr.code]['years'][curr.year]['totalWithoutStudyrightCredits'] += curr.totalWithoutStudyrightCredits
break
case 'transfer':
acc[curr.code]['years'][curr.year]['totalTransferStudents'] += curr.totalTransferStudents
acc[curr.code]['years'][curr.year]['totalTransferCredits'] += curr.totalTransferCredits
}
return acc
}, {})
const ayCourses = Object.keys(allCourses).filter(c => c.startsWith('AY'))
const properties = [
'totalAllStudents',
'totalAllCredits',
'totalProgrammeStudents',
'totalProgrammeCredits',
'totalOtherProgrammeStudents',
'totalOtherProgrammeCredits',
'totalWithoutStudyrightStudents',
'totalWithoutStudyrightCredits',
'totalTransferCredits',
'totalTransferStudents',
]
ayCourses.forEach(ayCourse => {
const normCode = isOpenUniCourseCode(ayCourse)[1]
if (allCourses[normCode]) {
const mergedCourse = {}
mergedCourse['code'] = allCourses[normCode].code
mergedCourse['name'] = allCourses[normCode].name
mergedCourse['years'] = {}
yearRange
.filter(year => year <= maxYear)
.forEach(year => {
if (!allCourses[normCode]['years'][year]) {
mergedCourse['years'][year] = {
totalAllStudents: 0,
totalAllCredits: 0,
totalProgrammeStudents: 0,
totalProgrammeCredits: 0,
totalOtherProgrammeStudents: 0,
totalOtherProgrammeCredits: 0,
totalWithoutStudyrightStudents: 0,
totalWithoutStudyrightCredits: 0,
totalTransferCredits: 0,
totalTransferStudents: 0,
}
} else {
mergedCourse['years'][year] = { ...allCourses[normCode]['years'][year] }
}
if (allCourses[ayCourse]['years'][year]) {
properties.forEach(prop => {
mergedCourse['years'][year][prop] =
mergedCourse['years'][year][prop] + allCourses[ayCourse]['years'][year][prop]
})
}
})
allCourses[normCode] = mergedCourse
delete allCourses[ayCourse]
}
})
return Object.values(allCourses)
}
module.exports = { getStudyprogrammeCoursesForStudytrack, getAllStudyprogrammeCourses }
@@ -5,16 +5,18 @@ const {
getStatsBasis,
defineYear,
isMajorStudentCredit,
isNonMajorCredit,
getYearsArray,
getStartDate,
isSpecialGroupCredit,
tableTitles,
} = require('./studyprogrammeHelpers')
const {
getProgrammesStudyrights,
getStudyRights,
getCreditsForStudyProgramme,
getTransferredCredits,
allTransfers,
getCourseCodesForStudyProgramme,
} = require('./studyprogramme')
// Fetches all the credits for the studyprogramme and divides them into major-students and non-major students credits
@@ -22,36 +24,54 @@ const {
// If special groups are excluded, the transfer students are filtered away from the major-students as well
const getRegularCreditStats = async ({ studyprogramme, since, years, isAcademicYear, includeAllSpecials }) => {
const providercode = mapToProviders([studyprogramme])[0]
let studyrights = await getProgrammesStudyrights(studyprogramme, since)
const courses = await getCourseCodesForStudyProgramme(providercode)
const credits = await getCreditsForStudyProgramme(courses, since)
const students = [...new Set(credits.map(({ student_studentnumber }) => student_studentnumber))]
//let studyrights = await getProgrammesStudyrights(studyprogramme, since)
let studyrights = await getStudyRights(students, since)
const transfers = (await allTransfers(studyprogramme, since)).map(t => t.studyrightid)
if (!includeAllSpecials) {
studyrights.filter(s => !transfers.includes(s.studyrightid))
studyrights = studyrights.filter(s => !transfers.includes(s.studyrightid))
}
const credits = await getCreditsForStudyProgramme(providercode, since)
let majors = getStatsBasis(years)
let nonMajors = getStatsBasis(years)
let nonDegree = getStatsBasis(years)
credits.forEach(({ student_studentnumber, attainment_date, credits }) => {
const studyright = studyrights.find(studyright => studyright.studentnumber == student_studentnumber)
const studentStudyrights = studyrights.filter(studyright => studyright.studentnumber === student_studentnumber)
const attainmentYear = defineYear(attainment_date, isAcademicYear)
if (!includeAllSpecials && isSpecialGroupCredit(studyright, attainment_date, transfers)) {
if (!includeAllSpecials && isSpecialGroupCredit(studentStudyrights, attainment_date, transfers)) {
return
}
if (isMajorStudentCredit(studyright, attainment_date)) {
if (isMajorStudentCredit(studentStudyrights, attainment_date, studyprogramme)) {
majors.graphStats[indexOf(years, attainmentYear)] += credits || 0
majors.tableStats[attainmentYear] += credits || 0
} else {
} else if (isNonMajorCredit(studentStudyrights, attainment_date, studyprogramme)) {
nonMajors.graphStats[indexOf(years, attainmentYear)] += credits || 0
nonMajors.tableStats[attainmentYear] += credits || 0
} else {
nonDegree.graphStats[indexOf(years, attainmentYear)] += credits || 0
nonDegree.tableStats[attainmentYear] += credits || 0
}
})
return { majors, nonMajors }
if (
majors.graphStats.every(year => year === 0) &&
nonMajors.graphStats.every(year => year === 0) &&
nonDegree.graphStats.every(year => year === 0)
) {
return {
majors: { graphStats: [], tableStats: {} },
nonMajors: { graphStats: [], tableStats: {} },
nonDegree: { graphStats: [], tablestats: {} },
}
}
return { majors, nonMajors, nonDegree }
}
// Fetches all the credits with the type "transferred / hyväksiluettu" and divides them by year
@@ -66,6 +86,10 @@ const getTransferredCreditStats = async ({ studyprogramme, since, years, isAcade
tableStats[attainmentYear] += credits || 0
})
if (graphStats.every(year => year === 0)) {
return { graphStats: [], tableStats: {} }
}
return { graphStats, tableStats }
}
@@ -77,47 +101,63 @@ const getCreditStatsForStudytrack = async ({ studyprogramme, settings }) => {
const years = getYearsArray(since.getFullYear(), isAcademicYear)
const queryParameters = { studyprogramme, since, years, isAcademicYear, includeAllSpecials }
const { majors, nonMajors } = await getRegularCreditStats(queryParameters)
const { majors, nonMajors, nonDegree } = await getRegularCreditStats(queryParameters)
const transferred = await getTransferredCreditStats(queryParameters)
const reversedYears = getYearsArray(since.getFullYear(), isAcademicYear).reverse()
const titles = tableTitles['credits'][includeAllSpecials ? 'SPECIAL_INCLUDED' : 'SPECIAL_EXCLUDED']
const dataFound = [majors, nonMajors, transferred].some(d => d.graphStats.length)
if (!dataFound) return null
const tableStats = reversedYears.map(year =>
includeAllSpecials
? [
year,
majors.tableStats[year] + nonMajors.tableStats[year] + transferred.tableStats[year],
majors.tableStats[year] +
nonMajors.tableStats[year] +
transferred.tableStats[year] +
nonDegree.tableStats[year],
majors.tableStats[year],
nonMajors.tableStats[year],
nonDegree.tableStats[year],
transferred.tableStats[year],
]
: [
year,
majors.tableStats[year] + nonMajors.tableStats[year] + transferred.tableStats[year],
majors.tableStats[year] +
nonMajors.tableStats[year] +
transferred.tableStats[year] +
nonDegree.tableStats[year],
majors.tableStats[year],
transferred.tableStats[year],
]
)
const graphStats = [
{
name: 'Major students credits',
data: majors.graphStats,
},
{
name: 'Non-major students credits',
data: nonMajors.graphStats,
},
{
name: 'Non-degree credits',
data: nonDegree.graphStats,
},
{
name: 'Transferred credits',
data: transferred.graphStats,
},
]
return {
id: studyprogramme,
years,
tableStats,
graphStats: [
{
name: 'Major students credits',
data: majors.graphStats,
},
{
name: 'Non-major students credits',
data: nonMajors.graphStats,
},
{
name: 'Transferred credits',
data: transferred.graphStats,
},
],
graphStats,
titles,
}
}
@@ -7,12 +7,15 @@ const {
getStatsBasis,
defineYear,
getYearsObject,
getAcademicYearsObject,
getYearsArray,
getMedian,
getMean,
getStartDate,
getThesisType,
alltimeStartDate,
alltimeEndDate,
getId,
getGoal,
} = require('./studyprogrammeHelpers')
const {
graduatedStudyRights,
@@ -22,30 +25,53 @@ const {
allStudyrights,
allTransfers,
} = require('./studyprogramme')
const { getYearStartAndEndDates } = require('../util/semester')
const { countTimeCategories, bachelorStudyright, getStatutoryAbsences } = require('./graduationHelpers')
const { getAllProgrammes } = require('./studyrights')
const checkStartdate = async (id, startdate) => {
if (id.slice(-2) === '-1') {
return startdate
}
const studyright = await bachelorStudyright(id.replace(/-2$/, '-1'))
if (studyright) return studyright.startdate
return null
}
const addGraduation = async (studyright, isAcademicYear, amounts, times) => {
const startdate = await checkStartdate(studyright.studyrightid, studyright.startdate)
const graduationYear = defineYear(studyright.enddate, isAcademicYear)
const totalTimeToGraduation = moment(studyright.enddate).diff(moment(startdate), 'months')
const statutoryAbsences = await getStatutoryAbsences(studyright.studentnumber, startdate, studyright.enddate)
const timeToGraduation = totalTimeToGraduation - statutoryAbsences
amounts[graduationYear] += 1
times[graduationYear] = [...times[graduationYear], timeToGraduation]
}
const getGraduatedStats = async ({ studyprogramme, since, years, isAcademicYear, includeAllSpecials }) => {
const { graphStats, tableStats } = getStatsBasis(years)
await Promise.all(
years.map(async year => {
const { startDate, endDate } = getYearStartAndEndDates(year, isAcademicYear)
const studentnumbersOfTheYear = await getCorrectStudentnumbers({
codes: [studyprogramme],
startDate,
endDate,
includeAllSpecials,
})
const studentnumbers = await getCorrectStudentnumbers({
codes: [studyprogramme],
startDate: alltimeStartDate,
endDate: alltimeEndDate,
includeAllSpecials,
})
const studyrights = await graduatedStudyRights(studyprogramme, since, studentnumbers)
studyrights.forEach(({ enddate, studyrightElements }) => {
// Check that the study right element ending to graduation belong to study programme
const elements = studyrightElements.filter(
sre => new Date(sre.enddate).toDateString() === new Date(enddate).toDateString()
)
if (elements.length) {
const graduationYear = defineYear(enddate, isAcademicYear)
graphStats[indexOf(years, graduationYear)] += 1
tableStats[graduationYear] += 1
}
})
const studyrights = await graduatedStudyRights(studyprogramme, since, studentnumbersOfTheYear)
studyrights.forEach(({ enddate }) => {
const graduationYear = defineYear(enddate, isAcademicYear)
graphStats[indexOf(years, graduationYear)] += 1
tableStats[graduationYear] += 1
})
})
)
return { graphStats, tableStats }
}
@@ -54,25 +80,20 @@ const getThesisStats = async ({ studyprogramme, since, years, isAcademicYear, in
const thesisType = getThesisType(studyprogramme)
const { graphStats, tableStats } = getStatsBasis(years)
await Promise.all(
years.map(async year => {
const { startDate, endDate } = getYearStartAndEndDates(year, isAcademicYear)
const studentnumbersOfTheYear = await getCorrectStudentnumbers({
codes: [studyprogramme],
startDate,
endDate,
includeAllSpecials,
})
const studentnumbers = await getCorrectStudentnumbers({
codes: [studyprogramme],
startDate: alltimeStartDate,
endDate: alltimeEndDate,
includeAllSpecials,
})
const credits = await getThesisCredits(providercode, since, thesisType, studentnumbersOfTheYear)
const credits = await getThesisCredits(providercode, since, thesisType, studentnumbers)
credits?.forEach(({ attainment_date }) => {
const attainmentYear = defineYear(attainment_date, isAcademicYear)
graphStats[indexOf(years, attainmentYear)] += 1
tableStats[attainmentYear] += 1
})
})
)
credits?.forEach(({ attainment_date }) => {
const attainmentYear = defineYear(attainment_date, isAcademicYear)
graphStats[indexOf(years, attainmentYear)] += 1
tableStats[attainmentYear] += 1
})
return { graphStats, tableStats }
}
@@ -80,29 +101,29 @@ const getThesisStats = async ({ studyprogramme, since, years, isAcademicYear, in
const getGraduationTimeStats = async ({ studyprogramme, since, years, isAcademicYear, includeAllSpecials }) => {
let graduationAmounts = getYearsObject({ years })
let graduationTimes = getYearsObject({ years, emptyArrays: true })
let totalAmounts = isAcademicYear ? getAcademicYearsObject({ years }) : getYearsObject({ years })
// for bc+ms combo
const doCombo = studyprogramme.startsWith('MH') && !['MH30_001', 'MH30_003'].includes(studyprogramme)
let graduationAmountsCombo = getYearsObject({ years })
let graduationTimesCombo = getYearsObject({ years, emptyArrays: true })
await Promise.all(
years.map(async year => {
const { startDate, endDate } = getYearStartAndEndDates(year, isAcademicYear)
const studentnumbersOfTheYear = await getCorrectStudentnumbers({
codes: [studyprogramme],
startDate,
endDate,
includeAllSpecials,
})
const studentnumbers = await getCorrectStudentnumbers({
codes: [studyprogramme],
startDate: alltimeStartDate,
endDate: alltimeEndDate,
includeAllSpecials,
})
const studyrights = await graduatedStudyRights(studyprogramme, since, studentnumbersOfTheYear)
totalAmounts[year] = studentnumbersOfTheYear.length
const studyrights = await graduatedStudyRights(studyprogramme, since, studentnumbers)
// for masters, separate bc+ms combo from just ms students (except some medical progs)
for (const right of studyrights) {
if (doCombo && right.studyrightid.slice(-2) === '-2') {
await addGraduation(right, isAcademicYear, graduationAmountsCombo, graduationTimesCombo)
} else {
await addGraduation(right, isAcademicYear, graduationAmounts, graduationTimes)
}
}
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
@@ -122,7 +143,35 @@ const getGraduationTimeStats = async ({ studyprogramme, since, years, isAcademic
['', comparisonValue - mean],
]
})
return { medians, means, graduationAmounts, totalAmounts }
// new version
const goal = getGoal(studyprogramme)
const times = { medians: [], means: [], goal }
const comboTimes = { medians: [], means: [], 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 }
}
// Creates (studyrightid, transfer)-map out of programmes transfers
@@ -136,7 +185,7 @@ const getTransferStudyrightMap = async (studyprogramme, since) => {
const formatStats = (stats, years) => {
const tableStats = Object.values(stats)
.filter(p => years.map(year => p[year]).find(started => started !== 0)) // Filter out programmes with no-one started between the selected years
.map(p => [p.code, p.name['fi'], ...years.map(year => p[year])])
.map(p => [p.code, getId(p.code), p.name['fi'], ...years.map(year => p[year])])
const graphStats = Object.values(stats)
.filter(p => years.map(year => p[year]).find(started => started !== 0)) // Filter out programmes with no-one started between the selected years
@@ -218,6 +267,12 @@ const getProgrammesAfterGraduation = async ({ studyprogramme, since, years, isAc
return graphAndTableStats
}
const getProgrammesBeforeOrAfter = async (studyprogramme, queryParameters) => {
if (studyprogramme.includes('KH')) return await getProgrammesAfterGraduation(queryParameters)
if (studyprogramme.includes('MH')) return await getProgrammesBeforeStarting(queryParameters)
return null
}
const getGraduationStatsForStudytrack = async ({ studyprogramme, settings }) => {
const { isAcademicYear, includeAllSpecials } = settings
const since = getStartDate(studyprogramme, isAcademicYear)
@@ -225,16 +280,17 @@ const getGraduationStatsForStudytrack = async ({ studyprogramme, settings }) =>
const queryParameters = { studyprogramme, since, years, isAcademicYear, includeAllSpecials }
const thesis = await getThesisStats(queryParameters)
const graduated = await getGraduatedStats(queryParameters)
const graduationTimeStats = await getGraduationTimeStats(queryParameters)
const programmesBeforeOrAfter = studyprogramme.includes('KH')
? await getProgrammesAfterGraduation(queryParameters)
: await getProgrammesBeforeStarting(queryParameters)
const programmesBeforeOrAfter = await getProgrammesBeforeOrAfter(studyprogramme, queryParameters)
const reversedYears = getYearsArray(since.getFullYear(), isAcademicYear).reverse()
const titles = ['', 'Graduated', 'Wrote thesis']
const programmesBeforeOrAfterTitles = ['Code', 'Programme', ...years]
const programmesBeforeOrAfterTitles = ['Code', 'Id', 'Programme', ...years]
const tableStats = reversedYears.map(year => [year, graduated.tableStats[year], thesis.tableStats[year]])
return {
@@ -255,9 +311,11 @@ const getGraduationStatsForStudytrack = async ({ studyprogramme, settings }) =>
graduationMedianTime: graduationTimeStats.medians,
graduationMeanTime: graduationTimeStats.means,
graduationAmounts: graduationTimeStats.graduationAmounts,
totalAmounts: graduationTimeStats.totalAmounts,
programmesBeforeOrAfterTableStats: programmesBeforeOrAfter.tableStats,
programmesBeforeOrAfterGraphStats: programmesBeforeOrAfter.graphStats,
graduationTimes: graduationTimeStats.times,
doCombo: graduationTimeStats.doCombo,
comboTimes: graduationTimeStats.comboTimes,
programmesBeforeOrAfterTableStats: programmesBeforeOrAfter?.tableStats,
programmesBeforeOrAfterGraphStats: programmesBeforeOrAfter?.graphStats,
programmesBeforeOrAfterTitles,
}
}
@@ -1,12 +1,11 @@
const { mean } = require('lodash')
const { studentnumbersWithAllStudyrightElements } = require('./populations')
const { codes } = require('../../config/programmeCodes')
// Helper functions
const getCorrectStudentnumbers = async ({ codes, startDate, endDate, includeAllSpecials, includeGraduated = true }) => {
let studentnumbers = []
const exchangeStudents = includeAllSpecials
const cancelledStudents = includeAllSpecials
const nondegreeStudents = includeAllSpecials
const transferredOutStudents = includeAllSpecials
const transferredToStudents = !includeAllSpecials
@@ -17,7 +16,6 @@ const getCorrectStudentnumbers = async ({ codes, startDate, endDate, includeAllS
startDate,
endDate,
exchangeStudents,
cancelledStudents,
nondegreeStudents,
transferredOutStudents,
null,
@@ -35,14 +33,24 @@ const defineStartDate = (studyright_elements, studystartdate) => {
return studystartdate
}
const resolveStudyRightCode = studyright_elements => {
if (!studyright_elements) return null
const studyRightElement = studyright_elements
.filter(sre => sre.element_detail.type === 20)
.sort((a, b) => new Date(b.startdate) - new Date(a.startdate))[0]
if (studyRightElement) return studyRightElement.code
return null
}
const formatStudyright = studyright => {
const {
studyrightid,
startdate,
studystartdate,
enddate,
givendate,
canceldate,
graduated,
active,
prioritycode,
extentcode,
student,
@@ -51,15 +59,17 @@ const formatStudyright = studyright => {
return {
studyrightid,
startdate,
studystartdate: defineStartDate(studyright_elements, studystartdate),
enddate,
givendate,
canceldate,
graduated,
active,
prioritycode,
extentcode,
studentnumber: student.studentnumber,
code: studyright_elements?.length ? studyright_elements[0].code : null,
code: resolveStudyRightCode(studyright_elements),
studyrightElements: studyright_elements,
name:
studyright_elements?.length && studyright_elements[0].element_detail && studyright_elements[0].element_detail.name
? studyright_elements[0].element_detail.name
@@ -68,12 +78,13 @@ const formatStudyright = studyright => {
}
const formatStudent = student => {
const { studentnumber, gender_code, home_country_en, creditcount } = student
const { studentnumber, gender_code, home_country_en, creditcount, credits } = student
return {
studentnumber,
gender_code,
home_country_en,
creditcount,
credits,
}
}
@@ -89,7 +100,8 @@ const formatTransfer = transfer => {
const getYearsArray = (since, isAcademicYear, yearsCombined) => {
const years = []
if (yearsCombined) return [`${since} - ${new Date().getFullYear()}`]
const allYears = 'Total'
if (yearsCombined) years.push(allYears)
for (let i = since; i <= new Date().getFullYear(); i++) {
const year = isAcademicYear ? `${i} - ${i + 1}` : i
years.push(year)
@@ -120,22 +132,36 @@ const getStatsBasis = years => {
}
}
const isMajorStudentCredit = (studyright, attainment_date) =>
studyright &&
(studyright.prioritycode === 1 || studyright.prioritycode === 30) && // Is studyright state = MAIN or state = GRADUATED
studyright.studystartdate && // The student has started studying in the programme
studyright.studystartdate <= attainment_date && // Has the credit been attained after studying in the programme started
studyright.enddate >= attainment_date && // Has the credit been attained before the studyright ended
(!studyright.canceldate || studyright.canceldate >= attainment_date) // If the studyright was cancelled, was the credit attained before it was cancelled
const isMajorStudentCredit = (studyrights, attainment_date, code) =>
studyrights.some(studyright => {
if (!studyright) return false
if (studyright.code !== code) return false
if (!studyright.graduated) return new Date(attainment_date) >= new Date(studyright.studystartdate)
return (
new Date(attainment_date) >= new Date(studyright.studystartdate) &&
new Date(attainment_date) <= new Date(studyright.enddate)
)
})
const isSpecialGroupCredit = (studyright, attainment_date, transfers) => {
if (!studyright) return true // If there is no studyright matching the credit, is not a major student credit
if (studyright.canceldate) return true // Cancelled studyrights are in the special groups
if (studyright.studystartdate > attainment_date) return true // Credits before the studyright started are not major student credits
if (studyright.enddate && attainment_date > studyright.enddate) return true // Credits after studyright are not major student credits
if ([7, 9, 16, 34, 33, 99, 14, 13].includes(studyright.extentcode)) return true // Excludes non-degree studyrights and exchange students
if (transfers.includes(studyright.studyrightid)) return true // Excludes both transfers in and out of the programme
return false
const isNonMajorCredit = (studyrights, attainment_date) =>
studyrights.some(studyright => {
if (!studyright) return false
if (!studyright.graduated) return new Date(attainment_date) >= new Date(studyright.studystartdate)
return (
new Date(attainment_date) >= new Date(studyright.studystartdate) &&
new Date(attainment_date) <= new Date(studyright.enddate)
)
})
const isSpecialGroupCredit = (studyrights, attainment_date, transfers) => {
studyrights.some(studyright => {
if (!studyright) return true // If there is no studyright matching the credit, is not a major student credit
if (studyright.studystartdate > attainment_date) return true // Credits before the studyright started are not major student credits
if (studyright.enddate && attainment_date > studyright.enddate) return true // Credits after studyright are not major student credits
if ([7, 9, 16, 34, 22, 99, 14, 13].includes(studyright.extentcode)) return true // Excludes non-degree studyrights and exchange students
if (transfers.includes(studyright.studyrightid)) return true // Excludes both transfers in and out of the programme
return false
})
}
const getMedian = values => {
@@ -166,6 +192,9 @@ const getStartDate = (studyprogramme, isAcademicYear) => {
return new Date('2017-01-01')
}
const alltimeStartDate = new Date('1900-01-01')
const alltimeEndDate = new Date()
// There are 9 course_unit_types
// 1. urn:code:course-unit-type:regular
// 2. urn:code:course-unit-type:bachelors-thesis
@@ -185,7 +214,7 @@ const getThesisType = studyprogramme => {
return ['urn:code:course-unit-type:bachelors-thesis']
if (/^(T)[0-9]{6}$/.test(studyprogramme))
return ['urn:code:course-unit-type:doctors-thesis', 'urn:code:course-unit-type:licentiate-thesis']
return 'thesis-type-not-defined'
return ['urn:code:course-unit-type:doctors-thesis', 'urn:code:course-unit-type:licentiate-thesis']
}
const getPercentage = (value, total) => {
@@ -198,8 +227,12 @@ const getPercentage = (value, total) => {
const getEmptyArray = length => new Array(length).fill(0)
const getBachelorCreditGraphStats = years => ({
lte15: {
name: 'Less than 15 credits',
data: getEmptyArray(years.length),
},
lte30: {
name: 'Less than 30 credits',
name: '15-29 credits',
data: getEmptyArray(years.length),
},
lte60: {
@@ -218,8 +251,12 @@ const getBachelorCreditGraphStats = years => ({
name: '120-149 credits',
data: getEmptyArray(years.length),
},
mte150: {
name: 'More than 150 credits',
lte180: {
name: '150-179 credits',
data: getEmptyArray(years.length),
},
mte180: {
name: '180 or more credits',
data: getEmptyArray(years.length),
},
})
@@ -245,46 +282,163 @@ const getMasterCreditGraphStats = years => ({
name: '260-279 credits',
data: getEmptyArray(years.length),
},
mte280: {
name: 'More than 280 credits',
lte300: {
name: '280-299 credits',
data: getEmptyArray(years.length),
},
mte300: {
name: '300 or more credits',
data: getEmptyArray(years.length),
},
})
const getVetenaryCreditGraphStats = years => ({
lte210: {
name: 'Less than 210 credits',
data: getEmptyArray(years.length),
},
lte240: {
name: '210-239 credits',
data: getEmptyArray(years.length),
},
lte270: {
name: '240-269 credits',
data: getEmptyArray(years.length),
},
lte300: {
name: '270-299 credits',
data: getEmptyArray(years.length),
},
lte330: {
name: '300-329 credits',
data: getEmptyArray(years.length),
},
lte360: {
name: '330-359 credits',
data: getEmptyArray(years.length),
},
mte360: {
name: '360 or more credits',
data: getEmptyArray(years.length),
},
})
const getOnlyMasterCreditGraphStats = years => ({
lte15: {
name: 'Less than 15 credits',
data: getEmptyArray(years.length),
},
lte30: {
name: '15-29 credits',
data: getEmptyArray(years.length),
},
lte60: {
name: '30-59 credits',
data: getEmptyArray(years.length),
},
lte90: {
name: '60-89 credits',
data: getEmptyArray(years.length),
},
lte120: {
name: '90-119 credits',
data: getEmptyArray(years.length),
},
mte120: {
name: '120 or more credits',
data: getEmptyArray(years.length),
},
})
const getCreditGraphStats = (studyprogramme, years) =>
studyprogramme.includes('KH') ? getBachelorCreditGraphStats(years) : getMasterCreditGraphStats(years)
const getDoctoralCreditGraphStats = years => ({
lte50: {
name: 'Less than 50 credits',
data: getEmptyArray(years.length),
},
lte100: {
name: '50-99 credits',
data: getEmptyArray(years.length),
},
lte150: {
name: '100-149 credits',
data: getEmptyArray(years.length),
},
lte200: {
name: '150-199 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',
data: getEmptyArray(years.length),
},
})
const bachelorCreditThresholds = ['lte30', 'lte60', 'lte90', 'lte120', 'lte150', 'mte150']
const masterCreditThresholds = ['lte200', 'lte220', 'lte240', 'lte260', 'lte280', 'mte280']
const bachelorCreditAmounts = [30, 60, 90, 120, 150, 150]
const masterCreditAmounts = [200, 220, 240, 260, 280, 280]
const getCreditGraphStats = (studyprogramme, years) => {
if (studyprogramme.includes('KH')) return getBachelorCreditGraphStats(years)
if (studyprogramme.includes('MH')) return getMasterCreditGraphStats(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 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 vetenaryCreditThresholds = ['lte210', 'lte240', 'lte270', 'lte300', 'lte330', 'lte360', 'mte360']
const vetenaryCreditAmounts = [210, 240, 270, 300, 330, 360, 360]
const getOnlyMasterThresholds = () => {
return { msOnlyCreditThresholdKeys: onlyMasterCreditThresholds, msOnlyCreditThresholdAmount: onlyMasterCreditAmounts }
}
const getCreditThresholds = studyprogramme => {
if (studyprogramme === 'MH90_001') {
return { creditThresholdKeys: vetenaryCreditThresholds, creditThresholdAmounts: vetenaryCreditAmounts }
}
if (studyprogramme.includes('KH')) {
return { creditThresholdKeys: bachelorCreditThresholds, creditThresholdAmounts: bachelorCreditAmounts }
}
return { creditThresholdKeys: masterCreditThresholds, creditThresholdAmounts: masterCreditAmounts }
if (studyprogramme.includes('MH')) {
return { creditThresholdKeys: masterCreditThresholds, creditThresholdAmounts: masterCreditAmounts }
}
return { creditThresholdKeys: doctoralCreditThresholds, creditThresholdAmounts: doctoralCreditAmounts }
}
const tableTitles = {
basics: {
SPECIAL_EXCLUDED: ['', 'Started studying', 'Graduated'],
SPECIAL_INCLUDED: ['', 'Started studying', 'Graduated', 'Cancelled', 'Transferred away', 'Transferred to'],
SPECIAL_INCLUDED: ['', 'Started studying', 'Graduated', 'Transferred away', 'Transferred to'],
},
credits: {
SPECIAL_EXCLUDED: ['', 'Total', 'Major students credits', 'Transferred credits'],
SPECIAL_INCLUDED: ['', 'Total', 'Major students credits', 'Non major students credits', 'Transferred credits'],
SPECIAL_INCLUDED: [
'',
'Total',
'Major students credits',
'Non-major students credits',
'Non-degree students credits',
'Transferred credits',
],
},
creditProgress: {
bachelor: [
'',
'All',
'< 30 credits',
'< 15 credits',
'15-29 credits',
'30-59 credits',
'60-89 credits',
'90-119 credits',
'120-149 credits',
'> 150 credits',
'150-179 credits',
'180 ≤ credits',
],
master: [
'',
@@ -294,7 +448,29 @@ const tableTitles = {
'220-239 credits',
'240-259 credits',
'260-279 credits',
'> 280 credits',
'280-299 credits',
'300 ≤ credits',
],
masterOnly: [
'',
'All',
'< 15 credits',
'15-29 credits',
'30-59 credits',
'60-89 credits',
'90-119 credits',
'120 ≤ credits',
],
doctoral: [
'',
'All',
'< 50 credits',
'50-99 credits',
'100-149 credits',
'150-199 credits',
'200-249 credits',
'250-299 credits',
'300 ≤ credits',
],
},
studytracks: [
@@ -303,7 +479,7 @@ const tableTitles = {
'Started studying',
'Currently enrolled',
'Absent',
'Cancelled',
'Inactive',
'Graduated',
'Men',
'Women',
@@ -311,8 +487,52 @@ const tableTitles = {
],
}
const getCreditProgressTableTitles = studyprogramme =>
studyprogramme.includes('KH') ? tableTitles.bachelorCreditProgress : tableTitles.masterCreditProgress
const getCreditProgressTableTitles = studyprogramme => {
if (studyprogramme.includes('KH')) return tableTitles.creditProgress.bachelor
if (studyprogramme.includes('MH')) return tableTitles.creditProgress.master
return tableTitles.creditProgress.doctoral
}
const mapCodesToIds = data => {
// Add programme id e.g. TKT
const keys = Object.keys(codes)
const progs = Object.keys(data)
for (const prog of progs) {
if (keys.includes(prog)) {
data[prog].progId = codes[prog].toUpperCase()
}
}
}
const getId = code => {
if (Object.keys(codes).includes(code)) {
return codes[code].toUpperCase()
}
return ''
}
const getGoal = programme => {
if (programme.startsWith('KH') || programme.endsWith('-ba')) {
return 36
}
if (programme.startsWith('MH') || programme.endsWith('-ma')) {
if (['MH30_004', '420420-ma'].includes(programme)) {
return 24 + 6
}
if (['MH30_001', 'MH30_003', '320011-ma', '320001-ma', '320002-ma'].includes(programme)) {
return 36 + 24 + 12 // medical, no separate bachelor
}
return 24
}
if (programme.includes('T')) {
return 48
}
if (programme.startsWith('LI')) {
return 78
}
return 48 //unknown, likely old doctor or licentiate
}
module.exports = {
getCorrectStudentnumbers,
@@ -329,12 +549,22 @@ module.exports = {
getMean,
defineYear,
getStartDate,
alltimeStartDate,
alltimeEndDate,
getThesisType,
getPercentage,
getBachelorCreditGraphStats,
getMasterCreditGraphStats,
getVetenaryCreditGraphStats,
getDoctoralCreditGraphStats,
getOnlyMasterCreditGraphStats,
getCreditGraphStats,
getCreditThresholds,
tableTitles,
getCreditProgressTableTitles,
getOnlyMasterThresholds,
isNonMajorCredit,
mapCodesToIds,
getId,
getGoal,
}
@@ -30,7 +30,6 @@ const updateBasicView = async code => {
}
const options = [specialCalendar, specialAcademic, specialExcludedCalendar, specialExcludedAcademic]
for (const option of options) {
try {
const basicStats = await getBasicStatsForStudytrack({
@@ -62,61 +61,33 @@ const updateBasicView = async code => {
} catch (e) {
logger.error(e)
}
return 'OK'
}
return 'OK'
}
const updateStudytrackView = async code => {
const specialGraduatedCombined = {
const specialGraduated = {
graduated: 'GRADUATED_INCLUDED',
specialGroups: 'SPECIAL_INCLUDED',
yearsCombined: 'YEARS_COMBINED',
}
const specialExcludedGraduatedCombined = {
const specialExcludedGraduated = {
graduated: 'GRADUATED_INCLUDED',
specialGroups: 'SPECIAL_INCLUDED',
yearsCombined: 'YEARS_SEPARATED',
specialGroups: 'SPECIAL_EXCLUDED',
}
const specialGraduatedExcludedCombined = {
graduated: 'GRADUATED_INCLUDED',
const specialGraduatedExcluded = {
graduated: 'GRADUATED_EXCLUDED',
specialGroups: 'SPECIAL_INCLUDED',
yearsCombined: 'YEARS_COMBINED',
}
const specialExcludedGraduatedExcludedCombined = {
graduated: 'GRADUATED_INCLUDED',
specialGroups: 'SPECIAL_INCLUDED',
yearsCombined: 'YEARS_COMBINED',
}
const specialGraduatedSeparated = {
graduated: 'GRADUATED_INCLUDED',
specialGroups: 'SPECIAL_INCLUDED',
yearsCombined: 'YEARS_COMBINED',
}
const specialExcludedGraduatedSeparated = {
graduated: 'GRADUATED_INCLUDED',
specialGroups: 'SPECIAL_INCLUDED',
yearsCombined: 'YEARS_SEPARATED',
}
const specialGraduatedExcludedSeparated = {
graduated: 'GRADUATED_INCLUDED',
specialGroups: 'SPECIAL_INCLUDED',
yearsCombined: 'YEARS_COMBINED',
}
const specialExcludedGraduatedExcludedSeparated = {
graduated: 'GRADUATED_INCLUDED',
specialGroups: 'SPECIAL_INCLUDED',
yearsCombined: 'YEARS_COMBINED',
const specialExcludedGraduatedExcluded = {
graduated: 'GRADUATED_EXCLUDED',
specialGroups: 'SPECIAL_EXCLUDED',
}
const options = [
specialGraduatedCombined,
specialExcludedGraduatedCombined,
specialGraduatedExcludedCombined,
specialExcludedGraduatedExcludedCombined,
specialGraduatedSeparated,
specialExcludedGraduatedSeparated,
specialGraduatedExcludedSeparated,
specialExcludedGraduatedExcludedSeparated,
specialGraduated,
specialExcludedGraduated,
specialGraduatedExcluded,
specialExcludedGraduatedExcluded,
]
for (const option of options) {
@@ -126,10 +97,9 @@ const updateStudytrackView = async code => {
settings: {
specialGroups: option.specialGroups === 'SPECIAL_INCLUDED',
graduated: option.graduated === 'GRADUATED_INCLUDED',
yearsCombined: option.yearsCombined === 'YEARS_COMBINED',
},
})
await setStudytrackStats(stats, option.graduated, option.specialGroups, option.yearsCombined)
await setStudytrackStats(stats, option.graduated, option.specialGroups)
} catch (e) {
logger.error(e)
}
+10 -2
View File
@@ -1,7 +1,7 @@
const {
dbConnections: { sequelize },
} = require('../database/connection')
const { Studyright, StudyrightElement, ElementDetail } = require('../models')
const { Studyright, StudyrightElement, ElementDetail, Transfer } = require('../models')
const moment = require('moment')
const { redisClient } = require('../services/redis')
const _ = require('lodash')
@@ -187,6 +187,10 @@ const StudyRightType = {
const calculateAssociationsFromDb = async (chunksize = 100000) => {
// bottlenecked by Studyright.findAll in associatedStudyrightElements()
const transfers = await Transfer.findAll({
attributes: ['studyrightid'],
})
const transferedStudyrightIds = transfers.map(transfer => transfer.studyrightid)
const getSemester = momentstartdate => {
if (momentstartdate < moment(`${momentstartdate.utc().year()}-07-31 21:00:00+00`)) return 'SPRING'
return 'FALL'
@@ -247,8 +251,12 @@ const calculateAssociationsFromDb = async (chunksize = 100000) => {
name: e.name,
code: e.code,
}
if (!associations.programmes[code].studytracks.includes(e.code))
if (
!associations.programmes[code].studytracks.includes(e.code) &&
!transferedStudyrightIds.includes(e.studyrightid)
) {
associations.programmes[code].studytracks.push(e.code)
}
}
associations.studyTracks[e.code] = associations.studyTracks[e.code] || {
type: e.type,
@@ -13,22 +13,28 @@ const {
getCreditThresholds,
getCreditGraphStats,
tableTitles,
getCreditProgressTableTitles,
} = require('./studyprogrammeHelpers')
const {
studytrackStudents,
allStudyrights,
startedStudyrights,
inactiveStudyrights,
enrolledStudents,
absentStudents,
graduatedStudyRights,
} = require('./studyprogramme')
const { getAcademicYearDates } = require('../util/semester')
const getStudentData = (students, thresholdKeys, thresholdAmounts) => {
const getStudentData = (startDate, students, thresholdKeys, thresholdAmounts) => {
let data = { female: 0, male: 0, finnish: 0 }
thresholdKeys.forEach(t => (data[t] = 0))
students.forEach(({ gender_code, home_country_en, creditcount }) => {
students.forEach(({ gender_code, home_country_en, credits }) => {
const creditcount = credits
.filter(credit => moment(credit.attainment_date).isAfter(startDate))
.reduce((prev, curr) => (prev += curr.credits), 0)
data.male += gender_code === '1' ? 1 : 0
data.female += gender_code === '2' ? 1 : 0
data.finnish += home_country_en === 'Finland' ? 1 : 0
@@ -37,7 +43,14 @@ const getStudentData = (students, thresholdKeys, thresholdAmounts) => {
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[5] ? 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[6]] += creditcount >= thresholdAmounts[5] && creditcount < thresholdAmounts[6] ? 1 : 0
data[thresholdKeys[7]] += creditcount >= thresholdAmounts[6] ? 1 : 0
} else {
data[thresholdKeys[6]] += creditcount >= thresholdAmounts[5] ? 1 : 0
}
})
return data
}
@@ -78,6 +91,7 @@ const getGraduationTimeStats = async ({
// Goes through the programme and all its studytracks for the said year and adds the wanted stats to the data objects
const getStudytrackDataForTheYear = async ({
studyprogramme,
since,
settings,
studytracks,
studytrackNames,
@@ -87,8 +101,9 @@ const getStudytrackDataForTheYear = async ({
creditThresholdKeys,
creditThresholdAmounts,
}) => {
const { specialGroups: includeAllSpecials, graduated: includeGraduated, yearsCombined } = settings
const { startDate, endDate } = getAcademicYearDates(year, yearsCombined)
const { specialGroups: includeAllSpecials, graduated: includeGraduated } = settings
const { startDate, endDate } = getAcademicYearDates(year, since)
const {
mainStatsByYear,
mainStatsByTrack,
@@ -116,11 +131,11 @@ const getStudytrackDataForTheYear = async ({
// Get all the studyrights and students for the calculations
const all = await allStudyrights(track, studentnumbers)
const students = await studytrackStudents(studentnumbers)
const studentData = getStudentData(students, creditThresholdKeys, creditThresholdAmounts)
const studentData = getStudentData(startDate, students, creditThresholdKeys, creditThresholdAmounts)
const started = await startedStudyrights(track, startDate, studentnumbers)
const enrolled = await enrolledStudents(track, startDate, studentnumbers)
const absent = await absentStudents(track, studentnumbers)
const cancelled = all.filter(s => s.canceldate !== null && s.canceldate !== undefined)
const inactive = await inactiveStudyrights(track, studentnumbers)
const graduated = await graduatedStudyRights(track, startDate, studentnumbers)
// If the track has no stats for that year, it should be removed from the table and dropdown options
@@ -133,6 +148,7 @@ const getStudytrackDataForTheYear = async ({
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],
@@ -152,8 +168,8 @@ const getStudytrackDataForTheYear = async ({
getPercentage(enrolled.length, all.length),
absent.length,
getPercentage(absent.length, all.length),
cancelled.length,
getPercentage(cancelled.length, all.length),
inactive.length,
getPercentage(inactive.length, all.length),
graduated.length,
getPercentage(graduated.length, all.length),
studentData.male,
@@ -178,8 +194,8 @@ const getStudytrackDataForTheYear = async ({
getPercentage(enrolled.length, all.length),
absent.length,
getPercentage(absent.length, all.length),
cancelled.length,
getPercentage(cancelled.length, all.length),
inactive.length,
getPercentage(inactive.length, all.length),
graduated.length,
getPercentage(graduated.length, all.length),
studentData.male,
@@ -257,8 +273,9 @@ const getEmptyStatsObjects = (years, studytracks, studyprogramme) => {
// Combines all the data for the Populations and Studytracks -view
const getStudytrackStatsForStudyprogramme = async ({ studyprogramme, settings }) => {
const isAcademicYear = true
const includeYearsCombined = true
const since = getStartDate(studyprogramme, isAcademicYear)
const years = getYearsArray(since.getFullYear(), isAcademicYear, settings.yearsCombined)
const years = getYearsArray(since.getFullYear(), isAcademicYear, includeYearsCombined)
const associations = await getAssociations()
const studytracks = associations.programmes[studyprogramme]
@@ -274,6 +291,7 @@ const getStudytrackStatsForStudyprogramme = async ({ studyprogramme, settings })
years.map(async year => {
return await getStudytrackDataForTheYear({
studyprogramme,
since,
settings,
studytracks,
studytrackNames,
@@ -290,19 +308,12 @@ const getStudytrackStatsForStudyprogramme = async ({ studyprogramme, settings })
return {
id: studyprogramme,
years: getYearsArray(since.getFullYear(), isAcademicYear, settings.yearsCombined),
mainStatsByTrack: data.mainStatsByTrack,
mainStatsByYear: data.mainStatsByYear,
creditTableStats: data.creditTableStats,
creditGraphStats: data.creditGraphStats,
graduationMedianTime: data.graduationMedianTime,
graduationMeanTime: data.graduationMeanTime,
graduationAmounts: data.graduationAmounts,
totalAmounts: data.totalAmounts,
years: getYearsArray(since.getFullYear(), isAcademicYear, includeYearsCombined),
...data,
studytrackOptions,
includeGraduated: settings.graduated,
populationTitles: tableTitles['studytracks'],
creditTableTitles: tableTitles['creditProgress'][studyprogramme.includes('KH') ? 'bachelor' : 'master'],
creditTableTitles: getCreditProgressTableTitles(studyprogramme),
}
}
+5 -6
View File
@@ -43,7 +43,7 @@ const getCategoriesAndYears = async () => {
const creditsWithTeachersForYear = yearcode =>
Credit.findAll({
attributes: ['id', 'credits', 'credittypecode', 'isStudyModule'],
attributes: ['id', 'credits', 'credittypecode', 'isStudyModule', 'is_open'],
include: [
{
model: Semester,
@@ -106,21 +106,20 @@ const findTopTeachers = async yearcode => {
credits
.filter(isRegularCourse)
.map(credit => {
const { credits, course, credittypecode } = credit
const { credits, credittypecode, is_open } = credit
const teachers = credit.teachers
.filter(({ id }) => !id.includes('hy-hlo-org')) // Remove faculties from the leaderboards
.map(({ id, name }) => ({ id, name }))
const passed = Credit.passed(credit) || Credit.improved(credit)
const failed = Credit.failed(credit)
const transferred = credittypecode === 9
const isOpenUni = course.code[0] === 'A'
return { passed, failed, credits, teachers, isOpenUni, transferred }
return { passed, failed, credits, teachers, is_open, transferred }
})
.forEach(credit => {
const { passed, failed, credits, teachers, isOpenUni, transferred } = credit
const { passed, failed, credits, teachers, is_open, transferred } = credit
teachers.forEach(teacher => {
all[teacher.id] = updatedStats(all, teacher, passed, failed, credits, transferred)
if (isOpenUni) {
if (is_open) {
openuni[teacher.id] = updatedStats(openuni, teacher, passed, failed, credits, transferred)
}
})
+25 -6
View File
@@ -2,15 +2,28 @@ const _ = require('lodash')
const { getRedisCDS, saveToRedis, getTargetStudentCounts } = require('./shared')
const { getAssociations } = require('../studyrights')
const REDIS_KEY_PROTOC = 'PROTOC_DATA_V2'
const REDIS_KEY_PROTOC = 'PROTOC_DATA_V3.3'
const REDIS_KEY_PROTOC_PROGRAMME = 'PROTOC_PROGRAMME_DATA_V2'
const tryParseInt = (number, defaultValue) => {
try {
return parseInt(number, 10)
} catch (e) {
return defaultValue
}
}
const calculateProtoC = async query => {
const associations = await getAssociations()
const startYear = tryParseInt(query.startYear, null)
const endYear = tryParseInt(query.endYear, null)
const data = await getTargetStudentCounts({
includeOldAttainments: query.include_old_attainments === 'true',
excludeNonEnrolled: query.exclude_non_enrolled === 'true',
startYear,
endYear,
})
const programmeData = data.filter(d => d.programmeType !== 30)
@@ -97,10 +110,16 @@ const calculateProtoCProgramme = async query => {
const codes = associations.programmes[query.code]
? [...associations.programmes[query.code].studytracks, query.code]
: []
const startYear = tryParseInt(query.startYear, null)
const endYear = tryParseInt(query.endYear, null)
const data = await getTargetStudentCounts({
codes: codes,
codes,
includeOldAttainments: query.include_old_attainments === 'true',
excludeNonEnrolled: query.exclude_non_enrolled === 'true',
startYear,
endYear,
})
const programmeData = data.find(d => d.programmeType === 20)
@@ -157,10 +176,10 @@ const calculateProtoCProgramme = async query => {
}
const getProtoC = async (query, doRefresh = false) => {
const { include_old_attainments, exclude_non_enrolled } = query
const { include_old_attainments, exclude_non_enrolled, startYear, endYear } = query
// redis keys for different queries
const KEY = `${REDIS_KEY_PROTOC}_OLD_${include_old_attainments.toUpperCase()}_ENR_${exclude_non_enrolled.toUpperCase()}`
const KEY = `${REDIS_KEY_PROTOC}_OLD_${include_old_attainments.toUpperCase()}_ENR_${exclude_non_enrolled.toUpperCase()}_FROM_${startYear}_TO_${endYear}`
const protoC = await getRedisCDS(KEY)
if (!protoC || doRefresh) {
const data = await calculateProtoC(query)
@@ -172,8 +191,8 @@ const getProtoC = async (query, doRefresh = false) => {
// used for studytrack view
const getProtoCProgramme = async (query, doRefresh = false) => {
const { include_old_attainments, exclude_non_enrolled, code } = query
const KEY = `${REDIS_KEY_PROTOC_PROGRAMME}_CODE_${code}_OLD_${include_old_attainments.toUpperCase()}_ENR_${exclude_non_enrolled.toUpperCase()}`
const { include_old_attainments, exclude_non_enrolled, code, startYear, endYear } = query
const KEY = `${REDIS_KEY_PROTOC_PROGRAMME}_CODE_${code}_OLD_${include_old_attainments.toUpperCase()}_ENR_${exclude_non_enrolled.toUpperCase()}_FROM_${startYear}_TO_${endYear}`
const protoCProgramme = await getRedisCDS(KEY)
if (!protoCProgramme || doRefresh) {
+56 -115
View File
@@ -16,7 +16,16 @@ const saveToRedis = async (data, REDIS_KEY, expire = false) => {
}
}
const getTargetStudentCounts = async ({ codes, includeOldAttainments, excludeNonEnrolled }) => {
const getTargetStudentCounts = async ({
codes,
includeOldAttainments,
excludeNonEnrolled,
startYear = 2017,
endYear = 2020,
}) => {
// only pick studyrights during which the student has enrolled at least once, whether it's
// a present or non-present enrollment
return await sequelize.query(
`
SELECT
@@ -25,39 +34,42 @@ const getTargetStudentCounts = async ({ codes, includeOldAttainments, excludeNon
ss.programme_code "programmeCode",
ss.programme_name "programmeName",
ss.programme_type "programmeType",
COUNT(ss.studentnumber) "programmeTotalStudents",
SUM(
public.is_in_target(
CURRENT_TIMESTAMP,
ss.studystartdate,
public.next_date_occurrence(ss.studystartdate), -- e.g if studystartdate = 2017-07-31 and it's now 2020-02-02, this returns 2020-07-31. However, if it's now 2020-11-01 (we've passed the date already), it returns 2021-07-31.
ss.credits,
CASE
-- HAX: instead of trying to guess which category the student is in if
-- the studyright is currently cancelled, just make the target HUGE
-- so we can subtract these from the non-goal students
WHEN ss.currently_cancelled = 1 THEN 999999999
-- credit target: 60 credits per year since starting
ELSE 60 * ceil(EXTRACT(EPOCH FROM (now() - ss.studystartdate) / 365) / 86400)
END
)
0 AS "students3y",
COUNT(ss.studentnumber) FILTER (
WHERE
public.is_in_target(
CURRENT_TIMESTAMP,
ss.studystartdate,
public.next_date_occurrence(ss.studystartdate), -- e.g if studystartdate = 2017-07-31 and it's now 2020-02-02, this returns 2020-07-31. However, if it's now 2020-11-01 (we've passed the date already), it returns 2021-07-31.
ss.credits,
CASE
-- HAX: instead of trying to guess which category the student is in if
-- the studyright is currently cancelled, just make the target HUGE
-- so we can subtract these from the non-goal students
WHEN ss.currently_cancelled = 1 THEN 999999999
-- credit target: 60 credits per year since starting
ELSE 60 * ceil(EXTRACT(EPOCH FROM (now() - ss.studystartdate) / 365) / 86400)
END
) = 1
) "students3y",
SUM(
public.is_in_target(
CURRENT_TIMESTAMP,
ss.studystartdate,
public.next_date_occurrence(ss.studystartdate),
ss.credits,
CASE
-- HAX: instead of trying to guess which category the student is in if
-- the studyright is currently cancelled, just make the target HUGE
-- so we can subtract these from the non-goal students
WHEN ss.currently_cancelled = 1 THEN 999999999
-- credit target: 45 credits per year since starting
ELSE 45 * ceil(EXTRACT(EPOCH FROM (now() - ss.studystartdate) / 365) / 86400)
END
)
COUNT(ss.studentnumber) FILTER (
WHERE
public.is_in_target(
CURRENT_TIMESTAMP,
ss.studystartdate,
public.next_date_occurrence(ss.studystartdate),
ss.credits,
CASE
-- HAX: instead of trying to guess which category the student is in if
-- the studyright is currently cancelled, just make the target HUGE
-- so we can subtract these from the non-goal students
WHEN ss.currently_cancelled = 1 THEN 999999999
-- credit target: 45 credits per year since starting
ELSE 45 * ceil(EXTRACT(EPOCH FROM (now() - ss.studystartdate) / 365) / 86400)
END
) = 1
) "students4y",
COUNT(ss.studentnumber) "programmeTotalStudents",
SUM(ss.currently_cancelled) "currentlyCancelled"
FROM (
SELECT
@@ -68,92 +80,16 @@ const getTargetStudentCounts = async ({ codes, includeOldAttainments, excludeNon
programme_type,
studystartdate,
studentnumber,
SUM(credits) credits,
SUM(credits) FILTER (WHERE NOT credits.is_old OR :include_old_attainments) credits,
CASE
WHEN SUM(currently_cancelled) > 0 THEN 1
ELSE 0
END currently_cancelled
FROM (
SELECT
org.code org_code,
org.name->>'fi' org_name,
element_details.code programme_code,
element_details.name->>'fi' programme_name,
element_details.type programme_type,
studyright.studystartdate studystartdate,
studyright.student_studentnumber studentnumber,
credit.credits credits,
CASE
WHEN studyright.canceldate IS NOT NULL AND studyright.graduated != 1 THEN 1
ELSE 0
END currently_cancelled
FROM
organization org
${
excludeNonEnrolled
? `
-- only pick studyrights during which the student has enrolled at least once, whether it's
-- a present or non-present enrollment
INNER JOIN (
SELECT DISTINCT -- remove duplicate join rows from se with DISTINCT
studyright.*
FROM
studyright
INNER JOIN (
SELECT
studentnumber,
startdate
FROM
semester_enrollments
INNER JOIN semesters
ON semester_enrollments.semestercode = semesters.semestercode
) se
ON se.studentnumber = studyright.student_studentnumber
WHERE
se.startdate IS NOT NULL
AND se.startdate >= studyright.studystartdate
) studyright
ON org.code = studyright.faculty_code`
: `
INNER JOIN studyright
ON org.code = studyright.faculty_code`
}
INNER JOIN (
-- HACK: fix updater writing duplicates where enddate has changed
SELECT DISTINCT ON (studyrightid, startdate, code, studentnumber)
*
FROM studyright_elements
) s_elements
ON studyright.studyrightid = s_elements.studyrightid
INNER JOIN element_details
ON s_elements.code = element_details.code
LEFT JOIN transfers
ON studyright.studyrightid = transfers.studyrightid
LEFT JOIN LATERAL (
SELECT
student_studentnumber,
attainment_date,
credits
FROM credit
WHERE credit.credittypecode IN (4, 9) -- Completed or Transferred
AND (credit."isStudyModule" = false OR credit."isStudyModule" IS NULL)
${
includeOldAttainments
? ''
: "AND credit.attainment_date >= studyright.studystartdate -- only include credits attained during studyright's time"
}
) credit
ON credit.student_studentnumber = studyright.student_studentnumber
WHERE
studyright.extentcode = 1 -- Bachelor's
AND org.code NOT IN ('01', 'H02955')
AND studyright.prioritycode IN (1, 30) -- Primary or Graduated
AND studyright.studystartdate IN ('2017-08-01 00:00:00+00', '2018-08-01 00:00:00+00', '2019-08-01 00:00:00+00')
AND element_details.type IN (20,30) -- programme
${!!codes && codes.length > 0 ? 'AND element_details.code IN (:codes)' : ''}
AND transfers.studyrightid IS NULL -- Not transferred within faculty
) s
FROM organization_yearly_credits credits
WHERE
(NOT :exclude_non_enrolled OR credits.enrollment_exists) AND
(:ignore_codes OR credits.programme_code IN (:codes)) AND
credits.startyear BETWEEN :start_year AND :end_year
GROUP BY (1,2), (3,4) ,5 , 6, 7
) ss
GROUP BY (1, 2), (3, 4), 5;
@@ -161,7 +97,12 @@ const getTargetStudentCounts = async ({ codes, includeOldAttainments, excludeNon
{
type: sequelize.QueryTypes.SELECT,
replacements: {
codes: codes,
codes: !codes || codes.length === 0 ? ['DUMMY'] : codes,
ignore_codes: !codes || codes.length === 0,
exclude_non_enrolled: excludeNonEnrolled,
start_year: startYear,
end_year: endYear,
include_old_attainments: includeOldAttainments,
},
}
)
@@ -200,11 +200,14 @@ const getTotalOpenUniCreditsOfCoursesBetween = async (a, b, alias = 'sum', alias
INNER JOIN organization o ON o.id = cp.organizationcode
AND cr.attainment_date BETWEEN :a AND :b
AND (cr."isStudyModule" = false OR cr."isStudyModule" IS NULL)
AND cr.credittypecode IN (4, 9) AND cp.organizationcode = 'hy-org-48645785'
AND cr.credittypecode IN (4, 9)
AND cr.is_open = true
AND cr.student_studentnumber NOT IN
(SELECT student_studentnumber FROM studyright
WHERE studyright.student_studentnumber = cr.student_studentnumber
AND cr.attainment_date BETWEEN studyright.startdate AND studyright.enddate)
(
SELECT student_studentnumber FROM studyright_elements
WHERE studyright_elements.studentnumber = cr.student_studentnumber
AND cr.attainment_date BETWEEN studyright_elements.startdate AND studyright_elements.enddate
)
GROUP BY co.id, o.code -- HAVING SUM(cr.credits) > 0
`,
{
@@ -284,6 +287,7 @@ const calculateStatusStatistics = async (unixMillis, showByYear) => {
from: new Date(startTime - diff * YEAR_TO_MILLISECONDS),
to: new Date(startTime - (diff - 1) * YEAR_TO_MILLISECONDS),
})
const yearlyAccCreditsPromises = makeYearlyCreditsPromises(
startYear,
yearRange,
@@ -342,7 +346,7 @@ const calculateStatusStatistics = async (unixMillis, showByYear) => {
if (!regexresult) return code
return regexresult[1]
}
// console.log('yearly open uni acc credits: ', yearlyOpenUniAccCredits)
const fromAyToNormalCourses = [
...new Set(_.flatten(yearlyOpenUniAccCredits).map(c => unifyOpenUniversity(c.code))),
...new Set(_.flatten(yearlyOpenUniTotalCredits).map(c => unifyOpenUniversity(c.code))),
@@ -1,4 +1,5 @@
const _ = require('lodash')
const Sentry = require('@sentry/node')
const {
dbConnections: { sequelize },
} = require('../../database/connection')
@@ -95,10 +96,13 @@ const calculateStatusGraduated = async (unixMillis, showByYear) => {
const org = orgs.find(o => o.code === curr.faculty_code)
if (org) {
acc[curr.faculty_code] = { name: org.name, yearly: {}, drill: {} }
} else {
Sentry.captureException(new Error(`Failed to find org for faculty code: ${curr.faculty_code}`))
}
}
if (!acc[curr.faculty_code]['yearly'][data.year])
if (!acc[curr.faculty_code]['yearly'][data.year]) {
acc[curr.faculty_code]['yearly'][data.year] = { acc: 0, total: 0 }
}
// init programme
if (!acc[curr.faculty_code]['drill'][curr.code]) {
const element = elements.find(e => e.code === curr.code)
@@ -130,7 +134,11 @@ const calculateStatusGraduated = async (unixMillis, showByYear) => {
// if no accumulated init faculty
if (!mankeled[curr.faculty_code]) {
const org = orgs.find(o => o.code === curr.faculty_code)
mankeled[curr.faculty_code] = { name: org.name, yearly: {}, drill: {} }
if (org) {
mankeled[curr.faculty_code] = { name: org.name, yearly: {}, drill: {} }
} else {
Sentry.captureException(new Error(`Failed to find org for faculty code: ${curr.faculty_code}`))
}
}
// if no accumulated init programme
if (!mankeled[curr.faculty_code]['drill'][curr.code]) {
+28 -2
View File
@@ -7,10 +7,13 @@ const { sendNotificationAboutNewUser } = require('../services/mailservice')
const { checkStudyGuidanceGroupsAccess, getAllStudentsUserHasInGroups } = require('../services/studyGuidanceGroups')
const _ = require('lodash')
const { User, UserElementDetails, AccessGroup, UserFaculties, sequelizeUser } = require('../models/models_user')
const { getOrganizationAccess } = require('../util/organizationAccess')
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({
@@ -190,8 +193,20 @@ const formatUser = async userFromDb => {
const rights = _.uniqBy([...programmes, ...enrichProgrammesFromFaculties(faculties)])
const iamRights = Object.keys(await getOrganizationAccess(userFromDb)).filter(
programmeCode => !rights.includes(programmeCode)
)
const {
dataValues: { id, username: userId, full_name: name, email, language, sisu_person_id: sisPersonId },
dataValues: {
id,
username: userId,
full_name: name,
email,
language,
sisu_person_id: sisPersonId,
iam_groups: iamGroups,
},
} = userFromDb
const studentsUserCanAccess = _.uniqBy(
@@ -205,9 +220,11 @@ const formatUser = async userFromDb => {
name,
language,
sisPersonId, //
iamGroups,
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'),
@@ -240,6 +257,7 @@ const getUser = async ({ username, name, email, iamGroups, sisId }) => {
email,
language,
sisu_person_id: sisId,
iam_groups: iamGroups,
last_login: new Date(),
})
@@ -251,9 +269,17 @@ const getUser = async ({ username, name, email, iamGroups, sisId }) => {
let newAccessGroups = []
if (await checkStudyGuidanceGroupsAccess(sisId)) newAccessGroups.push('studyGuidanceGroups')
if (iamGroups.includes(courseStatisticsGroup)) newAccessGroups.push('courseStatistics')
if (iamGroups.includes(facultyStatisticsGroup) || iamGroups.some(element => /-jory$/.test(element)))
newAccessGroups.push('facultyStatistics')
if (iamGroups.includes(hyOneGroup) || currentAccessGroups.includes('teachers')) newAccessGroups.push('teachers')
if (iamGroups.includes(toskaGroup) || currentAccessGroups.includes('admin')) newAccessGroups.push('admin')
if (_.difference(newAccessGroups, currentAccessGroups).length > 0) {
if (iamGroups.includes(openUniGroup)) newAccessGroups.push('openUniSearch')
// In the difference method "the order and references of result values are determined by the first array." https://lodash.com/docs/4.17.15#difference
// Both directions needs to be checked in order to update roles when the access should no longer exists.
if (
_.difference(newAccessGroups, currentAccessGroups).length > 0 ||
_.difference(currentAccessGroups, newAccessGroups).length > 0
) {
userFromDb.setAccessgroup(
(
await AccessGroup.findAll({
+239
View File
@@ -0,0 +1,239 @@
const {
isSuperAdminIam,
isAdminIam,
isUniversityWideIam,
isDoctoralIam,
getStudyLeaderGroup,
iamToOrganisationCode,
isEmployeeIam,
iamToDoctoralSchool,
kosuIamToFaculties,
dekaaniIamToFaculty,
opetusVaradekaani,
} = require('../../config/IAMConfig')
const { data } = require('../../config/data')
const { mapToDegreeCode } = require('../../config/common')
/**
* Return given access to all programmes where predicate is true
* (all if no predicate defined)
* @param {object} accessLevel
* @param {(program: object) => boolean} where
* @returns {object} access to programmes
*/
const getAllProgrammeAccess = (accessLevel, where) => {
const access = {}
data.forEach(faculty => {
faculty.programmes.forEach(program => {
if (where?.(program) === false) return
access[program.key] = { ...accessLevel }
})
})
return access
}
/**
* NOT USED
* Grant super-admin rights if the user has correct iams (eg. grp-toska)
* @param {string[]} hyGroups
* @returns superAdmin special group
*/
const getSuperAdmin = hyGroups => {
const isToska = hyGroups.some(isSuperAdminIam)
if (isToska) {
return { specialGroup: { superAdmin: true } }
}
return {}
}
/**
* NOT USED
* Grant admin rights if the user has correct iams (eg. grp-ospa)
* @param {string[]} hyGroups
* @returns admin special group
*/
const getAdmin = hyGroups => {
const isOspa = hyGroups.some(isAdminIam)
if (isOspa) {
return { specialGroup: { admin: true } }
}
return {}
}
/**
* Grant reading rights to all programmes if user has uni wide IAM (eg. hy-rehtoraatti)
* @param {string[]} hyGroups
* @returns read access to ALL programmes
*/
const getUniversityReadingRights = hyGroups => {
const hasUniversityReadingRights = hyGroups.some(isUniversityWideIam)
if (!hasUniversityReadingRights) {
return {}
}
const access = getAllProgrammeAccess({ read: true })
const specialGroup = { allProgrammes: true }
return { access, specialGroup }
}
/**
* Grant reading rights to programmes of faculties if user is kosu or dekaanaatti of some faculties
* @param {string[]} hyGroups
*/
const getFacultyReadingRights = hyGroups => {
// faculty codes from kosu iam
const facultyCodes = hyGroups
.flatMap(kosuIamToFaculties)
// faculty codes from dekanaatti iam
.concat(hyGroups.map(dekaaniIamToFaculty))
.filter(Boolean)
const access = {}
facultyCodes.forEach(fc => {
const faculty = data.find(faculty => faculty.code === fc)
const programmeCodes = faculty.programmes.map(p => p.key)
programmeCodes.forEach(code => {
access[code] = { read: true }
})
})
return { access, specialGroup: {} }
}
/**
* Grant admin rights to faculty programmes if user is opetusvaradekaani of that faculty
* @param {string[]} hyGroups
*/
const getFacultyAdminRights = hyGroups => {
if (!hyGroups.includes(opetusVaradekaani)) return {}
const facultyCodes = hyGroups.map(dekaaniIamToFaculty).filter(Boolean)
const access = {}
facultyCodes.forEach(fc => {
const faculty = data.find(faculty => faculty.code === fc)
const programmeCodes = faculty.programmes.map(p => p.key)
programmeCodes.forEach(code => {
access[code] = { read: true, write: true, admin: true }
})
})
return { access, specialGroup: {} }
}
/**
* Grant reading rights to all doctoral programmes if the user belongs to doctoral IAM
* @param {string[]} hyGroups
* @returns read access to ALL doctoral programs
*/
const getDoctoralAccess = hyGroups => {
const hasDoctoralReadingRights = hyGroups.some(isDoctoralIam)
if (!hasDoctoralReadingRights) return {}
const access = getAllProgrammeAccess({ read: true }, program => program.level === 'doctoral')
const specialGroup = { doctoral: true }
return { access, specialGroup }
}
/**
* Grants reading rights to all doctoral programmes that belong to user's
* doctoral school IAMs
* @param {string[]} hyGroups
* @returns read access to doctoral programs
*/
const getDoctoralSchoolAccess = hyGroups => {
const doctoralProgrammeCodes = hyGroups.flatMap(iamToDoctoralSchool)
const access = {}
doctoralProgrammeCodes.forEach(code => {
if (!code) return
access[code] = { read: true }
})
return { access }
}
/**
* Grant admin access if the user belongs to studyprogramme's manager group and is a study program leader
* @param {string[]} hyGroups
*/
const getProgrammeAdminAccess = hyGroups => {
const orgCodes = hyGroups
.filter(iam => hyGroups.includes(getStudyLeaderGroup(iam)))
.map(iam => iamToOrganisationCode(iam))
.filter(Boolean)
const degreeCodes = orgCodes.flatMap(codes => codes.map(mapToDegreeCode))
if (!degreeCodes?.length > 0) {
return {}
}
const access = {}
degreeCodes.forEach(code => {
access[code] = { read: true, write: true, admin: true }
})
return { access }
}
/**
* Grant write and read access if the user belongs to employees group and studyprogramme's manager group
* @param {string[]} hyGroups
*/
const getProgrammeWriteAccess = hyGroups => {
if (!hyGroups.some(isEmployeeIam)) return {}
const orgCodes = hyGroups.map(iam => iamToOrganisationCode(iam)).filter(Boolean)
const degreeCodes = orgCodes.flatMap(codes => codes.map(mapToDegreeCode))
const access = {}
degreeCodes.forEach(code => {
if (!code) return
access[code] = { read: true, write: true }
})
return { access }
}
/**
* Grant read access if the user belongs to studyprogramme's manager group
* @param {string[]} hyGroups
*/
const getProgrammeReadAccess = hyGroups => {
const orgCodes = hyGroups.map(iam => iamToOrganisationCode(iam)).filter(Boolean)
const degreeCodes = orgCodes.flatMap(codes => codes.map(mapToDegreeCode))
const access = {}
degreeCodes.forEach(code => {
if (!code) return
access[code] = { read: true }
})
return { access }
}
/**
* Gets access rights and special groups,
* based on IAM-groups in IAM header string
* @param {string} hyGroupsHeader
*/
const getIAMRights = hyGroups => {
let access = {}
let specialGroup = {}
;[
getUniversityReadingRights,
getFacultyReadingRights,
getDoctoralAccess,
getDoctoralSchoolAccess,
getProgrammeReadAccess,
getProgrammeWriteAccess,
getProgrammeAdminAccess,
getFacultyAdminRights,
getAdmin,
getSuperAdmin,
]
.map(f => f(hyGroups))
.forEach(({ access: newAccess, specialGroup: newSpecialGroup }) => {
access = { ...access, ...newAccess }
specialGroup = { ...specialGroup, ...newSpecialGroup }
})
return { access, specialGroup }
}
module.exports = {
getIAMRights,
}
@@ -0,0 +1,190 @@
/* eslint-disable */
const { Credit, Enrollment, Studyright } = require('../models')
const moment = require('moment')
const { Op } = require('sequelize')
const hasValidStudyright = (studyrights, date) => {
const within = (right, date) => {
const first = moment(right.startdate)
const last = moment(right.enddate)
return first.isSameOrBefore(date) && last.isSameOrAfter(date)
}
return studyrights.find(r => within(r, date))
}
const studyrightsOf = async student => {
const rights = await Studyright.findAll({
where: {
student_studentnumber: student,
},
})
return rights
}
const uniq = objects => [...new Set(objects)]
const courseInfo = async course_code => {
const credits = await Credit.findAll({
where: {
course_code,
is_open: true,
grade: {
[Op.in]: ['1', '2', '3', '4', '5', 'Hyv.'],
},
attainment_date: {
[Op.gte]: moment('8-1-2020', 'MM-DD-YYYY').toDate(),
},
},
})
const enrollments = await Enrollment.findAll({
where: {
course_code,
is_open: true,
enrollment_date_time: {
[Op.lte]: moment('8-1-2022', 'MM-DD-YYYY').toDate(),
[Op.gte]: moment('8-1-2020', 'MM-DD-YYYY').toDate(),
},
},
})
let avoin_credited = []
for (let credit of credits) {
const { attainment_date, student_studentnumber: studentnumber } = credit
const rights = await studyrightsOf(studentnumber)
if (!hasValidStudyright(rights, attainment_date)) {
avoin_credited.push(studentnumber)
}
}
let avoin_enrolled = []
for (let enrollment of enrollments) {
const { enrollment_date_time, studentnumber } = enrollment
const rights = await studyrightsOf(studentnumber)
if (!hasValidStudyright(rights, enrollment_date_time)) {
avoin_enrolled.push(studentnumber)
}
}
avoin_credited = uniq(avoin_credited)
avoin_enrolled = uniq(avoin_enrolled)
return {
credits,
enrollments,
avoin_credited,
avoin_enrolled,
}
}
const unifiedCourseInfo = async course_code => {
const a = await courseInfo(course_code)
const b = await courseInfo('AY' + course_code)
avoin_credited = uniq(a.avoin_credited.concat(b.avoin_credited))
avoin_enrolled = uniq(a.avoin_enrolled.concat(b.avoin_enrolled))
return {
credits: uniq(a.credits.concat(b.credits)),
enrollments: uniq(a.enrollments.concat(b.enrollments)),
avoin_credited,
avoin_enrolled,
only_enrolled: avoin_enrolled.filter(e => !avoin_credited.includes(e)),
}
}
const statsOf = async courses => {
console.log(Object.keys(courses).join('\n'))
console.log()
let students = []
for (let course_code of Object.keys(courses)) {
const { credits, enrollments, avoin_enrolled, avoin_credited, only_enrolled } = await unifiedCourseInfo(course_code)
courses[course_code] = { credits, enrollments, avoin_enrolled, avoin_credited, only_enrolled }
students = uniq(students.concat(avoin_enrolled).concat(avoin_credited))
}
const rows = []
for (let s of students) {
let string = s + ' '
let score = 0
for (let course_code of Object.keys(courses)) {
const { enrollments, avoin_credited, only_enrolled } = courses[course_code]
let course_info = '-'
if (avoin_credited.includes(s)) {
course_info = 'X'
score += 10
}
if (only_enrolled.includes(s)) {
const studentsEnrolments = enrollments.filter(e => e.studentnumber === s)
course_info = '' + studentsEnrolments.length
score += 1
}
string += course_info + ' '
}
const rights = await studyrightsOf(s)
const stRight = hasValidStudyright(rights, new Date('2022-8-1')) ? 'yes' : 'no'
string += stRight
rows.push({ string, score })
}
const byScore = (a, b) => b.score - a.score
console.log(
rows
.sort(byScore)
.map(r => r.string)
.join('\n')
)
}
const main = async () => {
const courses_psyk_a = {
'PSYK-111': null,
'PSYK-121': null,
'PSYK-131': null,
'PSYK-141': null,
'PSYK-181': null,
}
const courses_psyk_p = {
'PSYK-251': null,
'PSYK-221': null,
'PSYK-232': null,
'PSYK-226': null,
'PSYK-241': null,
'PSYK-212': null,
}
const courses_hist = {
'HISK-110': null,
'HISK-120': null,
'HISK-131A': null,
'HISK-131': null,
'HISK-141': null,
'HISK-142': null,
}
await statsOf(courses_psyk_a)
console.log()
console.log()
await statsOf(courses_psyk_p)
console.log()
console.log()
await statsOf(courses_hist)
process.exit()
}
main()
@@ -0,0 +1,96 @@
/* 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 normalizeOrganizationCode = r => {
if (!r.includes('_')) {
return r
}
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
}
const getOrganizationAccess = async user => {
const access = {
...(await getLomakeAccess(user)),
...getOrganizationAccessFromIamGroups(user),
}
return access
}
module.exports = {
getOrganizationAccess,
}
+11 -4
View File
@@ -8,7 +8,7 @@ const getSemester = date => (isSpring(date) ? 'SPRING' : 'FALL')
const semesterStart = {
SPRING: '01-01',
FALL: '07-31',
FALL: '08-01',
}
const semesterEnd = {
@@ -29,12 +29,19 @@ const getPassingSemester = (startYear, date) => {
return yearCount < 6 ? `${yearCount}-${semester}` : 'LATER'
}
const getAcademicYearDates = (year, yearsCombined) => {
const getAcademicYearDates = (year, since) => {
if (year === 'Total') {
return {
startDate: `${since.getFullYear()}-${semesterStart['FALL']}`,
endDate: `${moment(new Date()).add(1, 'years').format('YYYY')}-${semesterEnd['SPRING']}`,
}
}
const startYear = typeof year === 'number' ? year : Number(year.slice(0, 4))
const correctEndYear = yearsCombined ? new Date().getFullYear() : startYear
const endYear =
typeof year === 'number' ? moment(year, 'YYYY').add(1, 'years').format('YYYY') : Number(year.slice(7, 11))
return {
startDate: `${startYear}-${semesterStart['FALL']}`,
endDate: `${moment(correctEndYear, 'YYYY').add(1, 'years').format('YYYY')}-${semesterEnd['SPRING']}`,
endDate: `${endYear}-${semesterEnd['SPRING']}`,
}
}
+7 -2
View File
@@ -1,5 +1,5 @@
const mapToProviders = elementDetails =>
elementDetails.map(r => {
const mapToProviders = elementDetails => {
return elementDetails.map(r => {
const isNumber = str => !Number.isNaN(Number(str))
if (r.includes('_')) {
const [left, right] = r.split('_')
@@ -8,8 +8,13 @@ const mapToProviders = elementDetails =>
const providercode = `${prefix}0-${suffix}`
return providercode
}
if (/^(T)[0-9]{6}$/.test(r)) {
const numbers = r.substring(1)
return `7${numbers}`
}
return r
})
}
// sort substitutions so that main code is first
const newLetterBasedCode = /^[A-Za-z]/ // new letter based codes come first
+11 -1
View File
@@ -23,7 +23,14 @@
"no-shadow": "off",
"react/jsx-props-no-spreading": "off",
"react/jsx-filename-extension": "off",
"no-async-promise-executor": "off",
"react/function-component-definition": [
2,
{
"namedComponents": "arrow-function",
"unnamedComponents": "arrow-function"
}
],
"default-param-last": "off",
"no-implied-eval": "off",
"react/state-in-constructor": "off",
"react/static-property-placement": "off",
@@ -31,6 +38,9 @@
"react/no-unescaped-entities": "off",
"react/prop-types": "off",
"react/prefer-stateless-function": "error",
"react/no-unstable-nested-components": "off",
"react/jsx-no-constructed-context-values": "off",
"react/jsx-no-useless-fragment": "off",
"import/no-unresolved": "off",
"react/jsx-no-bind": "error",
"prefer-object-spread/prefer-object-spread": "error",
+25249 -3173
View File
File diff suppressed because it is too large Load Diff

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