Compare commits

...
562 Commits
Author SHA1 Message Date
Otto Ahoniemi 055492b76b Add admin user to always have admin rights 2022-01-13 08:56:22 +02:00
Otto Ahoniemi c761899962 Fix populations failing because of invalid admin role check 2022-01-13 08:53:55 +02:00
Otto Ahoniemi 998bb3a453 Promote custom populations to navbar & fix normal populations being show to studyguidancegroup users 2022-01-13 08:06:53 +02:00
Otto Ahoniemi b9e9c89376 Fix embarassing bug where I forgot to return mocked user from function 2022-01-13 07:55:46 +02:00
Otto Ahoniemi 943a2145e9 Add toska group for mluukkai dev user 2022-01-13 07:35:27 +02:00
Otto Ahoniemi 4fef6a8582 Cleanup bunch of extra logic from userservice 2022-01-13 07:35:27 +02:00
Otto Ahoniemi 4cf3003dec Race when waiting for studyguidancegroup answer from importer 2022-01-13 07:35:27 +02:00
Otto Ahoniemi ec47fab422 Cleanup user and error handling for feedback 2022-01-13 07:35:27 +02:00
Otto Ahoniemi c391d7786b Merge roles and rights to be part of user object in request 2022-01-13 07:35:27 +02:00
Mitja Karhusaari cb0e4eae16 Hyvää synttäriä, I guess 🤷‍♂️ 🍰 2022-01-11 15:23:28 +02:00
Mitja Karhusaari 3b10c19a81 Fix tests after FilterCard open/close logic change 2022-01-11 15:23:28 +02:00
Mitja Karhusaari c60955c133 Fix chaining of the .cs cypress command 2022-01-11 15:23:28 +02:00
Mitja Karhusaari 14fba8d8ba Open filter options automatically if changed 2022-01-11 15:23:28 +02:00
Petri Palmu 9843e9fafa courses of population students are great again 2022-01-11 14:33:08 +02:00
Petri Palmu 08a51b05b1 Merge branch 'master' of github.com:UniversityOfHelsinkiCS/oodikone 2022-01-11 11:46:47 +02:00
Mitja Karhusaari 1a7df991e8 Remove tag and study track selectors from population search view 2022-01-11 11:14:43 +02:00
Mitja Karhusaari 2391062e71 Do not fetch tags for study track if query does not specify a study track 2022-01-11 11:09:13 +02:00
Mitja Karhusaari f0c179e23d Fix usage of the isBetween method in the context of study rights 2022-01-11 11:09:13 +02:00
Petri Palmu b183c438bb added bscs regex in code sorting order 2022-01-06 20:21:34 +02:00
Petri Palmu 60db28f4bb fix with null check for subs in services population 2022-01-05 10:09:00 +02:00
Mitja Karhusaari e8be70bf9f Make linter happy 2022-01-05 02:06:08 +02:00
Mitja Karhusaari 7b60bc220c Improve grade filter fault tolerance 2022-01-05 01:59:15 +02:00
Mitja Karhusaari 44dfaf20a6 Adjust tests 2022-01-05 01:50:43 +02:00
Mitja Karhusaari 009a9f5397 Implement tests for the modified filters 2022-01-05 01:49:45 +02:00
Mitja Karhusaari 5ac2db44e3 Change study track filter to consider only the currently active study rights by default 2022-01-05 01:20:50 +02:00
Mitja Karhusaari 337b1f05e4 Generalize the implementation of programme filter and add it to all views 2022-01-05 01:20:50 +02:00
Mitja Karhusaari 537d9ecc33 Display a message when opening the tags filter and no tags are available 2022-01-05 01:20:50 +02:00
Mitja Karhusaari 4e9d1cd190 Display filters in alphabetical order for consistency's sake 2022-01-05 01:20:50 +02:00
Mitja Karhusaari fbe1cc93fb Implement study track filter 2022-01-05 01:20:50 +02:00
Petri Palmu 12c3c9430d little correction to the test 2022-01-05 01:05:00 +02:00
Petri Palmu 6251b69feb lint fixes 2022-01-04 23:33:03 +02:00
Petri Palmu 6960b67bfb changed anon oodi sis-db data 2022-01-04 23:26:05 +02:00
Otto AhoniemiandGitHub cb3da3d1bc Update anon db guide + fix some typos
Some of these things are now part of CI + there were some possible places for misunderstandings.
2022-01-04 17:49:17 +02:00
Otto Ahoniemi 79df421846 Remove old forwarders used for sisu api transition 2022-01-04 16:35:43 +02:00
Otto Ahoniemi d32c3c9bd9 Remove references to decodedToken in backend 2022-01-04 16:35:43 +02:00
Otto Ahoniemi af38c9fc97 Transform roles and admin thingys already in backend 2022-01-04 16:35:43 +02:00
Otto Ahoniemi 843fc4c8ec Merge usage of userRoles and roles 2022-01-04 16:35:43 +02:00
Petri Palmu 97105588df restore tests 2022-01-04 14:22:30 +02:00
Petri Palmu 6a68dd7817 Merge branch 'master' of github.com:UniversityOfHelsinkiCS/oodikone 2022-01-04 13:04:34 +02:00
Petri Palmu acfd918d80 removed more temp ay code 2022-01-04 13:03:31 +02:00
Otto Ahoniemi 3aea117f2f Classic happy birthday to this one guy on the dataset -change 2022-01-04 12:06:26 +02:00
Otto Ahoniemi f290a1a3d3 Stop intercepting /superlogin path on user tests 2022-01-04 11:53:22 +02:00
Otto Ahoniemi 93db8810a3 Run migrations against built images in ci pipeline 2022-01-04 11:10:30 +02:00
Otto Ahoniemi 4675f52c2d Unify user-db and kone-db connections, fix circular logging error 2022-01-04 11:07:22 +02:00
Petri Palmu 73c29b2edf temporary ay codes removed 2022-01-03 17:42:13 +02:00
Petri Palmu 5102ba4ab3 little change to the tests 2022-01-03 12:18:14 +02:00
Petri Palmu 87746582c3 changed language distribution info text 2022-01-03 12:07:39 +02:00
Petri Palmu 2b739e9ff3 course statistics find courses works 2022-01-03 12:07:12 +02:00
Petri Palmu 3d32244238 added migration for the new avoin studyright 2022-01-03 11:45:12 +02:00
Otto Ahoniemi 3380b41e60 Refactor sending mails to users 2021-12-30 17:41:02 +02:00
Otto Ahoniemi a07129d704 Migrate sentryuser setting to current user middleware 2021-12-30 17:30:05 +02:00
Otto Ahoniemi 5adfaa6858 Implement new superlogin 2021-12-30 17:04:53 +02:00
Otto Ahoniemi 07d6ffb7e2 Timeout if importer is unresponsive 2021-12-30 17:04:53 +02:00
Otto Ahoniemi b025526e6b Re-implement auth middleware and fix some other middleware thingys 2021-12-30 17:04:52 +02:00
Otto Ahoniemi cf4ef78cbe Cleanup ignore files 2021-12-30 17:02:45 +02:00
Otto Ahoniemi 6d3ccea413 Remove unused ping route 2021-12-30 17:02:45 +02:00
Otto Ahoniemi f2ca3b4d45 Replace old auth logic with RTK query and separate login functionality from Errorboundary 2021-12-30 17:02:45 +02:00
Otto Ahoniemi 8fa6ce72e6 Remove error container 2021-12-30 17:02:45 +02:00
Otto Ahoniemi 61dba16e6c Unify index and app setup with toskaboiler 2021-12-30 17:02:45 +02:00
Otto Ahoniemi 9b1968cf2c Refactor all token and user related redux selectors under one hook 2021-12-30 17:02:44 +02:00
Otto Ahoniemi a91494fc86 Merge userservice to backend 2021-12-30 17:02:44 +02:00
Otto Ahoniemi 4a71ce4ee9 Superlogin without token 2021-12-30 17:02:44 +02:00
Otto Ahoniemi e90dd00d4d Tokenless in backend and frontend 2021-12-30 17:02:43 +02:00
Otto Ahoniemi e24df11422 Support login without token in userservice 2021-12-30 17:00:39 +02:00
Otto AhoniemiandGitHub 220d21066b Log headers temporarily 2021-12-30 14:27:58 +02:00
Saara Satokangas b80b5e5097 Make credit progress table more compact 2021-12-30 11:02:49 +02:00
Saara Satokangas 88dfb372d3 Fix studytrack overview chart colors 2021-12-30 10:59:07 +02:00
Saara Satokangas dc8edb38e5 Clean up & add some clarifying comments 2021-12-30 10:48:34 +02:00
Saara Satokangas 5edaa3abd9 Style the studytrack table better 2021-12-30 10:13:09 +02:00
Saara Satokangas 20da53a22e Add option of all the students back to the selector 2021-12-30 10:07:05 +02:00
Saara Satokangas f598536b6d Exclude tracks with empty stats from studytrack options 2021-12-30 09:21:17 +02:00
Saara Satokangas d2862364db Make studytrack overview work with other programmes 2021-12-30 08:39:10 +02:00
Saara Satokangas b5b032c33c Enable single studytrack for the maintable in front 2021-12-29 16:31:37 +02:00
Petri Palmu f86dce3fb8 Fixes to the codeless avoin 2021-12-29 16:04:44 +02:00
Saara Satokangas b34a38e479 Add data for the main studytrack table by studytrack 2021-12-29 15:58:42 +02:00
Saara Satokangas ba7a0cd531 Add a no-data-message when no studytrack data 2021-12-29 12:37:06 +02:00
Saara Satokangas 57d98d33aa Enable selecting data only for some studytrack 2021-12-29 12:07:54 +02:00
Saara Satokangas f8d17179a9 Add exporting possibility to the studytrack graph 2021-12-29 09:35:01 +02:00
Saara Satokangas c8c6565208 Fix studytrack-selector overflowing 2021-12-29 09:24:57 +02:00
Saara Satokangas f207aee266 Add infotexts for the progress chart and table 2021-12-29 08:37:52 +02:00
Saara Satokangas 8c20daf686 Style the graph container for studytrack overview graph 2021-12-28 15:41:30 +02:00
Saara Satokangas 8bddad1795 Separate programmes stat -counting and studytrack stat -counting 2021-12-28 15:39:05 +02:00
Saara Satokangas 04b5627f8d Move academic year startDate and endDate defining into its own function 2021-12-28 15:11:08 +02:00
Saara Satokangas d89799cb89 Add loader to the new studytrack overview 2021-12-28 15:01:50 +02:00
Saara Satokangas ca35323b50 Initial adding of data to the credits graph in studytrackoverview 2021-12-28 14:48:38 +02:00
Saara Satokangas 3db15898d8 Clean up css for studytrack overview 2021-12-28 11:17:31 +02:00
Saara Satokangas b264685b16 Show progress only for the entire studyprogramme
Not individual studytracks at first
2021-12-28 11:12:31 +02:00
Saara Satokangas 6713d45f98 Add credit-stats to the table 2021-12-28 11:01:32 +02:00
Saara Satokangas 89e8c4508f Add counting student progress in the backend 2021-12-28 10:46:18 +02:00
Saara Satokangas 47f70469f6 Add basis for the credit-tables in studytrack-overview 2021-12-28 10:08:33 +02:00
Saara Satokangas ec1e77ddbb Add some unique keys to the datatable 2021-12-28 09:46:18 +02:00
Saara Satokangas 5446befb81 Simplify how names are fetched for studytracks 2021-12-28 09:36:05 +02:00
Saara Satokangas 85a56083f4 Don't show studytracks by default 2021-12-28 08:53:11 +02:00
Saara Satokangas 137ca05095 Add track codes to studytrack names 2021-12-28 08:48:38 +02:00
Saara Satokangas 36b8c81ec1 Map studytrack names to the selector 2021-12-28 08:48:38 +02:00
Saara Satokangas 33c0210347 Only show arrows if there is data hidden 2021-12-28 08:48:14 +02:00
Saara Satokangas 8da67b2998 Most recent year first in studytrack overview 2021-12-28 08:48:14 +02:00
Saara Satokangas f0864bcd33 Add infobox-text for the new studytrack-overview 2021-12-28 08:48:14 +02:00
Saara Satokangas 5634d23a51 Add percentages to the studytrack-table 2021-12-28 08:48:14 +02:00
Saara Satokangas c2008b634c Exclude studytracks with no students from the datatable 2021-12-28 08:48:14 +02:00
Otto Ahoniemi e7783af0f9 Drop affiliations from userservice 2021-12-27 14:51:59 +02:00
Petri Palmu 229904187e course statistics find courses works 2021-12-23 08:54:52 +02:00
Saara Satokangas a0e92a1944 Add studyright startdates to the excel export 2021-12-21 15:42:11 +02:00
Saara Satokangas b4468d3713 Change how totals are shown for the new studytrack overview 2021-12-21 15:02:11 +02:00
Saara Satokangas e94ccaae3e Move new studyprogramme tabs to be the last ones 2021-12-21 14:52:33 +02:00
Saara Satokangas 0dfeaff1f9 Add initial datatable for studytrack data 2021-12-21 14:50:30 +02:00
Saara Satokangas 39cc589acc Initial version for mankeling studytrack-data
This should still be cleaned up, but just saving it here before
it gets deleted
2021-12-21 14:01:47 +02:00
Petri Palmu 88a14f235b Merge branch 'master' into feature/newAvoin 2021-12-21 10:45:40 +02:00
Petri Palmu 67736b295b added migration for the new avoin studyright 2021-12-21 10:44:31 +02:00
Saara Satokangas ee11c3b47d Barebones backend pipeline for studytracks-data 2021-12-20 11:20:16 +02:00
Saara Satokangas 9dd9e41001 Combine infotexts for graduation mean and median times 2021-12-20 09:02:34 +02:00
Saara Satokangas e467689e27 Initial commit for new Studytrack-overview 2021-12-20 08:58:22 +02:00
Saara Satokangas d0a23b6038 Add toggle for choosing average graduation time 2021-12-20 08:12:44 +02:00
Saara Satokangas 42a5bff039 Remove enrolled-stats from the graphs for now 2021-12-20 07:42:51 +02:00
Otto Ahoniemi d601dbe597 Run all the filter tests again 2021-12-17 00:15:51 +02:00
Otto Ahoniemi e356186152 Remove sis warning flag toggle since it's not used anymore 2021-12-16 23:17:13 +02:00
Otto Ahoniemi 4c6153c76d Restore couple of student tests and test that student page doesn't crash without studyrights or courses 2021-12-16 23:17:10 +02:00
Otto Ahoniemi abebc5d410 Start adding separate and agreement studyrights to sis-db 2021-12-16 21:45:56 +02:00
Otto Ahoniemi 3dcb5f299c Fix credit graph failing if student has no studyrights 2021-12-16 21:19:39 +02:00
Otto Ahoniemi 0632fa94f7 Don't crash if study guidance groups not available 2021-12-16 19:57:28 +02:00
Mitja Karhusaari ed85513290 Make courses filter resilient against missing course information 2021-12-16 12:47:25 +02:00
Mitja Karhusaari 07ad8cb434 Use rtk query to fetch course statistics 2021-12-16 12:47:25 +02:00
Mitja Karhusaari 901b719888 Second try at the previous 2021-12-16 03:03:37 +02:00
Mitja Karhusaari 9850faf400 Don't show custom population filters before population has been loaded
Resolves #3341
2021-12-16 02:17:21 +02:00
Mitja Karhusaari 176eca36eb Refetch population course data whenever filter results change 2021-12-16 01:38:07 +02:00
Saara Satokangas 77d5435c30 Start using the course_unit_type in the new studyprogramme-view 2021-12-14 16:17:00 +02:00
Saara Satokangas 3f99df4971 Add course_unit_type to customAttainmentParser in updater 2021-12-14 09:18:50 +02:00
Saara Satokangas 819ba0db15 Add course_unit_type to the course-mapper in updater 2021-12-14 09:06:21 +02:00
Mitja Karhusaari e8871cc845 Make the date selection not persistent 2021-12-10 17:26:55 +02:00
Mitja Karhusaari e2c152ddc1 Extract common functionality from status pages 2021-12-10 16:58:09 +02:00
Mitja Karhusaari 63c9d316d0 Move info texts from component files to under the InfoToolTips directory 2021-12-10 08:04:49 +02:00
Mitja Karhusaari 6b1ff9e9cb Fix graduation trends page on initial page load 2021-12-10 07:47:12 +02:00
Petri Palmu 7f913c9927 Merge branch 'master' of github.com:UniversityOfHelsinkiCS/oodikone 2021-12-09 13:58:00 +02:00
Petri Palmu b7616ec2ed moved info texts to commons 2021-12-09 11:54:14 +02:00
Mitja Karhusaari d0840a7de3 Fix data export 2021-12-09 11:12:51 +02:00
Petri Palmu cc214179f6 solved merge conflicts with pukinkontti 2021-12-09 10:45:13 +02:00
Otto Ahoniemi cbfe5aedc1 Fix workflow job dependencies 2021-12-09 02:35:08 +02:00
Otto Ahoniemi c0b61d6d4e Run migrations also for anon user-db and kone-db 2021-12-09 02:33:09 +02:00
Otto Ahoniemi dd28ee3c48 Run migration push also when test yaml changes + fix script trying to run down docker-compose 2021-12-09 02:24:09 +02:00
Otto Ahoniemi 3f5e1e53b9 Add sis person ids to students 2021-12-09 02:14:15 +02:00
Otto Ahoniemi a5ef63bc2f Add creds 2021-12-09 02:04:55 +02:00
Mitja Karhusaari 5f7f496472 Copy settings and usage instruction components from attainment status page 2021-12-09 02:01:00 +02:00
Mitja Karhusaari 8058f8ca0a Use the DrillStack component on the graduation trends page 2021-12-09 02:01:00 +02:00
Mitja Karhusaari 6fd5304414 Separate drilling into a separate component 2021-12-09 02:01:00 +02:00
Otto Ahoniemi 2fde626606 Remove garbage column from student model 2021-12-09 02:00:16 +02:00
Otto Ahoniemi 75ff72e545 Fix typo in ci docker-compose container name and add profile so sis-updater won't be run by default 2021-12-09 01:56:18 +02:00
Otto Ahoniemi 8b80f11728 Build anon sis-db and push to toska hub if there are new sis-db migrations 2021-12-09 01:42:16 +02:00
Otto Ahoniemi 4648f49ebc Fix lint staged shell linter failing because it was being ran inside alpine container 2021-12-09 01:31:59 +02:00
Otto Ahoniemi 92fbe3b2d3 Move anonyymioodi scripts and readme to this main repo 2021-12-09 01:31:29 +02:00
Otto Ahoniemi 1e6c418c7e Hotfix user right check when user has rights for programme and studyguidancegroups 2021-12-09 01:00:30 +02:00
Otto Ahoniemi c120b9c6a9 Add note about tests failing if anon db hasn't been updated manually 2021-12-08 23:12:39 +02:00
Saara Satokangas ec7d77dca0 Add course_unit_type to course-model 2021-12-08 14:02:51 +02:00
Saara Satokangas 4a15d97207 Show Studyprogramme options for others than admins as well 2021-12-08 13:03:06 +02:00
Saara Satokangas 40bb1dbc3e Make new studyprogramme view to admins with teachers-role 2021-12-08 13:03:06 +02:00
Saara Satokangas ad95ab6ce3 Initial version of adding enrolled students to the graphs 2021-12-08 13:03:06 +02:00
Saara Satokangas 4d0a62f26c Separate helpers, stats and db-queries for newStudyprogramme
And try to avoid modules with 500 rows
2021-12-08 13:00:11 +02:00
Saara Satokangas f30b415f05 Add loader to new studyprogramme overview 2021-12-08 13:00:11 +02:00
Saara Satokangas c0de93ce55 Add total-column to credits of the studyprogramme 2021-12-08 13:00:11 +02:00
Saara Satokangas 6560e7bf26 Adjust data table size 2021-12-08 13:00:11 +02:00
Saara Satokangas 4d782f9ec0 Correct order for radio-labels 2021-12-08 12:59:42 +02:00
Saara Satokangas 62dcd0a4ca Enable fetching graduation and thesis status by academic year 2021-12-08 12:59:42 +02:00
Saara Satokangas 994cb180a0 Enable getting credits stats also by academic year 2021-12-08 12:59:42 +02:00
Saara Satokangas a28a1b0a6c Add possibility to get basic stats by academic year 2021-12-08 12:59:42 +02:00
Petri Palmu 2a5c690ab5 added test for the language filter 2021-12-08 12:19:32 +02:00
Saara SatokangasandGitHub d7e739e20e Update importer-path for api-gateway 2021-12-08 10:04:35 +02:00
Mitja Karhusaari e2c77fe63b Revert changes to studyprogramme tests 2021-12-08 09:28:35 +02:00
Petri Palmu 39e9a3084b little correction to the language dist 2021-12-07 10:37:21 +02:00
Petri Palmu 13dc659d18 language distribution added to the course stats 2021-12-03 18:44:42 +02:00
Mitja Karhusaari aeee083d0a Implement "Show all credits" button using the new filter system 2021-12-03 04:37:04 +02:00
Mitja Karhusaari cd56b74d25 Add spinner study guidance group course stats 2021-12-03 04:26:21 +02:00
Mitja Karhusaari 77eb31f840 Fix course statistics in study guidance groups view 2021-12-03 04:25:21 +02:00
Mitja Karhusaari 1988182134 Fix course filter in study guidance groups 2021-12-03 03:55:09 +02:00
Mitja Karhusaari 1b248665c4 Reimplement #3303 using the new filter system 2021-12-03 03:44:08 +02:00
Mitja Karhusaari 942b02c8e2 Take initial filter view options into account in filter selectors 2021-12-03 03:23:58 +02:00
Mitja Karhusaari 7a34ad66e8 Improve the behavior of default and initial filter options when reseting a filter 2021-12-03 03:23:58 +02:00
Mitja Karhusaari 4fc7b97813 Fix course statistics 2021-12-03 03:23:58 +02:00
Mitja Karhusaari 77a87c5b1e Implement default filter values for study guidance groups 2021-12-03 03:23:58 +02:00
Mitja Karhusaari c7d2a552c9 Fix tests and make them pass 2021-12-03 03:23:58 +02:00
Mitja Karhusaari 63205fbfa1 Implement functionality for the "Clear All Filters" -button 2021-12-03 03:23:58 +02:00
Mitja Karhusaari df68671181 Change filter reset button look 2021-12-03 03:23:58 +02:00
Mitja Karhusaari 00cee9f14b Fix filter card layouts 2021-12-03 03:23:58 +02:00
Mitja Karhusaari 905d19f46f Update admission type filter 2021-12-03 03:23:58 +02:00
Mitja Karhusaari 019d10c938 Fix filters in study guidance groups 2021-12-03 03:23:58 +02:00
Mitja Karhusaari 7a0d91a510 Hide filter tray until a population has been selected 2021-12-03 03:23:58 +02:00
Mitja Karhusaari 10169e92a6 Fix filter view layout and tray stickiness 2021-12-03 03:23:58 +02:00
Mitja Karhusaari c74112a356 Implement actions and selectors for filters 2021-12-03 03:23:58 +02:00
Mitja Karhusaari 9dee8d4669 Implement a new filter architecture 2021-12-03 03:23:58 +02:00
Mitja Karhusaari fc875d30b6 Allow clearing of the credit date filter dates 2021-12-03 03:23:58 +02:00
Mitja Karhusaari 47fc6ea215 Change student credit column title based on the credit date filter
options
2021-12-03 03:23:58 +02:00
Saara Satokangas 26420f9a36 Add initial infobox-texts for new studyprogramme-view 2021-12-02 13:50:04 +02:00
Saara Satokangas c8f461b7f5 Add some unique keys 2021-12-02 13:31:11 +02:00
Saara Satokangas c17e53cec5 Organize stuff 2021-12-02 13:06:50 +02:00
Saara Satokangas 9cff76f498 Add possibility to export charts from new overview 2021-12-02 13:03:56 +02:00
Saara Satokangas b91991ac37 Betterments to responsiveness of the graphs 2021-12-02 12:20:24 +02:00
Saara Satokangas 9eff7057e7 Include only thesis with ATTAINED-state to the graphs
So no improvements, and one per student
2021-12-02 10:35:48 +02:00
Saara Satokangas 4c096199d2 Start using RTK Query for new studyprogramme overview 2021-12-01 20:33:10 +02:00
Saara Satokangas 9f39ee6b87 Add graduation mean and median time charts 2021-12-01 19:20:42 +02:00
Petri Palmu d06462d727 Merge branch 'master' of github.com:UniversityOfHelsinkiCS/oodikone 2021-12-01 16:36:14 +02:00
Petri Palmu e4f65407ed Merge branch 'kieli' 2021-12-01 16:36:01 +02:00
Petri Palmu 26d8f2befc language added to course statitics 2021-12-01 16:34:54 +02:00
Saara Satokangas 2713545eda Remove proptypes from Thesis Courses 2021-12-01 13:41:08 +02:00
Saara Satokangas fa619fed38 Fix Thesis Courses -tab breaking because of unselected language 2021-12-01 13:36:48 +02:00
Saara Satokangas 6d4f49879b Add graph and table for graduated and thesis-writers 2021-12-01 13:27:58 +02:00
Saara Satokangas 37aeb9ebe4 Add percentages to barchart tooltips 2021-12-01 09:34:30 +02:00
Saara Satokangas 5f5423971c Add colors to new overview charts 2021-12-01 09:34:21 +02:00
Petri Palmu 39b3fde1a0 backend ready for the language 2021-11-30 15:44:03 +02:00
Petri Palmu 382c900d28 Merge branch 'master' of github.com:UniversityOfHelsinkiCS/oodikone 2021-11-30 13:37:50 +02:00
Petri Palmu 5ac2cfb13c moved filter info texts to front common infotooltip filters 2021-11-30 13:02:02 +02:00
Otto Ahoniemi aa6d2b83f5 Revert hiding of programme filter 2021-11-29 15:08:12 +02:00
Petri Palmu 201250b8af fix that open uni course goes into substitutions only once 2021-11-29 13:56:48 +02:00
Otto Ahoniemi ff36a0e8f7 Add date to study guidance group filter set 2021-11-29 13:48:11 +02:00
Otto Ahoniemi 71c1ce703a Add better selector for datetime picker header hider 2021-11-29 13:42:27 +02:00
Otto Ahoniemi c94711234f Don't show tag filter when tags aren't available 2021-11-29 13:34:08 +02:00
Otto Ahoniemi 563d2cb4b0 Fix course tab in population student table 2021-11-29 12:59:50 +02:00
Otto Ahoniemi 3727bd03dd Set datepicker to start from 2017 + hide year header 2021-11-29 11:05:36 +02:00
Otto Ahoniemi efb0b98cdc Update README 2021-11-29 10:19:12 +02:00
Petri Palmu ef5aec5fc1 language migration added to the credit 2021-11-25 08:36:11 +02:00
Petri Palmu cdbbf06c18 added info options to the filters 2021-11-24 08:55:02 +02:00
Saara SatokangasandGitHub e471b1e73a Merge pull request #3310 from UniversityOfHelsinkiCS/new_studyprogramme
New studyprogramme overview -pukinkontti
2021-11-18 16:09:59 +02:00
Saara Satokangas 1d7f9d250a Remove waste 2021-11-18 16:01:54 +02:00
Saara Satokangas 0207e58fa3 Years in correct order for tables and graphs 2021-11-18 16:01:54 +02:00
Saara Satokangas aecca8426d Modify tooltips 2021-11-18 16:01:54 +02:00
Saara Satokangas ef0d0a5eb7 Refactor stats basis into its own function 2021-11-18 16:01:54 +02:00
Saara Satokangas 284c5b270d Rewrite how major and non-major students' credits are defined 2021-11-18 16:01:54 +02:00
Saara Satokangas 51f2a7573e Increase height of the containers 2021-11-18 16:01:54 +02:00
Saara Satokangas 0837e1d972 Add transferred credits and non-major credits to studyprogramme credits
And general betterments
2021-11-18 16:01:54 +02:00
Saara Satokangas e468186af2 Add major students credit stats to the data table 2021-11-18 16:01:54 +02:00
Saara Satokangas 3fcfe56bd0 Show credits for major students in the graph 2021-11-18 16:01:54 +02:00
Saara Satokangas 85ad68c97d Move also new studyprogramme-stuff in backend into its own file
Again, why did I not do this already last week?
2021-11-18 16:01:54 +02:00
Saara Satokangas ede3b8efac Move the new view into its own folder
Should have done this from the start
2021-11-18 16:01:54 +02:00
Saara Satokangas cd1d65b29b Add basis for fetching credit stats 2021-11-18 16:01:54 +02:00
Saara Satokangas 456fe927e7 Initial adding of credits stats to studyprogramme-view 2021-11-18 16:01:54 +02:00
Saara Satokangas df36d58288 Add datatable, clean up graph 2021-11-18 16:01:54 +02:00
Saara Satokangas 918869b307 Add also started to the initial graph 2021-11-18 16:01:54 +02:00
Saara Satokangas 2ba06ae787 Add also cancelled and transfers to the initial new graph 2021-11-18 16:01:54 +02:00
Saara Satokangas ac0dc90661 Very initial commit for new studyprogramme graphs 2021-11-18 16:01:54 +02:00
Otto AhoniemiandGitHub 8622bd8e6d Merge pull request #3307 from UniversityOfHelsinkiCS/dependabot/npm_and_yarn/services/frontend/url-parse-1.5.3
Bump url-parse from 1.5.1 to 1.5.3 in /services/frontend
2021-11-17 19:01:48 +02:00
Otto Ahoniemi 76988e3ec5 show admission type for study guidance group students as well 2021-11-17 18:59:50 +02:00
Otto Ahoniemi b0a9fb48d7 Fix some study guidance group filtering related bugs 2021-11-17 18:56:37 +02:00
Otto Ahoniemi 16d603538f Fix study guidance group layout failing when tags not set 2021-11-17 18:35:09 +02:00
Otto Ahoniemi 7dd810cad6 Allow users with study guidance group accesses to view student page 2021-11-17 18:28:59 +02:00
Otto Ahoniemi 42afea5e0c Show student statistics search if user has studyguidancegroup role 2021-11-17 17:30:32 +02:00
Otto Ahoniemi 6d0c5e552b Add study guidance group spesific stuff to generaltab 2021-11-17 16:54:00 +02:00
Otto Ahoniemi 897c2e4c88 Add variants to generaltab + render study guidance groups in a custom manner 2021-11-17 16:13:56 +02:00
Otto Ahoniemi 673ce9f9c0 Change from population booleans to variant in studenttable 2021-11-17 16:10:31 +02:00
Otto Ahoniemi 77054187a0 Refactor column if elses away from generaltab 2021-11-17 15:27:17 +02:00
Otto Ahoniemi a9c244b1f6 Remove hocs and passing unnecessary props to generaltab 2021-11-17 13:00:01 +02:00
Otto Ahoniemi ed5e2da875 Declare populationtable contents in container instead of using if elses 2021-11-17 11:48:54 +02:00
Otto Ahoniemi 25d5599466 Remove unused components 2021-11-17 11:18:09 +02:00
Otto Ahoniemi 33c0aa71d9 Destructure props on function declaration 2021-11-17 11:02:59 +02:00
Otto Ahoniemi 2e4572c871 Drop use of hocs in populationstudents 2021-11-17 11:01:28 +02:00
Otto Ahoniemi f6b14777fe Combine tabs to studenttable 2021-11-17 10:37:13 +02:00
Otto Ahoniemi f251f9456e Make studyprogramme and associated year sortable 2021-11-17 10:25:59 +02:00
Petri Palmu f364eb4f67 few changes to the population statistics tests 2021-11-16 14:01:03 +02:00
Petri Palmu 4896f9e458 Merge branch 'master' of github.com:UniversityOfHelsinkiCS/oodikone 2021-11-12 12:43:07 +02:00
Petri Palmu 096535e470 little change how updater handles open uni courses 2021-11-12 12:42:56 +02:00
Otto Ahoniemi 7c949b47a7 Initial refactoring of populationStudents from class component to functional component 2021-11-11 17:01:55 +02:00
dependabot[bot]andGitHub 2a6653c700 Bump url-parse from 1.5.1 to 1.5.3 in /services/frontend
Bumps [url-parse](https://github.com/unshiftio/url-parse) from 1.5.1 to 1.5.3.
- [Release notes](https://github.com/unshiftio/url-parse/releases)
- [Commits](https://github.com/unshiftio/url-parse/compare/1.5.1...1.5.3)

---
updated-dependencies:
- dependency-name: url-parse
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-11-11 13:47:08 +00:00
Otto AhoniemiandGitHub c3b7d96b06 Merge pull request #3304 from UniversityOfHelsinkiCS/dependabot/npm_and_yarn/services/frontend/axios-0.21.2
Bump axios from 0.21.1 to 0.21.2 in /services/frontend
2021-11-11 15:46:17 +02:00
Otto AhoniemiandGitHub 9a3783bc45 Merge pull request #3305 from UniversityOfHelsinkiCS/dependabot/npm_and_yarn/services/frontend/tmpl-1.0.5
Bump tmpl from 1.0.4 to 1.0.5 in /services/frontend
2021-11-11 15:46:10 +02:00
Otto AhoniemiandGitHub 4882d1d9d3 Merge pull request #3306 from UniversityOfHelsinkiCS/dependabot/npm_and_yarn/services/frontend/tar-6.1.11
Bump tar from 6.1.0 to 6.1.11 in /services/frontend
2021-11-11 15:46:00 +02:00
dependabot[bot]andGitHub 9575d86d9c Bump tar from 6.1.0 to 6.1.11 in /services/frontend
Bumps [tar](https://github.com/npm/node-tar) from 6.1.0 to 6.1.11.
- [Release notes](https://github.com/npm/node-tar/releases)
- [Changelog](https://github.com/npm/node-tar/blob/main/CHANGELOG.md)
- [Commits](https://github.com/npm/node-tar/compare/v6.1.0...v6.1.11)

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

Signed-off-by: dependabot[bot] <support@github.com>
2021-11-11 12:57:47 +00:00
dependabot[bot]andGitHub 85c7e86ed9 Bump tmpl from 1.0.4 to 1.0.5 in /services/frontend
Bumps [tmpl](https://github.com/daaku/nodejs-tmpl) from 1.0.4 to 1.0.5.
- [Release notes](https://github.com/daaku/nodejs-tmpl/releases)
- [Commits](https://github.com/daaku/nodejs-tmpl/commits/v1.0.5)

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

Signed-off-by: dependabot[bot] <support@github.com>
2021-11-11 12:57:46 +00:00
dependabot[bot]andGitHub d527c6ba5f Bump axios from 0.21.1 to 0.21.2 in /services/frontend
Bumps [axios](https://github.com/axios/axios) from 0.21.1 to 0.21.2.
- [Release notes](https://github.com/axios/axios/releases)
- [Changelog](https://github.com/axios/axios/blob/master/CHANGELOG.md)
- [Commits](https://github.com/axios/axios/compare/v0.21.1...v0.21.2)

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

Signed-off-by: dependabot[bot] <support@github.com>
2021-11-11 12:57:32 +00:00
Otto AhoniemiandGitHub 6d5b91782c Merge pull request #3287 from UniversityOfHelsinkiCS/dependabot/npm_and_yarn/services/backend/validator-13.7.0
Bump validator from 13.6.0 to 13.7.0 in /services/backend
2021-11-11 14:56:48 +02:00
Otto AhoniemiandGitHub b8762aae34 Merge pull request #3288 from UniversityOfHelsinkiCS/dependabot/npm_and_yarn/services/userservice/validator-13.7.0
Bump validator from 13.6.0 to 13.7.0 in /services/userservice
2021-11-11 14:56:41 +02:00
Otto Ahoniemi 4da78df1e6 Refactor studyguidancegroup logic 2021-11-11 14:12:39 +02:00
Otto Ahoniemi 93258ad12a Fix toggling between panel statuses throwing user away from current panel 2021-11-11 13:29:19 +02:00
Otto Ahoniemi 0028b69661 Revert tunkki solution for getting study guidance group population instead of generic population from redux 2021-11-11 12:52:27 +02:00
Otto Ahoniemi ef902b1c0c Fix population stats failing 2021-11-11 08:10:25 +02:00
Otto Ahoniemi d8e3ee092f Merge branch 'master' of github.com:UniversityOfHelsinkiCS/oodikone 2021-11-11 07:48:49 +02:00
Mitja Karhusaari 36e8c2f7f8 Fix lint 2021-11-11 05:01:21 +02:00
Otto Ahoniemi 8763b1868e Prevent manual typing to datetime field 2021-11-11 02:40:51 +02:00
Otto Ahoniemi 700d799b00 Add more filters to study guidance groups 2021-11-11 02:31:37 +02:00
Otto Ahoniemi b0ecc3d4f5 Fix showing students in study guidance groups, add better indicators for links 2021-11-11 02:27:43 +02:00
Otto Ahoniemi e681d5a94f Style betterments 2021-11-11 01:42:01 +02:00
Otto Ahoniemi 980cd8ecf0 Don't load mandatory programmes if already in state 2021-11-11 01:17:24 +02:00
Otto Ahoniemi 357c0b3f44 Make it possible to toggle showing programme structure / flat list in courses of population 2021-11-11 01:12:43 +02:00
Otto Ahoniemi 6467f5046e Make it possible to toggle showing credits from associated year 2021-11-10 23:09:57 +02:00
Otto Ahoniemi 21ff440ebd Cleanup unused props and proptypes 2021-11-10 23:07:23 +02:00
Otto Ahoniemi 73228fb26f Allow absolute imports 2021-11-10 21:46:43 +02:00
Otto Ahoniemi 2c1807fe10 Migrate studyguidancegroup redux logic to rtk query, fix some issues in overview 2021-11-10 21:02:30 +02:00
Otto Ahoniemi 50091a3098 Add RTK query baseapi and migrate studyguidancegroups to use it 2021-11-10 20:59:12 +02:00
Otto Ahoniemi 3ba7965402 Add original token to state and export dev headers in order to use them in rtk query 2021-11-10 20:32:04 +02:00
Otto Ahoniemi 067a53a072 Return copy instead of modifying course array 2021-11-10 16:41:39 +02:00
Otto Ahoniemi 9a3956832d Random fixes' 2021-11-10 00:40:28 +02:00
Otto Ahoniemi 831b638c99 Migrate populationcoursestatsflat from hoc to hooks, fix state updates on unmounted 2021-11-09 13:32:51 +02:00
Otto Ahoniemi 38c343b8d2 Add study guidance group filterset 2021-11-08 13:33:24 +02:00
Otto Ahoniemi c05346ad9a proptype cleaning 2021-11-08 13:25:33 +02:00
Petri Palmu dd12cd3626 Merge branch 'master' of github.com:UniversityOfHelsinkiCS/oodikone 2021-11-08 10:58:22 +02:00
Petri Palmu 55eebe32a6 groomed course search method 2021-11-08 10:58:05 +02:00
Mitja Karhusaari 58592df2a0 Avoid accidental indirect dependency 2021-11-06 01:39:04 +02:00
Mitja Karhusaari dd110d21e3 🎉 Happy birthday to this one guy in the test dataset 2021-11-06 01:22:27 +02:00
Mitja Karhusaari 88443f0117 Scroll the instructions popup if neccessary when clicking a "See more..." info link 2021-11-06 00:37:10 +02:00
Mitja Karhusaari accd5c9120 Add a toggle for choosing between credit and student counts 2021-11-06 00:37:10 +02:00
Mitja Karhusaari c11f5d78ba Remove selectedStudents prop from the graph component
The component does not use the non-selected students for anything,
so we can just pass the pre-filtered set of students to the component.

The previous method of filtering students based on student numbers
in-component caused problems with the credit date filter. As this filter
not only filtered the students, but also modified them, the list of
filtered students needs to be passed to the components. Filtering the
whole population of students based on student numbers afterwards would
ignore these changes.
2021-11-06 00:09:38 +02:00
Mitja Karhusaari 9f4466d246 Convert PopulationStatistics into a functional component 2021-11-06 00:08:29 +02:00
Mitja Karhusaari 5504f02c31 Refoactor how credit graph data ranges are passed 2021-11-06 00:08:16 +02:00
Mitja Karhusaari 863e84e226 Add course credit date filter onto the population statistics page 2021-11-05 23:20:40 +02:00
Mitja Karhusaari 0ce965abe3 Rewrite the credit accumulation graph using a functional component 2021-11-05 23:20:40 +02:00
Petri Palmu e9242b3063 update frontend packages 2021-11-04 08:22:58 +02:00
Petri Palmu 6ccf320bb2 Fix to the open uni programme trends 2021-11-04 08:14:03 +02:00
Petri Palmu 4e9b579843 Merge branch 'master' of github.com:UniversityOfHelsinkiCS/oodikone 2021-11-04 08:06:13 +02:00
Petri Palmu 985084864c Added open uni programme view to the trends 2021-11-04 08:05:57 +02:00
dependabot[bot]andGitHub 8099dff1b6 Bump validator from 13.6.0 to 13.7.0 in /services/userservice
Bumps [validator](https://github.com/validatorjs/validator.js) from 13.6.0 to 13.7.0.
- [Release notes](https://github.com/validatorjs/validator.js/releases)
- [Changelog](https://github.com/validatorjs/validator.js/blob/master/CHANGELOG.md)
- [Commits](https://github.com/validatorjs/validator.js/compare/13.6.0...13.7.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2021-11-03 19:43:02 +00:00
dependabot[bot]andGitHub 1528ff3d80 Bump validator from 13.6.0 to 13.7.0 in /services/backend
Bumps [validator](https://github.com/validatorjs/validator.js) from 13.6.0 to 13.7.0.
- [Release notes](https://github.com/validatorjs/validator.js/releases)
- [Changelog](https://github.com/validatorjs/validator.js/blob/master/CHANGELOG.md)
- [Commits](https://github.com/validatorjs/validator.js/compare/13.6.0...13.7.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2021-11-03 19:42:56 +00:00
Otto Ahoniemi 71c61c3393 Fix single group loading failing 2021-11-03 15:26:36 +02:00
Otto Ahoniemi f3b1f639b1 Disable immutable & serializablechecks from middleware 2021-11-03 15:01:15 +02:00
Otto Ahoniemi e599e620c0 Add student counts 2021-11-03 14:34:12 +02:00
Otto Ahoniemi f5b081656f use native isnan instead of lodash 2021-11-03 14:04:16 +02:00
Otto Ahoniemi 7af8e1a4df Change single year to academic year 2021-11-03 14:00:18 +02:00
Otto Ahoniemi 8c092c8d9f Remove more redux boilerplate 2021-11-03 12:47:52 +02:00
Otto Ahoniemi e0789186ba Change combinereducer import location 2021-11-03 12:26:56 +02:00
Otto Ahoniemi c798a4747f Remove unnecessary crudreducer abstraction 2021-11-03 12:20:18 +02:00
Otto Ahoniemi 8f396a9913 Cleanup studyguidancegroup reducer 2021-11-03 12:11:30 +02:00
Otto Ahoniemi 148a5acc25 Use one source of truth for redux types 2021-11-03 09:49:09 +02:00
Otto Ahoniemi 9cf1b87b3f Remove deprecated context dev tool pkg 2021-11-03 09:34:58 +02:00
Otto Ahoniemi 82ed55e1f9 Simplify store config 2021-11-03 09:30:41 +02:00
Otto Ahoniemi 82041e673d Swap individual redux&reselect to official redux toolkit package 2021-11-03 09:25:45 +02:00
Otto Ahoniemi 1e4405aac9 Change frontpage to work with hooks 2021-11-02 22:06:42 +02:00
Otto Ahoniemi dcefdd12cb Fix error when year tag not set 2021-11-02 20:29:11 +02:00
Otto AhoniemiandGitHub fd775252b8 Merge pull request #3282 from UniversityOfHelsinkiCS/dependabot/npm_and_yarn/ansi-regex-5.0.1
Bump ansi-regex from 5.0.0 to 5.0.1
2021-11-02 20:19:09 +02:00
Otto Ahoniemi acd7dc65f6 Add prop for creditacc starting year, cleanup proptypes 2021-11-02 20:14:32 +02:00
Otto Ahoniemi a82d272a27 moar cleanups 2021-11-02 20:14:32 +02:00
Otto Ahoniemi 1bb5ddba96 Finalize form on frontend 2021-11-02 20:14:32 +02:00
Otto Ahoniemi ae14a55885 Cleanups 2021-11-02 20:14:32 +02:00
Otto Ahoniemi 9308016cf3 Show previosly selected value as placeholder when editing 2021-11-02 20:14:32 +02:00
Otto Ahoniemi b5252a01f8 Add dropdown to search studyprogrammes 2021-11-02 20:14:32 +02:00
Otto Ahoniemi 4b22902fa4 Add support for studyguidancegroups in backend and add edits to frontend 2021-11-02 20:14:32 +02:00
Otto Ahoniemi 267ebce457 Cleaning up again some proptypes 2021-11-02 20:14:32 +02:00
Otto Ahoniemi ee8013cba2 Add initial support for adding study guidance group programme & year in front 2021-11-02 20:14:32 +02:00
Otto Ahoniemi 4ce07eaf31 Refactor 2021-11-02 20:14:32 +02:00
Mitja Karhusaari e0d4e4072d Improve date selection behavior when no date is selected or it is the current date 2021-11-02 16:31:38 +02:00
Mitja Karhusaari d4aeece78e Minor UI behaviour fix in a tooltip popup 2021-11-02 16:19:37 +02:00
dependabot[bot]andGitHub 02adcbc4f8 Bump ansi-regex from 5.0.0 to 5.0.1
Bumps [ansi-regex](https://github.com/chalk/ansi-regex) from 5.0.0 to 5.0.1.
- [Release notes](https://github.com/chalk/ansi-regex/releases)
- [Commits](https://github.com/chalk/ansi-regex/compare/v5.0.0...v5.0.1)

---
updated-dependencies:
- dependency-name: ansi-regex
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-11-02 13:58:40 +00:00
Mitja Karhusaari a9d44b3d76 Fix tests for population statistics
The tests were dependent of the current month being October of 2021.
2021-11-02 15:57:31 +02:00
Mitja Karhusaari 3d310ee184 Update tooltips 2021-11-01 19:11:38 +02:00
Mitja Karhusaari 0adf1eae76 Misc UI asjustments in trends status page 2021-11-01 18:48:33 +02:00
Mitja Karhusaari e10d3dd3c7 Wrap Status and Graduated tabs' contents in a pane 2021-11-01 16:29:39 +02:00
Mitja Karhusaari fce0e482d3 Move settings pane next to the usage guide and behind a popup 2021-11-01 16:29:39 +02:00
Mitja Karhusaari 4d57d00ddb Move trends status usage info behind a popup window 2021-11-01 16:29:39 +02:00
Mitja Karhusaari 7cc90a4378 Implement drilling in trends so that the browser navigation buttons work 2021-11-01 16:29:39 +02:00
Mitja Karhusaari ff0f1a545f Display accumulated / total values for course student counts 2021-11-01 16:29:39 +02:00
Saara Satokangas 6eb69a036a Add tests for Credits Gained -tab 2021-10-28 18:34:50 +03:00
Saara Satokangas 3c8a47fc76 Add hover to make the toggle more noticeable 2021-10-28 16:28:26 +03:00
Saara Satokangas 735ec7efa0 Move "NEW!" label to specifically admission types 2021-10-28 16:11:46 +03:00
Saara Satokangas c33f842c9a Do not show dividers if no admission type data available 2021-10-28 16:03:17 +03:00
Saara Satokangas 2340e39fb4 Don't show admission type credit table if only zeros 2021-10-28 11:51:29 +03:00
Saara Satokangas 0221d1f70e Add divider to credit statistics 2021-10-28 11:48:14 +03:00
Saara Satokangas c6fe105b46 Add credit gains by admission type 2021-10-28 11:47:33 +03:00
Saara Satokangas 6ec1bc118d Move label "NEW!" from age stats to credit stats 2021-10-28 09:53:30 +03:00
Saara Satokangas 098dd5425e Remove proptypes from CreditGainStats-tables 2021-10-28 09:49:03 +03:00
Saara Satokangas 9eeed5094f Remove proptypes from CreditGainStats 2021-10-28 09:47:57 +03:00
Otto Ahoniemi ada4f66105 Add navbar styles back 2021-10-28 08:22:42 +03:00
Otto Ahoniemi b25c90e338 remove jstat 2021-10-28 08:22:42 +03:00
Otto Ahoniemi 47995056cb Report errors in case normal logging process fails 2021-10-28 08:22:42 +03:00
Otto Ahoniemi 46ee5b7764 Hardcode importer url instead of getting it from env 2021-10-28 08:22:42 +03:00
Petri Palmu c8f0e421fe little correction to the avoin trends 2021-10-28 08:21:38 +03:00
Petri Palmu 3db6a4443e Merge branch 'master' of github.com:UniversityOfHelsinkiCS/oodikone 2021-10-28 08:04:58 +03:00
Petri Palmu 77d20ad189 open uni added to the trends 2021-10-28 08:01:46 +03:00
Otto Ahoniemi bd6b2adef0 Change test to target cypress attr instead of classname 2021-10-27 19:50:14 +03:00
Otto Ahoniemi 7457ada25a Move select prompt reset to main cli.sh 2021-10-27 19:46:23 +03:00
Otto Ahoniemi ab94a0b27c Add option to reset single database interactively from cli 2021-10-27 19:34:54 +03:00
Otto Ahoniemi 41e86af663 Remove old sis button configs from navbar, allow it to overflow before stacking 2021-10-27 19:34:54 +03:00
Otto Ahoniemi 19e752b7ca Add formik 2021-10-27 19:34:54 +03:00
Otto Ahoniemi 3d4f71dbad Merge logging logic in different modules, fixing #3243 2021-10-27 19:34:53 +03:00
Saara Satokangas ff4d858efc Remove the quartiles also from tests 2021-10-27 11:20:57 +03:00
Saara Satokangas 59271c9e38 Remove mention of Oodi-Sis -credit mismatch from frontend 2021-10-27 10:30:47 +03:00
Saara Satokangas d87fb6d0fa Remove the mention of oodi-sisu-credit-mismatch 2021-10-27 10:28:38 +03:00
Saara Satokangas 646d87ab9e Add comments about the BH modules and programmes 2021-10-27 10:21:24 +03:00
Saara Satokangas 9e687f27a3 Update infotexts for population credit statistics 2021-10-27 10:11:40 +03:00
Saara Satokangas b699322e6f Make credit stats adjust to chosen admission type filter better 2021-10-27 10:11:19 +03:00
Saara Satokangas 3c01e3b7b1 Add admission types to credit statistics 2021-10-27 10:11:05 +03:00
Petri Palmu cb256976d7 kielikeskus added to the trends 2021-10-26 11:13:13 +03:00
Petri Palmu 1424461e26 open uni sql added to the trends and lot of pull merges 2021-10-25 09:55:07 +03:00
Otto Ahoniemi 86bd472c41 re-introduce the fix I just somehow managed to revert 2021-10-22 14:00:40 +03:00
Otto Ahoniemi 8984ea14f1 Add better error reporting to userservice 2021-10-22 13:58:54 +03:00
Otto Ahoniemi 1af42bf012 Fix userservice failing because of wrong property check 2021-10-22 13:38:17 +03:00
Mitja Karhusaari f606a7e8b7 Display course, programme and faculty codes on hover 2021-10-22 11:33:38 +03:00
Mitja Karhusaari 6d938a34af Add "type": "course" properties to the generated data 2021-10-22 11:28:30 +03:00
Mitja Karhusaari 77c5721dff Show course student numbers in trends 2021-10-22 11:28:30 +03:00
Mitja Karhusaari c5561ab6dc Bump trends redis key version number 2021-10-22 11:27:54 +03:00
Mitja Karhusaari 0b647e1b32 Revert "Calculate student counts in trends based on attainments instead of study rights"
This reverts commit 9e282ae634.
2021-10-22 11:27:54 +03:00
Mitja Karhusaari 1437bb9d0e Display zero instead of infinity if both the nominator and the
denominator are zero
2021-10-22 11:27:54 +03:00
Mitja Karhusaari 66249aabd8 Display yearly stats up to the current year, even if nothing to show 2021-10-22 11:27:54 +03:00
Otto AhoniemiandGitHub 02c63d1d9f Stop logging to file in updater 2021-10-22 10:12:30 +03:00
Mitja Karhusaari 00b7dc933e Add back accumulated statistics to the trends view
Resolves #3269
2021-10-21 12:58:56 +03:00
Mitja Karhusaari 9f089fb6db Fix year labels wrapping in the trends view on Chrome
Resolves #3270
2021-10-21 12:58:56 +03:00
Mitja Karhusaari 542267042e Add explanation of the relative statistics option in trends 2021-10-21 12:58:56 +03:00
Mitja Karhusaari 9e282ae634 Calculate student counts in trends based on attainments instead of study rights 2021-10-21 12:58:56 +03:00
Otto Ahoniemi 6a625b1da7 Remove faculty programmes from userservice 2021-10-20 16:45:25 +03:00
Otto Ahoniemi 4982bd8f3c Wrap everything needed to enricher 2021-10-20 16:35:18 +03:00
Otto Ahoniemi 908e33ccd3 Moar userservice cleanups 2021-10-19 15:12:39 +03:00
Otto Ahoniemi d429480f80 Remove faculties from rights returned by userservice 2021-10-19 14:21:09 +03:00
Otto Ahoniemi dada995398 Remove facultyprogramme route 2021-10-19 14:08:26 +03:00
Otto Ahoniemi 69c9b05657 Moar enriching from faculties 2021-10-19 14:06:06 +03:00
Petri Palmu db66880e25 kielikeskus added to the trends 2021-10-19 13:35:06 +03:00
Otto Ahoniemi 0b5ac0b61b Set modifyaccess to work on username, not internal id 2021-10-19 11:20:39 +03:00
Otto Ahoniemi a902961942 Cleanup users elementdetails usage 2021-10-19 11:08:06 +03:00
Otto Ahoniemi 7c2fa532d4 Merge some user data fetching routes 2021-10-19 11:08:06 +03:00
Otto Ahoniemi 36267f9fca Enrich all users list with facultyprogrammes on backend side 2021-10-19 11:08:06 +03:00
Otto Ahoniemi c2d99d6944 remove unused userservice ping 2021-10-19 11:08:06 +03:00
Otto Ahoniemi 0b1be20780 remove facultyprogramme tests 2021-10-19 11:08:06 +03:00
Otto Ahoniemi cc9f5f6906 Set trends to use static facultyarray instead of fetching from userservice 2021-10-19 11:08:06 +03:00
Otto Ahoniemi d5bca4dc57 proptypes :trash: 2021-10-19 11:08:06 +03:00
Mitja Karhusaari 5f8c468ceb Hotfix part 3 2021-10-18 14:48:17 +03:00
Otto AhoniemiandGitHub 434d45987d fix staging sourcemap prefix 2021-10-18 14:15:14 +03:00
Mitja Karhusaari 0cb8acc46a Hotfix part 2 2021-10-18 13:20:38 +03:00
Mitja Karhusaari 6e996fbc4c Hotfix: Trends data refreshing did not work in staging/prod 2021-10-18 13:11:46 +03:00
Saara Satokangas 86fbaaf1ca Improvements and cleaning up to Bachelor Honours 2021-10-18 07:33:35 +03:00
Saara Satokangas 517c216a5d Fix explorer not working if course not found 2021-10-18 07:33:16 +03:00
Mitja Karhusaari a04fb68aaa Allow displaying trends proportional to the number of students
Changes the yearly stats UI a bit, in order to make the currently
displayed unit more apparent.

Resolves #2665
2021-10-15 15:07:33 +03:00
Mitja Karhusaari 72c01fa345 Rework credit and student counting in trends 2021-10-15 15:07:33 +03:00
Petri Palmu 62f1cc5459 show by programme button color changed 2021-10-14 12:50:56 +03:00
Petri Palmu c9eceaebe4 Merge branch 'master' of github.com:UniversityOfHelsinkiCS/oodikone 2021-10-13 12:10:21 +03:00
Otto Ahoniemi 92e8709ef6 Fix uudet ohjelmat to koulutusohjelmat 2021-10-13 11:33:30 +03:00
Otto Ahoniemi 9c5c135d0f Connect eläinlääkis lisensiaatti to faculty 2021-10-13 09:59:17 +03:00
Otto Ahoniemi c4f65a1f29 Explicitly set logging filename 2021-10-13 09:30:52 +03:00
Otto Ahoniemi 96f1c9d9cf Drop organisations from teacher stats 2021-10-13 09:05:09 +03:00
Otto Ahoniemi a8cfcb3371 Explicitly specify logging levels 2021-10-13 08:42:45 +03:00
Petri Palmu 63cbe523b1 correction to the ay codeless updater object spread 2021-10-12 12:49:36 +03:00
Mitja Karhusaari 7092fbbda0 UX fix: display filter's options always if it is in use 2021-10-11 15:27:42 +03:00
Mitja Karhusaari 82573c9d17 Allow filtering course statistics by students' study programmes
Resolves #2435
2021-10-11 15:27:42 +03:00
Mitja Karhusaari 1b98b0a847 Allow filtering by courses in course population view 2021-10-11 15:27:42 +03:00
Otto Ahoniemi b73174f8a5 Set backend to temporarily push logs to file as well 2021-10-11 15:21:43 +03:00
Otto Ahoniemi b1167eebf3 Update deprecated jStat to jstat package 2021-10-11 11:40:22 +03:00
Otto Ahoniemi 35391f9b36 cleanup some proptypes 2021-10-11 11:29:13 +03:00
Otto Ahoniemi 4fde6c319a Fix study guidance groups layout 2021-10-11 08:27:40 +03:00
Otto Ahoniemi ebd1a98ead Refactor users overview to same folder with single user page 2021-10-11 08:18:12 +03:00
Otto Ahoniemi b3ef64fc2e Remove unused json for oodis 2021-10-07 16:54:27 +03:00
Otto Ahoniemi 7bbc3c0cb0 sos revert what was i thinking 2021-10-07 16:39:16 +03:00
Otto Ahoniemi 90abe7eb22 Don't forward all 404's to index.html 2021-10-07 16:21:28 +03:00
Otto Ahoniemi 08dd4019af Fix sourcemaps prefix 2021-10-07 16:21:28 +03:00
Otto Ahoniemi 2841fd69a3 Refactor mocking under hook 2021-10-07 11:58:08 +03:00
Otto Ahoniemi 827d45c481 Add show as user to user list 2021-10-07 11:58:08 +03:00
Otto Ahoniemi 52ba8d582b Refactor userpage to use hooks instead of HOCs 2021-10-07 11:58:08 +03:00
Otto Ahoniemi d151521eae Refactor userpage to func component 2021-10-07 11:58:08 +03:00
Petri Palmu 6188fe6fe4 Merge branch 'filter' 2021-10-07 10:19:35 +03:00
Otto Ahoniemi 1f0eb93174 remove sis notification 2021-10-06 14:14:54 +03:00
Petri Palmu f50e632cdb possibility to select most attained courses or programme structure in study programmes 2021-10-06 13:12:01 +03:00
Mitja Karhusaari bbd83b501b Pre-define importer API URL in .env template file 2021-10-06 12:35:00 +03:00
Mitja Karhusaari 48048e5ecf Fix credit accumulation graph not showing on the first render.
Resolves #3242
2021-10-06 12:25:34 +03:00
Petri Palmu 5be0a74a60 proptype correction 2021-10-05 10:03:59 +03:00
Petri Palmu e8e10bcb9a Merge branch 'master' of github.com:UniversityOfHelsinkiCS/oodikone 2021-10-04 09:19:41 +03:00
Petri Palmu 3e3a95a113 possible correction to ay codeless course_units 2021-10-04 09:19:19 +03:00
Otto Ahoniemi 9a190b301d Setup updater + oodikone with cli 1 2021-10-01 09:53:29 +03:00
Otto AhoniemiandGitHub f976eca1a5 Merge pull request #3233 from UniversityOfHelsinkiCS/dependabot/npm_and_yarn/updater/sis-updater-scheduler/i-0.3.7
Bump i from 0.3.6 to 0.3.7 in /updater/sis-updater-scheduler
2021-10-01 09:21:50 +03:00
Petri Palmu a32b4695a9 Merge branch 'master' of github.com:UniversityOfHelsinkiCS/oodikone 2021-10-01 08:46:52 +03:00
Petri Palmu d55e27f1d9 console logs removed 2021-10-01 08:46:39 +03:00
Otto Ahoniemi b749ad4033 Fix refreshers not awaited 2021-10-01 00:04:39 +03:00
Otto Ahoniemi eddc514907 Fix access check when reaching for study guidance group members 2021-09-30 19:13:08 +03:00
Otto Ahoniemi e0c4db7bd9 Guidance group fixes, refactoring 2021-09-30 18:26:12 +03:00
Otto Ahoniemi 3b384cceb6 Some group right checks 2021-09-30 14:41:48 +03:00
Petri Palmu 6e15b8c355 ay codeless attainments mapped to ay courses 2021-09-30 13:08:13 +03:00
Petri Palmu f1196b49ae Merge branch 'master' of github.com:UniversityOfHelsinkiCS/oodikone 2021-09-30 13:03:41 +03:00
Petri Palmu e5b89dc844 codeless ay code attainments to ay courses 2021-09-30 13:01:07 +03:00
Otto Ahoniemi 5b27c42945 Moar sequentiallizeeee to trends updating + trends update button 2021-09-30 10:24:57 +03:00
Otto Ahoniemi 5ae10d2695 Restore stats cronjob, only update last 2 years of teachers and on-demand update teachers button 2021-09-30 08:30:12 +03:00
Otto Ahoniemi c4f7529973 Replace old tietosuojaseloste 2021-09-30 08:16:57 +03:00
dependabot[bot]andGitHub 5d84d2f9a3 Bump i from 0.3.6 to 0.3.7 in /updater/sis-updater-scheduler
Bumps [i](https://github.com/pksunkara/inflect) from 0.3.6 to 0.3.7.
- [Release notes](https://github.com/pksunkara/inflect/releases)
- [Commits](https://github.com/pksunkara/inflect/compare/v0.3.6...v0.3.7)

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

Signed-off-by: dependabot[bot] <support@github.com>
2021-09-29 18:10:13 +00:00
Otto Ahoniemi 40fbcd9750 Fix teacher refreshing and add cronjob for statisticrefreshing again 2021-09-29 15:13:07 +03:00
Petri Palmu e06f12017d oodis to oodikone works 2021-09-29 14:22:07 +03:00
Otto Ahoniemi c686d2ad73 Fix teacher name mapping when first name not available 2021-09-29 10:34:29 +03:00
Otto Ahoniemi 7306d261ce Fix teacher id to be sis person id instead of employee number 2021-09-29 10:29:48 +03:00
Petri Palmu db0e924f36 correction to ay codeless courseprovider 2021-09-28 12:01:07 +03:00
Petri Palmu 10d13b751b Merge branch 'master' into feature/avoin 2021-09-28 11:58:39 +03:00
Petri Palmu 87ef487fe8 creating courseprovider for non ay course if not exist 2021-09-28 11:27:50 +03:00
Petri Palmu 50968b4b66 Merge branch 'master' of github.com:UniversityOfHelsinkiCS/oodikone 2021-09-28 10:49:08 +03:00
Petri Palmu 791e295eb7 adding codeless open uni works 2021-09-28 10:27:47 +03:00
Saara Satokangas 6818ab94fe Try fixing frontend crashing when programmeModules can't be found 2021-09-27 16:42:35 +03:00
Petri Palmu 4bef9c8daa possible way to add non ay courses 2021-09-27 13:59:07 +03:00
Petri Palmu 8050a56847 added oodis to oodikone 2021-09-27 09:07:09 +03:00
Otto Ahoniemi 04e2d44665 Fix studyprogramme view not working before data has been correctly fetched 2021-09-24 00:08:08 +03:00
Otto Ahoniemi 32b13df638 tmp disable events cron 2021-09-23 23:52:22 +03:00
Otto Ahoniemi 04f2244337 cleanups 2021-09-23 23:29:04 +03:00
Otto Ahoniemi 93c7267942 Precalc overviews only for new programmes 2021-09-23 23:29:04 +03:00
Otto Ahoniemi 94ff7c1218 Remove non grad students precalc from events + backend 2021-09-23 23:29:04 +03:00
Otto Ahoniemi 161fdc25e7 Remove present students tab from old / doctoral programme views 2021-09-23 23:29:04 +03:00
Otto AhoniemiandGitHub 9c069c8082 Merge pull request #3194 from UniversityOfHelsinkiCS/dependabot/npm_and_yarn/services/backend/axios-0.21.2
Bump axios from 0.21.1 to 0.21.2 in /services/backend
2021-09-23 19:20:05 +03:00
Otto AhoniemiandGitHub 21e3848207 Merge pull request #3195 from UniversityOfHelsinkiCS/dependabot/npm_and_yarn/updater/sis-updater-scheduler/axios-0.21.2
Bump axios from 0.21.1 to 0.21.2 in /updater/sis-updater-scheduler
2021-09-23 19:19:54 +03:00
Otto AhoniemiandGitHub 251db49a05 Merge pull request #3218 from UniversityOfHelsinkiCS/dependabot/npm_and_yarn/services/userservice/tmpl-1.0.5
Bump tmpl from 1.0.4 to 1.0.5 in /services/userservice
2021-09-23 19:19:45 +03:00
Otto Ahoniemi 07d9ef9901 Drop admissiontype test for now 2021-09-23 19:18:32 +03:00
Otto Ahoniemi bf414f856b Drop admissiontypes for under 2020 from table + excel too 2021-09-23 19:17:22 +03:00
Otto Ahoniemi b8a0fff57e proptypes :trash: 2021-09-23 18:52:05 +03:00
Otto Ahoniemi dd8ecb7152 Show admission types only for +2020 populations 2021-09-23 18:50:02 +03:00
Otto Ahoniemi 0c3c6a3442 typofix 2021-09-23 18:10:16 +03:00
Otto Ahoniemi 006984930f Merge branch 'master' of github.com:UniversityOfHelsinkiCS/oodikone 2021-09-23 18:08:59 +03:00
Otto Ahoniemi cab91265b0 Change events to run update sequentially 2021-09-23 18:05:08 +03:00
Petri Palmu e48247d80b added explanation to student stats if course is upgraded 2021-09-23 13:07:36 +03:00
Otto Ahoniemi 9e0265c439 Don't catch new groups all the time 2021-09-23 10:25:30 +03:00
Petri Palmu c5fd7bf26c Merge branch 'master' of github.com:UniversityOfHelsinkiCS/oodikone 2021-09-23 09:05:55 +03:00
Petri Palmu 2a68c60892 fix to the population of course AY grades 2021-09-22 20:44:07 +03:00
Saara Satokangas f9a974fdd2 Add last login to the users page 2021-09-22 16:37:45 +03:00
Saara Satokangas d64ac35a4f Add last_login to users-table 2021-09-22 16:24:05 +03:00
Saara Satokangas 484f4ecf94 Add admission type to the exported excel 2021-09-22 13:20:52 +03:00
Saara Satokangas 10ef70a22e Remove console.log 2021-09-22 13:20:25 +03:00
Saara Satokangas fa82b720f5 Include gender Other into studyprogramme-views 2021-09-22 11:44:42 +03:00
dependabot[bot]andGitHub 3b17923761 Bump tmpl from 1.0.4 to 1.0.5 in /services/userservice
Bumps [tmpl](https://github.com/daaku/nodejs-tmpl) from 1.0.4 to 1.0.5.
- [Release notes](https://github.com/daaku/nodejs-tmpl/releases)
- [Commits](https://github.com/daaku/nodejs-tmpl/commits/v1.0.5)

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

Signed-off-by: dependabot[bot] <support@github.com>
2021-09-22 08:38:07 +00:00
Saara Satokangas e42a348ec7 More improvements to degree programmes 2021-09-22 09:04:32 +03:00
Saara Satokangas d33f28c776 Betterments to DegreeCourses 2021-09-21 17:53:39 +03:00
Saara Satokangas 3fd883c17b Acually fix improved credits showing as failed 2021-09-21 17:21:59 +03:00
Saara Satokangas e980035e11 Fix failed grades in course stats 2021-09-21 17:16:45 +03:00
Saara Satokangas 2c51e5fb07 Nope, this probably does not work everywhere, revert 2021-09-21 17:16:45 +03:00
Saara Satokangas ea792758d6 Fix Credit Statistics -pane test since improved grades are passed 2021-09-21 16:40:05 +03:00
Saara Satokangas 998caed914 Alter tests now that improved grades are passed grades
Can be checked with anon data TKT20001, spring 2019, and student
number 013683126.
2021-09-21 16:30:42 +03:00
Saara Satokangas 595025c976 Fix improved grades showing as FAILED 2021-09-21 15:52:17 +03:00
Saara Satokangas e277175e5f Add ollikehy to oodikone coders -list 2021-09-21 15:22:39 +03:00
Saara Satokangas c510ac2258 Add full urls to backup-db addresses
The mere virtual machine names do not always resolve
2021-09-21 15:22:14 +03:00
Saara Satokangas e9d7df45db Fix Statistics-tab crashing if stat is undefined 2021-09-21 13:56:23 +03:00
Saara Satokangas 492475722c Fix info Population stats: coursePopulation 2021-09-21 13:29:32 +03:00
Petri Palmu afdab20014 Merge branch 'master' into feature/avoin 2021-09-21 12:34:18 +03:00
Saara Satokangas d51b357b38 Fix info of Population stats: Search 2021-09-21 12:26:46 +03:00
Petri Palmu f477b3ef50 correction to the ay regex 2021-09-21 12:23:26 +03:00
Saara Satokangas 17cecd0d5e Fix info in Population stats: CoursesOfPopulation 2021-09-21 12:23:08 +03:00
Saara Satokangas 9624b06c4f Fix info Population stats: CreditStatistics 2021-09-21 12:18:14 +03:00
Petri Palmu ab19b429ca new ay codeless courses works 2021-09-21 11:52:20 +03:00
Saara Satokangas 51f0409d26 Fix info for Population stats: credit accumulation 2021-09-21 10:54:04 +03:00
Saara Satokangas 003f8e2293 Fix info of Population stats: Query card 2021-09-21 10:47:27 +03:00
Saara Satokangas 2757e34338 Add mention about VPN 2021-09-21 10:33:30 +03:00
Petri Palmu 2e6fdb4970 calculations for new ayless courses 2021-09-21 09:33:07 +03:00
Saara Satokangas 5ea08b3e20 Fix infobox for Trends: graduations 2021-09-16 14:27:14 +03:00
Saara Satokangas 99b8cdabcd Fix infobox for Trends: status 2021-09-16 14:27:06 +03:00
Saara Satokangas d04fa26cdd Fix infobox for Trends: compare over time 2021-09-16 14:26:57 +03:00
Saara Satokangas 1237cb61bf Fix infoboxes for Trends: numerical trends 2021-09-16 14:26:38 +03:00
Saara Satokangas 39731667ec Fix Trends "Relative-view" infobox 2021-09-16 14:26:38 +03:00
Petri Palmu c918f862e3 Merge branch 'master' into feature/avoin 2021-09-16 13:29:24 +03:00
Otto AhoniemiandGitHub d79b5b3f81 typofix 2021-09-16 11:18:35 +03:00
Otto AhoniemiandGitHub 94af91274f acual fix db backup urls 2021-09-15 11:53:48 +03:00
Otto Ahoniemi af5c5209c5 header fixes 2021-09-13 17:23:44 +03:00
Otto Ahoniemi a96d7dbdea Hide grad fiitsu on prod 2021-09-13 17:14:43 +03:00
Otto Ahoniemi e1ca5bac5c Prettier plz 2021-09-13 17:09:31 +03:00
Otto Ahoniemi 295c3b6311 Update oodi db urls 2021-09-13 16:40:18 +03:00
Otto Ahoniemi c1a66a32f8 Don't call importerclient if it's not available 2021-09-13 16:29:26 +03:00
dependabot[bot]andGitHub 69942c2c17 Bump axios from 0.21.1 to 0.21.2 in /updater/sis-updater-scheduler
Bumps [axios](https://github.com/axios/axios) from 0.21.1 to 0.21.2.
- [Release notes](https://github.com/axios/axios/releases)
- [Changelog](https://github.com/axios/axios/blob/master/CHANGELOG.md)
- [Commits](https://github.com/axios/axios/compare/v0.21.1...v0.21.2)

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

Signed-off-by: dependabot[bot] <support@github.com>
2021-09-10 20:03:23 +00:00
dependabot[bot]andGitHub 7f0fa7fb63 Bump axios from 0.21.1 to 0.21.2 in /services/backend
Bumps [axios](https://github.com/axios/axios) from 0.21.1 to 0.21.2.
- [Release notes](https://github.com/axios/axios/releases)
- [Changelog](https://github.com/axios/axios/blob/master/CHANGELOG.md)
- [Commits](https://github.com/axios/axios/compare/v0.21.1...v0.21.2)

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

Signed-off-by: dependabot[bot] <support@github.com>
2021-09-10 20:03:18 +00:00
Otto Ahoniemi 56ee488b9b update teachers starting from year 2001 2021-09-10 23:02:36 +03:00
Otto Ahoniemi 6f8cee5153 No console.log erroring on userservice 2021-09-10 22:50:03 +03:00
Otto Ahoniemi 2e00415a40 No kone db for programmemodules 2021-09-10 22:47:48 +03:00
Otto Ahoniemi 8ef53a58be logger cleanups 2021-09-10 22:46:33 +03:00
Otto Ahoniemi f9abd1c028 remove unnecessary try catches now handled with errormiddleware 2021-09-10 21:57:36 +03:00
Otto Ahoniemi f912ccb27d Console.log removals and cleanup 2021-09-10 21:44:46 +03:00
Otto Ahoniemi 6224ec2307 Add sis person id to token, use token instead of headers when grabbing id in backend 2021-09-10 21:33:28 +03:00
Otto Ahoniemi 87e5bd9ef6 Set no-console to warning instead of error so linting doesn't fail in ci 2021-09-10 20:54:26 +03:00
Otto Ahoniemi 1beb4892fc Add route protection to frontend corresponding to backend access rights 2021-09-10 20:53:01 +03:00
Otto Ahoniemi ccb2722b6e Accessdenied to hooks 2021-09-10 20:16:42 +03:00
Otto Ahoniemi e9fcf3dd8c studentinfocard and throughput role check betterments 2021-09-10 15:32:27 +03:00
Otto Ahoniemi 376848fb70 Navbar betterments 2021-09-10 13:58:49 +03:00
Otto Ahoniemi 82f0fe0cdd console.log fixes 2021-09-10 13:58:24 +03:00
Otto Ahoniemi 871507d310 cleaning 2021-09-10 13:34:55 +03:00
Otto Ahoniemi 0ab5b70acc remove moar access groups 2021-09-10 13:17:11 +03:00
Otto Ahoniemi 1cf05f4e1d Clean up console.logs 2021-09-10 13:08:17 +03:00
Otto Ahoniemi 0bae2d36f5 Reduce try-catch boilerplate with async errors and error middleware 2021-09-10 13:00:55 +03:00
Otto Ahoniemi 29da912df9 Update logger config 2021-09-10 12:42:43 +03:00
Otto Ahoniemi 64a211e17f Don't allow mocking as self 2021-09-10 08:46:37 +03:00
Otto Ahoniemi 776b74c777 Replace console.logs with logger in updater worker 2021-09-10 08:26:11 +03:00
Otto Ahoniemi 4a35505af3 Replace console.logs with logger in updater-scheduler 2021-09-10 07:47:40 +03:00
Otto Ahoniemi 2a20befb67 No error on missing prop-types 2021-09-10 07:34:42 +03:00
Otto Ahoniemi 3824e43345 Error on console.log 2021-09-09 14:06:55 +03:00
Petri Palmu 59bfbfcca3 adding ay code to the credits work 2021-09-09 13:59:50 +03:00
Otto Ahoniemi 6181f89cc7 Disable courseproviders console.log 2021-09-09 12:56:36 +03:00
Petri Palmu 22e11948f5 avoin check from studyright_id implemented 2021-09-08 11:28:46 +03:00
Petri Palmu 5b5884b25d get avoi, studyright mapping works 2021-09-08 09:47:56 +03:00
394 changed files with 13106 additions and 63305 deletions
-1
View File
@@ -1,4 +1,3 @@
# Populate and change this to .env
IMPORTER_DB_API_TOKEN=
IMPORTER_DB_API_URL=
PATE_TOKEN=
+3 -10
View File
@@ -8,17 +8,10 @@
"parserOptions": {
"requireConfigFile": false
},
"extends": [
"eslint:recommended",
"plugin:prettier/recommended",
"plugin:cypress/recommended"
],
"plugins": [
"prettier",
"cypress"
],
"extends": ["eslint:recommended", "plugin:prettier/recommended", "plugin:cypress/recommended"],
"plugins": ["prettier", "cypress"],
"rules": {
"no-console": "off",
"no-console": "error",
"react/no-unescaped-entities": "off",
"no-async-promise-executor": "off",
"prettier/prettier": "error"
+24 -58
View File
@@ -1,4 +1,4 @@
name: "Release oodikone to production"
name: 'Release oodikone to production'
on:
release:
@@ -6,23 +6,23 @@ on:
jobs:
build_and_release_backend:
name: "Build and release backend"
name: 'Build and release backend'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: docker/setup-buildx-action@v1
- name: "Login to DockerHub"
- name: 'Login to DockerHub'
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: "Extract release version"
- 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/}"
- name: "Build and push"
- name: 'Build and push'
uses: docker/build-push-action@v2
with:
build-args: |
@@ -38,26 +38,26 @@ jobs:
target: production
build_and_release_frontend:
name: "Build and release frontend"
name: 'Build and release frontend'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: docker/setup-buildx-action@v1
- name: "Login to DockerHub"
- name: 'Login to DockerHub'
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: "Get current time"
- name: 'Get current time'
id: get_current_time
run: echo ::set-output name=NOW::"$(date --iso-8601=seconds)"
- name: "Extract release version"
- 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/}"
- name: "Build and push"
- name: 'Build and push'
uses: docker/build-push-action@v2
with:
build-args: |
@@ -72,11 +72,11 @@ jobs:
push: true
tags: toska/oodikone2-frontend:latest
target: production
- name: "Create a container from built image "
- name: 'Create a container from built image '
run: docker create --name frontend toska/oodikone2-frontend:latest
- name: "Copy assets from the container"
- name: 'Copy assets from the container'
run: docker cp frontend:/usr/src/app/build ./build
- name: "Create Sentry release"
- name: 'Create Sentry release'
uses: getsentry/action-release@v1
env:
SENTRY_URL: ${{ secrets.SENTRY_URL }}
@@ -87,52 +87,19 @@ jobs:
environment: production
set_commits: 'skip'
sourcemaps: './build/static/js'
url_prefix: '~/oodikone/static/js'
url_prefix: '~/static/js'
version: ${{ steps.extract_version.outputs.VERSION }}
build_and_release_userservice:
name: "Build and release userservice"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: docker/setup-buildx-action@v1
- name: "Login to DockerHub"
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- 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/}"
- name: "Build and push"
uses: docker/build-push-action@v2
with:
build-args: |
SENTRY_RELEASE=${{ steps.extract_version.outputs.VERSION }}
SENTRY_ENVIRONMENT=production
cache-from: |
type=registry,ref=toska/oodikone2-userservice:staging
type=registry,ref=toska/oodikone2-userservice:latest
cache-to: type=inline
context: services/userservice
push: true
tags: toska/oodikone2-userservice:latest
target: production
report_success:
runs-on: ubuntu-latest
name: "Report deployment success to oodikone notifications"
name: 'Report deployment success to oodikone notifications'
needs:
- build_and_release_backend
- build_and_release_frontend
- build_and_release_userservice
- build_and_release_backend
- build_and_release_frontend
if: success()
steps:
- name: "Send notification to slack"
uses: UniversityOfHelsinkiCS/slackbot-action@v1.4.2
- name: 'Send notification to slack'
uses: UniversityOfHelsinkiCS/slackbot-action@v1.4.2
with:
webhook-url: ${{ secrets.WEBHOOK_URL }}
message-type: deployment
@@ -141,15 +108,14 @@ jobs:
report_failure:
runs-on: ubuntu-latest
name: "Report deployment failure to oodikone notifications"
name: 'Report deployment failure to oodikone notifications'
needs:
- build_and_release_backend
- build_and_release_frontend
- build_and_release_userservice
- build_and_release_backend
- build_and_release_frontend
if: failure()
steps:
- name: "Send notification to slack"
uses: UniversityOfHelsinkiCS/slackbot-action@v1.4.2
- name: 'Send notification to slack'
uses: UniversityOfHelsinkiCS/slackbot-action@v1.4.2
with:
webhook-url: ${{ secrets.WEBHOOK_URL }}
message-type: deployment-failure
+106 -64
View File
@@ -45,21 +45,7 @@ jobs:
'eslint --ext .jsx,.js services/frontend' \
'stylelint services/frontend/src/**/*.css'
lint_userservice:
name: 'Lint userservice'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: 14
- name: 'Install dependencies'
run: npm ci && cd services/userservice && npm ci
- name: 'Run frontend linter'
run: npm run eslint services/userservice
# === Build images ===
build_backend:
name: 'Build backend'
runs-on: ubuntu-latest
@@ -116,50 +102,106 @@ jobs:
with:
path: /tmp/frontend.tar
build_userservice:
name: 'Build userservice'
build_and_push_anon_sis_db:
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: docker/setup-buildx-action@v1
- name: 'Build image and export it'
uses: docker/build-push-action@v2
- uses: dorny/paths-filter@v2
id: check_migrations
with:
build-args: |
SENTRY_RELEASE=${{ github.sha }}
SENTRY_ENVIRONMENT=staging
cache-from: |
type=registry,ref=toska/oodikone2-userservice:staging
type=registry,ref=toska/oodikone2-userservice:latest
cache-to: type=inline
context: services/userservice
push: false
tags: toska/oodikone2-userservice:staging
target: production
outputs: type=docker,dest=/tmp/userservice.tar
- name: 'Upload image for other jobs'
uses: actions/upload-artifact@v2
filters: |
migrations:
- 'updater/sis-updater-worker/src/db/migrations/**'
- '.github/workflows/test-oodikone.yaml'
- name: 'Login to toska docker registry'
uses: docker/login-action@v1
with:
path: /tmp/userservice.tar
registry: registry.toska.cs.helsinki.fi
username: ${{ secrets.TOSKAREGISTRY_USERNAME }}
password: ${{ secrets.TOSKAREGISTRY_PASSWORD }}
- name: 'Let sis-updater-worker run migrations, then push new version of image to toska hub'
if: steps.check_migrations.outputs.migrations == 'true'
run: |
docker-compose --file docker-compose.ci.yml up --detach sis-updater-worker
sleep 30 # wait for sis-updater-worker to start and run migrations
./anonyymioodi/commit_and_push_to_toska_hub.sh sis-db
# === Integration tests ===
#
run_userservice_integration_tests:
name: 'Run userservice integration tests'
build_and_push_anon_kone_db:
name: 'Build anon kone-db and push to toska hub if new migrations have been added'
needs:
- build_backend
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: 'Run tests included in docker build'
run: docker-compose --file docker-compose.test.yml up --build userservice-test
- uses: dorny/paths-filter@v2
id: check_migrations
with:
filters: |
migrations:
- 'services/backend/src/database/migrations_kone/**'
- '.github/workflows/test-oodikone.yaml'
- name: 'Download images'
uses: actions/download-artifact@v2
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
with:
registry: registry.toska.cs.helsinki.fi
username: ${{ secrets.TOSKAREGISTRY_USERNAME }}
password: ${{ secrets.TOSKAREGISTRY_PASSWORD }}
- name: 'Let backend run migrations, then push new version of image to toska hub'
if: steps.check_migrations.outputs.migrations == 'true'
run: |
docker-compose --file docker-compose.ci.yml up --detach backend
sleep 30 # wait for sis-updater-worker to start and run migrations
./anonyymioodi/commit_and_push_to_toska_hub.sh kone-db
build_and_push_anon_user_db:
name: 'Build anon user-db and push to toska hub if new migrations have been added'
needs:
- build_backend
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: dorny/paths-filter@v2
id: check_migrations
with:
filters: |
migrations:
- 'services/backend/src/database/migrations_user/**'
- '.github/workflows/test-oodikone.yaml'
- name: 'Download images'
uses: actions/download-artifact@v2
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
with:
registry: registry.toska.cs.helsinki.fi
username: ${{ secrets.TOSKAREGISTRY_USERNAME }}
password: ${{ secrets.TOSKAREGISTRY_PASSWORD }}
- name: 'Let backend run migrations, then push new version of image to toska hub'
if: steps.check_migrations.outputs.migrations == 'true'
run: |
docker-compose --file docker-compose.ci.yml up --detach backend
sleep 30 # wait for sis-updater-worker to start and run migrations
./anonyymioodi/commit_and_push_to_toska_hub.sh user-db
# === End-to-end tests ===
test_course_statistics:
name: 'Test course statistics'
needs:
- build_backend
- build_frontend
- build_userservice
- 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@v2
@@ -171,7 +213,6 @@ jobs:
run: |
docker load --input /tmp/artifact/backend.tar
docker load --input /tmp/artifact/frontend.tar
docker load --input /tmp/artifact/userservice.tar
- name: 'Login to toska docker registry'
uses: docker/login-action@v1
with:
@@ -202,7 +243,9 @@ jobs:
needs:
- build_backend
- build_frontend
- build_userservice
- 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@v2
@@ -214,7 +257,6 @@ jobs:
run: |
docker load --input /tmp/artifact/backend.tar
docker load --input /tmp/artifact/frontend.tar
docker load --input /tmp/artifact/userservice.tar
- name: 'Login to toska docker registry'
uses: docker/login-action@v1
with:
@@ -245,7 +287,9 @@ jobs:
needs:
- build_backend
- build_frontend
- build_userservice
- 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@v2
@@ -257,7 +301,6 @@ jobs:
run: |
docker load --input /tmp/artifact/backend.tar
docker load --input /tmp/artifact/frontend.tar
docker load --input /tmp/artifact/userservice.tar
- name: 'Login to toska docker registry'
uses: docker/login-action@v1
with:
@@ -288,7 +331,9 @@ jobs:
needs:
- build_backend
- build_frontend
- build_userservice
- 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@v2
@@ -300,7 +345,6 @@ jobs:
run: |
docker load --input /tmp/artifact/backend.tar
docker load --input /tmp/artifact/frontend.tar
docker load --input /tmp/artifact/userservice.tar
- name: 'Login to toska docker registry'
uses: docker/login-action@v1
with:
@@ -331,7 +375,9 @@ jobs:
needs:
- build_backend
- build_frontend
- build_userservice
- 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@v2
@@ -343,7 +389,6 @@ jobs:
run: |
docker load --input /tmp/artifact/backend.tar
docker load --input /tmp/artifact/frontend.tar
docker load --input /tmp/artifact/userservice.tar
- name: 'Login to toska docker registry'
uses: docker/login-action@v1
with:
@@ -374,7 +419,9 @@ jobs:
needs:
- build_backend
- build_frontend
- build_userservice
- 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@v2
@@ -386,7 +433,6 @@ jobs:
run: |
docker load --input /tmp/artifact/backend.tar
docker load --input /tmp/artifact/frontend.tar
docker load --input /tmp/artifact/userservice.tar
- name: 'Login to toska docker registry'
uses: docker/login-action@v1
with:
@@ -417,7 +463,9 @@ jobs:
needs:
- build_backend
- build_frontend
- build_userservice
- 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@v2
@@ -429,7 +477,6 @@ jobs:
run: |
docker load --input /tmp/artifact/backend.tar
docker load --input /tmp/artifact/frontend.tar
docker load --input /tmp/artifact/userservice.tar
- name: 'Login to toska docker registry'
uses: docker/login-action@v1
with:
@@ -464,7 +511,9 @@ jobs:
needs:
- build_backend
- build_frontend
- build_userservice
- 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@v2
@@ -476,7 +525,6 @@ jobs:
run: |
docker load --input /tmp/artifact/backend.tar
docker load --input /tmp/artifact/frontend.tar
docker load --input /tmp/artifact/userservice.tar
- name: 'Login to toska docker registry'
uses: docker/login-action@v1
with:
@@ -504,8 +552,6 @@ jobs:
needs:
- lint_backend
- lint_frontend
- lint_userservice
- run_userservice_integration_tests
- test_course_statistics
- test_custom_population
- test_filters
@@ -524,7 +570,6 @@ jobs:
run: |
docker load --input /tmp/artifact/backend.tar
docker load --input /tmp/artifact/frontend.tar
docker load --input /tmp/artifact/userservice.tar
- name: 'Login to DockerHub'
uses: docker/login-action@v1
with:
@@ -545,13 +590,12 @@ jobs:
environment: staging
set_commits: 'skip'
sourcemaps: './build/static/js'
url_prefix: '~/oodikone/static/js'
url_prefix: '~/static/js'
version: ${{ github.sha }}
- name: 'Push to docker hub'
run: |
docker push toska/oodikone2-backend:staging
docker push toska/oodikone2-frontend:staging
docker push toska/oodikone2-userservice:staging
- name: 'Send deployment success notification to slack'
if: success()
uses: UniversityOfHelsinkiCS/slackbot-action@v1.4.2
@@ -574,8 +618,6 @@ jobs:
needs:
- lint_backend
- lint_frontend
- lint_userservice
- run_userservice_integration_tests
- test_course_statistics
- test_custom_population
- test_filters
+2 -2
View File
@@ -7,8 +7,8 @@ module.exports = {
'{services,updater}/**/*.{js,jsx}': files => `eslint --fix ${files.join(' ')}`,
'*.{js,json,md,yml,yaml,html}': files => `prettier --write ${files.join(' ')}`,
'*.css': files => `stylelint --fix ${files.join(' ')}`,
Dockerfile: files => `${dockerCmdBase} hadolint/hadolint:v2.7.0-alpine hadolint ${relativeFilePaths(files)}`,
'*.sh': files => `${dockerCmdBase} koalaman/shellcheck-alpine:v0.7.2 ${relativeFilePaths(files)} -x`,
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)}`,
'docker-compose*': files => {
const composeFiles = file => {
+16 -4
View File
@@ -43,8 +43,8 @@ What different CLI options do:
- Cleans up any previous real data databases
- Downloads needed database dumps from production servers.
- Creates real data databases and populates them with downloaded dumps.
3. Reset sis-importer data
- Like option 2, but only for sis-importer database. Useful when developing updater microservice.
3. Reset single database
- Like option 2, but only for single database. Leaves other databases untouched.
## ⌨️ Development
@@ -79,7 +79,7 @@ It is recommended to spend some time to become familiar with `docker` and `docke
- `docker-compose ps`: view the containers in the running environment
- `docker-compose logs frontend`: print logs for just frontend
- `docker-compose logs --follow --tail 100 backend`: print last hundred rows of backend logs and begin to follow them in your terminal window
- `docker exec -it userservice sh`: open bash terminal inside userservice container
- `docker exec -it backend sh`: open bash terminal inside backend container
- `docker exec -it sis-db psql -U postgres sis-db-real`: open psql client to investigate sis real data database.
### User types for development
@@ -124,7 +124,15 @@ Continuous integration (CI) works with Github actions and is defined in workflow
### Modules are missing after updating package.json
You should always install the dependencies **inside** the container to have the application **inside** the container access them. This might be the case when someone else installs a new library and you only pull the changes in package.json. Use `docker exec -it <service> sh` to get inside the container and run `npm ci` to install modules.
You should always install the dependencies **inside** the container to have the application **inside** the container access them. Module might be missing for example when someone else installs a new library and you only pull the changes in package.json. Use `docker exec <service> npm ci` to install modules inside the container.
### I get the "You are not authorized to use Oodikone" -message when I try to work with anon-data
Make sure you have your VPN on
### I made a change to sis-db models aand created a migration, but tests are failing in CI!
Migrations are handled by sis-importer-worker, since oodikone is using only replica. Since CI is testing only services included in oodikone, your new changes aren't migrated and thus are failing. Open anonyymioodi-folder in this repo to find more about how to to update the anon sis-db.
### Everything is broken, can't get oodikone running, data is not there etc.
@@ -135,3 +143,7 @@ Just do clean install by launching cli with `npm cli` and running option 1: _Set
Toska of course.
University of Helsinki
## Tietosuoja / data protection
See [tietosuoja.md](tietosuoja.md).
Binary file not shown.
+2
View File
@@ -0,0 +1,2 @@
*.sqz
+65
View File
@@ -0,0 +1,65 @@
# Anonyymioodi - anonymous database setup for oodikone
(name anonyymioodi for :culture: reasons)
This folder contains a guide and scripts to create anonymous database images for oodikone, mainly used for testing.
## How to create / modify database images
Creating / modifying images varies a bit depending on the db. Generally, to modify images, you can either use `psql` directly in docker by running `docker exec -it <dbname> psql -U postgres <dbname>` or start adminer with oodikone's docker-compose.
By default, previous db images from Toska Hub are used as a base. If needed, comment out Toska hub image and uncomment corresponding clean postgres image in docker-compose before running other steps.
See more details on how to create / modify databases below.
### Sis-importer-db
The following steps are for building a clean sis-importer-db image from scratch. If you want to just modify the existing image a bit, follow the steps defined for user-db / kone-db.
- See [sis-importer/importer-db-staging-sampletaker](https://github.com/UniversityOfHelsinkiCS/sis-importer/tree/master/importer-db-staging-sampletaker) and create `sis-importer-db.sqz` dump
- Move `sis-importer-db.sqz` to this folder
- Run `./create_container_from_dump.sh sis-importer-db`
- Enrich db with command in file `enrich_data.sql` either by piping command to container or by running command inside container.
### Sis-db:
The following steps are for building a clean sis-db image from scratch using oodikone's updater. If you want to just modify the existing image a bit, follow the steps defined for user-db / kone-db.
- Temporarily replace sis-db -service in oodikone's docker-compose.yml with sis-db -service from this repo's docker-compose.yml
- Ensure the newest sis-importer-db image is pulled from toska hub in oodikone repo
- Modify sis scheduler limit in config.js by setting: `module.exports.DEV_SCHEDULE_COUNT = null`
- Start updater, see oodikone readme for more info.
- Populate sis-db from sis-importer-db:
```
curl --request GET --url 'http://localhost:8082/v1/meta?token=dev' && \
curl --request GET --url 'http://localhost:8082/v1/programmes?token=dev' && \
curl --request GET --url 'http://localhost:8082/v1/students?token=dev'
```
(or run these from frontend. You might also want to follow sis-updater-workers logs to ensure things are running smoothly.)
- Clean up any extra stuff you don't want to keep in sis-db.
- Bonus: check that cypress tests run okay
### User-db / kone-db
- Temporarily replace the db service in oodikone's docker-compose.yml with the same db service from this folder's docker-compose.yml
- Ensure the newest user-db/kone-db image is pulled from the registry with `docker-compose pull`
- Modify db to wanted state. Tips:
- Run oodikone if you want to upgrade something from frontend (e.g. tags in kone-db, courses in sis-db etc.).
- Easy way to create new users to user-db is to modify dev user details in the frontend API config. A new user is created during login if the given uid is not present in user-db.
## Publishing the image
After completing the steps above and making sure your local version has the correct data, run `./commit_and_push_to_toska_hub.sh <dbname>` to publish the db image to Toska Hub.
## How to update Postgres versions
When Postgres is updated on the server, anon db images need to be updated to match server versions. Do following:
- Ensure you're using toska hub image in docker-compose and that you've pulled the newest image from toska hub: `docker-compose pull <dbname>`.
- Start database with docker-compose
- Create dump from database by running `docker exec -i <dbname> pg_dump -Fc -U postgres <dbname> > <dbname>.sqz`
- Close container with docker-compose down.
- Update the Postgres version in docker-compose.yml in this folder.
- Uncomment row with Postgres image and comment row with Toskahub image.
- Run `./create_container_from_dump.sh <dbname>`, followed by `./commit_and_push_to_toska_hub.sh <dbname>`
+48
View File
@@ -0,0 +1,48 @@
#!/usr/bin/env bash
# === Config ===
# Quit if failing
set -euo pipefail
# Print messages and logs that are not script output to stderr
msg() {
echo >&2 -e "${1-}"
}
# Some special types of messages with colours
successmsg() {
msg "${GREEN}$1${NOFORMAT}
"
}
errormsg() {
msg "${RED}$1${NOFORMAT}
"
}
infomsg() {
msg "${BLUE}$1${NOFORMAT}
"
}
warningmsg() {
msg "${ORANGE}$1${NOFORMAT}
"
}
# Setup colors for messages if running interactive shell
if [[ -t 2 ]] && [[ "${TERM-}" != "dumb" ]]; then
NOFORMAT='\033[0m' RED='\033[0;31m' GREEN='\033[0;32m' ORANGE='\033[0;33m' BLUE='\033[0;34m'
else
NOFORMAT='' RED='' GREEN='' ORANGE='' BLUE=''
fi
args=("$@")
if [[ ${#args[@]} -ne 1 ]]; then
errormsg "No database given, exiting"
exit 1
fi
infomsg "Committing and pushing to registry"
database=${args[0]}
docker commit "$database" "registry.toska.cs.helsinki.fi/$database"
docker push "registry.toska.cs.helsinki.fi/$database"
successmsg "Succesfully pushed"
+64
View File
@@ -0,0 +1,64 @@
#!/usr/bin/env bash
# === Config ===
# Quit if failing
set -euo pipefail
# Print messages and logs that are not script output to stderr
msg() {
echo >&2 -e "${1-}"
}
# Some special types of messages with colours
successmsg() {
msg "${GREEN}$1${NOFORMAT}
"
}
errormsg() {
msg "${RED}$1${NOFORMAT}
"
}
infomsg() {
msg "${BLUE}$1${NOFORMAT}
"
}
warningmsg() {
msg "${ORANGE}$1${NOFORMAT}
"
}
# Setup colors for messages if running interactive shell
if [[ -t 2 ]] && [[ "${TERM-}" != "dumb" ]]; then
NOFORMAT='\033[0m' RED='\033[0;31m' GREEN='\033[0;32m' ORANGE='\033[0;33m' BLUE='\033[0;34m'
else
NOFORMAT='' RED='' GREEN='' ORANGE='' BLUE=''
fi
# === Create images ===
retry () {
sleep 5
for i in {1..60}; do
"$@" && break || warningmsg "Retry attempt $i failed, waiting..." && sleep 5;
done
}
args=("$@")
if [[ ${#args[@]} -ne 1 ]]; then
errormsg "No database given, exiting"
exit 1
fi
infomsg "Restoring PostgreSQL dumps from backups. This might take a while."
database=${args[0]}
docker-compose down --remove-orphans
docker-compose up -d "$database"
database_dump="$database.sqz"
infomsg "Attempting to restore database $database from dump $database_dump"
retry docker-compose exec "$database" pg_isready --dbname="$database"
docker exec -i "$database" /bin/bash -c "pg_restore --username=postgres \
--format=custom --dbname=$database --no-owner" < "$database_dump"
successmsg "Database $database succesfully created"
+50
View File
@@ -0,0 +1,50 @@
x-defaults: &defaults
restart: unless-stopped
services:
kone-db:
<<: *defaults
container_name: kone-db
environment:
PGDATA: /data
POSTGRES_DB: kone-db
POSTGRES_HOST_AUTH_METHOD: trust
image: registry.toska.cs.helsinki.fi/kone-db
# image: postgres:13.4-alpine
profiles:
- oodikone
sis-db:
<<: *defaults
container_name: sis-db
environment:
PGDATA: /data
POSTGRES_DB: sis-db
POSTGRES_HOST_AUTH_METHOD: trust
# image: registry.toska.cs.helsinki.fi/sis-db
image: postgres:13.2
shm_size: 2gb
sis-importer-db:
<<: *defaults
container_name: sis-importer-db
environment:
PGDATA: /data
POSTGRES_DB: sis-importer-db
POSTGRES_HOST_AUTH_METHOD: trust
# image: registry.toska.cs.helsinki.fi/sis-importer-db
image: postgres:13.1
profiles:
- updater
user-db:
<<: *defaults
container_name: user-db
environment:
PGDATA: /data
POSTGRES_DB: user-db
POSTGRES_HOST_AUTH_METHOD: trust
image: registry.toska.cs.helsinki.fi/user-db
# image: postgres:13.4-alpine
profiles:
- oodikone
+133
View File
@@ -0,0 +1,133 @@
-- add these to sis-importer db by running:
-- docker exec -i sis-importer-db psql -U postgres sis-importer-db < enrich_sampledata.sql
-- if you need to run updater for these students only, see studentnumberlist in the end
-- this file
-- Add admission types (valintaperusteet) to studyrights for some of the computer
-- science 2018-2019 population students. See person_ids / student_number below
-- (in case you need to update dumps).
-- TODO: this should be moved into the sampledata creator script in
-- importer repo, but since this file contains studentnumbers and person ids, it's
-- better to keep this here
-- KP (koepisteet)
-- hy-hlo-1412620 | 013683126
-- hy-hlo-1485246 | 014340523
-- hy-hlo-32230770 | 010589388
-- hy-hlo-35626233 | 010775888
-- hy-hlo-40150144 | 011065995
UPDATE studyrights
SET admission_type_urn = 'urn:code:admission-type:kp'
WHERE person_id IN (
'hy-hlo-1412620',
'hy-hlo-1485246',
'hy-hlo-32230770',
'hy-hlo-35626233',
'hy-hlo-40150144'
);
-- TV (todistusvalinta)
-- hy-hlo-45783433 | 011321150
-- hy-hlo-48537946 | 010764653
-- hy-hlo-49801905 | 011194400
-- hy-hlo-53321302 | 011338871
-- hy-hlo-51223300 | 013951984
-- hy-hlo-53310137 | 012520688
-- hy-hlo-53621583 | 011196796
-- hy-hlo-53863064 | 011736884
-- hy-hlo-57711592 | 010262566
-- hy-hlo-57732872 | 013670647
UPDATE studyrights
SET admission_type_urn = 'urn:code:admission-type:tv'
WHERE person_id IN (
'hy-hlo-45783433',
'hy-hlo-48537946',
'hy-hlo-49801905',
'hy-hlo-53321302',
'hy-hlo-51223300',
'hy-hlo-53310137',
'hy-hlo-53621583',
'hy-hlo-53863064',
'hy-hlo-57711592',
'hy-hlo-57732872'
);
-- YP:
-- hy-hlo-67768022 | 011952325
-- hy-hlo-73740025 | 011736606
-- hy-hlo-74780917 | 012775433
-- hy-hlo-79615371 | 014113941
-- hy-hlo-78118455 | 014300615
UPDATE studyrights
SET admission_type_urn = 'urn:code:admission-type:yp'
WHERE person_id IN (
'hy-hlo-67768022',
'hy-hlo-73740025',
'hy-hlo-74780917',
'hy-hlo-79615371',
'hy-hlo-78118455'
);
-- AV:
-- hy-hlo-78777265 | 010779392
-- hy-hlo-81909550 | 011894153
-- hy-hlo-84571890 | 013897158
-- hy-hlo-80330834 | 015085926
-- hy-hlo-84775722 | 011791272
UPDATE studyrights
SET admission_type_urn = 'urn:code:admission-type:av'
WHERE person_id IN (
'hy-hlo-78777265',
'hy-hlo-81909550',
'hy-hlo-84571890',
'hy-hlo-80330834',
'hy-hlo-84775722'
);
-- M:
-- hy-hlo-87439827 | 011928456
-- hy-hlo-96612371 | 014418361
-- hy-hlo-84840261 | 010975895
-- hy-hlo-90330681 | 010522688
-- hy-hlo-96594603 | 013951573
UPDATE studyrights
SET admission_type_urn = 'urn:code:admission-type:m'
WHERE person_id IN (
'hy-hlo-87439827',
'hy-hlo-96612371',
'hy-hlo-84840261',
'hy-hlo-90330681',
'hy-hlo-96594603'
);
-- all studentnumbers for running updater:
013683126
014340523
010589388
010775888
011065995
011321150
010764653
011194400
011338871
013951984
012520688
011196796
011736884
010262566
013670647
011952325
011736606
012775433
014113941
014300615
010779392
011894153
013897158
015085926
011791272
011928456
014418361
010975895
010522688
013951573
+15 -11
View File
@@ -44,20 +44,22 @@ will take care of setting up Oodikone for you. See README for more details."
msg ""
}
# Define custom shell prompt for the interactive select loop
PS3="Please enter your choice: "
set_custom_select_prompt() {
PS3="Please enter your choice: "
options=(
"Set up oodikone from scratch."
"Reset all real data."
"Reset sis-importer data."
"Quit."
)
options=(
"Set up oodikone from scratch."
"Reset all real data."
"Reset single database."
"Quit."
)
}
# Run scripts before showing interactive prompt
# Run scripts and set the prompt
show_welcome
init_dirs
get_username
set_custom_select_prompt
while true; do
select opt in "${options[@]}"; do
@@ -66,8 +68,10 @@ while true; do
set_up_oodikone_from_scratch;;
"Reset all real data.")
reset_all_real_data;;
"Reset sis-importer data.")
reset_sis_importer_data;;
"Reset single database.")
reset_single_database
set_custom_select_prompt # Set prompt back to initial values
;;
"Quit.")
break 2;;
*) msg "${RED}Invalid option:${NOFORMAT} $REPLY
+12
View File
@@ -186,6 +186,18 @@ describe('Course Statistics tests', () => {
cy.contains('td', '010674989').siblings().contains('1')
})
it.skip('Language distribution is correct', () => {
cy.get("input[placeholder='Search by a course code']").type('TKT20003')
cy.contains(/^TKT20003$/).click()
cy.contains('Fetch statistics').should('be.enabled').click()
cy.contains('TKT20003, 582219 Käyttöjärjestelmät')
cy.get(':nth-child(3) > :nth-child(1) > div > .item > .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')
cy.contains('td', 'english').siblings().contains('5')
})
describe('When searching unified course stats', () => {
beforeEach(() => {
cy.url().should('include', '/coursestatistics')
+179 -84
View File
@@ -9,23 +9,38 @@ const checkFilteringResult = studentCount => {
// Semantic UI doesn't allow injection of data-cy:s for single multiple dropdown selection.
// This function tries to click "x" on all selections inside dropdown matching given attribute
const clearSemanticUIMultipleDropDownSelection = dataCyAttribute => {
cy.cs(dataCyAttribute).find('i.delete').click()
cy.cs(dataCyAttribute).find('i.delete').click({ force: true })
cy.cs(dataCyAttribute).click()
}
const clearSingleDropdownSelection = dataCyAttribute => {
cy.cs(dataCyAttribute).find('i.clear').click()
cy.cs(dataCyAttribute).find('i.clear').click({ force: true })
}
// 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.
const createRunTestStepWithPreAndPostPartsFunction = amountWithoutFiltering => {
return (dataCyAttributeOfHeaderToClick, testStepFunctionToRun) => {
cy.cs(dataCyAttributeOfHeaderToClick).click()
checkFilteringResult(amountWithoutFiltering)
testStepFunctionToRun()
checkFilteringResult(amountWithoutFiltering)
cy.cs(dataCyAttributeOfHeaderToClick).click()
return (filterName, testStepFunctionToRun) => {
const card = cy.cs(`${filterName}-filter-card`)
card.invoke('attr', 'data-open').then(open => {
console.log('open', open)
const getHeader = () => cy.cs(`${filterName}-header`)
if (open === 'false') {
getHeader().click({ force: true })
}
checkFilteringResult(amountWithoutFiltering)
testStepFunctionToRun()
checkFilteringResult(amountWithoutFiltering)
if (open === 'false') {
getHeader().click({ force: true })
}
})
}
}
@@ -41,90 +56,121 @@ describe('Population Statistics', () => {
it('Transfer filter is on not transferred by default', () => {
cy.contains('By default only students who have not transferred to this study programme are shown.')
cy.cs('transferredToProgrammeFilter-havenot').find('input').should('be.checked')
runTestStepWithPreAndPostParts('TransferredToProgramme', () => {
const card = cy.cs('TransferredToProgramme-filter-card')
card.get('[data-cy="option-havenot"] input').should('be.checked')
})
})
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', () => {
runTestStepWithPreAndPostParts('StudyTrack', () => {
const card = cy.cs('StudyTrack-filter-card')
const programmeDropdown = card.cs('StudyTrack-filter-dropdown').selectFromDropdown(0)
checkFilteringResult(1)
programmeDropdown.get('i.delete').click()
})
})
it('Graduation filter works', () => {
runTestStepWithPreAndPostParts('graduatedFromProgrammeFilter-header', () => {
runTestStepWithPreAndPostParts('GraduatedFromProgramme', () => {
const getCard = () => cy.cs('GraduatedFromProgramme-filter-card')
const graduated = 11
cy.cs('graduatedFromProgrammeFilter-graduated-true').click()
getCard().cs('option-graduated-true').click()
checkFilteringResult(graduated)
cy.cs('graduatedFromProgrammeFilter-graduated-false').click()
getCard().cs('option-graduated-false').click()
checkFilteringResult(defaultAmountOfStudents - graduated)
cy.cs('graduatedFromProgrammeFilter-all').click()
getCard().cs('option-all').click()
})
})
it('Transfer filter works', () => {
runTestStepWithPreAndPostParts('transferredToProgrammeFilter-header', () => {
runTestStepWithPreAndPostParts('TransferredToProgramme', () => {
const transferred = 1
cy.cs('transferredToProgrammeFilter-have').click()
cy.cs('TransferredToProgramme-filter-card').cs('option-have').click()
checkFilteringResult(transferred)
cy.cs('transferredToProgrammeFilter-all').click()
cy.cs('TransferredToProgramme-filter-card').cs('option-all').click()
checkFilteringResult(defaultAmountOfStudents + transferred)
cy.cs('transferredToProgrammeFilter-havenot').click()
cy.cs('TransferredToProgramme-filter-card').cs('option-havenot').click()
})
})
it('Enrollment filter works', () => {
runTestStepWithPreAndPostParts('enrollmentStatusFilter-header', () => {
cy.selectFromDropdown('enrollmentStatusFilter-status', 0)
cy.selectFromDropdown('enrollmentStatusFilter-semesters', [0])
runTestStepWithPreAndPostParts('EnrollmentStatus', () => {
cy.cs('enrollmentStatusFilter-status').selectFromDropdown(0)
cy.cs('enrollmentStatusFilter-semesters').selectFromDropdown(0)
checkFilteringResult(137)
clearSemanticUIMultipleDropDownSelection('enrollmentStatusFilter-semesters')
})
})
it('Credit filter works', () => {
runTestStepWithPreAndPostParts('credit-filter-header', () => {
runTestStepWithPreAndPostParts('CreditsEarned', () => {
cy.cs('credit-filter-min').type('50')
cy.cs('credit-filter-max').type('150')
checkFilteringResult(116)
cy.cs('credit-filter-min').find('input').clear()
cy.cs('credit-filter-max').find('input').clear()
cy.cs('credit-filter-min').clear()
cy.cs('credit-filter-max').clear()
})
})
it('Age filter works', () => {
runTestStepWithPreAndPostParts('ageFilter-header', () => {
runTestStepWithPreAndPostParts('Age', () => {
cy.cs('ageFilter-min').type('20')
cy.cs('ageFilter-max').type('40')
checkFilteringResult(43)
checkFilteringResult(40)
cy.cs('ageFilter-min').find('input').clear()
cy.cs('ageFilter-max').find('input').clear()
})
})
it('Gender filter works', () => {
runTestStepWithPreAndPostParts('genderFilter-header', () => {
cy.selectFromDropdown('genderFilter-dropdown', 0)
runTestStepWithPreAndPostParts('Gender', () => {
cy.cs('genderFilter-dropdown').selectFromDropdown(0)
checkFilteringResult(36)
cy.selectFromDropdown('genderFilter-dropdown', 1)
cy.cs('genderFilter-dropdown').selectFromDropdown(1)
checkFilteringResult(112)
cy.selectFromDropdown('genderFilter-dropdown', 2)
cy.cs('genderFilter-dropdown').selectFromDropdown(2)
checkFilteringResult(0)
cy.selectFromDropdown('genderFilter-dropdown', 3)
cy.cs('genderFilter-dropdown').selectFromDropdown(3)
checkFilteringResult(0)
clearSingleDropdownSelection('genderFilter-dropdown')
})
})
it('Starting year filter works', () => {
runTestStepWithPreAndPostParts('startYearAtUni-header', () => {
cy.selectFromDropdown('startYearAtUni-dropdown', 0)
runTestStepWithPreAndPostParts('StartYearAtUni', () => {
cy.cs('startYearAtUni-dropdown').selectFromDropdown(0)
checkFilteringResult(1)
clearSemanticUIMultipleDropDownSelection('startYearAtUni-dropdown')
cy.selectFromDropdown('startYearAtUni-dropdown', 14)
cy.cs('startYearAtUni-dropdown').selectFromDropdown(14)
checkFilteringResult(121)
clearSemanticUIMultipleDropDownSelection('startYearAtUni-dropdown')
})
})
it('Admission type filter works', () => {
runTestStepWithPreAndPostParts('admissionTypeFilter-header', () => {
cy.selectFromDropdown('admissionTypeFilter-dropdown', 0)
// Return when we have +2020 population with admission types in data
it.skip('Admission type filter works', () => {
runTestStepWithPreAndPostParts('AdmissionTypeFilter', () => {
cy.cs('admissionTypeFilter-dropdown').selectFromDropdown(0)
checkFilteringResult(5)
cy.selectFromDropdown('admissionTypeFilter-dropdown', 2)
cy.cs('admissionTypeFilter-dropdown').selectFromDropdown(2)
checkFilteringResult(10)
clearSingleDropdownSelection('admissionTypeFilter-dropdown')
})
@@ -134,31 +180,32 @@ describe('Population Statistics', () => {
it.skip('Courses filter works', () => {
// courses takes some time to load, wait for it to complete
cy.wait(10000)
runTestStepWithPreAndPostParts('courseFilter-header', () => {
runTestStepWithPreAndPostParts('Courses', () => {
cy.cs('courseFilter-course-dropdown').click()
const courses = ['TKT20001', 'MAT11002']
cy.cs('courseFilter-course-dropdown').click().contains(courses[0]).click()
checkFilteringResult(140)
cy.selectFromDropdown(`courseFilter-${courses[0]}-dropdown`, 1)
cy.cs(`courseFilter-${courses[0]}-dropdown`).selectFromDropdown(1)
checkFilteringResult(131)
cy.cs('courseFilter-course-dropdown').click().contains(courses[1]).click()
checkFilteringResult(56)
cy.selectFromDropdown(`courseFilter-${courses[1]}-dropdown`, 2)
cy.cs(`courseFilter-${courses[1]}-dropdown`).selectFromDropdown(2)
checkFilteringResult(2)
courses.forEach(course => cy.cs(`courseFilter-${course}-clear`).click())
})
})
it('Filter combinations work', () => {
runTestStepWithPreAndPostParts('graduatedFromProgrammeFilter-header', () => {
runTestStepWithPreAndPostParts('ageFilter-header', () => {
cy.cs('graduatedFromProgrammeFilter-graduated-true').click()
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()
cy.cs('graduatedFromProgrammeFilter-all').click()
getCard().cs('option-all').click()
})
})
})
@@ -175,7 +222,7 @@ describe('Course Statistics', () => {
})
it('Grade filter works', () => {
runTestStepWithPreAndPostParts('gradeFilter-header', () => {
runTestStepWithPreAndPostParts('Grade', () => {
cy.cs('gradeFilter-5').click()
checkFilteringResult(20)
cy.cs('gradeFilter-3').click()
@@ -185,44 +232,68 @@ describe('Course Statistics', () => {
})
})
it('Programme defaults to "Attainment" mode', () => {
runTestStepWithPreAndPostParts('Programme', () => {
cy.cs('Programme-filter-mode-selector').children().eq(0).should('contain', 'Attainment')
})
})
it('Programme filter works', () => {
runTestStepWithPreAndPostParts('Programme', () => {
const card = cy.cs('Programme-filter-card')
const programmeDropdown = card.cs('Programme-filter-dropdown').selectFromDropdown(1)
checkFilteringResult(116)
programmeDropdown.get('i.delete').click()
})
})
it('Study track filter works', () => {
runTestStepWithPreAndPostParts('StudyTrack', () => {
const card = cy.cs('StudyTrack-filter-card')
const programmeDropdown = card.cs('StudyTrack-filter-dropdown').selectFromDropdown(0)
checkFilteringResult(1)
programmeDropdown.get('i.delete').click()
})
})
it('Age filter works', () => {
runTestStepWithPreAndPostParts('ageFilter-header', () => {
runTestStepWithPreAndPostParts('Age', () => {
cy.cs('ageFilter-min').type('20')
cy.cs('ageFilter-max').type('40')
checkFilteringResult(33)
checkFilteringResult(31)
cy.cs('ageFilter-min').find('input').clear()
cy.cs('ageFilter-max').find('input').clear()
})
})
it('Gender filter works', () => {
runTestStepWithPreAndPostParts('genderFilter-header', () => {
cy.selectFromDropdown('genderFilter-dropdown', 0)
runTestStepWithPreAndPostParts('Gender', () => {
cy.cs('genderFilter-dropdown').selectFromDropdown(0)
checkFilteringResult(27)
cy.selectFromDropdown('genderFilter-dropdown', 1)
cy.cs('genderFilter-dropdown').selectFromDropdown(1)
checkFilteringResult(91)
cy.selectFromDropdown('genderFilter-dropdown', 2)
cy.cs('genderFilter-dropdown').selectFromDropdown(2)
checkFilteringResult(0)
cy.selectFromDropdown('genderFilter-dropdown', 3)
cy.cs('genderFilter-dropdown').selectFromDropdown(3)
checkFilteringResult(0)
clearSingleDropdownSelection('genderFilter-dropdown')
})
})
it('Starting year filter works', () => {
runTestStepWithPreAndPostParts('startYearAtUni-header', () => {
cy.selectFromDropdown('startYearAtUni-dropdown', [0])
runTestStepWithPreAndPostParts('StartYearAtUni', () => {
cy.cs('startYearAtUni-dropdown').selectFromDropdown([0])
checkFilteringResult(1)
clearSemanticUIMultipleDropDownSelection('startYearAtUni-dropdown')
cy.selectFromDropdown('startYearAtUni-dropdown', 12)
cy.cs('startYearAtUni-dropdown').selectFromDropdown(12)
checkFilteringResult(29)
clearSemanticUIMultipleDropDownSelection('startYearAtUni-dropdown')
})
})
it('Filter combinations work', () => {
runTestStepWithPreAndPostParts('gradeFilter-header', () => {
runTestStepWithPreAndPostParts('ageFilter-header', () => {
runTestStepWithPreAndPostParts('Grade', () => {
runTestStepWithPreAndPostParts('Age', () => {
cy.cs('gradeFilter-3').click()
cy.cs('ageFilter-min').type('20')
cy.cs('ageFilter-max').type('30')
@@ -244,13 +315,14 @@ describe('Custom Population Statistics', () => {
cy.fixture('customPopulations').then(({ studentNumbersForCSStudentsSet1, studentNumbersForCSStudentsSet2 }) => {
const students = [...studentNumbersForCSStudentsSet1, ...studentNumbersForCSStudentsSet2]
runTestStepWithPreAndPostParts = createRunTestStepWithPreAndPostPartsFunction(students.length)
cy.cs('student-no-input').click().type(students.join('/n'))
cy.cs('student-no-input').click().type(students.join('\n'))
cy.wait(10000)
})
cy.cs('search-button').click()
})
it('Age filter works', () => {
runTestStepWithPreAndPostParts('ageFilter-header', () => {
runTestStepWithPreAndPostParts('Age', () => {
cy.cs('ageFilter-min').type('20')
cy.cs('ageFilter-max').type('30')
checkFilteringResult(1)
@@ -259,36 +331,59 @@ describe('Custom Population Statistics', () => {
})
})
it('Gender filter works', () => {
cy.cs('genderFilter-header').click()
cy.selectFromDropdown('genderFilter-dropdown', 0)
checkFilteringResult(3)
cy.selectFromDropdown('genderFilter-dropdown', 1)
checkFilteringResult(2)
cy.selectFromDropdown('genderFilter-dropdown', 2)
checkFilteringResult(0)
clearSingleDropdownSelection('genderFilter-dropdown')
checkFilteringResult(5, true)
})
it('Starting year filter works', () => {
runTestStepWithPreAndPostParts('startYearAtUni-header', () => {
cy.selectFromDropdown('startYearAtUni-dropdown', [0])
checkFilteringResult(1)
clearSemanticUIMultipleDropDownSelection('startYearAtUni-dropdown')
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('Courses filter works', () => {
runTestStepWithPreAndPostParts('courseFilter-header', () => {
const courses = ['MAT11001', 'TKT20004']
cy.cs('courseFilter-course-dropdown').click().contains(courses[0]).click()
it('Programme filter works', () => {
runTestStepWithPreAndPostParts('Programme', () => {
const card = cy.cs('Programme-filter-card')
const programmeDropdown = card.cs('Programme-filter-dropdown').selectFromDropdown(0)
checkFilteringResult(5)
cy.selectFromDropdown(`courseFilter-${courses[0]}-dropdown`, 1)
checkFilteringResult(5)
cy.cs('courseFilter-course-dropdown').click().contains(courses[1]).click()
programmeDropdown.get('i.delete').click()
})
})
it('Gender filter works', () => {
runTestStepWithPreAndPostParts('Gender', () => {
cy.cs('genderFilter-dropdown').selectFromDropdown(0)
checkFilteringResult(3)
cy.selectFromDropdown(`courseFilter-${courses[1]}-dropdown`, 3)
cy.cs('genderFilter-dropdown').selectFromDropdown(1)
checkFilteringResult(2)
cy.cs('genderFilter-dropdown').selectFromDropdown(2)
checkFilteringResult(0)
clearSingleDropdownSelection('genderFilter-dropdown')
checkFilteringResult(5, true)
})
})
it('Starting year filter works', () => {
runTestStepWithPreAndPostParts('StartYearAtUni', () => {
cy.cs('startYearAtUni-dropdown').selectFromDropdown([0])
checkFilteringResult(1)
clearSemanticUIMultipleDropDownSelection('startYearAtUni-dropdown')
cy.cs('startYearAtUni-dropdown').click()
})
})
it.skip('Courses filter works', () => {
runTestStepWithPreAndPostParts('Courses', () => {
const courses = ['MAT11001', 'TKT20004']
cy.cs('courseFilter-course-dropdown')
.click()
.contains(courses[0] + ' ')
.click()
checkFilteringResult(5)
cy.cs(`courseFilter-${courses[0]}-dropdown`).selectFromDropdown(1)
checkFilteringResult(5)
cy.cs('courseFilter-course-dropdown')
.click()
.contains(courses[1] + ' ')
.click()
checkFilteringResult(3)
cy.cs(`courseFilter-${courses[1]}-dropdown`).selectFromDropdown(3)
checkFilteringResult(1)
courses.forEach(course => cy.cs(`courseFilter-${course}-clear`).click())
})
+56 -5
View File
@@ -1,5 +1,8 @@
/// <reference types="Cypress" />
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 => {
@@ -13,6 +16,8 @@ const setPopStatsUntil = (until, includeSettings = []) => {
describe('Population Statistics tests', () => {
const pathToCSBach2017 =
'/populations?months=36&semesters=FALL&semesters=SPRING&studyRights={%22programme%22%3A%22KH50_005%22}&tag&year=2017'
const pathToCSMaster2019 =
'/populations?months=27&semesters=FALL&semesters=SPRING&studyRights=%7B%22programme%22%3A%22MH50_009%22%7D&tag&year=2019'
describe('when using basic user', () => {
beforeEach(() => {
cy.init('/populations')
@@ -59,6 +64,7 @@ describe('Population Statistics tests', () => {
it('Population statistics is usable on general level', () => {
cy.visit(pathToCSBach2017)
cy.cs('filtered-students')
setPopStatsUntil('toukokuu 2020')
cy.get('.card').within(() => {
@@ -69,7 +75,8 @@ describe('Population Statistics tests', () => {
cy.contains('Excludes students with non-degree study right')
cy.contains('Excludes students who have transferred out of this programme')
})
cy.contains('Courses of population').click({ force: true })
cy.cs('filtered-students')
cy.contains('Courses of population').click()
cy.intercept('/api/v3/courseyearlystats**').as('coursePage')
// eslint-disable-next-line cypress/no-unnecessary-waiting
@@ -123,18 +130,62 @@ describe('Population Statistics tests', () => {
cy.contains('Credit accumulation (for 170 students)')
})
it('Credit Statistics, Statistics pane works', () => {
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())
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)')
for (const [start, end] of ranges) {
let value = 'credits'
if (start !== null) {
value = `${start} ≤ ${value}`
}
if (end !== null) {
value = `${value} < ${end}`
}
cy.get('.credits-gained-table').should('contain', value)
}
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)
})
cy.get("[data-cy='credits-gained-table-Ei valintatapaa']").should('not.exist')
})
it('Credit Statistics, Credits Gained tab shows stats by admissions', () => {
cy.visit(pathToCSMaster2019)
cy.cs('filtered-students')
cy.contains('Credit statistics')
cy.get("[data-cy='credit-statistics']").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')
cy.get("[data-cy='credits-gained-table-Muu']").should('exist')
cy.get("[data-cy='credits-gained-table-Ei valintatapaa']").should('exist')
})
it('Credit Statistics, Statistics pane works', () => {
cy.selectStudyProgramme('Tietojenkäsittelytieteen kandiohjelma')
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-q1']").should('contain', '70')
cy.get("[data-cy='credit-stats-q2']").should('contain', '129')
cy.get("[data-cy='credit-stats-q3']").should('contain', '168')
cy.get("[data-cy='credit-stats-max']").should('contain', '314')
})
})
+12 -2
View File
@@ -13,7 +13,7 @@ describe('Student Statistics tests', () => {
studentnumber: '010654019',
}
it.only('Student statistics search form is usable', () => {
it('Student statistics search form is usable', () => {
cy.contains('Show student names')
cy.url().should('include', '/students')
cy.get('.prompt').type(student.lastname)
@@ -80,7 +80,7 @@ describe('Student Statistics tests', () => {
cy.contains('Student number').should('not.exist')
})
it.only('Can jump to population page', () => {
it('Can jump to population page', () => {
cy.get('.prompt').type(student.studentnumber)
cy.contains(student.studentnumber).click()
cy.get('i.level.up.alternate.icon').eq(0).click()
@@ -88,3 +88,13 @@ describe('Student Statistics tests', () => {
cy.contains('Tietojenkäsittelytieteen kandiohjelma')
})
})
// Use admin to see all students
describe('Testing with admin rights', () => {
it('Does not crash if student has no studyright or courses', () => {
const studentNumber = '011825096'
cy.init(`/students/${studentNumber}`, 'admin') // use admin to see all students
cy.contains('Started: Unavailable')
cy.contains('Credits: 0')
})
})
@@ -79,7 +79,7 @@ describe('Studyprogramme overview', () => {
let originalProductivityCalculatedText
const doRecalculation = () => {
// Wait for page to load before clicking to admin page, TODO: change to cy.intercept
cy.wait(2000)
cy.wait(5000)
cy.get('.attached').contains('Admin').click()
cy.contains('recalculate productivity').click()
cy.contains('recalculate throughput').click()
+17 -22
View File
@@ -4,16 +4,16 @@ describe('Users tests', () => {
describe('Using user with just grp-oodikone-user, no other rights', () => {
it('shows only frontpage, trends feedback', () => {
cy.init('', 'norights') // login with norights user
cy.get('.navBar').contains('Trends').should('exist')
cy.get('.navBar').contains('Give feedback').should('exist')
cy.get('[data-cy="navBar"]').contains('Trends').should('exist')
cy.get('[data-cy="navBar"]').contains('Give feedback').should('exist')
cy.get('.navBar').contains('Study programme').should('not.exist')
cy.get('.navBar').contains('Student statistics').should('not.exist')
cy.get('.navBar').contains('Course statistics').should('not.exist')
cy.get('.navBar').contains('Teachers').should('not.exist')
cy.get('.navBar').contains('Users').should('not.exist')
cy.get('.navBar').contains('Faculty').should('not.exist')
cy.get('.navBar').contains('Updater').should('not.exist')
cy.get('[data-cy="navBar"]').contains('Study programme').should('not.exist')
cy.get('[data-cy="navBar"]').contains('Student statistics').should('not.exist')
cy.get('[data-cy="navBar"]').contains('Course statistics').should('not.exist')
cy.get('[data-cy="navBar"]').contains('Teachers').should('not.exist')
cy.get('[data-cy="navBar"]').contains('Users').should('not.exist')
cy.get('[data-cy="navBar"]').contains('Faculty').should('not.exist')
cy.get('[data-cy="navBar"]').contains('Updater').should('not.exist')
})
})
@@ -29,32 +29,27 @@ describe('Users tests', () => {
})
it('should see more stuff than others', () => {
cy.get('.navBar').contains('Study programme').should('exist')
cy.get('.navBar').contains('Student statistics').should('exist')
cy.get('.navBar').contains('Course statistics').should('exist')
// TODO: admin should probably see this as well, since can access this page: cy.get('.navBar').contains('Teachers').should('exist')
cy.get('.navBar').contains('Users').should('exist')
cy.get('.navBar').contains('Trends').should('exist')
cy.get('[data-cy="navBar"]').contains('Study programme').should('exist')
cy.get('[data-cy="navBar"]').contains('Student statistics').should('exist')
cy.get('[data-cy="navBar"]').contains('Course statistics').should('exist')
// TODO: admin should probably see this as well, since can access this page: cy.get('[data-cy="navBar"]').contains('Teachers').should('exist')
cy.get('[data-cy="navBar"]').contains('Users').should('exist')
cy.get('[data-cy="navBar"]').contains('Trends').should('exist')
})
it("mocking normal user shows only the mocked user's programmes", () => {
cy.contains('mocking').should('not.exist')
cy.contains('tr', 'basic').within($row => {
cy.contains('tr', 'basic').within(() => {
cy.contains('.button', 'Edit').click()
})
cy.intercept({
method: 'POST',
path: '/api/superlogin/basic',
}).as('superlogin')
cy.get('i.spy').click()
cy.wait('@superlogin')
cy.contains('mocking as basic')
cy.wait(1000)
cy.contains('Study programme').click().siblings().contains('Search by class').click()
cy.contains('label', 'Study programme')
cy.contains('label', 'Study programme')
.siblings()
.within($row => {
.within(() => {
cy.get("div[role='option']").should('have.length', 2).contains('Tietojenkäsittelytieteen kandiohjelma')
})
})
+19 -8
View File
@@ -7,7 +7,7 @@ const adminUserHeaders = {
uid: 'admin',
displayname: 'Admin User',
'shib-session-id': 'mock-cypress-session',
hygroupcn: 'grp-oodikone-users;grp-oodikone-basic-users',
hygroupcn: 'grp-oodikone-users;grp-oodikone-basic-users;grp-toska',
edupersonaffiliation: 'member;employee;faculty',
mail: 'grp-toska+mockadminuser@helsinki.fi',
hypersonsisuid: 'hy-hlo-6666666',
@@ -54,8 +54,6 @@ Cypress.Commands.add('init', (path = '', userId = 'basic') => {
if (!headersToUse) throw Error(`${userId} is not valid user id!`)
req.headers = headersToUse
})
// Hide SIS warning flag to avoid screen size problems when running headlessly
window.localStorage.setItem('SIS_WARNING_FLAG', 'true')
const baseUrl = Cypress.config().baseUrl
cy.visit(baseUrl.concat(path))
})
@@ -63,24 +61,37 @@ Cypress.Commands.add('init', (path = '', userId = 'basic') => {
/**
* Shorthand for using "Cypress Selectors" (CS), i.e., `data-cy` attributes.
*/
Cypress.Commands.add('cs', name => {
return cy.get(`[data-cy='${name}']`)
Cypress.Commands.add('cs', { prevSubject: 'optional' }, (subject, name) => {
const selector = `[data-cy="${name}"]`
if (subject === undefined) {
return cy.get(selector)
} else {
return cy.wrap(subject).find(selector)
}
})
/**
* Select item specified by `index` number from a semantic-ui dropdown with
* `data-cy` attribute value `name`.
*/
Cypress.Commands.add('selectFromDropdown', (name, index) => {
Cypress.Commands.add('selectFromDropdown', { prevSubject: true }, (s, index) => {
const subject = cy.wrap(s)
const indexes = Array.isArray(index) ? index : [index]
indexes.forEach(i => {
cy.cs(name).click().children('.menu').children().eq(i).click({ force: true })
subject.click()
if (typeof i === 'number') {
subject.children('.menu').children().eq(i).click({ force: true })
} else {
subject.children('.menu').containing(i).eq(0).click({ force: true })
}
})
// Close multiple selection so it does not block elements underneath it.
if (Array.isArray(index)) {
cy.cs(name).children('.icon').click({ force: true })
cy.wrap(s).children('.icon').click({ force: true })
}
})
+56 -20
View File
@@ -2,7 +2,6 @@ x-defaults: &defaults
restart: unless-stopped
services:
backend:
<<: *defaults
container_name: backend
@@ -10,17 +9,16 @@ services:
- kone-db
- redis
- sis-db
- user-db
environment:
CI: "true"
CI: 'true'
DB_URL_USER: postgres://postgres@user-db:5432/user-db
DB_URL_KONE: postgres://postgres@kone-db:5432/kone-db
FRONT_URL: http://localhost:1337
REDIS: redis
SECRET_TOKEN: dev
SIS_DB_URL: postgres://postgres@sis-db:5432/sis-db
SIS_UPDATER_URL: http://sis-updater-scheduler:8082
TOKEN_SECRET: IWannaBeTheVeryBest
USERSERVICE_SECRET: Luukk4inenApprov3dVitunMinttuSalainenAvain666NurmivaaraPJ2019
USERSERVICE_URL: http://userservice:4567
image: toska/oodikone2-backend:staging
frontend:
@@ -31,11 +29,15 @@ services:
kone-db:
<<: *defaults
container_name: kone-db
environment:
PGDATA: /data
POSTGRES_DB: kone-db
POSTGRES_HOST_AUTH_METHOD: trust
image: registry.toska.cs.helsinki.fi/kone-db
redis:
<<: *defaults
command: "redis-server --appendonly yes"
command: 'redis-server --appendonly yes'
container_name: redis
image: redis
ports:
@@ -45,23 +47,20 @@ services:
<<: *defaults
container_name: sis-db
image: registry.toska.cs.helsinki.fi/sis-db
environment:
PGDATA: /data
POSTGRES_DB: sis-db
POSTGRES_HOST_AUTH_METHOD: trust
shm_size: 2gb
user-db:
<<: *defaults
container_name: user-db
image: registry.toska.cs.helsinki.fi/user-db
userservice:
<<: *defaults
container_name: userservice
depends_on:
- user-db
environment:
CI: "true"
DB_URL: postgres://postgres@user-db:5432/user-db
SECRET: Luukk4inenApprov3dVitunMinttuSalainenAvain666NurmivaaraPJ2019
TOKEN_SECRET: IWannaBeTheVeryBest
image: toska/oodikone2-userservice:staging
PGDATA: /data
POSTGRES_DB: user-db
POSTGRES_HOST_AUTH_METHOD: trust
image: registry.toska.cs.helsinki.fi/user-db
nginx:
image: nginx:latest
@@ -69,8 +68,45 @@ services:
depends_on:
- frontend
- backend
- userservice
ports:
- 1337:80
- 1337:80
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf
# Following services are used to run sis-updater-worker when anon sis-db needs to be updated. Could probably also use production version, but this certainly works.
sis-importer-db:
<<: *defaults
container_name: sis-importer-db
image: registry.toska.cs.helsinki.fi/sis-importer-db
profiles:
- updater
sis-updater-worker:
<<: *defaults
build:
target: development
context: updater/sis-updater-worker
depends_on:
- redis
- sis-db
- sis-importer-db
- sis-updater-nats
environment:
DB_URL: postgres://postgres@sis-db:5432/sis-db
REDIS_URI: //redis:6379
SIS_IMPORTER_DATABASE: sis-importer-db
SIS_IMPORTER_HOST: sis-importer-db
SIS_IMPORTER_PASSWORD: postgres
SIS_IMPORTER_USER: postgres
SIS_NATS_TOKEN: dev
SIS_NATS_URI: sis-updater-nats://sis-updater-nats:4222
profiles:
- updater
sis-updater-nats:
<<: *defaults
command: '-cid sis-updater-nats -m 8222 -p 4222 --max_age 2h --hb_fail_count 2 --hb_timeout 10s --hb_interval 10s --auth dev --file_slice_max_bytes 0 --file_slice_max_age 60s --max_msgs 0 -store file -dir datastore'
container_name: sis-updater-nats
image: nats-streaming:0.16.2
profiles:
- updater
+1 -5
View File
@@ -1,7 +1,7 @@
services:
backend:
environment:
DB_URL_USER: postgres://postgres@user-db:5432/user-db-real
DB_URL_KONE: postgres://postgres@kone-db:5432/kone-db-real
SIS_DB_URL: postgres://postgres@sis-db:5432/sis-db-real
@@ -18,9 +18,5 @@ services:
DB_URL: postgres://postgres@sis-db:5432/sis-db-real
SIS_IMPORTER_DATABASE: sis-importer-db-real
userservice:
environment:
DB_URL: postgres://postgres@user-db:5432/user-db-real
volumes:
redis-data-real:
-10
View File
@@ -1,10 +0,0 @@
services:
userservice-test:
build:
context: services/userservice
target: test
container_name: userservice-test
environment:
SECRET: Luukk4inenApprov3dVitunMinttuSalainenAvain666NurmivaaraPJ2019
TOKEN_SECRET: IWannaBeTheVeryBest
+2 -24
View File
@@ -26,18 +26,17 @@ services:
- kone-db
- redis
- sis-db
- user-db
env_file:
- .env
environment:
DB_URL_KONE: postgres://postgres@kone-db:5432/kone-db
DB_URL_USER: postgres://postgres@user-db:5432/user-db
FRONT_URL: http://localhost:3000
REDIS: redis
SECRET_TOKEN: dev
SIS_DB_URL: postgres://postgres@sis-db:5432/sis-db
SIS_UPDATER_URL: http://sis-updater-scheduler:8082
TOKEN_SECRET: IWannaBeTheVeryBest
USERSERVICE_SECRET: Luukk4inenApprov3dVitunMinttuSalainenAvain666NurmivaaraPJ2019
USERSERVICE_URL: http://userservice:4567
ports:
- 8080:8080
profiles:
@@ -165,26 +164,6 @@ services:
volumes:
- user-db-data:/data
userservice:
<<: *defaults
build:
<<: *build_defaults
context: services/userservice
container_name: userservice
depends_on:
- user-db
environment:
DB_URL: postgres://postgres@user-db:5432/user-db
SECRET: Luukk4inenApprov3dVitunMinttuSalainenAvain666NurmivaaraPJ2019
TOKEN_SECRET: IWannaBeTheVeryBest
ports:
- 4567:4567
profiles:
- oodikone
volumes:
- userservice-nodemod:/usr/src/app/node_modules
- ./services/userservice/:/usr/src/app
volumes:
backend-nodemod:
frontend-nodemod:
@@ -195,4 +174,3 @@ volumes:
sis-updater-scheduler-nodemod:
sis-updater-worker-nodemod:
user-db-data:
userservice-nodemod:
+34 -6593
View File
File diff suppressed because it is too large Load Diff
+5
View File
@@ -6,6 +6,7 @@
"concurrently": "concurrently",
"eslint": "eslint",
"stylelint": "stylelint",
"prettier": "prettier",
"cli": "./cli.sh",
"oodikone": "./run.sh oodikone anon up --detach",
"oodikone:real": "./run.sh oodikone real up --detach",
@@ -48,5 +49,9 @@
"stylelint-config-prettier": "^8.0.2",
"stylelint-config-standard": "^22.0.0",
"stylelint-prettier": "^1.2.0"
},
"dependencies": {
"lodash": "^4.17.21",
"moment": "^2.29.1"
}
}
+1 -2
View File
@@ -64,8 +64,7 @@ parse_profiles() {
return 0
}
# Set which docker-compose files to use based on version. In ci, profiles are also
# emptied, since they're passed from github actions.
# Set which docker-compose files to use based on version.
parse_env() {
env=""
if [[ "$version" == "real" ]]; then
+53 -12
View File
@@ -20,10 +20,10 @@ USER_DB_NAME="user-db"
DATABASES=("$KONE_DB_NAME" "$SIS_DB_NAME" "$SIS_IMPORTER_DB_NAME" "$USER_DB_NAME")
## Urls should be in same order as databases as both are iterated through by indexes.
KONE_DB_REAL_DUMP_URL="oodikone.cs.helsinki.fi:/home/tkt_oodi/backups/latest-kone-pg.sqz"
KONE_DB_REAL_DUMP_URL="svm-101.cs.helsinki.fi:/home/oodikone_user/backups/latest-kone-db.sqz"
SIS_DB_REAL_DUMP_URL="svm-96.cs.helsinki.fi:/home/updater_user/backups/latest-sis.sqz"
SIS_IMPORTER_DB_REAL_DUMP_URL="importer:/home/importer_user/importer-db/backup/importer-db.sqz"
USER_DB_REAL_DUMP_URL="oodikone.cs.helsinki.fi:/home/tkt_oodi/backups/latest-user-pg.sqz"
SIS_IMPORTER_DB_REAL_DUMP_URL="svm-92.cs.helsinki.fi:/home/importer_user/importer-db/backup/importer-db.sqz"
USER_DB_REAL_DUMP_URL="svm-101.cs.helsinki.fi:/home/oodikone_user/backups/latest-user-db.sqz"
REAL_DUMP_URLS=("$KONE_DB_REAL_DUMP_URL" "$SIS_DB_REAL_DUMP_URL" "$SIS_IMPORTER_DB_REAL_DUMP_URL" "$USER_DB_REAL_DUMP_URL")
# Source utility functions
@@ -110,12 +110,54 @@ reset_all_real_data() {
reset_databases "${DATABASES[@]}"
}
reset_sis_importer_data() {
infomsg "Downloading sis-importer-db dump"
local database=$SIS_IMPORTER_DB_NAME
local url=$SIS_IMPORTER_DB_REAL_DUMP_URL
download_real_dump $database $url
reset_databases $database
reset_single_database() {
# Define custom shell prompt for the nested interactive select loop
PS3="Please enter which database to reset: "
options=(
"kone-db"
"sis-db"
"sis-importer-db"
"user-db"
"Return to main menu."
)
local database=""
local url=""
while true; do
select opt in "${options[@]}"; do
case $opt in
"kone-db")
local database=$KONE_DB_NAME
local url=$KONE_DB_REAL_DUMP_URL
break 2;;
"sis-db")
local database=$SIS_DB_NAME
local url=$SIS_DB_REAL_DUMP_URL
break 2;;
"sis-importer-db")
local database=$SIS_IMPORTER_DB_NAME
local url=$SIS_IMPORTER_DB_REAL_DUMP_URL
break 2;;
"user-db")
local database=$USER_DB_NAME
local url=$USER_DB_REAL_DUMP_URL
break 2;;
"Return to main menu.")
break 2;;
*) msg "${RED}Invalid option:${NOFORMAT} $REPLY
";;
esac
break
done
done
if [ -n "$database" ]; then
infomsg "Downloading $database dump"
download_real_dump $database $url
reset_databases $database
fi
}
set_up_oodikone_from_scratch() {
@@ -123,7 +165,6 @@ set_up_oodikone_from_scratch() {
folders_to_set_up=(
"$PROJECT_ROOT"
"$PROJECT_ROOT/services/frontend"
"$PROJECT_ROOT/services/userservice"
"$PROJECT_ROOT/services/backend"
"$PROJECT_ROOT/updater/sis-updater-scheduler"
"$PROJECT_ROOT/updater/sis-updater-worker"
@@ -152,10 +193,10 @@ linting and formatting"
npm run prepare
infomsg "Pulling images"
"$PROJECT_ROOT"/run.sh oodikone anon pull
"$PROJECT_ROOT"/run.sh both anon pull
infomsg "Building images"
"$PROJECT_ROOT"/run.sh oodikone anon build
"$PROJECT_ROOT"/run.sh both anon build
successmsg "Setup ready, oodikone can be started! See README for more info."
}
-3
View File
@@ -1,6 +1,3 @@
.dockerignore
Dockerfile
node_modules/
requests/
scripts/
*.log
-3
View File
@@ -1,3 +0,0 @@
# Student diff output
/student_diff_*
/diff_partial_att_data.json
+2 -1
View File
@@ -1,5 +1,6 @@
require('./src/app')
const logger = require('./src/util/logger')
process.on('unhandledRejection', reason => {
console.log(reason)
logger.error({ message: 'unhandledRejection: ', meta: reason })
})
+52 -4527
View File
File diff suppressed because it is too large Load Diff
+3 -5
View File
@@ -15,12 +15,12 @@
"@sentry/tracing": "^6.11.0",
"async": "^3.2.1",
"async-lock": "^1.3.0",
"axios": "^0.21.1",
"axios": "^0.21.2",
"bluebird": "^3.7.2",
"cors": "^2.8.5",
"cron": "^1.8.2",
"express": "^4.17.1",
"jsonwebtoken": "^8.5.1",
"express-async-errors": "^3.1.1",
"lodash": "^4.17.21",
"lru-cache": "^5.1.1",
"moment": "^2.29.1",
@@ -32,9 +32,7 @@
"umzug": "^2.3.0",
"unfuck-utf8-headers-middleware": "^1.0.1",
"uuid": "^3.4.0",
"winston": "^3.3.3",
"winston-log2gelf": "^2.4.0",
"winston-transport": "^4.4.0"
"winston": "^3.3.3"
},
"devDependencies": {
"nodemon": "^1.19.4"
+9 -25
View File
@@ -1,51 +1,35 @@
require('express-async-errors')
const express = require('express')
const cors = require('cors')
const Sentry = require('@sentry/node')
const { baseUrl, backendPort, frontUrl } = require('./conf-backend')
const { baseUrl, backendPort } = require('./conf-backend')
const routes = require('./routes')
const { startCron } = require('./events')
const { initializeDatabaseConnection, dbConnections } = require('./database/connection')
const initializeSentry = require('./util/sentry')
const logger = require('./util/logger')
initializeDatabaseConnection()
.then(() => {
dbConnections.connect()
dbConnections.on('connect', () => {
console.log('Connected to sis db successfully')
logger.info('Connected to sis db successfully')
})
dbConnections.on('error', () => {
console.log('Failed connecting to sis db')
logger.error('Failed to connect to sis db!')
})
const app = express()
initializeSentry(app)
app.use(Sentry.Handlers.requestHandler())
app.use(Sentry.Handlers.tracingHandler())
startCron()
app.use(cors({ credentials: true, origin: frontUrl }))
app.use(express.json())
const app = express()
routes(app, baseUrl)
app.get('*', async (req, res) => {
const results = { error: 'unknown endpoint' }
res.status(404).json(results)
})
app.use(Sentry.Handlers.errorHandler())
const server = app.listen(backendPort, () => console.log(`Backend listening on port ${backendPort}!`))
const server = app.listen(backendPort, () => logger.info(`Backend listening on port ${backendPort}!`))
process.on('SIGTERM', () => {
server.close(() => {
console.log('Process terminated')
logger.info('Process terminated')
})
})
})
.catch(e => {
process.exitCode = 1
console.log(e)
logger.error(e)
})
+2 -9
View File
@@ -18,7 +18,6 @@ const pateToken = process.env.PATE_API_TOKEN || ''
// Importer client
const importerToken = process.env.IMPORTER_DB_API_TOKEN || ''
const importerUrl = process.env.IMPORTER_DB_API_URL || ''
// Networking: Urls & ports
const baseUrl = isDev ? '/api' : ''
@@ -37,6 +36,7 @@ const MATOMO_URL = process.env.MATOMO_URL
// Other stuff
const DB_URL_KONE = process.env.DB_URL_KONE
const DB_URL_USER = process.env.DB_URL_USER
let DB_MAX_CONNECTIONS = parseInt(process.env.DB_MAX_CONNECTIONS, 10)
if (isNaN(DB_MAX_CONNECTIONS)) {
@@ -48,10 +48,8 @@ if (DB_MAX_CRON_CONNECTIONS < 1) {
}
const redis = process.env.REDIS
const TOKEN_SECRET = process.env.TOKEN_SECRET
const SECRET_TOKEN = process.env.SECRET_TOKEN
const DB_SCHEMA_KONE = process.env.DB_SCHEMA_KONE || 'public'
const USERSERVICE_URL = process.env.USERSERVICE_URL
const SIS_UPDATER_URL = process.env.SIS_UPDATER_URL
const formatURL = url => {
@@ -60,18 +58,14 @@ const formatURL = url => {
const addSlashToEnd = url => (url.endsWith('/') ? url : url + '/')
const ACCESS_TOKEN_HEADER_KEY = 'x-access-token'
module.exports = {
DB_URL_USER,
DB_URL_KONE,
DB_MAX_CONNECTIONS,
DB_MAX_CRON_CONNECTIONS,
redis,
TOKEN_SECRET,
SECRET_TOKEN,
DB_SCHEMA_KONE,
USERSERVICE_URL: formatURL(USERSERVICE_URL),
ACCESS_TOKEN_HEADER_KEY,
requiredGroup,
SIS_UPDATER_URL,
ANALYTICS_INFLUXDB_URL: formatURL(ANALYTICS_INFLUXDB_URL),
@@ -89,5 +83,4 @@ module.exports = {
frontUrl,
backendPort,
importerToken,
importerUrl,
}
+44 -37
View File
@@ -3,6 +3,8 @@ const EventEmitter = require('events')
const Umzug = require('umzug')
const conf = require('../conf-backend')
const { SIS_DB_URL } = process.env
const logger = require('../util/logger')
class DbConnection extends EventEmitter {
constructor() {
super()
@@ -31,74 +33,79 @@ class DbConnection extends EventEmitter {
this.emit('error', e)
return
}
console.log(`Sis database connection failed! Attempt ${attempt}/${this.RETRY_ATTEMPTS}`)
console.log(e)
logger.error({ message: `Sis database connection failed! Attempt ${attempt}/${this.RETRY_ATTEMPTS}`, meta: e })
setTimeout(() => this.connect(attempt + 1), 1000 * attempt)
}
}
}
// Old-style kone + user db connections
const sequelizeKone = new Sequelize(conf.DB_URL_KONE, {
schema: conf.DB_SCHEMA_KONE,
searchPath: conf.DB_SCHEMA_KONE,
logging: false,
})
sequelizeKone.query(`SET SESSION search_path to ${conf.DB_SCHEMA_KONE}`)
const sequelizeUser = new Sequelize(conf.DB_URL_USER, {
logging: false,
})
const initializeDatabaseConnection = async () => {
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms))
const waitSeconds = 60
for (let i = 1; i <= waitSeconds; i++) {
try {
await sequelizeKone.authenticate()
break
} catch (e) {
if (i === waitSeconds) {
console.log(`Could not connect to database in ${waitSeconds} seconds`)
throw e
const rounds = 60
for (const [seq, dbName] of [
[sequelizeKone, 'kone-db'],
[sequelizeUser, 'user-db'],
]) {
logger.info(`Connecting to ${dbName}...`)
for (let round = 1; round <= rounds; round++) {
try {
await seq.authenticate()
break
} catch (error) {
if (round === rounds) {
logger.error(`${dbName} database connection failed!`)
throw error
}
await sleep(1000)
}
console.log('.')
await sleep(1000)
}
}
try {
logger.info(`${dbName} database connection established`)
const schema = dbName === 'kone-db' ? conf.DB_SCHEMA_KONE : undefined
const migrationsFolder = dbName === 'kone-db' ? 'migrations_kone' : 'migrations_user'
const migrator = new Umzug({
storage: 'sequelize',
storageOptions: {
sequelize: sequelizeKone,
sequelize: seq,
tableName: 'migrations',
schema: conf.DB_SCHEMA_KONE,
schema,
},
logging: console.log,
migrations: {
params: [sequelizeKone.getQueryInterface(), Sequelize],
path: `${process.cwd()}/src/database/migrations_kone`,
params: [seq.getQueryInterface(), Sequelize],
path: `${process.cwd()}/src/database/${migrationsFolder}`,
pattern: /\.js$/,
schema: conf.DB_SCHEMA_KONE,
schema,
},
})
const migrations = await migrator.up()
console.log('Kone Migrations up to date', migrations)
} catch (e) {
console.log('Kone Migration error')
throw e
try {
const migrations = await migrator.up()
logger.info({ message: `${dbName} migrations up to date: `, meta: migrations.map(m => m.file) })
} catch (error) {
logger.error({ message: `${dbName} migrations failed`, meta: error })
throw error
}
}
}
const forceSyncDatabase = async () => {
try {
await sequelizeKone.getQueryInterface().createSchema(conf.DB_SCHEMA_KONE)
} catch (e) {
//console.log(e)
}
await sequelizeKone.sync({ force: true })
}
const dbConnections = new DbConnection()
module.exports = {
dbConnections,
sequelizeKone,
sequelizeUser,
initializeDatabaseConnection,
forceSyncDatabase,
}
@@ -0,0 +1,34 @@
const { BIGINT, STRING, DATE } = require('sequelize')
module.exports = {
up: async queryInterface => {
await queryInterface.createTable(
'study_guidance_group_tags',
{
id: {
primaryKey: true,
type: BIGINT,
autoIncrement: true,
},
study_guidance_group_id: {
type: STRING,
unique: true,
},
study_programme: {
type: STRING,
},
year: {
type: STRING,
},
created_at: {
type: DATE,
},
updated_at: {
type: DATE,
},
},
{ underscored: true, timestamps: true }
)
},
down: async () => {},
}
@@ -0,0 +1,10 @@
module.exports = {
up: (queryInterface, Sequelize) => {
const Op = Sequelize.Op
return queryInterface.bulkDelete('access_groups', [
{
group_code: { [Op.in]: ['dev', 'users', 'cooldata', 'studyprogramme'] },
},
])
},
}
@@ -0,0 +1,10 @@
const { DATE } = require('sequelize')
module.exports = {
up: async queryInterface => {
await queryInterface.addColumn('users', 'last_login', DATE)
},
down: async queryInterface => {
await queryInterface.removeColumn('users', 'last_login')
},
}
@@ -0,0 +1,8 @@
module.exports = {
up: async queryInterface =>
queryInterface.sequelize.query(`
--- add lisensiaatti to lääkis faculty
INSERT INTO faculty_programmes VALUES ('H90', 'MH90_001', 'NOW()', 'NOW()');
`),
down: async () => {},
}
@@ -0,0 +1,6 @@
module.exports = {
up: async queryInterface => {
await queryInterface.dropTable('faculty_programmes')
},
down: async () => {},
}
@@ -0,0 +1,9 @@
module.exports = {
up: async queryInterface => {
await queryInterface.dropTable('user_affiliation')
await queryInterface.sequelize.query('DROP SEQUENCE IF EXISTS user_affilation_id_seq')
await queryInterface.dropTable('affiliations')
await queryInterface.sequelize.query('DROP SEQUENCE IF EXISTS affiliations_id_seq')
},
down: async () => {},
}
@@ -0,0 +1,9 @@
module.exports = {
up: async queryInterface => {
await queryInterface.dropTable('user_hy_group')
await queryInterface.sequelize.query('DROP SEQUENCE IF EXISTS user_hy_group_id_seq')
await queryInterface.dropTable('hy_groups')
await queryInterface.sequelize.query('DROP SEQUENCE IF EXISTS hy_groups_id_seq')
},
down: async () => {},
}
+120 -160
View File
@@ -9,209 +9,168 @@ const {
getStartYears,
} = require('./services/trends')
const { refreshAssociationsInRedis } = require('./services/studyrights')
const { getAllProgrammes, nonGraduatedStudentsOfElementDetail } = require('./services/studyrights')
const { getAllProgrammes } = require('./services/studyrights')
const { productivityStatsForStudytrack, throughputStatsForStudytrack } = require('./services/studyprogramme')
const { findAndSaveTeachers } = require('./services/topteachers')
const {
setProductivity,
setThroughput,
patchProductivity,
patchThroughput,
patchNonGraduatedStudents,
} = require('./services/analyticsService')
const { isNewHYStudyProgramme } = require('./util')
const { setProductivity, setThroughput, patchProductivity, patchThroughput } = require('./services/analyticsService')
const { isProduction } = require('./conf-backend')
const { getCurrentSemester } = require('./services/semesters')
const logger = require('./util/logger')
const schedule = (cronTime, func) => new CronJob({ cronTime, onTick: func, start: true, timeZone: 'Europe/Helsinki' })
const refreshStudyrightAssociations = async () => {
try {
console.log('Refreshing studyright associations...')
await refreshAssociationsInRedis()
} catch (e) {
console.error(e)
}
await refreshAssociationsInRedis()
logger.info('Studyright associations refreshed!')
}
const refreshOverview = async () => {
try {
console.log('Refreshing throughput and productivity for programmes...')
const codes = (await getAllProgrammes()).map(p => p.code)
let ready = 0
for (const code of codes) {
let programmeStatsSince = new Date('2017-07-31')
if (code.includes('MH') || code.includes('KH')) {
programmeStatsSince = new Date('2017-07-31')
} else {
programmeStatsSince = new Date('2000-07-31')
}
try {
await patchThroughput({ [code]: { status: 'RECALCULATING' } })
const data = await throughputStatsForStudytrack(code, programmeStatsSince.getFullYear())
await setThroughput(data)
} catch (e) {
try {
await patchThroughput({ [code]: { status: 'RECALCULATION ERRORED' } })
} catch (e) {
console.error(e)
}
console.error(e)
console.log(`Failed to update throughput stats for code: ${code}, reason: ${e.message}`)
}
try {
await patchProductivity({ [code]: { status: 'RECALCULATING' } })
const data = await productivityStatsForStudytrack(code, programmeStatsSince)
await setProductivity(data)
} catch (e) {
try {
await patchProductivity({
[code]: { status: 'RECALCULATION ERRORED' },
})
} catch (e) {
console.error(e)
}
console.error(e)
console.log(`Failed to update productivity stats for code: ${code}, reason: ${e.message}`)
}
ready += 1
console.log(`${ready}/${codes.length} programmes done`)
logger.info('Refreshing throughput and productivity for programmes...')
const codes = (await getAllProgrammes()).map(p => p.code).filter(code => code.includes('KH') || code.includes('MH'))
let ready = 0
for (const code of codes) {
let programmeStatsSince = new Date('2017-07-31')
if (code.includes('MH') || code.includes('KH')) {
programmeStatsSince = new Date('2017-07-31')
} else {
programmeStatsSince = new Date('2000-07-31')
}
} catch (e) {
console.error(e)
try {
await patchThroughput({ [code]: { status: 'RECALCULATING' } })
const data = await throughputStatsForStudytrack(code, programmeStatsSince.getFullYear())
await setThroughput(data)
} catch (e) {
logger.error({ message: `failed to recalculate throughput for ${code}`, meta: e })
try {
await patchThroughput({ [code]: { status: 'RECALCULATION ERRORED' } })
} catch (e) {
logger.error({ message: 'failed to update throughtput status to error', meta: e })
}
}
try {
await patchProductivity({ [code]: { status: 'RECALCULATING' } })
const data = await productivityStatsForStudytrack(code, programmeStatsSince)
await setProductivity(data)
} catch (e) {
logger.error({ message: `failed to recalculate productivity for ${code}`, meta: e })
try {
await patchProductivity({
[code]: { status: 'RECALCULATION ERRORED' },
})
} catch (e) {
logger.error({ message: 'failed to update productivity status to error', meta: e })
}
}
ready += 1
logger.info(`${ready}/${codes.length} programmes done`)
}
logger.info('Throughput and productivity for programmes refreshed')
}
const refreshTeacherLeaderboard = async () => {
try {
const startyearcode = new Date().getFullYear() - 1950
const endyearcode = startyearcode + 1
console.log('Refreshing teacher leaderboard...')
await findAndSaveTeachers(startyearcode, endyearcode)
} catch (e) {
console.log(e)
}
}
const refreshNonGraduatedStudentsOfOldProgrammes = async () => {
try {
const oldProgrammeCodes = (await getAllProgrammes()).map(p => p.code).filter(c => !isNewHYStudyProgramme(c))
let i = 0
console.log('Refreshing non-graduated students of old programmes...')
await Promise.all(
oldProgrammeCodes.map(
c =>
new Promise(async res => {
try {
const [nonGraduatedStudents, studentnumbers] = await nonGraduatedStudentsOfElementDetail(c)
await patchNonGraduatedStudents({ [c]: { formattedData: nonGraduatedStudents, studentnumbers } })
console.log(`${++i}/${oldProgrammeCodes.length}`)
} catch (e) {
console.log(`Failed refreshing non-graduated students of programme ${c}!`)
}
res()
})
)
)
} catch (e) {
console.log(e)
}
// refresh this and previous year
const currentSemestersYearCode = (await getCurrentSemester()).getDataValue('yearcode')
await findAndSaveTeachers(currentSemestersYearCode - 1, currentSemestersYearCode)
}
const refreshProtoCtoRedis = async () => {
try {
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' }
console.log('Refreshing CDS ProtoC')
await refreshProtoC(defaultQuery)
await refreshProtoC(onlyOld)
await refreshProtoC(onlyEnr)
await refreshProtoC(bothToggles)
} catch (e) {
console.log(e)
}
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 () => {
try {
const unixMillis = moment().valueOf()
const date = new Date(Number(unixMillis))
const unixMillis = moment().valueOf()
const date = new Date(Number(unixMillis))
date.setHours(23, 59, 59, 999)
const showByYearOff = 'false'
const showByYear = 'true'
console.log('Refreshing CDS Status')
await refreshStatus(date.getTime(), showByYearOff)
await refreshStatus(date.getTime(), showByYear)
date.setHours(23, 59, 59, 999)
const showByYearOff = 'false'
const showByYear = 'true'
logger.info('Refreshing CDS Status')
await refreshStatus(date.getTime(), showByYearOff)
await refreshStatus(date.getTime(), showByYear)
logger.info('Refreshing CDS Status doned')
console.log('Refreshing CDS Graduated')
await refreshStatusGraduated(date.getTime(), showByYearOff)
await refreshStatusGraduated(date.getTime(), showByYear)
} catch (e) {
console.log(e)
}
logger.info('Refreshing CDS Graduated')
await refreshStatusGraduated(date.getTime(), showByYearOff)
await refreshStatusGraduated(date.getTime(), showByYear)
logger.info('Refreshing CDS graduated doned')
}
const refreshUberToRedis = async () => {
try {
const years = await getStartYears()
const mappedYears = years.map(({ studystartdate }) => studystartdate)
mappedYears.forEach(async year => {
console.log('Refreshing CDS Uber data for date', year)
const defaultQuery = { include_old_attainments: 'false', start_date: year }
const oldAttainmentsQuery = { include_old_attainments: 'true', start_date: year }
const years = (await getStartYears()).map(({ studystartdate }) => studystartdate)
for (const year of years) {
const formattedYear = new Date(year).getFullYear()
logger.info(`Refreshing CDS uber year ${formattedYear}`)
const defaultQuery = { include_old_attainments: 'false', start_date: year }
const oldAttainmentsQuery = { include_old_attainments: 'true', start_date: year }
try {
await refreshUber(defaultQuery)
await refreshUber(oldAttainmentsQuery)
})
} catch (e) {
console.log(e)
logger.info(`Refreshing CDS uber year ${formattedYear} doned`)
} catch (e) {
logger.error({ message: `Error when refreshing CDS uber year ${formattedYear}`, meta: e })
}
}
logger.info(`Refreshing CDS Uber data doned`)
}
const refreshProtoCProgrammeToRedis = async () => {
try {
const codes = (await getAllProgrammes()).map(p => p.code).filter(code => code.includes('KH'))
codes.forEach(async code => {
console.log('refreshing 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 }
console.log('Refreshing CDS ProtoCProgramme for code ', code)
await refreshProtoCProgramme(defaultQuery)
await refreshProtoCProgramme(onlyOld)
await refreshProtoCProgramme(onlyEnr)
await refreshProtoCProgramme(bothToggles)
})
} catch (e) {
console.log(e)
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 () => {
console.log('Starting refreshing statistics')
await refreshStudyrightAssociations()
await refreshOverview()
await refreshNonGraduatedStudentsOfOldProgrammes()
await refreshTeacherLeaderboard()
console.log('Statistics refreshed!')
const statfuncs = [refreshStudyrightAssociations, refreshOverview, refreshTeacherLeaderboard]
logger.info('Refreshing statistics')
for (const func of statfuncs) {
await func()
}
logger.info('Statistics refreshed!')
}
const refreshTrends = async () => {
console.log('Starting refreshing trends')
await refreshProtoCtoRedis()
await refreshStatusToRedis()
await refreshUberToRedis()
await refreshProtoCProgrammeToRedis()
console.log('Trends refreshed!')
const trendfuncs = [refreshProtoCtoRedis, refreshStatusToRedis, refreshUberToRedis, refreshProtoCProgrammeToRedis]
logger.info('Refreshing trends')
for (const func of trendfuncs) {
await func()
}
logger.info('Trends refreshed!')
}
const startCron = () => {
if (isProduction) {
schedule('0 6 * * *', async () => {
await refreshStatistics()
await refreshTrends()
// refresh 3am every day
schedule('0 3 * * *', async () => {
for (const func of [refreshStatistics, refreshTrends]) {
await func()
}
})
}
}
@@ -219,4 +178,5 @@ const startCron = () => {
module.exports = {
startCron,
refreshStatistics,
refreshTrends,
}
@@ -1,5 +1,6 @@
const morgan = require('morgan')
const logger = require('../util/logger')
const _ = require('lodash')
// So this appears to be a hack to get neatly formatted stats like response-time etc. from morgan
// without actually using morgan what it's used for (LOGGING REQUESTS!).
@@ -7,7 +8,8 @@ const logger = require('../util/logger')
// so don't e.g. make it into an async function or it'll print [object Promise] on every request
const accessLogger = morgan((tokens, req, res) => {
const fields = ['method', 'url', 'status', 'response-time', 'remote-addr', 'remote-user', 'user-agent', 'referrer']
const meta = { ...req.decodedToken }
const { user } = req
const meta = { ...user }
fields.forEach(field => (meta[field] = tokens[field](req, res)))
meta['time'] = tokens['date'](req, res, 'iso')
if (req.route) {
@@ -15,7 +17,7 @@ const accessLogger = morgan((tokens, req, res) => {
}
const message = [
req.decodedToken.name,
user.name,
':',
tokens['method'](req, res),
tokens['url'](req, res),
@@ -26,7 +28,8 @@ const accessLogger = morgan((tokens, req, res) => {
].join(' ')
logger.info(message, {
...meta,
// 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,
})
+4 -70
View File
@@ -1,43 +1,8 @@
const jwt = require('jsonwebtoken')
const conf = require('../conf-backend')
const blacklist = require('../services/blacklist')
const { getUserDataFor } = require('../services/userService')
const { ACCESS_TOKEN_HEADER_KEY } = require('../conf-backend')
const { hasRequiredGroup, parseHyGroups } = require('../util/utils')
const TOKEN_VERSION = 1 // When token structure changes, increment in userservice, backend and frontend
const checkAuth = async (req, res, next) => {
const token = req.headers[ACCESS_TOKEN_HEADER_KEY]
const uid = req.headers['uid']
if (!token) {
return res.status(403).json({ error: 'No token in headers' })
}
jwt.verify(token, conf.TOKEN_SECRET, async (err, decoded) => {
if (err) {
return res.status(403).json(err)
}
if (decoded.version !== TOKEN_VERSION) {
return res.status(401).json({ error: 'Token needs to be refreshed - invalid version' })
}
if (decoded.mockedBy ? decoded.mockedBy !== uid : decoded.userId !== uid) {
return res.status(403).json({ error: 'User shibboleth id and token id did not match' })
}
const userData = await getUserDataFor(decoded.userId)
req.decodedToken = decoded
Object.assign(req, userData)
next()
})
}
const roles = requiredRoles => async (req, res, next) => {
if (req.decodedToken) {
const { roles } = req
if (req.user) {
const {
user: { roles },
} = req
if (requiredRoles.every(r => roles.indexOf(r) >= 0) || roles.includes('admin')) {
return next()
@@ -47,37 +12,6 @@ const roles = requiredRoles => async (req, res, next) => {
res.status(403).json({ error: 'missing required roles' })
}
const checkRequiredGroup = async (req, res, next) => {
const hyGroups = parseHyGroups(req.headers['hygroupcn'])
const enabled = hasRequiredGroup(hyGroups)
const tokenOutdated = req.decodedToken.enabled !== enabled
if (tokenOutdated) {
return res.status(401).json({
error: 'Token needs to be refreshed - enabled does not match hy-group requirement',
})
}
if (!enabled) {
return res.status(403).json({ error: 'User is not enabled' })
}
next()
}
const checkUserBlacklisting = async (req, res, next) => {
const { userId, createdAt } = req.decodedToken
const isBlacklisted = await blacklist.isUserBlacklisted(userId, createdAt)
if (isBlacklisted) {
return res.status(401).json({ error: 'Token needs to be refreshed - blacklisted' })
}
next()
}
module.exports = {
checkAuth,
roles,
checkRequiredGroup,
checkUserBlacklisting,
}
@@ -0,0 +1,53 @@
const Sentry = require('@sentry/node')
const { ApplicationError } = require('../util/customErrors')
const { getUser, getMockedUser } = require('../services/userService')
const { requiredGroup } = require('../conf-backend')
const _ = require('lodash')
const parseIamGroups = iamGroups => iamGroups?.split(';').filter(Boolean) ?? []
const hasRequiredIamGroup = iamGroups => _.intersection(iamGroups, requiredGroup).length > 0
const currentUserMiddleware = async (req, _res, next) => {
const {
'shib-session-id': sessionId,
'x-show-as-user': showAsUser,
displayname: name,
hygroupcn,
hypersonsisuid: sisId,
mail: email,
shib_logout_url: logoutUrl,
uid: username,
} = req.headers
if (!sessionId || !username) {
throw new ApplicationError('Not enough data in request headers', 403, { logoutUrl })
}
const iamGroups = parseIamGroups(hygroupcn)
if (!hasRequiredIamGroup(iamGroups)) {
throw new ApplicationError('User does not have required iam group', 403, { logoutUrl })
}
let user = await getUser({
username,
name,
email,
iamGroups,
sisId,
})
if (showAsUser && user.isAdmin) {
user = await getMockedUser({ userToMock: showAsUser, mockedBy: username })
}
Sentry.setUser({ username: user.mockedBy ?? username })
req.user = user
req.logoutUrl = logoutUrl
next()
}
module.exports = currentUserMiddleware
@@ -0,0 +1,19 @@
const Sentry = require('@sentry/node')
const { ApplicationError } = require('../util/customErrors')
const logger = require('../util/logger')
const errorHandler = (error, req, res, next) => {
logger.error(`${error.message} ${error.name} ${error.stack}`)
Sentry.captureException(error)
if (res.headersSent) {
return next(error)
}
const normalizedError = error instanceof ApplicationError ? error : new ApplicationError(error.message)
return res.status(normalizedError.status).json(normalizedError)
}
module.exports = errorHandler
@@ -1,13 +0,0 @@
const Sentry = require('@sentry/node')
const sentryUserIdMiddleware = (req, res, next) => {
Sentry.getCurrentHub().configureScope(scope => {
scope.setUser({
username: req.decodedToken && req.decodedToken.userId,
})
})
next()
}
module.exports = sentryUserIdMiddleware
@@ -0,0 +1,5 @@
const headersMiddleware = require('unfuck-utf8-headers-middleware')
const headers = ['uid', 'displayName', 'shib-session-id', 'hyGroupCn', 'mail', 'hyPersonSisuId']
module.exports = headersMiddleware(headers)
+3
View File
@@ -47,6 +47,9 @@ Course.init(
substitutions: {
type: JSONB,
},
course_unit_type: {
type: STRING,
},
},
{
underscored: false,
+6
View File
@@ -75,6 +75,12 @@ Credit.init(
updatedAt: {
type: DATE,
},
language: {
type: STRING,
},
is_open: {
type: BOOLEAN,
},
},
{
underscored: false,
@@ -135,6 +135,28 @@ const CustomPopulationSearch = sequelizeKone.define('custom_population_searches'
},
})
const StudyGuidanceGroupTag = sequelizeKone.define(
'study_guidance_group_tags',
{
id: {
primaryKey: true,
type: Sequelize.BIGINT,
autoIncrement: true,
},
studyGuidanceGroupId: {
type: Sequelize.STRING,
unique: true,
},
studyProgramme: {
type: Sequelize.STRING,
},
year: {
type: Sequelize.STRING,
},
},
{ underscored: true, timestamps: true }
)
MandatoryCourse.belongsTo(MandatoryCourseLabels, {
foreignKey: 'label',
sourceKey: 'id',
@@ -157,4 +179,5 @@ module.exports = {
CustomPopulationSearch,
sequelizeKone,
ExcludedCourse,
StudyGuidanceGroupTag,
}
@@ -1,7 +1,7 @@
const Sequelize = require('sequelize')
const { sequelize } = require('../database/connection')
const { sequelizeUser } = require('../database/connection')
const UserElementDetails = sequelize.define(
const UserElementDetails = sequelizeUser.define(
'user_elementdetails',
{
userId: {
@@ -18,7 +18,7 @@ const UserElementDetails = sequelize.define(
}
)
const AccessGroup = sequelize.define('access_group', {
const AccessGroup = sequelizeUser.define('access_group', {
id: {
primaryKey: true,
type: Sequelize.BIGINT,
@@ -39,7 +39,7 @@ const AccessGroup = sequelize.define('access_group', {
},
})
const UserFaculties = sequelize.define('user_faculties', {
const UserFaculties = sequelizeUser.define('user_faculties', {
userId: {
primaryKey: true,
type: Sequelize.BIGINT,
@@ -59,24 +59,8 @@ const UserFaculties = sequelize.define('user_faculties', {
type: Sequelize.DATE,
},
})
const FacultyProgrammes = sequelize.define('faculty_programmes', {
faculty_code: {
primaryKey: true,
type: Sequelize.STRING,
},
programme_code: {
primaryKey: true,
type: Sequelize.STRING,
},
createdAt: {
type: Sequelize.DATE,
},
updatedAt: {
type: Sequelize.DATE,
},
})
const User = sequelize.define(
const User = sequelizeUser.define(
'users',
{
id: {
@@ -92,47 +76,13 @@ const User = sequelize.define(
email: { type: Sequelize.STRING },
language: { type: Sequelize.STRING },
sisu_person_id: { type: Sequelize.STRING },
last_login: { type: Sequelize.DATE },
},
{
tableName: 'users',
timestamps: false,
}
)
const HyGroup = sequelize.define('hy_group', {
id: {
primaryKey: true,
type: Sequelize.BIGINT,
autoIncrement: true,
},
code: { type: Sequelize.STRING },
createdAt: {
type: Sequelize.DATE,
},
updatedAt: {
type: Sequelize.DATE,
},
})
const Affiliation = sequelize.define('affiliations', {
id: {
primaryKey: true,
type: Sequelize.BIGINT,
autoIncrement: true,
},
code: { type: Sequelize.STRING },
createdAt: {
type: Sequelize.DATE,
},
updatedAt: {
type: Sequelize.DATE,
},
})
User.hasMany(UserElementDetails, { as: 'programme' })
UserElementDetails.belongsTo(User)
@@ -143,29 +93,12 @@ User.belongsToMany(AccessGroup, {
})
AccessGroup.belongsToMany(User, { through: 'user_accessgroup' })
User.belongsToMany(HyGroup, { through: 'user_hy_group', as: 'hy_group' })
HyGroup.belongsToMany(User, { through: 'user_hy_group' })
User.belongsToMany(Affiliation, {
through: 'user_affiliation',
as: 'affiliation',
})
Affiliation.belongsToMany(User, { through: 'user_affiliation' })
User.hasMany(UserFaculties, { as: 'faculty', foreignKey: 'userId' })
UserFaculties.hasMany(FacultyProgrammes, {
as: 'programme',
foreignKey: 'faculty_code',
sourceKey: 'faculty_code',
})
module.exports = {
User,
UserElementDetails,
AccessGroup,
HyGroup,
Affiliation,
UserFaculties,
FacultyProgrammes,
sequelize,
sequelizeUser,
}
+1 -1
View File
@@ -17,7 +17,6 @@ Student.init(
dateofuniversityenrollment: { type: DATE },
email: { type: STRING },
national_student_number: { type: STRING },
home_county_id: { type: INTEGER },
country_fi: { type: STRING },
country_sv: { type: STRING },
country_en: { type: STRING },
@@ -25,6 +24,7 @@ Student.init(
home_country_sv: { type: STRING },
home_country_en: { type: STRING },
gender_code: { type: INTEGER },
sis_person_id: { type: STRING },
createdAt: {
type: DATE,
},
+32 -23
View File
@@ -1,50 +1,52 @@
const shibbolethHeadersFix = require('unfuck-utf8-headers-middleware')
const accessLogger = require('./middleware/accesslogger')
const sentryUserId = require('./middleware/sentryUserId')
const Sentry = require('@sentry/node')
const express = require('express')
const cors = require('cors')
const { frontUrl } = require('./conf-backend')
const shibbolethCharsetMiddleware = require('./middleware/shibbolethCharsetMiddleware')
const matomoInit = require('./routes/matomo-init')
const currentUserMiddleware = require('./middleware/currentUserMiddleware')
const accessLogger = require('./middleware/accessLogger')
const auth = require('./middleware/auth')
const courses = require('./routes/courses')
const students = require('./routes/students')
const population = require('./routes/population')
const login = require('./routes/login')
const superlogin = require('./routes/superlogin')
const language = require('./routes/language')
const users = require('./routes/users')
const elementdetails = require('./routes/elementdetails')
const auth = require('./middleware/auth')
const teachers = require('./routes/teachers')
const providers = require('./routes/providers')
const faculties = require('./routes/faculties')
const semesters = require('./routes/semesters')
const mandatoryCourses = require('./routes/mandatorycourses')
const mandatoryCourseLabels = require('./routes/mandatorycourselabels')
const ping = require('./routes/ping')
const feedback = require('./routes/feedback')
const tags = require('./routes/tags')
const updater = require('./routes/updater')
const tsaAnalytics = require('./routes/tsaAnalytics')
const matomoInit = require('./routes/matomo-init')
const customPopulationSearch = require('./routes/customPopulationSearch')
const trends = require('./routes/trends')
const programmeModules = require('./routes/programmeModules')
const studyGuidanceGroups = require('./routes/studyGuidanceGroups')
const studyProgramme = require('./routes/studyProgramme')
const initializeSentry = require('./util/sentry')
const errorMiddleware = require('./middleware/errorMiddleware')
module.exports = (app, url) => {
app.use(url, ping)
app.use(
shibbolethHeadersFix([
'hyGroupCn',
'SHIB_LOGOUT_URL',
'eduPersonAffiliation',
'uid',
'displayName',
'mail',
'hyPersonSisuId',
'preferredLanguage',
])
)
initializeSentry(app)
app.use(Sentry.Handlers.requestHandler())
app.use(Sentry.Handlers.tracingHandler())
app.use(cors({ credentials: true, origin: frontUrl }))
app.use(express.json())
app.use(shibbolethCharsetMiddleware)
app.use(url, matomoInit)
app.use(currentUserMiddleware)
app.use(accessLogger)
app.use(url, login)
app.use(`${url}/superlogin`, superlogin)
app.use(auth.checkAuth, auth.checkRequiredGroup, auth.checkUserBlacklisting, accessLogger, sentryUserId)
app.use(url, elementdetails)
app.use(url, courses)
app.use(url, students)
@@ -54,10 +56,11 @@ module.exports = (app, url) => {
app.use(url, semesters)
app.use(url, tags)
app.use(url, programmeModules)
app.use(url, studyProgramme)
app.use(`${url}/faculties`, faculties)
app.use(`${url}/updater`, auth.roles(['admin']), updater)
app.use(`${url}/teachers`, auth.roles(['teachers']), teachers)
app.use(`${url}/users`, auth.roles(['users']), users)
app.use(`${url}/users`, auth.roles(['admin']), users)
app.use(`${url}/feedback`, feedback)
app.use(`${url}/mandatory_courses`, mandatoryCourses)
app.use(`${url}/mandatory-course-labels`, mandatoryCourseLabels)
@@ -65,4 +68,10 @@ module.exports = (app, url) => {
app.use(`${url}/custom-population-search`, customPopulationSearch)
app.use(`${url}/cool-data-science`, trends)
app.use(`${url}/studyguidancegroups`, auth.roles(['studyGuidanceGroups']), studyGuidanceGroups)
app.get('*', async (_, res) => {
const results = { error: 'unknown endpoint' }
res.status(404).json(results)
})
app.use(Sentry.Handlers.errorHandler())
app.use(errorMiddleware)
}
+21 -28
View File
@@ -2,7 +2,6 @@ const router = require('express').Router()
const crypto = require('crypto')
const Course = require('../services/courses')
const { validateParamLength } = require('../util')
const logger = require('../util/logger')
router.get('/v2/coursesmulti', async (req, res) => {
let results = { courses: [] }
@@ -17,38 +16,32 @@ router.get('/v2/coursesmulti', async (req, res) => {
})
router.get('/v3/courseyearlystats', async (req, res) => {
try {
const { rights, roles } = req
const {
user: { rights, roles },
} = req
const allowedRoles = roles && ['admin', 'courseStatistics'].find(role => roles.includes(role))
const allowedRoles = roles && ['admin', 'courseStatistics'].find(role => roles.includes(role))
// If user has rights to see at least one programme, then they are allowed
// to see all of them
if (!allowedRoles && rights.length < 1) {
return res.status(403).json({ error: 'No programmes so no access to course stats' })
}
// If user has rights to see at least one programme, then they are allowed
// to see all of them
if (!allowedRoles && rights.length < 1) {
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, unifyOpenUniCourses: unify } = 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 {
// Studentnumbers should be obfuscated to all other users except admins
// 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)
res.json(results)
}
} catch (e) {
logger.error(e.message)
console.log(e)
res.status(500).send('Something went wrong with handling the request.')
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 {
// Studentnumbers should be obfuscated to all other users except admins
// 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)
res.json(results)
}
})
router.use('*', (req, res, next) => next())
module.exports = router
@@ -7,76 +7,56 @@ const {
} = require('../services/customPopulationSearch')
router.get('/', async (req, res) => {
try {
const {
decodedToken: { id },
} = req
const customPopulationSearches = await getCustomPopulationSearchesByUser(id)
res.json(customPopulationSearches)
} catch (e) {
console.error('e', e)
res.status(500).json({ error: 'error' })
}
const {
user: { id },
} = req
const customPopulationSearches = await getCustomPopulationSearchesByUser(id)
res.json(customPopulationSearches)
})
router.post('/', async (req, res) => {
try {
const {
decodedToken: { id },
body: { name, students },
} = req
const {
user: { id },
body: { name, students },
} = req
if (!name) return res.status(400).json({ error: 'name missing' })
if (students && !Array.isArray(students)) res.status(400).json({ error: 'students must be of type array' })
if (!name) return res.status(400).json({ error: 'name missing' })
if (students && !Array.isArray(students)) res.status(400).json({ error: 'students must be of type array' })
const customPopulationSearch = await createCustomPopulationSearch(name, id, students || [])
res.json(customPopulationSearch)
} catch (e) {
console.error('e', e)
res.status(500).json({ error: 'error' })
}
const customPopulationSearch = await createCustomPopulationSearch(name, id, students || [])
res.json(customPopulationSearch)
})
router.put('/:id', async (req, res) => {
try {
const {
decodedToken: { id: userId },
body: { students },
} = req
const { id } = req.params
const {
user: { id: userId },
body: { students },
} = req
const { id } = req.params
if (!id) return res.status(400).json({ error: 'id missing' })
if (!students) return res.status(400).json({ error: 'students missing' })
if (!Array.isArray(students)) res.status(400).json({ error: 'students must be of type array' })
if (!id) return res.status(400).json({ error: 'id missing' })
if (!students) return res.status(400).json({ error: 'students missing' })
if (!Array.isArray(students)) res.status(400).json({ error: 'students must be of type array' })
const updatedPopulationSearch = await updateCustomPopulationSearch(userId, id, students)
if (!updatedPopulationSearch) return res.status(404).json({ error: 'custom population search not found' })
const updatedPopulationSearch = await updateCustomPopulationSearch(userId, id, students)
if (!updatedPopulationSearch) return res.status(404).json({ error: 'custom population search not found' })
res.json(updatedPopulationSearch)
} catch (e) {
console.error('e', e)
res.status(500).json({ error: 'error' })
}
res.json(updatedPopulationSearch)
})
router.delete('/:id', async (req, res) => {
try {
const {
decodedToken: { id: userId },
} = req
const { id } = req.params
const {
user: { id: userId },
} = req
const { id } = req.params
if (!id) return res.status(400).json({ error: 'id missing' })
if (!id) return res.status(400).json({ error: 'id missing' })
const deletedSuccessfully = (await deleteCustomPopulationSearch(userId, id)) > 0
const deletedSuccessfully = (await deleteCustomPopulationSearch(userId, id)) > 0
if (!deletedSuccessfully) return res.status(404).json({ error: 'custom population search not found' })
if (!deletedSuccessfully) return res.status(404).json({ error: 'custom population search not found' })
res.json(id)
} catch (e) {
console.error('e', e)
res.status(500).json({ error: 'error' })
}
res.json(id)
})
module.exports = router
+20 -73
View File
@@ -1,5 +1,4 @@
const router = require('express').Router()
const { getStudentsUserCanAccess } = require('../services/userService')
const MandatoryCourses = require('../services/mandatoryCourses')
const {
productivityStatsForStudytrack,
@@ -7,7 +6,6 @@ const {
optionData,
} = require('../services/studyprogramme')
const { findProgrammeTheses, createThesisCourse, deleteThesisCourse } = require('../services/thesis')
const {
getProductivity,
setProductivity,
@@ -15,28 +13,20 @@ const {
setThroughput,
patchProductivity,
patchThroughput,
getNonGraduatedStudents,
} = require('../services/analyticsService')
const { getAllProgrammes, getAllElementDetails } = require('../services/studyrights')
const logger = require('../util/logger')
const programmeStatsSince = new Date('2017-07-31')
router.get('/elementdetails/all', async (req, res) => {
try {
const elementdetails = await getAllElementDetails()
res.json(elementdetails)
} catch (e) {
res.status(500).json(e)
}
const elementdetails = await getAllElementDetails()
res.json(elementdetails)
})
router.get('/studyprogrammes', async (req, res) => {
try {
const studyrights = await getAllProgrammes()
res.json(studyrights)
} catch (err) {
res.status(500).json(err)
}
const studyrights = await getAllProgrammes()
res.json(studyrights)
})
router.get('/v2/studyprogrammes/:id/optiondata', async (req, res) => {
@@ -55,34 +45,6 @@ router.get('/v2/studyprogrammes/:id/optiondata', async (req, res) => {
res.json(data)
})
router.get('/v2/studyprogrammes/:id/present_students', async (req, res) => {
try {
const {
roles,
decodedToken,
params: { id },
} = req
if (!id) return res.status(400).json({ error: 'programme id missing' })
const nonGraduatedStudents = await getNonGraduatedStudents(id)
if (!nonGraduatedStudents) return res.status(200).json({})
const {
data: { formattedData, studentnumbers },
} = nonGraduatedStudents
const filteredData = { ...formattedData }
const studentsUserCanAccess = await getStudentsUserCanAccess(studentnumbers, roles, decodedToken.userId)
Object.keys(filteredData).forEach(year => {
filteredData[year] = filteredData[year].filter(({ studentNumber }) => studentsUserCanAccess.has(studentNumber))
})
res.json(filteredData)
} catch (e) {
console.error('e', e)
res.status(500).json({ error: 'error' })
}
})
router.get('/v2/studyprogrammes/:id/mandatory_courses', async (req, res) => {
if (req.params.id) {
const codes = await MandatoryCourses.byStudyprogramme(req.params.id)
@@ -99,14 +61,14 @@ router.get('/v2/studyprogrammes/:id/productivity', async (req, res) => {
try {
data = await getProductivity(code)
} catch (e) {
console.error(e)
logger.error(`Failed to get code ${code} productivity`)
}
if (!data) {
try {
const stats = await productivityStatsForStudytrack(code, programmeStatsSince)
data = await setProductivity(stats)
} catch (e) {
console.error(e)
logger.error(`Failed to update code ${code} patching status to recalculation erroring`)
}
}
return res.json(data)
@@ -118,14 +80,8 @@ router.get('/v2/studyprogrammes/:id/productivity', async (req, res) => {
router.get('/v2/studyprogrammes/productivity/recalculate', async (req, res) => {
const code = req.query.code
console.log('Productivity stats recalculation starting')
try {
await patchThroughput({ [code]: { status: 'RECALCULATING' } })
res.status(200).end()
} catch (e) {
console.error(e)
return res.status(500).end()
}
await patchThroughput({ [code]: { status: 'RECALCULATING' } })
res.status(200).end()
try {
if (code.includes('MH') || code.includes('KH')) {
@@ -141,13 +97,13 @@ router.get('/v2/studyprogrammes/productivity/recalculate', async (req, res) => {
[code]: { status: 'RECALCULATION ERRORED' },
})
} catch (e) {
console.error(e)
logger.error(`Failed to update code ${code} patching status to recalculation erroring`)
return
}
console.error(e)
console.log(`Failed to update productivity stats for code: ${code}, reason: ${e.message}`)
logger.error(`Failed to update productivity stats for code: ${code}, reason: ${e.message}`)
}
console.log(`Productivity stats recalculation for studyprogramme ${code} done`)
logger.info(`Produdtiviy stats recalculation for studyprogramme ${code} done`)
})
router.get('/v2/studyprogrammes/:id/throughput', async (req, res) => {
@@ -159,14 +115,14 @@ router.get('/v2/studyprogrammes/:id/throughput', async (req, res) => {
try {
data = await getThroughput(code)
} catch (e) {
console.error(e)
logger.error(`Failed to get code ${code} throughput`)
}
if (!data) {
try {
const result = await throughputStatsForStudytrack(req.params.id, programmeStatsSince.getFullYear())
data = await setThroughput(result)
} catch (e) {
console.error(e)
logger.error(`Failed to update code ${code} patching status to recalculation erroring`)
}
}
return res.json({ ...data, thesis: await thesisPromise })
@@ -178,14 +134,8 @@ router.get('/v2/studyprogrammes/:id/throughput', async (req, res) => {
router.get('/v2/studyprogrammes/throughput/recalculate', async (req, res) => {
const code = req.query.code
console.log('Throughput stats recalculation starting')
try {
await patchThroughput({ [code]: { status: 'RECALCULATING' } })
res.status(200).end()
} catch (e) {
console.error(e)
return res.status(500).end()
}
await patchThroughput({ [code]: { status: 'RECALCULATING' } })
res.status(200).end()
try {
if (code.includes('MH') || code.includes('KH')) {
@@ -199,13 +149,12 @@ router.get('/v2/studyprogrammes/throughput/recalculate', async (req, res) => {
try {
await patchThroughput({ [code]: { status: 'RECALCULATION ERRORED' } })
} catch (e) {
console.error(e)
logger.error(`Failed to update code ${code} patching status to recalculation erroring`)
return
}
console.error(e)
console.log(`Failed to update throughput stats for code: ${code}, reason: ${e.message}`)
logger.error(`Failed to update throughput stats for code: ${code}, reason: ${e.message}`)
}
console.log(`Throughput stats recalculation for studyprogramme ${code} done`)
logger.info(`Throughput stats recalculation for studyprogramme ${code} done`)
})
router.get('/v2/studyprogrammes/:id/thesis', async (req, res) => {
@@ -239,6 +188,4 @@ router.delete('/v2/studyprogrammes/:id/thesis/:course', async (req, res) => {
}
})
router.use('*', (req, res, next) => next())
module.exports = router
+10 -14
View File
@@ -1,23 +1,19 @@
const router = require('express').Router()
const userService = require('../services/userService')
const { sendFeedbackToToska } = require('../services/mailservice')
const logger = require('../util/logger')
router.post('/email', async (req, res) => {
const { content } = req.body
const { uid } = req.headers
const { email, full_name } = await userService.byUsername(uid)
router.post('/email', async req => {
const {
body: { content },
user,
} = req
const result = await sendFeedbackToToska({
await sendFeedbackToToska({
feedbackContent: content,
userId: uid,
userEmail: email,
userFullName: full_name,
user,
})
if (result.error) {
return res.status(400).json(result).end()
}
console.log('Message sent succesfully!')
res.status(200).json('success').end()
logger.info(`${user.userId} succesfully sent feedback to toska`)
})
module.exports = router
+4 -2
View File
@@ -2,8 +2,10 @@ const router = require('express').Router()
const userService = require('../services/userService')
router.post('/language', async (req, res) => {
const { language } = req.body
const { userId } = req.decodedToken
const {
body: { language },
user: { userId },
} = req
if (!['fi', 'sv', 'en'].includes(language)) {
return res.status(400).json('invalid language')
}
+12 -48
View File
@@ -1,55 +1,19 @@
const router = require('express').Router()
const userService = require('../services/userService')
const { parseHyGroups } = require('../util/utils')
const { sendNotificationAboutNewUser } = require('../services/mailservice')
const { ApplicationError } = require('../util/customErrors')
const _ = require('lodash')
router.post('/login', async (req, res) => {
try {
const uid = req.headers['uid']
if (req.headers['shib-session-id'] && uid) {
const full_name = req.headers.displayname || ''
const mail = req.headers.mail || ''
const hyGroups = parseHyGroups(req.headers['hygroupcn'])
const affiliations = parseHyGroups(req.headers['edupersonaffiliation'])
const hyPersonSisuId = req.headers.hypersonsisuid || ''
console.log(uid, 'trying to login, referring to userservice.')
let { token, isNew } = await userService.login(uid, full_name, hyGroups, affiliations, mail, hyPersonSisuId)
if (isNew) {
const result = await sendNotificationAboutNewUser({ userId: uid, userFullName: full_name })
if (result.error) {
return res.status(400).json(result).end()
}
}
res.status(200).json({ token })
} else {
res
.status(401)
.json({
message: `Not enough headers login, uid: ${req.headers.uid}
session-id ${req.headers['shib-session-id']}`,
})
.end()
}
} catch (err) {
console.log(err)
res.status(401).json({ message: 'problem with login', err })
}
})
router.get('/login', async (req, res) => {
const { user, logoutUrl } = req
router.delete('/logout', async (req, res) => {
try {
const logoutUrl = req.headers.shib_logout_url
const { returnUrl } = req.body
if (logoutUrl) {
return res
.status(200)
.send({ logoutUrl: `${logoutUrl}?return=${returnUrl}` })
.end()
}
res.status(200).send({ logoutUrl: returnUrl }).end()
} catch (err) {
res.status(500).json({ message: 'Error with logout', err })
if (!user) {
throw new ApplicationError('User not found', 404)
}
res.send({
// don't send possibly huge list of students to frontend
user: _.omit(user, ['studentsUserCanAccess']),
logoutUrl,
})
})
module.exports = router
@@ -1,5 +1,6 @@
const router = require('express').Router()
const MandatoryCourseLabels = require('../services/mandatoryCourseLabels')
const logger = require('../util/logger')
router.get('/:programme/labels', async (req, res) => {
const { programme } = req.params
@@ -8,7 +9,7 @@ router.get('/:programme/labels', async (req, res) => {
const labels = await MandatoryCourseLabels.labelsByStudyprogramme(programme)
res.status(200).json(labels)
} catch (err) {
console.error(err)
logger.error(err)
res.status(400).json(err.message)
}
} else {
@@ -25,7 +26,7 @@ router.delete('/:programme/labels', async (req, res) => {
const labels = await MandatoryCourseLabels.labelsByStudyprogramme(programme)
res.status(200).json(labels)
} catch (err) {
console.error(err)
logger.error(err)
res.status(400).json(err.message)
}
} else {
@@ -42,7 +43,7 @@ router.post('/:programme/move', async (req, res) => {
const labels = await MandatoryCourseLabels.labelsByStudyprogramme(programme)
res.status(200).json(labels)
} catch (err) {
console.error(err)
logger.error(err)
res.status(400).json(err.message)
}
} else {
@@ -59,7 +60,7 @@ router.post('/:programme/labels', async (req, res) => {
const labels = await MandatoryCourseLabels.labelsByStudyprogramme(programme)
res.status(200).json(labels)
} catch (err) {
console.error(err)
logger.error(err)
res.status(400).json(err.message)
}
} else {
+14 -29
View File
@@ -5,47 +5,32 @@ router.delete('/:programme', async (req, res) => {
const { programme } = req.params
const { course } = req.body
if (!programme || !course) return res.status(400).end()
try {
await MandatoryCourses.remove(programme, course)
res.status(200).json(course)
} catch (err) {
console.error(err)
res.status(500).json(err.message)
}
await MandatoryCourses.remove(programme, course)
res.status(200).json(course)
})
router.post('/:programme', async (req, res) => {
const { programme } = req.params
const { course } = req.body
if (!programme || !course) return res.status(400).end()
try {
await MandatoryCourses.create(programme, course)
const newCourse = await MandatoryCourses.find(programme, course)
if (!newCourse) return res.status(400).end()
res.status(201).json({ name: newCourse.course.name, code: newCourse.course_code, label: newCourse.label })
} catch (err) {
console.error(err)
res.status(500).json(err.message)
}
await MandatoryCourses.create(programme, course)
const newCourse = await MandatoryCourses.find(programme, course)
if (!newCourse) return res.status(400).end()
res.status(201).json({ name: newCourse.course.name, code: newCourse.course_code, label: newCourse.label })
})
router.post('/:programme/label/:course', async (req, res) => {
const { programme, course } = req.params
const { label } = req.body
if (!programme || !course) return res.status(400).end()
try {
await MandatoryCourses.updateLabel(programme, course, label)
const updatedCourse = await MandatoryCourses.find(programme, course)
if (!updatedCourse) return res.status(400).end()
res.status(200).json({
name: updatedCourse.course.name,
code: updatedCourse.course_code,
label: updatedCourse.mandatory_course_label,
})
} catch (err) {
console.error(err)
res.status(500).json(err.message)
}
await MandatoryCourses.updateLabel(programme, course, label)
const updatedCourse = await MandatoryCourses.find(programme, course)
if (!updatedCourse) return res.status(400).end()
res.status(200).json({
name: updatedCourse.course.name,
code: updatedCourse.course_code,
label: updatedCourse.mandatory_course_label,
})
})
module.exports = router
+1 -1
View File
@@ -28,7 +28,7 @@ _paq.push(["enableLinkTracking"]);
const script = config.MATOMO_URL ? acual : ''
router.get('/matomo-init', (req, res) => {
router.get('/matomo-init', (_, res) => {
res.contentType('text/javascript; charset=UTF-8')
res.send(script)
})
-85
View File
@@ -1,85 +0,0 @@
/* eslint-disable */
const { router, wrapper } = require('./router').routerWithWrapper()
const axios = require('axios')
wrapper.get('/ping', async (req, res) => {
res.json({
message: 'pong'
})
})
wrapper.get('/v3/mandatory_courses/:code', async (req, res) => {
const courses = []
const included = new Set()
const dumb_flatten = module => {
if (module.children) {
module.children.forEach(child => {
dumb_flatten(child)
})
} else if (module.code && !module.code.startsWith('KK') && !courses.map(c => c.code).includes(module.code)) {
courses.push({
name: {
fi: module.name,
en: module.name
},
code: module.code,
label: { id: `${courses.length}`, label: module.code.replace('-', '').slice(0, 4), orderNumber: courses.length }
})
}
}
const better_flatten = (module, label) => {
if (module.children) {
module.children.forEach(child => {
better_flatten(child, module.module ? module.module.name : label)
})
} else if (module.code && !module.code.startsWith('KK')) {
courses.push({
name: {
fi: module.name,
en: module.name
},
code: module.code,
label: { id: `${courses.length}`, label, orderNumber: courses.length }
})
}
}
const superFlattenFlatten = (module, label) => {
if (module.children) {
module.children.forEach(child => {
superFlattenFlatten(child, module.module ? module.module.name : label)
})
} else if (Array.isArray(module)) {
module.forEach(elem => superFlattenFlatten(elem, label))
} else if (module.code && !module.code.startsWith('KK')) {
if (!included.has(`${label}${module.code}`)) {
included.add(`${label}${module.code}`)
courses.push({
name: {
fi: module.name,
en: module.name
},
code: module.code,
label: { id: `${courses.length}`, label, orderNumber: courses.length }
})
}
}
}
const code = req.params.code
const response = await axios.get(`http://sis-updater-scheduler:8082/v1/courses/${code}?token=dev&superFlatten=true`)
// dumb_flatten(response.data)
// better_flatten(response.data)
superFlattenFlatten(response.data)
const byCode = (c1, c2) => (c1.code < c2.code ? -1 : 1)
courses.sort(byCode)
res.json(courses)
})
module.exports = router
+280 -374
View File
@@ -1,13 +1,14 @@
const crypto = require('crypto')
const Sentry = require('@sentry/node')
const router = require('express').Router()
const _ = require('lodash')
const Population = require('../services/populations')
const Student = require('../services/students')
const StudyrightService = require('../services/studyrights')
const UserService = require('../services/userService')
const TagService = require('../services/tags')
const CourseService = require('../services/courses')
const StatMergeService = require('../services/statMerger')
const { ApplicationError } = require('../util/customErrors')
const filterPersonalTags = (population, userId) => {
return {
@@ -21,324 +22,262 @@ const filterPersonalTags = (population, userId) => {
// POST instead of GET because of too long params and "sensitive" data
router.post('/v2/populationstatistics/courses', async (req, res) => {
try {
if (!req.body.year || !req.body.semesters || !req.body.studyRights) {
Sentry.captureException(new Error('The body should have a year, semester and study rights defined'))
res.status(400).json({ error: 'The body should have a year, semester and study rights defined' })
if (!req.body.year || !req.body.semesters || !req.body.studyRights) {
Sentry.captureException(new Error('The body should have a year, semester and study rights defined'))
res.status(400).json({ error: 'The body should have a year, semester and study rights defined' })
return
}
if (!Array.isArray(req.body.studyRights)) {
// studyRights should always be an array
req.body.studyRights = [req.body.studyRights]
}
if (req.body.months == null) {
req.body.months = 12
}
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]
}
const newMonths = (upperYearBound - Number(year)) * 12
const query = { ...req.body, year, months: newMonths }
const coursestatistics = Population.bottlenecksOf(query)
return coursestatistics
})
)
const multicoursestats = await multicoursestatPromises
const result = StatMergeService.populationCourseStatsMerger(multicoursestats)
if (result.error) {
res.status(400).json(result)
return
}
if (!Array.isArray(req.body.studyRights)) {
// studyRights should always be an array
req.body.studyRights = [req.body.studyRights]
res.json(result)
} else {
const result = await Population.bottlenecksOf(req.body)
if (result.error) {
Sentry.captureException(new Error(result.error))
res.status(400).json(result)
return
}
if (req.body.months == null) {
req.body.months = 12
}
if (req.body.years) {
console.log('bottleneck on 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]
}
const newMonths = (upperYearBound - Number(year)) * 12
const query = { ...req.body, year, months: newMonths }
const coursestatistics = Population.bottlenecksOf(query)
return coursestatistics
})
)
const multicoursestats = await multicoursestatPromises
const result = StatMergeService.populationCourseStatsMerger(multicoursestats)
console.log('result: ', result)
if (result.error) {
res.status(400).json(result)
return
}
res.json(result)
} else {
console.log('bottleneck ei ole years')
const result = await Population.bottlenecksOf(req.body)
if (result.error) {
Sentry.captureException(new Error(result.error))
res.status(400).json(result)
return
}
res.json(result)
}
} catch (e) {
console.log(e)
Sentry.captureException(e)
res.status(500).json({ error: e })
res.json(result)
}
})
router.post('/v2/populationstatistics/coursesbycoursecode', async (req, res) => {
try {
const { from, to, coursecodes } = req.body
if (!from || !to || !coursecodes) {
return res.status(400).json({ error: 'The body should have a yearcode and coursecode defined' })
}
const maxYearsToCreatePopulationFrom = await CourseService.maxYearsToCreatePopulationFrom(coursecodes)
if (Math.abs(to - from + 1) > maxYearsToCreatePopulationFrom) {
return res.status(400).json({ error: `Max years to create population from is ${maxYearsToCreatePopulationFrom}` })
}
let studentnumberlist
const studentnumbers = await Student.findByCourseAndSemesters(coursecodes, from, to)
const {
decodedToken: { userId },
roles,
} = req
if (roles && roles.includes('admin')) {
studentnumberlist = studentnumbers
} else {
const unitsUserCanAccess = await UserService.getUnitsFromElementDetails(userId)
const codes = unitsUserCanAccess.map(unit => unit.id)
studentnumberlist = await Student.filterStudentnumbersByAccessrights(studentnumbers, codes)
}
const result = await Population.bottlenecksOf(
{
year: 1900,
studyRights: [],
semesters: ['FALL', 'SPRING'],
months: 10000,
},
studentnumberlist
)
if (result.error) {
res.status(400).json(result)
return
}
res.json(result)
} catch (e) {
console.log(e)
res.status(500).json({ error: e })
const { from, to, coursecodes } = req.body
if (!from || !to || !coursecodes) {
return res.status(400).json({ error: 'The body should have a yearcode and coursecode defined' })
}
const maxYearsToCreatePopulationFrom = await CourseService.maxYearsToCreatePopulationFrom(coursecodes)
if (Math.abs(to - from + 1) > maxYearsToCreatePopulationFrom) {
return res.status(400).json({ error: `Max years to create population from is ${maxYearsToCreatePopulationFrom}` })
}
const {
user: { isAdmin, studentsUserCanAccess },
} = req
const studentnumbers = await Student.findByCourseAndSemesters(coursecodes, from, to)
const studentnumberlist = isAdmin ? studentnumbers : _.intersection(studentnumbers, studentsUserCanAccess)
const result = await Population.bottlenecksOf(
{
year: 1900,
studyRights: [],
semesters: ['FALL', 'SPRING'],
months: 10000,
},
studentnumberlist
)
if (result.error) {
Sentry.captureException(new Error(result.error))
res.status(400).json(result)
return
}
res.json(result)
})
router.post('/v2/populationstatistics/coursesbytag', async (req, res) => {
try {
const { tag } = req.body
if (!tag) {
res.status(400).json({ error: 'The body should have a tag defined' })
return
}
let studentnumberlist
const studentnumbers = await Student.findByTag(tag)
const {
decodedToken: { userId },
roles,
} = req
if (roles && roles.includes('admin')) {
studentnumberlist = studentnumbers
} else {
const unitsUserCanAccess = await UserService.getUnitsFromElementDetails(userId)
const codes = unitsUserCanAccess.map(unit => unit.id)
studentnumberlist = await Student.filterStudentnumbersByAccessrights(studentnumbers, codes)
}
const result = await Population.bottlenecksOf(
{
year: 1900,
studyRights: [],
semesters: ['FALL', 'SPRING'],
months: 10000,
tag,
},
studentnumberlist
)
if (result.error) {
console.log(result.error)
res.status(400).end()
return
}
console.log(`request completed ${new Date()}`)
res.json(result)
} catch (e) {
console.log(e)
res.status(500).json({ error: e })
const { tag } = req.body
if (!tag) {
res.status(400).json({ error: 'The body should have a tag defined' })
return
}
const {
user: { isAdmin, studentsUserCanAccess },
} = req
const studentnumbers = await Student.findByTag(tag)
const studentnumberlist = isAdmin ? studentnumbers : _.intersection(studentnumbers, studentsUserCanAccess)
const result = await Population.bottlenecksOf(
{
year: 1900,
studyRights: [],
semesters: ['FALL', 'SPRING'],
months: 10000,
tag,
},
studentnumberlist
)
if (result.error) {
Sentry.captureException(new Error(result.error))
res.status(400).end()
return
}
res.json(result)
})
router.post('/v2/populationstatistics/coursesbystudentnumberlist', async (req, res) => {
try {
if (!req.body.studentnumberlist) {
res.status(400).json({ error: 'The body should have a studentnumberlist defined' })
return
}
let studentnumberlist
const {
decodedToken: { userId },
roles,
} = req
const {
user: { isAdmin, studentsUserCanAccess },
body: { studentnumberlist: studentnumbersInReq },
} = req
if (roles && roles.includes('admin')) {
studentnumberlist = req.body.studentnumberlist
} else {
const unitsUserCanAccess = await UserService.getUnitsFromElementDetails(userId)
const codes = unitsUserCanAccess.map(unit => unit.id)
studentnumberlist = await Student.filterStudentnumbersByAccessrights(req.body.studentnumberlist, codes)
}
const result = await Population.bottlenecksOf(
{
year: 1900,
studyRights: [],
semesters: ['FALL', 'SPRING'],
months: 10000,
},
studentnumberlist
)
if (result.error) {
res.status(400).json(result)
return
}
res.json(result)
} catch (err) {
console.log(err)
res.status(500).json({ error: err })
if (!studentnumbersInReq) {
throw new ApplicationError('The body should have a studentnumberlist defined', 400)
}
const studentnumberlist = isAdmin ? studentnumbersInReq : _.intersection(studentnumbersInReq, studentsUserCanAccess)
const result = await Population.bottlenecksOf(
{
year: 1900,
studyRights: [],
semesters: ['FALL', 'SPRING'],
months: 10000,
},
studentnumberlist
)
if (result.error) {
Sentry.captureException(new Error(result.error))
res.status(400).json(result)
return
}
res.json(result)
})
router.get('/v3/populationstatistics', async (req, res) => {
const { year, semesters, studyRights: studyRightsJSON } = req.query
const { decodedToken } = req
const { user } = req
if (!year || !semesters || !studyRightsJSON) {
res.status(400).json({ error: 'The query should have a year, semester and studyRights defined' })
return
}
let studyRights = null
try {
if (!year || !semesters || !studyRightsJSON) {
res.status(400).json({ error: 'The query should have a year, semester and studyRights defined' })
studyRights = JSON.parse(studyRightsJSON)
const {
user: { rights, isAdmin },
} = req
if (!isAdmin && !rights.includes(studyRights.programme)) {
res.status(403).json([])
return
}
let studyRights = null
try {
studyRights = JSON.parse(studyRightsJSON)
const { rights, roles } = req
if (!roles || !roles.includes('admin')) {
if (!rights.includes(studyRights.programme)) {
res.status(403).json([])
return
}
}
} catch (e) {
console.error(e)
res.status(400).json({ error: 'The query had invalid studyRights' })
return
}
if (req.query.months == null) {
req.query.months = 12
}
if (req.query.years) {
const upperYearBound = new Date().getFullYear() + 1
const multipopulationstudentPromises = Promise.all(
req.query.years.map(year => {
const newMonths = (upperYearBound - Number(year)) * 12
const populationStudents = Population.optimizedStatisticsOf({
...req.query,
studyRights,
year,
months: newMonths,
})
return populationStudents
})
)
const multipopulationstudents = await multipopulationstudentPromises
const result = StatMergeService.populationStudentsMerger(multipopulationstudents)
if (result.error) {
console.log(result.error)
res.status(400).end()
return
}
console.log(`request completed ${new Date()}`)
res.json(filterPersonalTags(result, decodedToken.id))
} else {
const result = await Population.optimizedStatisticsOf({ ...req.query, studyRights })
if (result.error) {
console.log(result.error)
res.status(400).end()
return
}
console.log(`request completed ${new Date()}`)
res.json(filterPersonalTags(result, decodedToken.id))
}
} catch (e) {
console.log(e)
res.status(500).json({ error: e })
res.status(400).json({ error: 'The query had invalid studyRights' })
return
}
if (req.query.months == null) {
req.query.months = 12
}
if (req.query.years) {
const upperYearBound = new Date().getFullYear() + 1
const multipopulationstudentPromises = Promise.all(
req.query.years.map(year => {
const newMonths = (upperYearBound - Number(year)) * 12
const populationStudents = Population.optimizedStatisticsOf({
...req.query,
studyRights,
year,
months: newMonths,
})
return populationStudents
})
)
const multipopulationstudents = await multipopulationstudentPromises
const result = StatMergeService.populationStudentsMerger(multipopulationstudents)
if (result.error) {
Sentry.captureException(new Error(result.error))
res.status(400).end()
return
}
res.json(filterPersonalTags(result, user.id))
} else {
const result = await Population.optimizedStatisticsOf({ ...req.query, studyRights })
if (result.error) {
Sentry.captureException(new Error(result.error))
res.status(400).end()
return
}
res.json(filterPersonalTags(result, user.id))
}
})
router.get('/v3/populationstatisticsbytag', async (req, res) => {
const { tag, studyRights: studyRightsJSON, months, year } = req.query
const { decodedToken } = req
const {
user: { id, isAdmin, studentsUserCanAccess },
query: { tag, studyRights: studyRightsJSON, months, year },
} = req
if (!tag) return res.status(400).json({ error: 'The query should have a tag defined' })
const foundTag = await TagService.findTagById(tag)
if (!foundTag) return res.status(404).json({ error: 'Tag not found' })
const semesters = ['FALL', 'SPRING']
let studentnumberlist
const studentnumbers = await Student.findByTag(tag)
const {
decodedToken: { userId },
roles,
} = req
if (roles && roles.includes('admin')) {
studentnumberlist = studentnumbers
} else {
const unitsUserCanAccess = await UserService.getUnitsFromElementDetails(userId)
const codes = unitsUserCanAccess.map(unit => unit.id)
studentnumberlist = await Student.filterStudentnumbersByAccessrights(studentnumbers, codes)
}
try {
const studyRights = JSON.parse(studyRightsJSON)
const newStartYear = await Population.getEarliestYear(studentnumberlist, studyRights)
const yearDifference = Number(year) - Number(newStartYear)
const newMonths = Number(months) + 12 * yearDifference
const result = await Population.optimizedStatisticsOf(
{
year: newStartYear,
studyRights,
semesters,
months: newMonths,
tag: foundTag,
},
studentnumberlist
)
if (result.error) {
console.log(result.error)
res.status(400).end()
return
}
const studentnumberlist = isAdmin ? studentnumbers : _.intersection(studentnumbers, studentsUserCanAccess)
console.log(`request completed ${new Date()}`)
res.json(filterPersonalTags(result, decodedToken.id))
} catch (e) {
console.log(e)
res.status(500).json({ error: e })
const studyRights = JSON.parse(studyRightsJSON)
const newStartYear = await Population.getEarliestYear(studentnumberlist, studyRights)
const yearDifference = Number(year) - Number(newStartYear)
const newMonths = Number(months) + 12 * yearDifference
const result = await Population.optimizedStatisticsOf(
{
year: newStartYear,
studyRights,
semesters,
months: newMonths,
tag: foundTag,
},
studentnumberlist
)
if (result.error) {
Sentry.captureException(new Error(result.error))
res.status(400).end()
return
}
res.json(filterPersonalTags(result, id))
})
router.get('/v3/populationstatisticsbycourse', async (req, res) => {
const { coursecodes, from, to, separate: sep } = req.query
const { decodedToken, roles } = req
const {
user: { id, isAdmin, studentsUserCanAccess: allStudentsUserCanAccess },
} = req
const separate = sep ? JSON.parse(sep) : false
if (!coursecodes || !from || !to) {
@@ -355,128 +294,95 @@ router.get('/v3/populationstatisticsbycourse', async (req, res) => {
const semesters = ['FALL', 'SPRING']
const studentnumbers = await Student.findByCourseAndSemesters(JSON.parse(coursecodes), from, to, separate)
try {
const result = await Population.optimizedStatisticsOf(
{
// Useless, because studentnumbers are already filtered above by from & to.
// We should probably refactor this to avoid more confusement.
year: 1900,
studyRights: [],
semesters,
months: 10000,
},
studentnumbers
)
const result = await Population.optimizedStatisticsOf(
{
// Useless, because studentnumbers are already filtered above by from & to.
// We should probably refactor this to avoid more confusement.
year: 1900,
studyRights: [],
semesters,
months: 10000,
},
studentnumbers
)
let studentsUserCanAccess
if (roles && roles.includes('admin')) {
studentsUserCanAccess = new Set(studentnumbers)
} else {
const unitsUserCanAccess = await UserService.getUnitsFromElementDetails(decodedToken.userId)
const codes = unitsUserCanAccess.map(unit => unit.id)
studentsUserCanAccess = new Set(await Student.filterStudentnumbersByAccessrights(studentnumbers, codes))
}
const studentsUserCanAccess = isAdmin
? new Set(studentnumbers)
: new Set(_.intersection(studentnumbers, allStudentsUserCanAccess))
const randomHash = crypto.randomBytes(12).toString('hex')
const obfuscateStudent = ({ studyrights, studentNumber, courses, gender_code }) => ({
courses,
studyrights,
gender_code,
studentNumber: crypto.createHash('md5').update(`${studentNumber}${randomHash}`).digest('hex'),
firstnames: '',
lastname: '',
name: '',
email: '',
started: null,
credits: '',
tags: [],
obfuscated: true,
})
const randomHash = crypto.randomBytes(12).toString('hex')
const obfuscateStudent = ({ studyrights, studentNumber, courses, gender_code }) => ({
courses,
studyrights,
gender_code,
studentNumber: crypto.createHash('md5').update(`${studentNumber}${randomHash}`).digest('hex'),
firstnames: '',
lastname: '',
name: '',
email: '',
started: null,
credits: '',
tags: [],
obfuscated: true,
})
result.students = result.students.map(s => (studentsUserCanAccess.has(s.studentNumber) ? s : obfuscateStudent(s)))
result.students = result.students.map(s => (studentsUserCanAccess.has(s.studentNumber) ? s : obfuscateStudent(s)))
if (result.error) {
console.log(result.error)
res.status(400).end()
return
}
console.log(`request completed ${new Date()}`)
res.json(filterPersonalTags(result, decodedToken.id))
} catch (e) {
console.log(e)
res.status(500).json({ error: e })
if (result.error) {
Sentry.captureException(new Error(result.error))
res.status(400).end()
return
}
res.json(filterPersonalTags(result, id))
})
router.post('/v3/populationstatisticsbystudentnumbers', async (req, res) => {
const { studentnumberlist } = req.body
const { roles, decodedToken } = req
const {
user: { isAdmin, id, studentsUserCanAccess },
} = req
const studentsUserCanAccess = await UserService.getStudentsUserCanAccess(
studentnumberlist,
roles,
decodedToken.userId
const filteredStudentNumbers = isAdmin ? studentnumberlist : _.intersection(studentnumberlist, studentsUserCanAccess)
const result = await Population.optimizedStatisticsOf(
{
year: 1900,
studyRights: [],
semesters: ['FALL', 'SPRING'],
months: 10000,
},
filteredStudentNumbers
)
const filteredStudentNumbers = studentnumberlist.filter(s => studentsUserCanAccess.has(s))
try {
const result = await Population.optimizedStatisticsOf(
{
year: 1900,
studyRights: [],
semesters: ['FALL', 'SPRING'],
months: 10000,
},
filteredStudentNumbers
)
if (result.error) {
console.log(result.error)
res.status(400).end()
return
}
res.status(200).json(filterPersonalTags(result, decodedToken.id))
} catch (err) {
console.log(err)
res.status(400).end()
if (result.error) {
Sentry.captureException(new Error(result.error))
return
}
res.status(200).json(filterPersonalTags(result, id))
})
router.get('/v3/populationstatistics/studyprogrammes', async (req, res) => {
try {
const { rights, roles } = req
if (roles && roles.includes('admin')) {
const studyrights = await StudyrightService.getAssociations()
res.json(studyrights)
} else {
const studyrights = await StudyrightService.getFilteredAssociations(rights)
res.json(studyrights)
}
} catch (err) {
res.status(500).json(err)
const {
user: { rights, roles },
} = req
if (roles?.includes('admin')) {
const studyrights = await StudyrightService.getAssociations()
res.json(studyrights)
} else {
const studyrights = await StudyrightService.getFilteredAssociations(rights)
res.json(studyrights)
}
})
router.get('/v3/populationstatistics/studyprogrammes/unfiltered', async (req, res) => {
try {
const studyrights = await StudyrightService.getAssociations()
res.json(studyrights)
} catch (err) {
res.status(500).json(err)
}
const studyrights = await StudyrightService.getAssociations()
res.json(studyrights)
})
router.get('/v3/populationstatistics/maxYearsToCreatePopulationFrom', async (req, res) => {
try {
const { courseCodes } = req.query
const maxYearsToCreatePopulationFrom = await CourseService.maxYearsToCreatePopulationFrom(JSON.parse(courseCodes))
return res.json(maxYearsToCreatePopulationFrom)
} catch (err) {
console.log('err', err)
res.status(500).json(err)
}
const { courseCodes } = req.query
const maxYearsToCreatePopulationFrom = await CourseService.maxYearsToCreatePopulationFrom(JSON.parse(courseCodes))
return res.json(maxYearsToCreatePopulationFrom)
})
router.use('*', (req, res, next) => next())
module.exports = router
+15 -33
View File
@@ -8,17 +8,20 @@ const {
router.get('/v3/programme_modules/:code', async (req, res) => {
const { code } = req.params
const module = await byProgrammeCode(code)
if (!module) {
res.status(400).end()
return
}
res.json(module)
const result = await byProgrammeCode(code)
res.json(result)
})
router.get('/v3/programme_modules/:code/modules', async (req, res) => {
const { code } = req.params
const result = await modulesByProgrammeCode(code)
res.json(result)
})
router.delete('/v3/programme_modules', async (req, res) => {
const { programmecode, ids } = req.body
await removeExcludedCourses(ids)
const result = await byProgrammeCode(programmecode)
if (!result) {
res.status(400).end()
return
@@ -26,36 +29,15 @@ router.get('/v3/programme_modules/:code/modules', async (req, res) => {
res.json(result)
})
router.delete('/v3/programme_modules', async (req, res) => {
const { programmecode, ids } = req.body
try {
await removeExcludedCourses(ids)
const result = await byProgrammeCode(programmecode)
if (!result) {
res.status(400).end()
return
}
res.json(result)
} catch (e) {
console.log(e)
res.json(500).json(e)
}
})
router.post('/v3/programme_modules/:programmecode/', async (req, res) => {
const { programmecode, coursecodes } = req.body
try {
await addExcludedCourses(programmecode, coursecodes)
const result = await byProgrammeCode(programmecode)
if (!result) {
res.status(400).end()
return
}
res.json(result)
} catch (e) {
console.log(e)
res.json(500).json(e)
await addExcludedCourses(programmecode, coursecodes)
const result = await byProgrammeCode(programmecode)
if (!result) {
res.status(400).end()
return
}
res.json(result)
})
module.exports = router
-2
View File
@@ -6,6 +6,4 @@ router.get('/semesters/codes', async (req, res) => {
res.json(providers)
})
router.use('*', (req, res, next) => next())
module.exports = router
+15 -53
View File
@@ -1,8 +1,6 @@
const router = require('express').Router()
const Student = require('../services/students')
const userService = require('../services/userService')
const Unit = require('../services/units')
const { mismatchedStudents } = require('../services/student_credit_total_mismatches')
const { ApplicationError } = require('../util/customErrors')
const filterStudentTags = (student, userId) => {
return {
@@ -11,17 +9,9 @@ const filterStudentTags = (student, userId) => {
}
}
const creditTotalMismatch = student => {
return {
...student,
mismatch: mismatchedStudents.has(student.studentNumber),
}
}
router.get('/students', async (req, res) => {
const {
roles,
decodedToken: { userId },
user: { isAdmin, studentsUserCanAccess },
query: { searchTerm },
} = req
@@ -33,58 +23,30 @@ router.get('/students', async (req, res) => {
.slice(0, 2)
.find(t => t.length > 3)
) {
return res.status(400).json({ error: 'at least one search term must be longer than 3 characters' })
throw new ApplicationError('at least one search term must be longer than 3 characters', 400)
}
if (roles && roles.includes('admin')) {
let results = []
if (trimmedSearchTerm) {
results = await Student.bySearchTerm(trimmedSearchTerm)
}
return res.status(200).json(results).end()
} else {
const unitsUserCanAccess = await userService.getUnitsFromElementDetails(userId)
const codes = unitsUserCanAccess.map(unit => unit.id)
const matchingStudents = await Student.bySearchTermAndElements(trimmedSearchTerm, codes)
res.json(matchingStudents)
let results = []
if (trimmedSearchTerm) {
results = isAdmin
? await Student.bySearchTerm(trimmedSearchTerm)
: await Student.bySearchTermAndStudentNumbers(trimmedSearchTerm, studentsUserCanAccess)
}
res.json(results)
})
router.get('/students/:id', async (req, res) => {
const { id: studentId } = req.params
const { roles, decodedToken } = req
const {
user: { id, isAdmin, studentsUserCanAccess },
} = req
if (roles && roles.includes('admin')) {
const results = await Student.withId(studentId)
return results.error
? res.status(400).json({ error: 'error finding student' }).end()
: res
.status(200)
.json(creditTotalMismatch(filterStudentTags(results, decodedToken.id)))
.end()
if (!isAdmin && !studentsUserCanAccess.includes(studentId)) {
throw new ApplicationError('Error finding student', 400)
}
const uid = req.decodedToken.userId
const student = await Student.withId(studentId)
if (student.error) {
return res.status(400).json({ error: 'error finding student' }).end()
}
const units = await userService.getUnitsFromElementDetails(uid)
const rights = await Promise.all(
units.map(async unit => {
const jtn = await Unit.hasStudent(unit.id, student.studentNumber)
return jtn
})
)
if (rights.some(right => right !== null)) {
res.status(200).json(filterStudentTags(student, decodedToken.id)).end()
} else {
res.status(400).json({ error: 'error finding student' }).end()
}
res.json(filterStudentTags(student, id))
})
router.use('*', (req, res, next) => next())
module.exports = router
@@ -1,21 +1,39 @@
const router = require('express').Router()
const { getImporterClient } = require('../util/importerClient')
const { logger } = require('../util/logger')
const { getAllGroupsAndStudents, changeGroupTags } = require('../services/studyGuidanceGroups')
router.get('/', async (req, res) => {
const personId = req.headers.hypersonsisuid
if (!personId) {
const errorMessage = 'Not possible to get groups without personId header'
console.log(errorMessage)
return res.status(400).json({ error: errorMessage })
const {
user: { uid, sisPersonId },
} = req
if (!sisPersonId) {
logger.error(`User ${uid} tried to get person groups but personId was ${sisPersonId} in header`)
return res.status(400).json({ error: 'Not possible to get groups without personId' })
}
const importerClient = getImporterClient()
try {
const { data } = await importerClient.get(`/person-groups/person/${personId}`)
return res.json(Object.values(data))
} catch (e) {
console.log(e)
return res.status(500).json(e)
return res.json(await getAllGroupsAndStudents(sisPersonId))
})
router.put('/:id/tags', async (req, res) => {
const {
user: { uid, sisPersonId },
body: tags,
params: { id: groupId },
} = req
if (!sisPersonId) {
logger.error(`User ${uid} tried to change study guidance group tags but personId was ${sisPersonId} in header`)
return res.status(400).json({ error: 'Not possible to change groups without personId' })
}
const groupsUserHasAccessTo = (await getAllGroupsAndStudents(sisPersonId)).map(group => group.id)
if (!groupsUserHasAccessTo.includes(groupId)) {
logger.error(
`User ${uid} tried to change study guidance group tags, but didn't have permission for the group ${groupId}`
)
return res.status(400).json({ error: 'Access denied' })
}
return res.status(200).json(await changeGroupTags({ groupId, tags }))
})
module.exports = router
@@ -0,0 +1,129 @@
const router = require('express').Router()
const {
getBasicStatsForStudytrack,
getCreditStatsForStudytrack,
getGraduationStatsForStudytrack,
} = require('../services/studyprogrammeStats')
const { getStudytrackStatsForStudyprogramme } = require('../services/studytrackStats')
const {
getBasicStats,
setBasicStats,
getCreditStats,
setCreditStats,
getGraduationStats,
setGraduationStats,
getStudytrackStats,
setStudytrackStats,
} = require('../services/analyticsService')
const logger = require('../util/logger')
router.get('/v2/studyprogrammes/:id/basicstats', async (req, res) => {
const code = req.params.id
const yearType = req.query?.year_type
if (code) {
let data = null
try {
data = await getBasicStats(code, yearType)
} catch (e) {
logger.error(`Failed to get code ${code} basic stats: ${e}`)
}
if (!data) {
try {
const result = await getBasicStatsForStudytrack({
studyprogramme: req.params.id,
yearType,
})
data = await setBasicStats(result, yearType)
} catch (e) {
logger.error(`Failed to update code ${code} basic stats: ${e}`)
}
}
return res.json(data)
} else {
res.status(422).end()
}
})
router.get('/v2/studyprogrammes/:id/creditstats', async (req, res) => {
const code = req.params.id
const yearType = req.query?.year_type
if (code) {
let data = null
try {
data = await getCreditStats(code, yearType)
} catch (e) {
logger.error(`Failed to get code ${code} credit stats`)
}
if (!data) {
try {
const result = await getCreditStatsForStudytrack({
studyprogramme: req.params.id,
yearType,
})
data = await setCreditStats(result, yearType)
} catch (e) {
logger.error(`Failed to update code ${code} credit stats`)
}
}
return res.json(data)
} else {
res.status(422).end()
}
})
router.get('/v2/studyprogrammes/:id/graduationstats', async (req, res) => {
const code = req.params.id
const yearType = req.query?.year_type
if (code) {
let data = null
try {
data = await getGraduationStats(code, yearType)
} catch (e) {
logger.error(`Failed to get code ${code} graduation stats`)
}
if (!data) {
try {
const result = await getGraduationStatsForStudytrack({
studyprogramme: req.params.id,
yearType,
})
data = await setGraduationStats(result, yearType)
} catch (e) {
logger.error(`Failed to update code ${code} graduation stats`)
}
}
return res.json(data)
} else {
res.status(422).end()
}
})
router.get('/v2/studyprogrammes/:id/studytrackstats', async (req, res) => {
const code = req.params.id
if (code) {
let data = null
try {
data = await getStudytrackStats(code)
} catch (e) {
logger.error(`Failed to get code ${code} studytrack stats: ${e}`)
}
if (!data) {
try {
const result = await getStudytrackStatsForStudyprogramme({ studyprogramme: req.params.id })
data = await setStudytrackStats(result)
} catch (e) {
logger.error(`Failed to update code ${code} studytrack stats: ${e}`)
}
}
return res.json(data)
} else {
res.status(422).end()
}
})
module.exports = router
-26
View File
@@ -1,26 +0,0 @@
const router = require('express').Router()
const userService = require('../services/userService')
router.post('/:uid', async (req, res) => {
try {
const uid = req.headers['uid']
if (req.headers['shib-session-id'] && uid) {
const asUser = req.params.uid
const token = await userService.superlogin(uid, asUser)
res.status(200).json(token)
} else {
res
.status(401)
.json({
message: `Not enough headers login, uid:
${req.headers.uid} session-id ${req.headers['shib-session-id']}`,
})
.end()
}
} catch (err) {
console.log(err.message)
res.status(500).json({ message: 'problem with login' })
}
})
module.exports = router
+89 -134
View File
@@ -12,190 +12,145 @@ const filterRelevantStudentTags = (studentTags, userId) => {
return studentTags.filter(({ tag }) => !tag.personal_user_id || tag.personal_user_id === userId)
}
router.get('/tags', async (req, res) => {
try {
const tags = await Tags.findTags()
res.status(200).json(tags)
} catch (err) {
console.log(err)
res.status(500).json(err)
}
router.get('/tags', async (_req, res) => {
const tags = await Tags.findTags()
res.status(200).json(tags)
})
router.get('/tags/:studytrack', async (req, res) => {
try {
const { studytrack } = req.params
const { rights, roles, decodedToken } = req
const { studytrack } = req.params
const {
user: { rights, roles, id },
} = req
if (!rights.includes(studytrack) && !(roles && roles.includes('admin'))) return res.status(403).end()
if (!rights.includes(studytrack) && !roles?.includes('admin')) return res.status(403).end()
const tags = await Tags.findTagsByStudytrack(studytrack)
res.status(200).json(filterRelevantTags(tags, decodedToken.id))
} catch (err) {
console.log(err)
res.status(500).json(err)
}
const tags = await Tags.findTagsByStudytrack(studytrack)
res.status(200).json(filterRelevantTags(tags, id))
})
router.post('/tags', async (req, res) => {
try {
const {
tag: { studytrack, tagname, year, personal_user_id },
} = req.body
const { rights, roles, decodedToken } = req
const {
tag: { studytrack, tagname, year, personal_user_id },
} = req.body
const {
user: { rights, roles, id },
} = req
if (!rights.includes(studytrack) && !(roles && roles.includes('admin'))) return res.status(403).end()
if (!rights.includes(studytrack) && !roles?.includes('admin')) return res.status(403).end()
await Tags.createNewTag({ studytrack, tagname, year, personal_user_id })
const tags = await Tags.findTagsByStudytrack(studytrack)
res.status(200).json(filterRelevantTags(tags, decodedToken.id))
} catch (err) {
console.log(err)
res.status(500).json(err.message)
}
await Tags.createNewTag({ studytrack, tagname, year, personal_user_id })
const tags = await Tags.findTagsByStudytrack(studytrack)
res.status(200).json(filterRelevantTags(tags, id))
})
router.delete('/tags', async (req, res) => {
try {
const { tag } = req.body
const { rights, roles, decodedToken } = req
const { tag } = req.body
const {
user: { rights, roles, id },
} = req
if (
!rights.includes(tag.studytrack) &&
!(roles && roles.includes('admin')) &&
!(tag.personal_user_id === decodedToken.id)
)
return res.status(403).end()
if (!rights.includes(tag.studytrack) && !roles?.includes('admin') && !(tag.personal_user_id === id))
return res.status(403).end()
await Tags.deleteTag(tag)
const t = await Tags.findTagsByStudytrack(tag.studytrack)
res.status(200).json(filterRelevantTags(t, decodedToken.id))
} catch (err) {
console.log(err)
res.status(500).json(err)
}
await Tags.deleteTag(tag)
const t = await Tags.findTagsByStudytrack(tag.studytrack)
res.status(200).json(filterRelevantTags(t, id))
})
router.get('/studenttags', async (req, res) => {
try {
const result = await TagStudent.getStudentTags()
res.status(200).json(result)
} catch (err) {
console.log(err)
res.status(500).json(err)
}
const result = await TagStudent.getStudentTags()
res.status(200).json(result)
})
router.get('/studenttags/:studytrack', async (req, res) => {
try {
const { studytrack } = req.params
const { rights, roles, decodedToken } = req
const { studytrack } = req.params
const {
user: { rights, roles, id },
} = req
if (!rights.includes(studytrack) && !(roles && roles.includes('admin'))) return res.status(403).end()
if (!rights.includes(studytrack) && !roles?.includes('admin')) return res.status(403).end()
const result = await TagStudent.getStudentTagsByStudytrack(studytrack)
res.status(200).json(filterRelevantStudentTags(result, decodedToken.id))
} catch (err) {
console.log(err)
res.status(500).json(err)
}
const result = await TagStudent.getStudentTagsByStudytrack(studytrack)
res.status(200).json(filterRelevantStudentTags(result, id))
})
router.get('/studenttags/:studentnumber', async (req, res) => {
try {
const { studentnumber } = req.params
const { decodedToken } = req
const result = await TagStudent.getStudentTagsByStudentnumber(studentnumber)
res.status(200).json(filterRelevantStudentTags(result, decodedToken.id))
} catch (err) {
console.log(err)
res.status(500).json(err)
}
const { studentnumber } = req.params
const { user } = req
const result = await TagStudent.getStudentTagsByStudentnumber(studentnumber)
res.status(200).json(filterRelevantStudentTags(result, user.id))
})
router.post('/studenttags/:studentnumber', async (req, res) => {
try {
const { tag, studytrack } = req.body
const { rights, roles, decodedToken } = req
const { tag, studytrack } = req.body
const {
user: { roles, rights, id },
} = req
if (!rights.includes(studytrack) && !(roles && roles.includes('admin'))) return res.status(403).end()
if (!rights.includes(studytrack) && !roles?.includes('admin')) return res.status(403).end()
await TagStudent.createStudentTag(tag)
const result = await TagStudent.getStudentTagsByStudytrack(studytrack)
res.status(200).json(filterRelevantStudentTags(result, decodedToken.id))
} catch (err) {
console.log(err)
res.status(500).json(err)
}
await TagStudent.createStudentTag(tag)
const result = await TagStudent.getStudentTagsByStudytrack(studytrack)
res.status(200).json(filterRelevantStudentTags(result, id))
})
router.post('/studenttags', async (req, res) => {
try {
const { tags, studytrack } = req.body
const { rights, roles, decodedToken } = req
const { tags, studytrack } = req.body
const {
user: { rights, roles, id },
} = req
if (!rights.includes(studytrack) && !(roles && roles.includes('admin'))) return res.status(403).end()
if (!rights.includes(studytrack) && !roles?.includes('admin')) return res.status(403).end()
const existingtags = await Tags.findTagsByStudytrack(studytrack)
const existingTagids = existingtags.map(t => t.tag_id)
const tagids = [...new Set(tags.map(t => t.tag_id))]
if (!tagids.find(t => existingTagids.includes(t))) return res.status(400).json({ error: 'The tag does not exist' })
const existingtags = await Tags.findTagsByStudytrack(studytrack)
const existingTagids = existingtags.map(t => t.tag_id)
const tagids = [...new Set(tags.map(t => t.tag_id))]
if (!tagids.find(t => existingTagids.includes(t))) return res.status(400).json({ error: 'The tag does not exist' })
const studentnumbers = tags.map(t => t.studentnumber)
const students = await Students.filterStudentnumbersByAccessrights(studentnumbers, [studytrack])
const missingStudents = difference(studentnumbers, students)
if (missingStudents.length !== 0)
return res
.status(400)
.json({ error: `Could not find the following students from the programme: ${missingStudents.join(', ')}` })
const studentnumbers = tags.map(t => t.studentnumber)
const students = await Students.filterStudentnumbersByAccessrights(studentnumbers, [studytrack])
const missingStudents = difference(studentnumbers, students)
if (missingStudents.length !== 0)
return res
.status(400)
.json({ error: `Could not find the following students from the programme: ${missingStudents.join(', ')}` })
await TagStudent.createMultipleStudentTags(tags)
const result = await TagStudent.getStudentTagsByStudytrack(studytrack)
res.status(200).json(filterRelevantStudentTags(result, decodedToken.id))
} catch (err) {
console.log(err)
res.status(500).json(err)
}
await TagStudent.createMultipleStudentTags(tags)
const result = await TagStudent.getStudentTagsByStudytrack(studytrack)
res.status(200).json(filterRelevantStudentTags(result, id))
})
router.delete('/studenttags/delete_one', async (req, res) => {
try {
const { tag_id, studentnumber, studytrack } = req.body
const { rights, roles, decodedToken } = req
const { tag_id, studentnumber, studytrack } = req.body
const {
user: { rights, roles, id },
} = req
if (!rights.includes(studytrack) && !(roles && roles.includes('admin'))) return res.status(403).end()
if (!rights.includes(studytrack) && !roles?.includes('admin')) return res.status(403).end()
const tags = await Tags.findTagsFromStudytrackById(studytrack, [tag_id])
if (tags.length === 0) return res.status(403).json({ error: 'The tag does not exist' })
const tags = await Tags.findTagsFromStudytrackById(studytrack, [tag_id])
if (tags.length === 0) return res.status(403).json({ error: 'The tag does not exist' })
await TagStudent.deleteStudentTag(studentnumber, tag_id)
const result = await TagStudent.getStudentTagsByStudytrack(studytrack)
res.status(200).json(filterRelevantStudentTags(result, decodedToken.id))
} catch (err) {
console.log(err)
res.status(500).json(err)
}
await TagStudent.deleteStudentTag(studentnumber, tag_id)
const result = await TagStudent.getStudentTagsByStudytrack(studytrack)
res.status(200).json(filterRelevantStudentTags(result, id))
})
router.delete('/studenttags/delete_many', async (req, res) => {
try {
const { tagId, studentnumbers, studytrack } = req.body
const { rights, roles, decodedToken } = req
const { tagId, studentnumbers, studytrack } = req.body
const {
user: { rights, roles, id },
} = req
if (!rights.includes(studytrack) && !(roles && roles.includes('admin'))) return res.status(403).end()
if (!rights.includes(studytrack) && !roles?.includes('admin')) return res.status(403).end()
const tags = await Tags.findTagsFromStudytrackById(studytrack, [tagId])
if (tags.length === 0) return res.status(403).json({ error: 'The tag does not exist' })
const tags = await Tags.findTagsFromStudytrackById(studytrack, [tagId])
if (tags.length === 0) return res.status(403).json({ error: 'The tag does not exist' })
await TagStudent.deleteMultipleStudentTags(tagId, studentnumbers)
const result = await TagStudent.getStudentTagsByStudytrack(studytrack)
res.status(200).json(filterRelevantStudentTags(result, decodedToken.id))
} catch (err) {
console.log(err)
res.status(400).json(err)
}
await TagStudent.deleteMultipleStudentTags(tagId, studentnumbers)
const result = await TagStudent.getStudentTagsByStudytrack(studytrack)
res.status(200).json(filterRelevantStudentTags(result, id))
})
router.use('*', (req, res, next) => next())
module.exports = router
+4 -3
View File
@@ -25,6 +25,7 @@ router.get('/top', async (req, res) => {
if (!yearcode) {
return res.status(422).send('Missing required yearcode query param')
}
const result = await topteachers.getTeacherStats(category, yearcode)
res.json(result)
})
@@ -41,7 +42,9 @@ router.get('/top/categories', async (req, res) => {
})
router.get('/stats', async (req, res) => {
const { rights, roles } = req
const {
user: { rights, roles },
} = req
const { providers, semesterStart, semesterEnd } = req.query
if (!providers || !semesterStart) {
@@ -77,6 +80,4 @@ router.get('/:id', async (req, res) => {
res.json(result)
})
router.use('*', (req, res, next) => next())
module.exports = router
+5 -1
View File
@@ -9,8 +9,12 @@ router.post('/event', (req, res) => {
return res.status(400).json({ error: 'group, name and value are required' })
}
const {
user: { userId },
} = req
// don't await here because frontend doesn't care if it succeeds
tsaService.sendTsaEvent(req.decodedToken.userId, { group, name, label, value }).catch(e => {
tsaService.sendTsaEvent(userId, { group, name, label, value }).catch(e => {
Sentry.withScope(scope => {
// if error was caused by a 400, dig out the JSON response for Sentry
if (e.isAxiosError && e.response && e.response.data) {
+28 -81
View File
@@ -8,116 +8,63 @@ const {
abort,
updateCoursesByCourseCode,
} = require('../services/sisUpdaterService')
const { refreshStatistics } = require('../events')
const { refreshStatistics, refreshTrends } = require('../events')
router.get('/update/v2/meta', async (req, res) => {
const { roles } = req
if (!roles.includes('dev')) return res.status(403).send('No rights, please stop')
try {
const response = await updateSISMetadata(req)
if (response) {
res.status(200).json('Update SIS meta scheduled')
}
} catch (err) {
console.log(err)
res.status(500).json({ error: 'error' })
const response = await updateSISMetadata(req)
if (response) {
res.status(200).json('Update SIS meta scheduled')
}
})
router.get('/update/v2/students', async (req, res) => {
const { roles } = req
if (!roles.includes('dev')) return res.status(403).send('No rights, please stop')
try {
const response = await updateSISStudents()
if (response) {
res.status(200).json('Update SIS students scheduled')
}
} catch (err) {
console.log(err)
res.status(500).json({ error: 'error' })
const response = await updateSISStudents()
if (response) {
res.status(200).json('Update SIS students scheduled')
}
})
router.post('/update/v2/students', async (req, res) => {
const { roles } = req
if (!roles.includes('dev')) return res.status(403).send('No rights, please stop')
try {
const response = await updateStudentsByStudentNumber(req.body)
if (response) {
res.status(200).json('Update SIS students scheduled')
}
} catch (err) {
console.log(err)
res.status(500).json({ error: 'error' })
const response = await updateStudentsByStudentNumber(req.body)
if (response) {
res.status(200).json('Update SIS students scheduled')
}
})
router.post('/update/v2/courses', async (req, res) => {
const { roles } = req
if (!roles.includes('dev')) return res.status(403).send('No rights, please stop')
try {
const response = await updateCoursesByCourseCode(req.body)
if (response) {
res.status(200).json('Update SIS courses scheduled')
}
} catch (err) {
console.log(err)
res.status(500).json({ error: 'error' })
const response = await updateCoursesByCourseCode(req.body)
if (response) {
res.status(200).json('Update SIS courses scheduled')
}
})
router.get('/update/v2/programmes', async (req, res) => {
const { roles } = req
if (!roles.includes('dev')) return res.status(403).send('No rights, please stop')
try {
const response = await updateSISProgrammes()
if (response) {
res.status(200).json('Update SIS programmes scheduled')
}
} catch (err) {
console.log(err)
res.status(500).json({ error: 'error' })
const response = await updateSISProgrammes()
if (response) {
res.status(200).json('Update SIS programmes scheduled')
}
})
router.get('/refresh_redis_cache', async (req, res) => {
const { roles } = req
if (!roles.includes('dev')) return res.status(403).send('No rights, please stop')
try {
const response = await updateSISRedisCache()
if (response) {
res.status(200).json('Refreshing SIS redis cache scheduled')
}
} catch (err) {
console.log(err)
res.status(500).json({ error: 'error' })
const response = await updateSISRedisCache()
if (response) {
res.status(200).json('Refreshing SIS redis cache scheduled')
}
})
router.post('/refresh_statistic_v2', async (req, res) => {
try {
refreshStatistics()
res.status(200).json('Refreshing sis statistics')
} catch (err) {
console.error(err)
res.status(500).json({ error: 'error' })
}
await refreshStatistics()
res.status(200).json('Refreshing sis statistics')
})
router.post('/refresh_trends', async (req, res) => {
await refreshTrends()
res.status(200).json('Refreshing trends')
})
router.get('/abort', async (req, res) => {
try {
await abort()
res.status(200).json()
} catch (err) {
console.log(err)
res.status(500).json({ error: 'error' })
}
await abort()
res.status(200).json()
})
module.exports = router
+2 -17
View File
@@ -1,16 +1,7 @@
const router = require('express').Router()
const userService = require('../services/userService')
const blacklist = require('../services/blacklist')
const { userDataCache } = require('../services/cache')
const { sendNotificationAboutAccessToUser, previewNotificationAboutAccessToUser } = require('../services/mailservice')
const addUserToBlacklist = async (user, decodedToken) => {
if (user) {
const { username } = user
userDataCache.del(username)
if (username != decodedToken.userId) await blacklist.addUserToBlacklist(username)
}
}
const logger = require('../util/logger')
router.get('/', async (req, res) => {
const results = await userService.findAll()
@@ -24,10 +15,7 @@ router.get('/access_groups', async (req, res) => {
router.post('/modifyaccess', async (req, res) => {
try {
const { uid } = req.body
const result = await userService.modifyAccess(req.body)
const user = await userService.byId(uid)
await addUserToBlacklist(user, req.decodedToken)
res.status(200).json(result)
} catch (e) {
res.status(400).json(e)
@@ -49,7 +37,7 @@ router.post('/email', async (req, res) => {
if (result.error) {
return res.status(400).json(result).end()
}
console.log('Message sent successfully')
logger.info('Succesfully sent message about oodikone access to user')
res.status(200).end()
})
@@ -57,7 +45,6 @@ router.post('/:uid/elements', async (req, res) => {
const { uid } = req.params
const { codes } = req.body
const user = await userService.enableElementDetails(uid, codes)
await addUserToBlacklist(user, req.decodedToken)
res.json(user)
})
@@ -65,7 +52,6 @@ router.delete('/:uid/elements', async (req, res) => {
const { uid } = req.params
const { codes } = req.body
const user = await userService.removeElementDetails(uid, codes)
await addUserToBlacklist(user, req.decodedToken)
res.json(user)
})
@@ -73,7 +59,6 @@ router.post('/:uid/faculties', async (req, res) => {
const { uid } = req.params
const { faculties } = req.body
const user = await userService.setFaculties(uid, faculties)
await addUserToBlacklist(user, req.decodedToken)
res.json(user)
})
@@ -3,7 +3,10 @@ const moment = require('moment')
const createRedisKeyForProductivity = id => `PRODUCTIVITY_${id}`
const createRedisKeyForThroughput = id => `THROUGHPUT_${id}`
const createRedisKeyForNonGraduatedStudents = id => `NONGRADUATEDSTUDENTS_${id}`
const createRedisKeyForBasicStats = (id, yearType) => `BASIC_STATS_${id}_${yearType}`
const createRedisKeyForCreditStats = (id, yearType) => `CREDIT_STATS_${id}_${yearType}`
const createRedisKeyForGraduationStats = (id, yearType) => `GRADUATION_STATS_${id}_${yearType}`
const createRedisKeyForStudytrackStats = id => `STUDYTRACK_STATS_${id}`
const getProductivity = async id => {
const redisKey = createRedisKeyForProductivity(id)
@@ -88,32 +91,86 @@ const patchThroughput = async data => {
}
}
const patchNonGraduatedStudents = async data => {
const [id, dataToPatch] = Object.entries(data)[0]
const redisKey = createRedisKeyForNonGraduatedStudents(id)
const dataFromRedisRaw = await redisClient.getAsync(redisKey)
const dataFromRedis = dataFromRedisRaw ? JSON.parse(dataFromRedisRaw) : { data: null }
const patchedData = {
data: {
...dataFromRedis.data,
...dataToPatch,
},
lastUpdated: moment().format(),
}
const setOperationStatus = await redisClient.setAsync(redisKey, JSON.stringify(patchedData))
if (setOperationStatus !== 'OK') return null
return {
[id]: patchedData,
}
}
const getNonGraduatedStudents = async id => {
const redisKey = createRedisKeyForNonGraduatedStudents(id)
const getBasicStats = async (id, yearType) => {
const redisKey = createRedisKeyForBasicStats(id, yearType)
const dataFromRedis = await redisClient.getAsync(redisKey)
if (!dataFromRedis) return null
return JSON.parse(dataFromRedis)
}
const setBasicStats = async (data, yearType) => {
const { id } = data
const redisKey = createRedisKeyForBasicStats(id, yearType)
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) => {
const redisKey = createRedisKeyForCreditStats(id, yearType)
const dataFromRedis = await redisClient.getAsync(redisKey)
if (!dataFromRedis) return null
return JSON.parse(dataFromRedis)
}
const setCreditStats = async (data, yearType) => {
const { id } = data
const redisKey = createRedisKeyForCreditStats(id, yearType)
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, yearType) => {
const redisKey = createRedisKeyForGraduationStats(id, yearType)
const dataFromRedis = await redisClient.getAsync(redisKey)
if (!dataFromRedis) return null
return JSON.parse(dataFromRedis)
}
const setGraduationStats = async (data, yearType) => {
const { id } = data
const redisKey = createRedisKeyForGraduationStats(id, yearType)
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 getStudytrackStats = async id => {
const redisKey = createRedisKeyForStudytrackStats(id)
const dataFromRedis = await redisClient.getAsync(redisKey)
if (!dataFromRedis) return null
return JSON.parse(dataFromRedis)
}
const setStudytrackStats = async data => {
const { id } = data
const redisKey = createRedisKeyForStudytrackStats(id)
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 = {
getProductivity,
setProductivity,
@@ -121,6 +178,12 @@ module.exports = {
getThroughput,
setThroughput,
patchThroughput,
patchNonGraduatedStudents,
getNonGraduatedStudents,
getBasicStats,
setBasicStats,
getCreditStats,
setCreditStats,
getGraduationStats,
setGraduationStats,
getStudytrackStats,
setStudytrackStats,
}
@@ -1,21 +0,0 @@
const { redisClient } = require('./redis')
const moment = require('moment')
const MAKE_REDIS_HASH_KEY = id => `BLACKLIST ${id}`
const addUserToBlacklist = async userId => {
console.log('ADDED BLACKLIST', userId)
await redisClient.setAsync(MAKE_REDIS_HASH_KEY(userId), moment().toISOString(false))
}
const isUserBlacklisted = async (userId, tokenCreatedAt) => {
const isBlacklisted = async () => {
const blacklistTimestamp = await redisClient.getAsync(MAKE_REDIS_HASH_KEY(userId))
return tokenCreatedAt && blacklistTimestamp && moment(tokenCreatedAt) < moment(blacklistTimestamp)
}
const timer = () => new Promise(resolve => setTimeout(() => resolve(true), 5000))
return Promise.race([isBlacklisted(), timer()])
}
module.exports = { addUserToBlacklist, isUserBlacklisted }
-12
View File
@@ -1,12 +0,0 @@
const LRU = require('lru-cache')
// Max 25 users can be stored in the cache
const userDataCache = new LRU({
max: 25,
length: () => 1,
maxAge: 1000 * 60 * 60,
})
module.exports = {
userDataCache,
}
+33 -79
View File
@@ -13,7 +13,7 @@ const {
const { parseCredit } = require('./parseCredits')
const Op = Sequelize.Op
const { CourseYearlyStatsCounter } = require('./course_yearly_stats_counter')
const _ = require('lodash')
const { sortMainCode, getSortRank } = require('../util/utils')
const byNameOrCode = (searchTerm, language) =>
Course.findAll({
@@ -59,7 +59,15 @@ const findOneByCode = code => {
})
}
const creditsForCourses = async (codes, anonymizationSalt) => {
const creditsForCourses = async (codes, anonymizationSalt, unifyOpenUniCourses) => {
let is_open = false
if (unifyOpenUniCourses) {
is_open = {
[Op.in]: [false, true],
}
}
const credits = await Credit.findAll({
include: [
{
@@ -105,9 +113,11 @@ const creditsForCourses = async (codes, anonymizationSalt) => {
student_studentnumber: {
[Op.ne]: null,
},
[Op.or]: [{ is_open }, { is_open: null }],
},
order: [['attainment_date', 'ASC']],
})
const parsedCredits = credits.map(credit => parseCredit(credit, anonymizationSalt))
return parsedCredits
}
@@ -144,47 +154,20 @@ const unifyOpenUniversity = code => {
return regexresult[1]
}
const allCodeAltenatives = async code => {
let course = await Course.findAll({
raw: true,
attributes: ['id', 'code', 'substitutions'],
where: {
code: code,
},
const allCodeAlternatives = async code => {
const course = await Course.findOne({
where: { code: code },
})
const allSubstitutions = _.flatten(course.map(c => c.substitutions))
let subcodes = []
if (allSubstitutions) {
subcodes = await Course.findAll({
raw: true,
attributes: ['code'],
where: {
id: {
[Op.in]: allSubstitutions,
},
},
})
}
const courses = [...course, ...subcodes]
const temp = courses
.map(c => c.code)
.map(c => {
if (c.match(/^A/)) return [c, 4] // open university codes come last
if (c.match(/^\d/)) return [c, 2] // old numeric codes come second
if (c.match(/^[A-Za-z]/)) return [c, 1] // new letter based codes come first
return [c, 3] // unknown, comes before open uni?
})
.sort((a, b) => a[1] - b[1])
.map(c => c[0])
return allSubstitutions ? _.uniq(temp) : [code]
return sortMainCode([...course.substitutions, code])
}
const yearlyStatsOfNew = async (coursecode, separate, unifyOpenUniCourses, anonymizationSalt) => {
let codes = await allCodeAltenatives(coursecode)
const courseForSubs = await Course.findOne({
where: { code: coursecode },
})
let codes = courseForSubs.substitutions ? sortMainCode([...courseForSubs.substitutions, coursecode]) : [coursecode]
if (isOpenUniCourseCode(coursecode) && !unifyOpenUniCourses) {
codes = [coursecode]
@@ -195,7 +178,7 @@ const yearlyStatsOfNew = async (coursecode, separate, unifyOpenUniCourses, anony
}
const [credits, course] = await Promise.all([
creditsForCourses(codes, anonymizationSalt),
creditsForCourses(codes, anonymizationSalt, unifyOpenUniCourses),
Course.findOne({
where: {
code: coursecode,
@@ -204,6 +187,7 @@ const yearlyStatsOfNew = async (coursecode, separate, unifyOpenUniCourses, anony
])
const counter = new CourseYearlyStatsCounter()
for (let credit of credits) {
const {
studentnumber,
@@ -357,10 +341,6 @@ const codeLikeTerm = code =>
const byNameAndOrCodeLike = async (name, code) => {
let rawCourses = await Course.findAll({
include: {
model: Organization,
required: true,
},
where: {
...nameLikeTerm(name),
...codeLikeTerm(code),
@@ -368,40 +348,10 @@ const byNameAndOrCodeLike = async (name, code) => {
})
const courses = rawCourses
.map(
({
id,
code,
name,
latest_instance_date,
is_study_module,
coursetypecode,
startdate,
max_attainment_date,
min_attainment_date,
createdAt,
updatedAt,
substitutions,
organizations,
}) => {
return {
id,
code,
name,
latest_instance_date,
is_study_module,
coursetypecode,
startdate,
max_attainment_date,
min_attainment_date,
createdAt,
updatedAt,
substitutions,
organizations: organizations.map(o => o.id),
}
}
)
.sort(a => (a.code.match(/^[A-Za-z]{3}[0-9]{1}/) ? -1 : 1))
.map(course => {
return { ...course.dataValues }
})
.sort((x, y) => getSortRank(y.code) - getSortRank(x.code))
let substitutionGroupIndex = 0
const visited = []
@@ -409,7 +359,11 @@ const byNameAndOrCodeLike = async (name, code) => {
const organizeSubgroups = course => {
if (visited.includes(course.code)) return
let temp = courses.filter(c => (course.substitutions ? course.substitutions.includes(c.id) : false))
let temp = []
if (course.substitutions !== null) {
temp = courses.filter(c => course.substitutions.includes(c.code))
}
temp.unshift(course)
temp.forEach(cu => {
if (visited.includes(course.code)) return
@@ -437,6 +391,6 @@ module.exports = {
byCodes,
maxYearsToCreatePopulationFrom,
unifyOpenUniversity,
allCodeAltenatives,
allCodeAlternatives,
findOneByCode,
}
+10 -20
View File
@@ -1,5 +1,6 @@
const axios = require('axios')
const { isProduction, pateToken } = require('../conf-backend')
const { ApplicationError } = require('../util/customErrors')
const pateClient = axios.create({
baseURL: 'https://pate.toska.cs.helsinki.fi',
@@ -19,33 +20,22 @@ const baseSettings = {
}
const sendEmail = async (options = {}) => {
if (!isProduction) {
const errorMessage = 'Skipped sending email in non-production environment'
console.log(errorMessage, options)
return { error: errorMessage }
}
if (!pateToken) {
const errorMessage = 'Skipped sending email since pate token was not available'
console.log(errorMessage, options)
return { error: errorMessage }
}
const { data } = await pateClient.post('/', options)
return data
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)
}
const sendFeedbackToToska = ({ feedbackContent, userId, userEmail, userFullName }) => {
const userDetails = `Sent by ${userFullName}, userid: ${userId}, email: ${userEmail}`
const sendFeedbackToToska = async ({ feedbackContent, user }) => {
const { name, userId, email } = user
const userDetails = `Sent by ${name}, userid: ${userId}, email: ${email}`
const text = [feedbackContent, userDetails].join('<br />')
return sendEmail({
await sendEmail({
template,
emails: [
{
to: 'Toska <grp-toska@helsinki.fi>',
replyTo: userEmail,
subject: `Oodikone feedback from ${userFullName}`,
replyTo: email,
subject: `Oodikone feedback from ${name}`,
text,
},
],
@@ -0,0 +1,338 @@
const sequelize = require('sequelize')
const { Op } = sequelize
const {
Credit,
Course,
Organization,
Studyright,
StudyrightElement,
ElementDetail,
Transfer,
Student,
SemesterEnrollment,
Semester,
} = require('../models')
const { formatStudyright, formatStudent } = require('./studyprogrammeHelpers')
const whereStudents = studentnumbers => {
return studentnumbers ? studentnumbers : { [Op.not]: null }
}
const studytrackStudents = async studentnumbers =>
(
await Student.findAll({
where: {
studentnumber: {
[Op.in]: studentnumbers,
},
},
})
).map(formatStudent)
const enrolledStudyrights = async studytrack => {
const studyrights = await Studyright.findAll({
attributes: ['studyrightid', 'studystartdate', 'enddate', 'prioritycode'],
include: [
{
model: StudyrightElement,
required: true,
where: { code: studytrack },
},
{
model: Student,
attributes: ['studentnumber'],
required: true,
},
],
})
const studentnumbers = studyrights.map(s => s.student.studentnumber)
const students = await Student.findAll({
attributes: ['studentnumber'],
include: [
{
model: Studyright,
include: [
{
model: StudyrightElement,
required: true,
where: {
code: studytrack,
},
},
],
attributes: ['studyrightid'],
},
{
model: SemesterEnrollment,
attributes: ['semestercode'],
include: [
{
model: Semester,
required: true,
},
],
where: {
enrollmenttype: 1,
},
},
],
where: {
studentnumber: {
[Op.in]: studentnumbers,
},
},
})
let enrollments = new Set()
students.forEach(student => {
student.semester_enrollments.forEach(e => {
const enrollmentStartDate = e.semester.startdate
const enrollmentEndDate = e.semester.enddate
const correctStudyRight = studyrights.find(
s =>
student.studentnumber === s.student.studentnumber &&
s.studystartdate <= enrollmentStartDate && // Studying in the programme should have started on the same day or earlier than the enrollment
s.enddate >= enrollmentEndDate && // Studying in the programme should have ended on the same day or later than the enrollment
(!s.canceldate || s.canceldate > enrollmentStartDate) && // Studyright should not be canceled OR the possible canceldate should be after the enrollmentStartDate
(s.prioritycode === 1 || s.prioritycode === 30) // Studyright should be primary or the student should have graduated
)
if (correctStudyRight) {
enrollments.add({
studentnumber: student.studentnumber,
startdate: e.semester.startdate,
})
}
})
})
return enrollments
}
const startedStudyrights = async (studytrack, since, studentnumbers) =>
(
await Studyright.findAll({
include: [
{
model: StudyrightElement,
required: true,
include: {
model: ElementDetail,
required: true,
where: {
code: studytrack,
},
},
},
{
model: Student,
attributes: ['studentnumber'],
required: true,
},
],
where: {
studystartdate: {
[Op.gte]: since,
},
student_studentnumber: whereStudents(studentnumbers),
},
})
).map(formatStudyright)
const graduatedStudyRights = async (studytrack, since, studentnumbers) =>
await Studyright.findAll({
include: {
model: StudyrightElement,
required: true,
include: {
model: ElementDetail,
required: true,
where: {
code: studytrack,
},
},
},
where: {
graduated: 1,
enddate: {
[Op.gte]: since,
},
student_studentnumber: whereStudents(studentnumbers),
},
})
const cancelledStudyRights = async (studytrack, since) => {
return await Studyright.findAll({
include: {
model: StudyrightElement,
required: true,
where: {
code: {
[Op.eq]: studytrack,
},
},
},
where: {
canceldate: {
[Op.gte]: since,
},
},
})
}
const transfersAway = async (studytrack, since) => {
return await Transfer.findAll({
where: {
transferdate: {
[Op.gte]: since,
},
sourcecode: studytrack,
},
distinct: true,
col: 'studentnumber',
})
}
const transfersTo = async (studytrack, since) => {
return await Transfer.findAll({
where: {
transferdate: {
[Op.gte]: since,
},
targetcode: studytrack,
},
distinct: true,
col: 'studentnumber',
})
}
const getProgrammesStudents = async studyprogramme =>
(
await Studyright.findAll({
attributes: ['studyrightid', 'studystartdate', 'enddate', 'graduated', 'prioritycode', 'extentcode'],
include: [
{
model: StudyrightElement,
required: true,
where: {
code: {
[Op.in]: [studyprogramme],
},
},
},
{
model: Student,
attributes: ['studentnumber'],
required: true,
},
],
})
).map(formatStudyright)
const getCreditsForStudyProgramme = async (provider, since) =>
await Credit.findAll({
attributes: ['id', 'course_code', 'credits', 'attainment_date', 'student_studentnumber'],
include: {
model: Course,
attributes: ['code'],
required: true,
where: {
is_study_module: false,
},
include: {
model: Organization,
required: true,
where: {
code: provider,
},
},
},
where: {
credittypecode: {
[Op.notIn]: [10, 9, 7],
},
isStudyModule: {
[Op.not]: true,
},
attainment_date: {
[Op.gte]: since,
},
},
})
const getTransferredCredits = async (provider, since) =>
await Credit.findAll({
attributes: ['id', 'course_code', 'credits', 'attainment_date', 'credittypecode', 'student_studentnumber'],
include: {
model: Course,
attributes: ['code'],
required: true,
where: {
is_study_module: false,
},
include: {
model: Organization,
required: true,
where: {
code: provider,
},
},
},
where: {
credittypecode: {
[Op.eq]: [9],
},
attainment_date: {
[Op.gte]: since,
},
},
})
const getThesisCredits = async (provider, since, thesisType) =>
await Credit.findAll({
attributes: ['id', 'course_code', 'credits', 'attainment_date', 'student_studentnumber'],
include: {
model: Course,
attributes: ['code'],
required: true,
where: {
course_unit_type: {
[Op.in]: thesisType,
},
},
include: {
model: Organization,
required: true,
where: {
code: provider,
},
},
},
where: {
credittypecode: {
[Op.notIn]: [10, 9, 7],
},
isStudyModule: {
[Op.not]: true,
},
attainment_date: {
[Op.gte]: since,
},
},
})
module.exports = {
studytrackStudents,
enrolledStudyrights,
startedStudyrights,
graduatedStudyRights,
cancelledStudyRights,
transfersAway,
transfersTo,
getProgrammesStudents,
getCreditsForStudyProgramme,
getTransferredCredits,
getThesisCredits,
}
@@ -3,6 +3,154 @@ const { Organization, ProgrammeModule } = require('../models')
const Op = Sequelize.Op
const { dbConnections } = require('../database/connection')
// temp refactoring faculty <-> userservice mappings away from userservice
const facultiesAndProgrammesInUserService = [
['H10', 'MH10_001'],
['H10', 'MH40_011'],
['H10', 'KH10_001'],
['H20', 'KH20_001'],
['H20', 'MH20_001'],
['H20', 'MH20_002'],
['H30', 'KH30_001'],
['H30', 'KH30_002'],
['H30', 'MH30_002'],
['H30', 'MH30_003'],
['H30', 'MH30_004'],
['H30', 'MH30_005'],
['H30', 'MH30_001'],
['H40', 'KH40_001'],
['H40', 'KH40_002'],
['H40', 'KH40_003'],
['H40', 'KH40_004'],
['H40', 'KH40_005'],
['H40', 'KH40_006'],
['H40', 'MH40_001'],
['H40', 'MH40_002'],
['H40', 'MH40_003'],
['H40', 'MH40_004'],
['H40', 'MH40_005'],
['H40', 'MH40_006'],
['H40', 'MH40_007'],
['H40', 'MH40_008'],
['H40', 'MH40_009'],
['H40', 'MH40_010'],
['H40', 'MH40_011'],
['H40', 'MH40_012'],
['H40', 'MH40_013'],
['H40', 'MH40_014'],
['H40', 'MH40_015'],
['H40', 'MH70_001'],
['H40', 'MH70_006'],
['H50', 'KH50_001'],
['H50', 'KH50_002'],
['H50', 'KH50_003'],
['H50', 'KH50_004'],
['H50', 'KH50_005'],
['H50', 'KH50_006'],
['H50', 'KH50_007'],
['H50', 'MH50_001'],
['H50', 'MH50_002'],
['H50', 'MH50_003'],
['H50', 'MH50_004'],
['H50', 'MH50_005'],
['H50', 'MH50_006'],
['H50', 'MH50_007'],
['H50', 'MH50_008'],
['H50', 'MH50_009'],
['H50', 'MH50_010'],
['H50', 'MH50_011'],
['H50', 'MH50_012'],
['H50', 'MH50_013'],
['H55', 'KH55_001'],
['H55', 'MH55_001'],
['H57', 'KH57_001'],
['H57', 'KH57_002'],
['H57', 'KH57_003'],
['H57', 'MH50_002'],
['H57', 'MH50_013'],
['H57', 'MH57_001'],
['H57', 'MH57_002'],
['H57', 'MH57_003'],
['H57', 'MH57_004'],
['H57', 'MH57_005'],
['H57', 'MH80_007'],
['H60', 'KH60_001'],
['H60', 'MH60_001'],
['H70', 'KH40_001'],
['H70', 'KH50_001'],
['H70', 'KH70_001'],
['H70', 'KH70_002'],
['H70', 'KH70_003'],
['H70', 'KH70_004'],
['H70', 'MH50_001'],
['H70', 'MH57_005'],
['H70', 'MH70_001'],
['H70', 'MH70_002'],
['H70', 'MH70_003'],
['H70', 'MH70_004'],
['H70', 'MH70_005'],
['H70', 'MH70_006'],
['H70', 'MH70_007'],
['H70', 'MH70_008'],
['H70', 'MH70_009'],
['H70', 'MH70_010'],
['H74', 'KH74_001'],
['H80', 'KH57_002'],
['H80', 'KH80_001'],
['H80', 'KH80_002'],
['H80', 'KH80_003'],
['H80', 'KH80_004'],
['H80', 'MH57_002'],
['H80', 'MH57_003'],
['H80', 'MH57_005'],
['H80', 'MH80_001'],
['H80', 'MH80_002'],
['H80', 'MH80_003'],
['H80', 'MH80_004'],
['H80', 'MH80_005'],
['H80', 'MH80_006'],
['H80', 'MH80_007'],
['H90', 'KH90_001'],
['H30', '447000'],
['H30', '447001'],
['H30', '447002'],
['H60', 'EDUK730'],
['H60', 'K-MUUT-ERIL'],
['H60', 'ED400'],
['H60', 'ED200'],
['H60', 'ED700'],
['H60', 'ED100'],
['H60', 'ED300'],
['H60', 'ED600'],
['H60', '8009'],
['H60', '8118'],
['H60', '8600'],
['H60', 'DUK-PED'],
['H60', '65'],
['H60', '1102'],
['H60', '20018'],
['H60', '8601'],
['H60', '9290'],
['H60', '8172'],
['H60', '83600'],
['H60', 'DUM-PED'],
['H60', 'ED500'],
['H60', '83601'],
['H60', '0130'],
['H60', '20005-e'],
['H60', '1040'],
['H60', '8703'],
['H60', '0005'],
['H60', '7900'],
['H60', '0152 '],
['H90', 'MH90_001'],
]
const facultiesAndProgrammesForTrends = facultiesAndProgrammesInUserService.map(f => ({
faculty_code: f[0],
programme_code: f[1],
}))
// Have facultyfetching to work like it worked during oodi-db time
const facultiesInOodi = [
'H10',
@@ -66,4 +214,5 @@ module.exports = {
degreeProgrammeCodesOfFaculty,
isFaculty,
providersOfFaculty,
facultiesAndProgrammesForTrends,
}
+29 -17
View File
@@ -1,6 +1,7 @@
const { Op } = require('sequelize')
const moment = require('moment')
const { sortBy } = require('lodash')
const { sortMainCode } = require('../util/utils')
const {
Student,
@@ -19,7 +20,6 @@ const {
dbConnections: { sequelize },
} = require('../database/connection')
const { Tag, TagStudent } = require('../models/models_kone')
const { /*allCodeAltenatives, */ findOneByCode } = require('./courses')
const { CourseStatsCounter } = require('./course_stats_counter')
const { getPassingSemester, semesterEnd, semesterStart } = require('../util/semester')
const { getAllProgrammes } = require('./studyrights')
@@ -65,7 +65,7 @@ const formatStudentForPopulationStatistics = (
startDateMoment,
endDateMoment
) => {
const toCourse = ({ grade, attainment_date, credits, course_code, credittypecode, isStudyModule }) => {
const toCourse = ({ grade, attainment_date, credits, course_code, credittypecode, isStudyModule, language }) => {
const attainment_date_normailized =
attainment_date < startDate ? startDateMoment.clone().add(1, 'day').toISOString() : attainment_date
const passed = Credit.passed({ credittypecode })
@@ -78,6 +78,7 @@ const formatStudentForPopulationStatistics = (
credits,
isStudyModuleCredit: isStudyModule,
credittypecode,
language,
}
}
@@ -265,6 +266,7 @@ const getStudentsIncludeCoursesBetween = async (studentnumbers, startDate, endDa
'isStudyModule',
'student_studentnumber',
'course_code',
'language',
],
where: creditsOfStudent,
raw: true,
@@ -494,6 +496,7 @@ const studentnumbersWithAllStudyrightElements = async (
if (!newestStudytrack) return false
return studyRights.includes(newestStudytrack.code)
})
const studentnumberlist = filteredStudentnumbers.length > 0 ? filteredStudentnumbers : studentnumbers
// fetch students that have transferred out of the programme and filter out these studentnumbers
@@ -816,10 +819,6 @@ const findCourses = async (studentnumbers, beforeDate) => {
},
},
},
/* {
attributes: ['discipline_id', 'name'],
model: Discipline
}, */
{
attributes: ['coursetypecode', 'name'],
model: CourseType,
@@ -842,12 +841,24 @@ const parseCreditInfo = credit => ({
date: credit.attainment_date,
})
const isOpenUniCourseCode = code => code.match(/^AY?(.+?)(?:en|fi|sv)?$/)
const findMainCourse = async code => {
const course = await Course.findOne({
where: { code: code },
})
const unifyOpenUniversity = code => {
const regexresult = isOpenUniCourseCode(code)
if (!regexresult) return code
return regexresult[1]
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) => {
@@ -918,6 +929,7 @@ const bottlenecksOf = async (query, studentnumberlist) => {
transferredStudents,
tag
))
const allstudents = studentnumbers.reduce((numbers, num) => {
numbers[num] = true
return numbers
@@ -954,14 +966,14 @@ const bottlenecksOf = async (query, studentnumberlist) => {
for (const course of courses) {
let { course_type } = course
let maincourse = course
if (isOpenUniCourseCode) {
const sortedMaincourse = await findMainCourse(course.code)
if (sortedMaincourse) maincourse = sortedMaincourse
/* if (isOpenUniCourseCode) {
const nonOpen = unifyOpenUniversity(course.code)
const response = await findOneByCode(nonOpen)
*/
if (response) {
maincourse = response
}
}
if (!stats[maincourse.code]) {
stats[maincourse.code] = new CourseStatsCounter(maincourse.code, maincourse.name, allstudentslength)
}
@@ -982,7 +994,7 @@ const bottlenecksOf = async (query, studentnumberlist) => {
bottlenecks.coursestatistics = Object.values(stats).map(coursestatistics => coursestatistics.getFinalStats())
bottlenecks.allStudents = allstudentslength
console.log('bottleneck: ', bottlenecks)
return bottlenecks
}

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