Compare commits

...
829 Commits
Author SHA1 Message Date
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
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
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
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
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
Otto Ahoniemi ffa2fa1076 Bugfixes 2021-09-09 11:12:07 +03:00
Otto Ahoniemi d79037a2f9 Add studyguidance groups to navbar 2021-09-09 08:57:21 +03:00
Otto Ahoniemi d51ad67e4e Add studyguidance group access check to login phase 2021-09-09 08:52:28 +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
Olli Keski-Hynnilä c206b94455 remove showByStudents stuff from master to not confuse users 2021-09-07 15:30:29 +03:00
Petri Palmu 01b15c3dc1 lint fix 2021-09-07 09:37:12 +03:00
Petri Palmu 7b02c64e92 population statistics students courses shows right 2021-09-07 09:21:49 +03:00
Petri Palmu f694b6b77f mandatory passed has also attainments from old courses 2021-09-06 10:37:43 +03:00
Petri Palmu 2db43e0c87 Merge branch 'master' of github.com:UniversityOfHelsinkiCS/oodikone 2021-09-06 10:12:59 +03:00
Petri Palmu 6a419dc5a7 added sunstitutions to the course stats 2021-09-06 10:12:42 +03:00
Otto Ahoniemi 8c4a08eb97 Fix updater failing when studyright cancellation date not available 2021-09-02 18:35:00 +03:00
Otto Ahoniemi 2a095a7c18 usage and coursegroups byebye 2021-09-02 17:39:13 +03:00
Otto Ahoniemi 089cbc5cae Use correct attribute when filtering 2021-09-02 17:35:22 +03:00
Otto Ahoniemi 53166a1771 Catch programmes of faculty when fetching faculty teacher stats 2021-09-02 17:24:51 +03:00
Otto Ahoniemi 9cea69618d Take org to progmodule only if available 2021-09-02 16:47:05 +03:00
Otto Ahoniemi 69df95200c Add parent_ids to sis-db orgs 2021-09-02 16:46:34 +03:00
Otto Ahoniemi 9c60e78349 Add responsible organizations to degreeprogramme 2021-09-02 15:44:27 +03:00
Otto Ahoniemi ecedf3e73a Link programme modules to organizations 2021-09-02 15:23:05 +03:00
Otto Ahoniemi 9e5da80e4a Stop console.logging headers 2021-09-02 14:36:09 +03:00
Otto Ahoniemi 825940e226 Filter and enrich studyrights in one run to avoid accidental reads for nonexisting properties 2021-09-02 14:12:44 +03:00
Otto Ahoniemi 0c2a378f2c Filter non-degree education studyrights from students studyrights 2021-09-02 13:42:34 +03:00
Otto Ahoniemi a5b65f8648 Add teachers rights to one group 2021-09-02 11:43:47 +03:00
Otto Ahoniemi 038876401f Fix missing elementdetails breaking userpage 2021-09-02 11:21:22 +03:00
Otto Ahoniemi 1b40319f4a enrich cypress users with sis id 2021-08-30 11:11:46 +03:00
Otto Ahoniemi 91f847d72e Don't show filtered students in courses of population 2021-08-30 11:02:47 +03:00
Otto Ahoniemi 1f5908dd86 temp console log shibbo headers 2021-08-30 10:30:17 +03:00
Otto Ahoniemi c30a404074 Remove some unnecessary console.log 2021-08-30 10:22:29 +03:00
Otto Ahoniemi d0ace9a676 Also add studyguidance groups to userservice 2021-08-30 09:42:37 +03:00
Otto Ahoniemi 056a86e9fd Return accidentally removed elementdetail routes 2021-08-30 09:12:36 +03:00
Otto Ahoniemi 1e31be2e57 The return of the concurrently 2021-08-30 08:59:18 +03:00
Otto Ahoniemi 97660f5133 Now acually fix lint?? 2021-08-30 08:49:51 +03:00
Otto Ahoniemi fc445f44ed Add env vars to .env file 2021-08-30 08:39:21 +03:00
Otto Ahoniemi f8c050bf31 Add initial view for studyguidancegroups 2021-08-30 08:35:58 +03:00
Otto Ahoniemi 29be24ad76 study guidance groups to backend 2021-08-29 22:15:17 +03:00
Otto Ahoniemi 54a5937af2 acual fix linting 2021-08-29 15:53:42 +03:00
Otto Ahoniemi fc002c91e6 Fix lint + run lint 2021-08-29 14:28:25 +03:00
Otto Ahoniemi 92232e661a Enrich user data with sisu person id 2021-08-29 13:48:20 +03:00
Otto Ahoniemi 40daa44da9 Replace body-parser.json with express.json
body-parser now included in express, see:
https://expressjs.com/en/changelog/4x.html#4.16.0
2021-08-29 13:48:20 +03:00
Otto Ahoniemi 90ca7dc3ff Remove bunch of unused utils from backend 2021-08-29 13:48:20 +03:00
Otto Ahoniemi 5b7f3d7048 Make env var mayhem cleaner 2021-08-29 13:48:20 +03:00
Otto Ahoniemi 809eb3c26f Package updates 2021-08-29 13:48:20 +03:00
Otto Ahoniemi 435a288d59 lint-staged betterments 2021-08-29 13:48:20 +03:00
Otto Ahoniemi 7799f1b907 Update dockerfile copy path 2021-08-29 13:48:20 +03:00
Otto Ahoniemi cc9d7ff5ae Docker:down betterments 2021-08-29 13:48:20 +03:00
Petri Palmu 1d49b1d4d8 little correction to the tests 2021-08-27 14:26:09 +03:00
Petri Palmu 56934a168a removed console.logs 2021-08-27 13:22:45 +03:00
Petri Palmu 2c847a9561 Merge branch 'master' of github.com:UniversityOfHelsinkiCS/oodikone 2021-08-27 08:36:53 +03:00
Petri Palmu 68df931f8a Found population statistics missing people 2021-08-26 15:16:02 +03:00
Olli Keski-Hynnilä 1fcf635fd0 fix for enrollment status 2021-08-26 10:57:49 +03:00
Olli Keski-Hynnilä bcc1b75e1e Merge branch 'master' of github.com:UniversityOfHelsinkiCS/oodikone 2021-08-26 09:29:51 +03:00
Otto Ahoniemi c92f42b684 Get some new shibbo headers 2021-08-25 15:54:03 +03:00
Otto Ahoniemi 0b70388896 Remove nodemailer 2021-08-25 13:25:54 +03:00
Otto Ahoniemi b5775944ca Migrate rest of the email stuff to pate 2021-08-25 13:01:03 +03:00
Otto Ahoniemi 8ee6415a46 Migrate login to use pate 2021-08-25 10:13:04 +03:00
Otto Ahoniemi 758f47c94f Migrate feedbacks to use pate 2021-08-25 10:04:22 +03:00
Otto Ahoniemi 901293f9c5 Console log so it's easier to check when stat + trends refresh is ready 2021-08-24 16:09:18 +03:00
Otto Ahoniemi 61e4dc0e1b Return thesis routes to backend 2021-08-24 16:05:47 +03:00
Otto Ahoniemi e98cef18a9 Add admission types to students table 2021-08-24 14:36:19 +03:00
Otto Ahoniemi 019618d10c Add filter for students without admission types 2021-08-24 14:36:05 +03:00
Otto Ahoniemi 256a4032cb Acually release staging images 2021-08-24 13:22:55 +03:00
Otto Ahoniemi 84727fdfb5 Remove some unused packages from backend 2021-08-24 13:18:31 +03:00
Otto Ahoniemi 1761662dad Upgrade backend packages 2021-08-24 11:06:24 +03:00
Otto Ahoniemi bf27683b9a Upgrade userservice packages 2021-08-24 10:41:45 +03:00
Otto Ahoniemi f050325d39 Merge branch 'master' of github.com:UniversityOfHelsinkiCS/oodikone 2021-08-24 10:31:13 +03:00
Otto Ahoniemi 968cb1c543 Fix patching non grad students 2021-08-24 10:30:04 +03:00
Olli Keski-Hynnilä 70011b9969 added some preliminary settings for showByStudents 2021-08-24 10:07:04 +03:00
Otto AhoniemiandGitHub 4f6755c670 Merge pull request #3174 from UniversityOfHelsinkiCS/dependabot/npm_and_yarn/services/backend/path-parse-1.0.7
Bump path-parse from 1.0.6 to 1.0.7 in /services/backend
2021-08-24 09:09:26 +03:00
dependabot[bot]andGitHub bcbcab0863 Bump path-parse from 1.0.6 to 1.0.7 in /services/backend
Bumps [path-parse](https://github.com/jbgutierrez/path-parse) from 1.0.6 to 1.0.7.
- [Release notes](https://github.com/jbgutierrez/path-parse/releases)
- [Commits](https://github.com/jbgutierrez/path-parse/commits/v1.0.7)

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

Signed-off-by: dependabot[bot] <support@github.com>
2021-08-24 06:05:03 +00:00
Otto Ahoniemi a28eed070b Add sentry userservice stuff to workflows etc 2021-08-24 09:04:00 +03:00
Otto Ahoniemi bcd8dce49f Add sentry env vars to docker 2021-08-24 09:01:30 +03:00
Otto Ahoniemi 9316e9bcb2 typo fix 2021-08-24 08:59:04 +03:00
Otto Ahoniemi 2a78119a05 Add sentry setup to userservice 2021-08-24 08:56:32 +03:00
Otto Ahoniemi 28021c6cf5 Add sentry packages to userservice 2021-08-24 08:50:12 +03:00
Otto Ahoniemi bb0eee8a60 Refresh sentry setup on backend + some cleanup 2021-08-24 08:33:46 +03:00
Otto Ahoniemi 93ceb0940e Add moar sentry goodies to frontend 2021-08-23 19:33:19 +03:00
Otto Ahoniemi 3116e084e0 Conf cleanups 2021-08-23 19:30:52 +03:00
Otto Ahoniemi 7fd32f72b0 Acualest disable sentry when running in CI / cypress 2021-08-23 19:23:09 +03:00
Otto Ahoniemi 8e23d65a29 Add init sentry func to backend 2021-08-23 19:14:04 +03:00
Otto Ahoniemi d8107124c6 Add moar kasvis special programs to userservice 2021-08-23 14:30:36 +03:00
Otto Ahoniemi 1ef0ef990c Merge branch 'master' of github.com:UniversityOfHelsinkiCS/oodikone 2021-08-23 13:44:15 +03:00
Otto Ahoniemi 29a0a31a5b Acually disable sentry in CI 2021-08-23 13:43:35 +03:00
Petri Palmu 83178d005c Merge branch 'master' of github.com:UniversityOfHelsinkiCS/oodikone 2021-08-23 10:35:03 +03:00
Petri Palmu 3d6a395711 population of course grades is newest date grade 2021-08-23 10:34:43 +03:00
Otto Ahoniemi c9ec9764d1 Remove programme modules and children from kone-db 2021-08-23 10:20:47 +03:00
Otto Ahoniemi 1e3fe93dcd Don't report sentry stuff on CI 2021-08-23 09:48:02 +03:00
Otto Ahoniemi eb3a579e3b Merge branch 'master' of github.com:UniversityOfHelsinkiCS/oodikone 2021-08-23 08:32:17 +03:00
Otto Ahoniemi 1e1a038af9 Upgrade packages 2021-08-23 08:31:39 +03:00
Olli Keski-Hynnilä fe2b7a6686 sort table by descending and filter unwanted semesters 2021-08-20 09:31:14 +03:00
Olli Keski-Hynnilä 1a0c27cad6 changed enrollment rows to semester table 2021-08-19 15:47:53 +03:00
Petri Palmu 9d509ac54a Merge branch 'master' of github.com:UniversityOfHelsinkiCS/oodikone 2021-08-19 15:27:18 +03:00
Olli Keski-Hynnilä d298715397 added enrollment info to sutdent info page 2021-08-19 15:13:13 +03:00
Otto Ahoniemi 2717cfc83a Add study levels to programme modules 2021-08-19 13:26:35 +03:00
Petri Palmu 039ba5f5b7 Merge branch 'master' of github.com:UniversityOfHelsinkiCS/oodikone 2021-08-19 10:37:36 +03:00
Petri Palmu 6c2108ad7f Merge branch 'trunk' of github.com:UniversityOfHelsinkiCS/oodikone into trunk 2021-08-19 08:33:49 +03:00
Petri Palmu dc3516b67a fix to custom population course filter not showing results 2021-08-19 08:33:30 +03:00
Otto Ahoniemi 0160532410 Merge branch 'trunk' 2021-08-18 14:02:26 +03:00
Otto Ahoniemi 540c8b7295 Merge branch 'trunk' of github.com:UniversityOfHelsinkiCS/oodikone into trunk 2021-08-18 14:00:56 +03:00
Otto Ahoniemi d5ba59c199 Run all workflows only on master 2021-08-18 14:00:45 +03:00
Otto Ahoniemi 13f828fc73 No need for staging workflow 2021-08-18 13:58:18 +03:00
Otto Ahoniemi cb5fb9c8ac Deploy to staging after succesful test run 2021-08-18 13:57:56 +03:00
Otto Ahoniemi f457c9e61c Update docker-compose ci to use staging images 2021-08-18 13:56:07 +03:00
Olli Keski-Hynnilä f9dc2d91aa increased memory size on sis-db container 2021-08-18 13:37:12 +03:00
Olli Keski-Hynnilä fcb50dcd25 fixed same issue but with attainments 2021-08-18 13:17:30 +03:00
Olli Keski-Hynnilä 551566faa3 fixed year range for trends 2021-08-18 13:09:41 +03:00
Otto AhoniemiandGitHub 585d2c71ca Merge pull request #3157 from UniversityOfHelsinkiCS/trunk
Add some new special programmes for users with faculty rights
2021-08-18 12:48:06 +03:00
Otto Ahoniemi 732752b1e7 Add some new special programmes for users with faculty rights 2021-08-18 12:37:27 +03:00
Saara SatokangasandGitHub 35377d004b Merge pull request #3156 from UniversityOfHelsinkiCS/trunk
Remove degrees from Oodikone & improve clarity of Studyprogrammes Overview
2021-08-18 10:50:13 +03:00
Saara Satokangas fb81abe3fc Update tests to match the changes in studyprogrammes 2021-08-18 10:12:29 +03:00
Saara Satokangas 49dc5b9a9e Clarify "started" tooltip 2021-08-18 09:41:54 +03:00
Saara Satokangas 016ec4f025 Clarify "transferred from students" 2021-08-18 09:41:42 +03:00
Saara Satokangas c22dc947b9 Add infobox for yearly productivity 2021-08-18 09:41:33 +03:00
Saara Satokangas 7876835ae5 Make info-tooltips visible for other users than admins as well 2021-08-18 09:41:23 +03:00
Saara Satokangas 9a202ddb86 Add mention about the population by class 2021-08-18 09:41:10 +03:00
Saara Satokangas 73af973c6c Remove "transferred from" students && include all students to progress
Removed "transferred from" students as a separate column from population
progress. It was only showing for admins anyways. This commit removes
the code from backend as well.

Also includes all the studyrights to population progress as it already
was so in the total
2021-08-18 09:40:58 +03:00
Saara Satokangas c271ca0953 Clarify the Studyprogrammes Overview-table 2021-08-18 09:40:27 +03:00
Saara Satokangas ea38bbd194 Fix missing infobox-texts 2021-08-18 09:40:16 +03:00
Saara Satokangas e9a47c6658 Remove "Transferred from", from the total view 2021-08-18 09:40:06 +03:00
Otto Ahoniemi 509872bd60 Remove rest of the degree related stuff from frontend 2021-08-17 16:56:13 +03:00
Otto Ahoniemi 7ff8a9e864 Remove rest of the degree related stuff from updater 2021-08-17 16:40:46 +03:00
Otto Ahoniemi db2d1c7da6 Remove unused backend scripts 2021-08-17 16:34:03 +03:00
Otto Ahoniemi 9c415f234e Remove degrees from frontend 2021-08-17 16:33:24 +03:00
Otto Ahoniemi 750e35a56b Remove degrees from backend 2021-08-17 16:18:11 +03:00
Otto Ahoniemi d41ada15e6 Remove oodi-db link from adminer settings 2021-08-17 15:22:11 +03:00
Otto AhoniemiandGitHub 96e3c69a48 Merge pull request #3153 from UniversityOfHelsinkiCS/trunk
Some kone-db & user-db cleanups
2021-08-17 14:58:43 +03:00
Otto Ahoniemi d47d2ad154 Drop check for user asking for old oodi data 2021-08-17 14:47:13 +03:00
Otto Ahoniemi d465962d63 Remove oodi-db link from adminerurls 2021-08-17 14:46:41 +03:00
Otto Ahoniemi 2fdd7731a4 Remove user_unit from user-db 2021-08-17 14:44:59 +03:00
Otto Ahoniemi 20b28917b1 Remove unneeded migration model 2021-08-17 14:39:59 +03:00
Otto Ahoniemi bc66dca2c2 Remove course duplicates from kone 2021-08-17 14:36:49 +03:00
Otto Ahoniemi 31f02eba7a Remove migration model, not needed 2021-08-17 14:20:37 +03:00
Otto Ahoniemi e21e180160 Remove unit tags from kone 2021-08-17 14:18:19 +03:00
Otto Ahoniemi c2c938b7c2 Remove usagestats 2021-08-17 13:59:52 +03:00
Otto Ahoniemi 5f526292e0 Remove kone-db filters from backend 2021-08-17 13:58:25 +03:00
Otto AhoniemiandGitHub d4ecd45608 Merge pull request #3150 from UniversityOfHelsinkiCS/trunk
Remove oodi from oodikone + tiny fixes
2021-08-17 13:41:44 +03:00
Otto Ahoniemi 3ecda1bcae Merge branch 'remove-oodi' into trunk 2021-08-17 13:26:49 +03:00
Otto Ahoniemi 60e27d8049 Remove sending enable-sismode headers 2021-08-17 13:26:06 +03:00
Otto Ahoniemi e35ce688de Remove oodi-sis localstorage flags, switcher and doo-oodi related testing components 2021-08-17 13:24:17 +03:00
Otto Ahoniemi 04bbbac04f remove old updater from backend 2021-08-17 11:40:14 +03:00
Otto Ahoniemi e53d24de7c Cleanup backend conf from oodi stuff 2021-08-17 11:39:20 +03:00
Otto Ahoniemi 28e6ae91f4 Moar dco removes 2021-08-17 11:36:11 +03:00
Otto Ahoniemi 9afee2b4a5 Remove old updater 2021-08-17 11:34:51 +03:00
Otto Ahoniemi 3974dcc8e2 Remove oodi db setup from cli 2021-08-17 11:34:38 +03:00
Otto Ahoniemi 1370283972 Remove oodi db from docker-compose 2021-08-17 11:33:01 +03:00
Otto Ahoniemi cabdb699fb Merge modelsv2 to models folder 2021-08-17 11:29:13 +03:00
Otto Ahoniemi 97fb30167e Remove oodi db connections and combine databasev1&databasev2 folders 2021-08-17 11:22:52 +03:00
Olli Keski-Hynnilä 8fbd355dcf lifted legend to the top of chart so it doesnt obscure stuff 2021-08-17 11:10:08 +03:00
Otto Ahoniemi 7b35759cad remove sis router 2021-08-17 11:06:57 +03:00
Otto Ahoniemi 8399ee1d93 Merge old and new services and remove everything that relied on old service implementations 2021-08-17 11:05:05 +03:00
Otto Ahoniemi ca072ea6f0 Replace old oodi routes with their upgraded versions 2021-08-17 10:44:39 +03:00
Otto Ahoniemi 809a0c5ab7 Cleanup cool data science, oodi, tasks 2021-08-17 10:37:57 +03:00
Olli Keski-Hynnilä c3a0a878b2 Merge branch 'master' into trunk 2021-08-17 10:23:38 +03:00
Otto Ahoniemi d7938a7cc2 Update users and teachers to use services v2 as default 2021-08-17 09:52:31 +03:00
Otto Ahoniemi 974a1d53b3 Goodbye backend diffs 2021-08-17 09:37:35 +03:00
Otto Ahoniemi 74ba640a91 Remove oodisis course diff stuff from frontend 2021-08-17 09:36:00 +03:00
Otto Ahoniemi c3e1e10a83 Remove old semesters oodikone code 2021-08-17 09:31:23 +03:00
Otto Ahoniemi 50613d1373 Use v2 service for lang 2021-08-17 09:27:19 +03:00
Otto Ahoniemi 30e67f3be6 Remove old oodikone route stuff from populations 2021-08-17 09:24:38 +03:00
Petri Palmu de229c420e Merge branch 'trunk' of github.com:UniversityOfHelsinkiCS/oodikone into trunk 2021-08-17 08:05:21 +03:00
Otto Ahoniemi c81e0681b3 Remove old oodikone route stuff from students 2021-08-16 15:23:32 +03:00
Otto Ahoniemi 2df1120be2 Remove old course code mapper & related stuff from front 2021-08-16 15:18:57 +03:00
Otto Ahoniemi 682aa16a45 Remove oodi courses & coursesmulti routes 2021-08-16 15:07:42 +03:00
Otto Ahoniemi 172a857c4f Use v2 routes for courses and coursesmulti 2021-08-16 15:06:44 +03:00
Otto Ahoniemi 5801faab35 Cleanup old oodi stuff from elementdetail routes 2021-08-16 14:26:21 +03:00
Otto Ahoniemi a38f9663a0 Changes routes / services to use to use userServiceV2 2021-08-16 14:17:54 +03:00
Olli Keski-Hynnilä 56528b6fec add tags-filter for population statistics 2021-08-16 13:40:34 +03:00
Otto AhoniemiandGitHub 1fb642fe7e Merge pull request #3147 from UniversityOfHelsinkiCS/trunk
Cleanup course groups from oodikone
2021-08-16 13:29:34 +03:00
Otto Ahoniemi cd855dfc32 Cleanup course groups from kone 2021-08-16 13:17:14 +03:00
Otto Ahoniemi f5200906d3 Remove course group routes and services 2021-08-16 12:51:00 +03:00
Otto Ahoniemi 57fd9a5434 Cleanup unused stuff from conf 2021-08-16 12:44:51 +03:00
Otto Ahoniemi 1311999637 Remove course group routes 2021-08-16 12:42:33 +03:00
Otto Ahoniemi c3411112b2 Remove course groups components from frontend 2021-08-16 12:39:41 +03:00
Otto Ahoniemi 52e9d5dc51 Remove course groups from study programmes 2021-08-16 12:39:14 +03:00
Otto Ahoniemi 5347670a0a Update transfer instead of adding if studyrightid already present in transfer table 2021-08-16 10:46:12 +03:00
Otto Ahoniemi 133c65d147 Remove faculty codes from user access groups 2021-08-16 10:25:43 +03:00
Petri Palmu be94bbad7e course statistics not pass accept both Hyl. and HYL as values 2021-08-16 10:15:55 +03:00
Otto AhoniemiandGitHub 5cb8601cc9 Merge pull request #3145 from UniversityOfHelsinkiCS/trunk
Goodbye faculties
2021-08-16 10:09:39 +03:00
Otto Ahoniemi 74b16f7e50 Remove faculty stuff from new analyticsservice 2021-08-16 09:31:25 +03:00
Otto Ahoniemi ec2b1fe6a8 Remove old faculty stuff from events and services 2021-08-16 09:30:22 +03:00
Otto Ahoniemi a8ec3c2257 Change faculty fetching to work with sis-db 2021-08-16 09:26:51 +03:00
Otto Ahoniemi d9b4aa58f1 Remove unused facultystats route 2021-08-16 09:07:34 +03:00
Otto Ahoniemi f29c33cd60 Remove old providers logic 2021-08-16 09:02:05 +03:00
Otto Ahoniemi fccd0d7769 Cleanup unused stuff from faculties reducer 2021-08-14 17:38:04 +03:00
Otto Ahoniemi 5e59956485 Remove faculty component 2021-08-14 17:27:02 +03:00
Otto Ahoniemi 5c3fe43473 Remove faculties from frontend visible parts 2021-08-14 17:13:18 +03:00
Otto Ahoniemi 3fbcbbf0d2 Merge branch 'master' into trunk 2021-08-14 17:05:01 +03:00
Otto AhoniemiandGitHub 3d8d6e7a00 Merge pull request #3144 from UniversityOfHelsinkiCS/trunk
Fix provider stuff in teachers and add regular trendsv2 updates
2021-08-13 15:49:06 +03:00
Otto Ahoniemi b040bac675 Cleanup references to V2, since old oodi V1 versions are not used in events 2021-08-13 15:38:48 +03:00
Otto Ahoniemi c1689253ea Remove old oodikone cool data science stuff from events 2021-08-13 15:34:17 +03:00
Otto Ahoniemi 110abe4baf Skip courses test in popstats for now 2021-08-13 15:32:06 +03:00
Otto Ahoniemi 9ac9ed298c Don't intercept, just wait a lot with courses 2021-08-13 15:17:51 +03:00
Otto Ahoniemi 261b3f1384 Add trendsv2 refreshing to events.js 2021-08-13 15:16:38 +03:00
Otto Ahoniemi 515e3ea4c1 Fix protoc update failing when values not available 2021-08-13 15:15:43 +03:00
Otto Ahoniemi 3e81490f37 Moar waitingggg 2021-08-13 14:40:00 +03:00
Otto Ahoniemi 5b53233dc3 Remove extra only from filters 2021-08-13 14:39:12 +03:00
Otto Ahoniemi 2b04de96d2 Fix missing key & value for provider dropdown 2021-08-13 14:27:28 +03:00
Otto Ahoniemi abd13df96d Fix parsing provider in teacher stats 2021-08-13 10:29:28 +03:00
Otto Ahoniemi 4e28efd608 Use organisation v2 models to return correct providers 2021-08-12 09:53:13 +03:00
Otto Ahoniemi 63e72895f6 Merge branch 'trunk' of github.com:UniversityOfHelsinkiCS/oodikone into trunk 2021-08-12 09:00:27 +03:00
Petri PalmuandGitHub 74baf4ea47 Merge pull request #3140 from UniversityOfHelsinkiCS/trunk
Trunk
2021-08-12 08:48:31 +03:00
Petri Palmu 1563566e62 i gave more wait time for cypress 2021-08-11 15:56:14 +03:00
Petri Palmu 921b6a0e07 Merge branch 'trunk' of github.com:UniversityOfHelsinkiCS/oodikone into trunk 2021-08-11 14:13:28 +03:00
Petri Palmu 3717bed980 few new tests for course statistics 2021-08-11 14:13:19 +03:00
Otto AhoniemiandGitHub b646d540fe Merge pull request #3139 from UniversityOfHelsinkiCS/trunk
Correctly set sentry release in github action step
2021-08-11 11:27:54 +03:00
Otto AhoniemiandGitHub e5e246d826 Merge branch 'master' into trunk 2021-08-11 11:27:21 +03:00
Otto Ahoniemi 286b26d484 Remove old ooodikone stuff package.jsons to avoid dependabot spam 2021-08-11 11:26:23 +03:00
Otto Ahoniemi f84b05d804 Correctly set sentry release in github action step 2021-08-11 11:23:31 +03:00
Otto AhoniemiandGitHub b30124dc2e Merge pull request #3136 from UniversityOfHelsinkiCS/trunk
acually fix setting env vars in frontend
2021-08-11 11:09:55 +03:00
Otto Ahoniemi ac1f7039a4 Move all envs to start of docker build phase 2021-08-11 10:54:19 +03:00
Otto Ahoniemi 18943759a1 Remove unused env vars 2021-08-11 10:50:22 +03:00
Otto Ahoniemi 8c388119f3 Prevent linting failing because of file passed as an argument
If linting command is ran through npm scripts, linting just works.
See: https://github.com/okonet/lint-staged/issues/174
2021-08-11 10:49:30 +03:00
Otto Ahoniemi c486bbbbcf Ignore passed docker-compose file in lint-staged
Lint-staged file that is being linted as an argument, so our linting
command (`docker-compose config -q`) thinks argument is a docker-compose
service. This commit will ignore all arguments passed to command.

See:
https://github.com/okonet/lint-staged/issues/174#issuecomment-437468837
2021-08-11 10:46:13 +03:00
Otto Ahoniemi 76b28928a7 Remove console.logs used for debugging 2021-08-11 10:29:44 +03:00
Otto AhoniemiandGitHub 4389af86f5 Merge pull request #3134 from UniversityOfHelsinkiCS/trunk
env var mayyhem
2021-08-11 10:08:25 +03:00
Otto Ahoniemi e776c29aa4 Set env variables in frontend before building 2021-08-11 10:07:38 +03:00
Otto AhoniemiandGitHub 511f262d8b Merge pull request #3133 from UniversityOfHelsinkiCS/trunk
moar debugging
2021-08-11 09:54:57 +03:00
Otto Ahoniemi 9c48fe888b Merge branch 'master' into trunk 2021-08-11 09:53:40 +03:00
Otto Ahoniemi bd54d4f4e1 Log whole process env for staging debug 2021-08-11 09:52:28 +03:00
Otto AhoniemiandGitHub 2daa1c674d Merge pull request #3132 from UniversityOfHelsinkiCS/trunk
Debug in staging
2021-08-11 09:27:26 +03:00
Otto AhoniemiandGitHub d544220436 Merge branch 'master' into trunk 2021-08-11 09:26:55 +03:00
Otto Ahoniemi fcd0ad23cc Add some console.logs for staging debug 2021-08-11 09:26:10 +03:00
Petri Palmu 1e94accc55 Merge branch 'feature/org' into trunk 2021-08-11 08:50:46 +03:00
Petri Palmu 326da589de changed substitutions 2021-08-11 08:05:40 +03:00
Otto Ahoniemi 804a7f3f63 Acually take snapshot date time for first snapshot 2021-08-10 14:39:50 +03:00
Otto Ahoniemi 178d0d921c Add kludge to find correct snapshot date time timestamp 2021-08-10 13:34:56 +03:00
Otto Ahoniemi 1cefe9e410 Merge branch 'trunk' of github.com:UniversityOfHelsinkiCS/oodikone into trunk 2021-08-10 11:39:48 +03:00
Otto Ahoniemi b1cfc82aa2 Filter out snapshots with missing snapshot date time when trying to get first date time 2021-08-10 11:38:58 +03:00
Otto Ahoniemi a8b76c7253 Enrich missing accepted selection paths to snapshots 2021-08-10 11:38:28 +03:00
Otto AhoniemiandGitHub c6c2d6f314 Merge pull request #3131 from UniversityOfHelsinkiCS/trunk
Fix setting current time in actions
2021-08-10 10:54:57 +03:00
Otto AhoniemiandGitHub 15142e3750 Merge branch 'master' into trunk 2021-08-10 10:53:33 +03:00
Otto Ahoniemi 45b1d4cb7a Fix setting current time in actions 2021-08-10 10:52:44 +03:00
Otto AhoniemiandGitHub 3a7575b052 Merge pull request #3130 from UniversityOfHelsinkiCS/trunk
Add correct built at time + test fixes
2021-08-10 10:42:50 +03:00
Otto Ahoniemi f180480b5c Merge branch 'master' into trunk 2021-08-10 10:33:41 +03:00
Otto Ahoniemi 997a6e36ef Add correct built at time to oodikone frontpage 2021-08-10 10:23:39 +03:00
Otto Ahoniemi 0cbbaea27b Hide SIS warning flag to avoid screen size problems when running headlessly 2021-08-10 09:46:43 +03:00
Otto Ahoniemi 748e8b31ae Just get tag name instead of looking containing element first 2021-08-10 09:46:21 +03:00
Otto Ahoniemi 74b80e75df Merge branch 'trunk' of github.com:UniversityOfHelsinkiCS/oodikone into trunk 2021-08-09 20:51:46 +03:00
Otto Ahoniemi 8039bd369b Change viewports to defaults to speed up runtime when running headlessly
See: https://github.com/cypress-io/cypress/issues/15990
2021-08-09 20:51:18 +03:00
Matti Luukkainen 2312a8d662 Merge branch 'trunk' of github.com:UniversityOfHelsinkiCS/oodikone into trunk 2021-08-09 19:57:53 +03:00
Matti Luukkainen c25431e918 a fix in kasvatustiede kandi start date 2021-08-09 19:55:47 +03:00
Petri PalmuandGitHub 53865cafd3 Merge pull request #3127 from UniversityOfHelsinkiCS/trunk
Trunk
2021-08-09 16:03:55 +03:00
Otto Ahoniemi b0a8ce30a2 Upgrade packages 2021-08-09 10:25:18 +03:00
Otto Ahoniemi 2c1eb0c90e Upgrade cypress 2021-08-09 10:01:53 +03:00
Petri Palmu 48878b961c Merge branch 'trunk' into feature/org 2021-08-09 09:09:25 +03:00
Otto Ahoniemi 149cbbcf19 Merge branch 'trunk' of github.com:UniversityOfHelsinkiCS/oodikone into trunk 2021-08-09 09:06:28 +03:00
Otto Ahoniemi 749a52a375 Continue setup even if compose cleanup fails 2021-08-09 09:06:05 +03:00
Petri Palmu 999ffaaa9f added test for student grades 2021-08-06 16:49:47 +03:00
Petri Palmu 65cb7a99df Merge branch 'feature/grade' into trunk 2021-08-06 11:49:50 +03:00
Petri Palmu 14486e1116 changed course grades so that old courses are included 2021-08-06 11:49:28 +03:00
Petri Palmu 9922708975 Merge branch 'feature/grade' into trunk 2021-08-05 17:25:58 +03:00
Petri Palmu bf7588ba28 changed studyprogramme total gender right 2021-08-05 17:24:05 +03:00
Petri Palmu 5c92532aef studyprogramme view gender are now in right order 2021-08-05 17:02:27 +03:00
Petri Palmu 6acba8f523 another little change to backend substitutions 2021-08-05 15:37:03 +03:00
Petri Palmu 77c1a733c2 more minor changes to substitutions 2021-08-05 15:27:22 +03:00
Petri Palmu d3f6c72906 changed the way subgroups are made in backend 2021-08-05 15:02:54 +03:00
Petri Palmu 3d8b6d214a added grades for students in coursestats issue #3039 2021-08-04 16:18:35 +03:00
Petri Palmu 16b1ff5c59 added responsible_organisation to the course model and used it in frontend 2021-08-04 12:04:37 +03:00
Petri Palmu 43b08bc8e9 little change to cypress tests to get working in gh actions 2021-08-02 14:33:21 +03:00
Petri Palmu bc42dbee1a fixed issue #3109, user navigation responded 500 2021-08-02 11:00:05 +03:00
Otto AhoniemiandGitHub 5c438fb715 Merge pull request #3107 from UniversityOfHelsinkiCS/trunk
Add admission types, multiple fixes, anon / dev user fixes, cypress test betterments
2021-07-23 19:30:50 +03:00
Otto Ahoniemi 39349b06c5 Failing tests go brrrrr haha 2021-07-23 19:19:45 +03:00
Otto Ahoniemi d951cc82af Add moar waiting since don't have time for proper fix 2021-07-23 19:05:36 +03:00
Otto Ahoniemi 4e2e8ad2a2 Don't compress cypress videos at all 2021-07-23 19:05:29 +03:00
Otto AhoniemiandGitHub 313148d741 Add stuff about CI 2021-07-23 18:56:08 +03:00
Otto AhoniemiandGitHub 0f1ef41b98 Maintainer notes 2021-07-23 18:48:31 +03:00
Otto AhoniemiandGitHub 0ab536c4d9 Add info about linting and testing and development users 2021-07-23 18:47:06 +03:00
Otto Ahoniemi 11cac89ecc Check for different column values on admin / basic user 2021-07-23 18:02:33 +03:00
Otto Ahoniemi e42b8b1049 Cleanup double waiting for api call 2021-07-23 17:52:42 +03:00
Otto Ahoniemi 7fca1ab996 Fix population statistic tests to contain basic and admin user tests 2021-07-23 17:50:56 +03:00
Otto Ahoniemi 2a7d3e1aa5 Try to fix filter tests in CI and show error when using wrong user headers 2021-07-23 17:09:05 +03:00
Otto Ahoniemi 00eac1d879 Fix users tests to work with different user types 2021-07-23 17:08:41 +03:00
Otto Ahoniemi 39735755e6 Set teacher tests to use admin user 2021-07-23 16:40:24 +03:00
Otto Ahoniemi c8440db162 Separate studyprogramme tests for admin and basic user 2021-07-23 16:37:41 +03:00
Otto Ahoniemi 66ef0ae1ab Fix course waiting in filters 2021-07-23 16:00:45 +03:00
Otto Ahoniemi c3cea62386 Combine course population test to course stats 2021-07-23 15:30:39 +03:00
Otto Ahoniemi 49a8601080 Add logs to run.sh shortcuts 2021-07-23 14:35:45 +03:00
Otto Ahoniemi 58bbb51702 Acually print logs if test step fails 2021-07-23 10:11:05 +03:00
Otto Ahoniemi ef873549a6 Don't compress cypress videos so much + add lower timeouts 2021-07-23 10:01:33 +03:00
Otto Ahoniemi 304278db36 Merge branch 'trunk' of github.com:UniversityOfHelsinkiCS/oodikone into trunk 2021-07-22 23:38:16 +03:00
Otto Ahoniemi 282cf274d0 Add admission type filter test and other fixes 2021-07-22 23:24:49 +03:00
Otto Ahoniemi 9a8829d0bc Fix custom populations to work with new anon data / anon users 2021-07-22 23:24:30 +03:00
Otto Ahoniemi 519e2d88ad some fixes for course stat tests 2021-07-22 22:44:01 +03:00
Otto Ahoniemi d574c7bc8a correct mocked email 2021-07-22 17:37:40 +03:00
Otto Ahoniemi dd76149c10 Add different types of users for testing in cypress 2021-07-22 17:37:13 +03:00
Otto Ahoniemi f82182b331 Add separate user for using cypress 2021-07-22 10:37:50 +03:00
Otto Ahoniemi 2cddc5d7af Remove unused route for creating users 2021-07-22 09:49:02 +03:00
Otto Ahoniemi 7048160adb Add email to dev user 2021-07-22 09:44:35 +03:00
Otto Ahoniemi 396614bfcc Fix user details not being updated correctly 2021-07-22 09:44:13 +03:00
Otto Ahoniemi 46a2b5248e Mimic hy_groups for dev user too 2021-07-22 09:19:43 +03:00
Otto Ahoniemi 50bf7f47a4 Remove unused app 2021-07-21 23:50:04 +03:00
Otto Ahoniemi ad1a834b4b Remove some unused constants 2021-07-21 23:41:05 +03:00
Otto Ahoniemi c72ff0956a Remove unused dev user methods 2021-07-21 23:35:53 +03:00
Otto Ahoniemi 8dd51576b6 Acually set mluukkai always as default dev user 2021-07-21 23:34:20 +03:00
Otto Ahoniemi 7372278bed Migrate to toskaboilers style of basepath setting while keeping backwards compatibility 2021-07-21 22:03:21 +03:00
Otto Ahoniemi e83e003f17 Use only mluukkai as dev user 2021-07-21 21:57:02 +03:00
Otto Ahoniemi 8b02cd3237 Update cypress 2021-07-21 14:01:57 +03:00
Otto Ahoniemi bf961fbeb4 Add petri to oodikone users for prod 2021-07-21 14:00:05 +03:00
Otto AhoniemiandGitHub d659fe4002 Merge branch 'master' into trunk 2021-07-20 15:30:39 +03:00
Otto Ahoniemi bc23f448e1 Add counts to admissiontypes, remove unused gender counter func 2021-07-20 01:25:54 +03:00
Otto Ahoniemi b12524a229 Add initial setup for admission types 2021-07-20 01:16:28 +03:00
Otto Ahoniemi 0c8ee7570d Return admission types to frontend 2021-07-20 01:16:15 +03:00
Otto Ahoniemi 28241e522b Update eslint 2021-07-19 14:12:15 +03:00
Otto Ahoniemi d69a2861b3 Merge branch 'trunk' of github.com:UniversityOfHelsinkiCS/oodikone into trunk 2021-07-19 14:06:46 +03:00
Otto Ahoniemi be78e236fa Stop console logging entityids all the time 2021-07-19 14:00:15 +03:00
Otto Ahoniemi e407304e7a Add admission types to studyrights in updater 2021-07-19 13:40:16 +03:00
Otto Ahoniemi 2a739722e2 Fix updater-worker eslint issues 2021-07-19 13:37:42 +03:00
Petri Palmu b525e6a3af Fixed population statistic time selection not working 2021-07-16 15:48:10 +03:00
Petri Palmu 4a32b861a0 Merge branch 'trunk' of github.com:UniversityOfHelsinkiCS/oodikone into trunk 2021-07-16 11:36:08 +03:00
Otto Ahoniemi 8ba9273248 Add admission type to studyrights 2021-07-16 11:09:42 +03:00
Otto Ahoniemi b3be917c06 Add substitutions to sis-db models in backend too 2021-07-16 10:54:12 +03:00
Petri Palmu 6cec305cc1 Merge branch 'trunk' of github.com:UniversityOfHelsinkiCS/oodikone into trunk 2021-07-16 10:49:19 +03:00
Petri Palmu 7864e704d5 removed only from cypress populations statistics tests 2021-07-16 10:44:33 +03:00
Otto Ahoniemi 069a3a558d Merge branch 'trunk' of github.com:UniversityOfHelsinkiCS/oodikone into trunk 2021-07-16 10:42:39 +03:00
Otto Ahoniemi 550c2ebfc3 Add working adminerurls 2021-07-16 10:41:42 +03:00
Petri Palmu bed3d50d00 Merge branch 'advanced' into trunk 2021-07-16 10:27:39 +03:00
Petri Palmu 57753555f8 little correction to population statistics advanced setttins semesters, now work 2021-07-16 10:26:11 +03:00
Petri Palmu 574ad8fecd fixed cypress population statistics test Advanced settings work 2021-07-16 10:02:50 +03:00
Petri Palmu 60588e81bc correction to advanced settings semesters 2021-07-15 18:54:12 +03:00
655 changed files with 15744 additions and 165164 deletions
+3
View File
@@ -0,0 +1,3 @@
# Populate and change this to .env
IMPORTER_DB_API_TOKEN=
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"
@@ -1,138 +0,0 @@
name: "Release oodikone to staging"
on:
push:
branches:
- master
paths:
- '.github/workflows/release-oodikone-staging.yaml'
- 'services/**'
jobs:
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"
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: "Build and push"
uses: docker/build-push-action@v2
with:
build-args: |
SENTRY_RELEASE=${{ github.sha }}
SENTRY_ENVIRONMENT=staging
cache-from: |
type=registry,ref=toska/oodikone2-backend:trunk
type=registry,ref=toska/oodikone2-backend:staging
type=registry,ref=toska/oodikone2-backend:latest
cache-to: type=inline
context: services/backend
push: true
tags: toska/oodikone2-backend:staging
target: production
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"
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: "Build and push"
uses: docker/build-push-action@v2
with:
build-args: |
SENTRY_RELEASE=${{ github.sha }}
SENTRY_ENVIRONMENT=staging
cache-from: |
type=registry,ref=toska/oodikone2-frontend:trunk
type=registry,ref=toska/oodikone2-frontend:staging
type=registry,ref=toska/oodikone2-frontend:latest
cache-to: type=inline
context: services/frontend
push: true
tags: toska/oodikone2-frontend:staging
target: production
- name: "Create a container from built image "
run: docker create --name frontend toska/oodikone2-frontend:staging
- name: "Copy assets from the container"
run: docker cp frontend:/usr/src/app/build ./build
- name: "Create Sentry release"
uses: getsentry/action-release@v1
env:
SENTRY_URL: ${{ secrets.SENTRY_URL }}
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
SENTRY_ORG: toska
SENTRY_PROJECT: oodikone
with:
environment: staging
set_commits: 'skip'
sourcemaps: './build/static/js'
url_prefix: '~/oodikone/static/js'
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: "Build and push"
uses: docker/build-push-action@v2
with:
cache-from: |
type=registry,ref=toska/oodikone2-userservice:trunk
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:staging
target: production
report_success:
runs-on: ubuntu-latest
name: "Report deployment success to oodikone notifications"
needs:
- build_and_release_backend
- build_and_release_frontend
- build_and_release_userservice
if: success()
steps:
- name: "Send notification to slack"
uses: UniversityOfHelsinkiCS/slackbot-action@v1.4.2
with:
webhook-url: ${{ secrets.WEBHOOK_URL }}
message-type: deployment
softa-url: https://oodikone-staging.cs.helsinki.fi/
deployment-target: staging
report_failure:
runs-on: ubuntu-latest
name: "Report deployment failure to oodikone notifications"
needs:
- build_and_release_backend
- build_and_release_frontend
- build_and_release_userservice
if: failure()
steps:
- name: "Send notification to slack"
uses: UniversityOfHelsinkiCS/slackbot-action@v1.4.2
with:
webhook-url: ${{ secrets.WEBHOOK_URL }}
message-type: deployment-failure
deployment-target: staging
+28 -51
View File
@@ -1,4 +1,4 @@
name: "Release oodikone to production"
name: 'Release oodikone to production'
on:
release:
@@ -6,30 +6,29 @@ 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: |
SENTRY_RELEASE=${{ steps.extract_version.outputs.VERSION }}
SENTRY_ENVIRONMENT=production
cache-from: |
type=registry,ref=toska/oodikone2-backend:trunk
type=registry,ref=toska/oodikone2-backend:staging
type=registry,ref=toska/oodikone2-backend:latest
cache-to: type=inline
@@ -39,30 +38,33 @@ 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: "Extract release version"
- name: 'Get current time'
id: get_current_time
run: echo ::set-output name=NOW::"$(date --iso-8601=seconds)"
- 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: |
SENTRY_RELEASE=${{ steps.extract_version.outputs.VERSION }}
SENTRY_ENVIRONMENT=production
BUILT_AT=${{ steps.get_current_time.outputs.NOW }}
cache-from: |
type=registry,ref=toska/oodikone2-frontend:trunk
type=registry,ref=toska/oodikone2-frontend:staging
type=registry,ref=toska/oodikone2-frontend:latest
cache-to: type=inline
@@ -70,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 }}
@@ -85,43 +87,19 @@ jobs:
environment: production
set_commits: 'skip'
sourcemaps: './build/static/js'
url_prefix: '~/oodikone/static/js'
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: "Build and push"
uses: docker/build-push-action@v2
with:
cache-from: |
type=registry,ref=toska/oodikone2-userservice:trunk
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
url_prefix: '~/static/js'
version: ${{ steps.extract_version.outputs.VERSION }}
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
@@ -130,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
@@ -3,7 +3,7 @@ name: "Release sis-updater-scheduler"
on:
push:
branches:
- trunk
- master
paths:
- '.github/workflows/release-sis-updater-scheduler.yaml'
- 'updater/sis-updater-scheduler/**'
@@ -3,7 +3,7 @@ name: "Release sis-updater-worker"
on:
push:
branches:
- trunk
- master
paths:
- '.github/workflows/release-sis-updater-worker.yaml'
- 'updater/sis-updater-worker/**'
+288 -219
View File
@@ -1,454 +1,491 @@
name: "Test oodikone"
name: 'Test oodikone and release to staging'
on:
push:
branches-ignore:
branches:
- master
paths:
- .github/workflows/test-oodikone.yaml
- cypress/**
- cypress.json
- services/**
- docker-compose.ci.yml
- nginx.conf
- package*.json
- '.github/workflows/test-oodikone.yaml'
- 'cypress/**'
- 'cypress.json'
- 'services/**'
- 'docker-compose.ci.yml'
- 'docker-compose.test.yml'
- 'nginx.conf'
- 'package*.json'
jobs:
# === Static tests / linting ===
lint_backend:
name: "Lint backend"
name: 'Lint backend'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: 14
- name: "Install dependencies"
- name: 'Install dependencies'
run: npm ci && cd services/backend && npm ci
- name: "Run backend linter"
- name: 'Run backend linter'
run: npm run eslint services/backend
lint_frontend:
name: "Lint frontend"
name: 'Lint frontend'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: 14
- name: "Install dependencies"
- name: 'Install dependencies'
run: npm ci && cd services/frontend && npm ci
- name: "Run frontend linter"
- name: 'Run frontend linter'
run: npm run concurrently \
'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
'eslint --ext .jsx,.js services/frontend' \
'stylelint services/frontend/src/**/*.css'
# === Build images ===
build_backend:
name: "Build backend"
name: 'Build backend'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: docker/setup-buildx-action@v1
- name: "Build image and export it"
- name: 'Build image and export it'
uses: docker/build-push-action@v2
with:
build-args: |
SENTRY_RELEASE=${{ github.sha }}
SENTRY_ENVIRONMENT=staging
cache-from: |
type=registry,ref=toska/oodikone2-backend:trunk
type=registry,ref=toska/oodikone2-backend:staging
type=registry,ref=toska/oodikone2-backend:latest
cache-to: type=inline
context: services/backend
push: false
tags: toska/oodikone2-backend:trunk
tags: toska/oodikone2-backend:staging
target: production
outputs: type=docker,dest=/tmp/backend.tar
- name: "Upload image for other jobs"
- name: 'Upload image for other jobs'
uses: actions/upload-artifact@v2
with:
path: /tmp/backend.tar
build_frontend:
name: "Build frontend"
name: 'Build frontend'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: docker/setup-buildx-action@v1
- name: "Build image and export it"
- name: 'Get current time'
id: get_current_time
run: echo ::set-output name=NOW::"$(date --iso-8601=seconds)"
- name: 'Build image and export it'
uses: docker/build-push-action@v2
with:
build-args: |
SENTRY_RELEASE=${{ github.sha }}
SENTRY_ENVIRONMENT=staging
BUILT_AT=${{ steps.get_current_time.outputs.NOW }}
cache-from: |
type=registry,ref=toska/oodikone2-frontend:trunk
type=registry,ref=toska/oodikone2-frontend:staging
type=registry,ref=toska/oodikone2-frontend:latest
cache-to: type=inline
context: services/frontend
push: false
tags: toska/oodikone2-frontend:trunk
tags: toska/oodikone2-frontend:staging
target: production
outputs: type=docker,dest=/tmp/frontend.tar
- name: "Upload image for other jobs"
- name: 'Upload image for other jobs'
uses: actions/upload-artifact@v2
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:
cache-from: |
type=registry,ref=toska/oodikone2-userservice:trunk
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:trunk
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_population:
name: "Test course population"
needs:
- build_backend
- build_frontend
- build_userservice
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: "Download images"
uses: actions/download-artifact@v2
with:
path: /tmp/
- name: "Load built images"
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:
registry: registry.toska.cs.helsinki.fi
username: ${{ secrets.TOSKAREGISTRY_USERNAME }}
password: ${{ secrets.TOSKAREGISTRY_PASSWORD }}
- name: "Run services, pull rest of the needed images"
run: docker-compose --file docker-compose.ci.yml up --detach --no-recreate
- name: "Run cypress"
uses: cypress-io/github-action@v2
with:
browser: chrome
spec: cypress/integration/Course_population.js
env:
CYPRESS_baseUrl: http://localhost:1337
- name: "Upload video if tests fail"
uses: actions/upload-artifact@v2
if: failure()
with:
name: Cypress videos
path: ./cypress/videos/*
test_course_statistics:
name: "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
- name: "Download images"
- name: 'Download images'
uses: actions/download-artifact@v2
with:
path: /tmp/
- name: "Load built images"
- name: 'Load built images'
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"
- name: 'Login to toska docker registry'
uses: docker/login-action@v1
with:
registry: registry.toska.cs.helsinki.fi
username: ${{ secrets.TOSKAREGISTRY_USERNAME }}
password: ${{ secrets.TOSKAREGISTRY_PASSWORD }}
- name: "Run services, pull rest of the needed images"
- name: 'Run services, pull rest of the needed images'
run: docker-compose --file docker-compose.ci.yml up --detach --no-recreate
- name: "Run cypress"
- name: 'Run cypress'
uses: cypress-io/github-action@v2
with:
browser: chrome
spec: cypress/integration/Course_statistics.js
env:
CYPRESS_baseUrl: http://localhost:1337
- name: "Upload video if tests fail"
- name: 'Upload video if tests fail'
uses: actions/upload-artifact@v2
if: failure()
with:
name: Cypress videos
path: ./cypress/videos/*
- name: 'Print logs on failure'
if: failure()
run: docker-compose --file docker-compose.ci.yml logs
test_custom_population:
name: "Test custom population"
name: 'Test custom population'
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
- name: "Download images"
- name: 'Download images'
uses: actions/download-artifact@v2
with:
path: /tmp/
- name: "Load built images"
- name: 'Load built images'
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"
- name: 'Login to toska docker registry'
uses: docker/login-action@v1
with:
registry: registry.toska.cs.helsinki.fi
username: ${{ secrets.TOSKAREGISTRY_USERNAME }}
password: ${{ secrets.TOSKAREGISTRY_PASSWORD }}
- name: "Run services, pull rest of the needed images"
- name: 'Run services, pull rest of the needed images'
run: docker-compose --file docker-compose.ci.yml up --detach --no-recreate
- name: "Run cypress"
- name: 'Run cypress'
uses: cypress-io/github-action@v2
with:
browser: chrome
spec: cypress/integration/Custom_population.js
env:
CYPRESS_baseUrl: http://localhost:1337
- name: "Upload video if tests fail"
- name: 'Upload video if tests fail'
uses: actions/upload-artifact@v2
if: failure()
with:
name: Cypress videos
path: ./cypress/videos/*
- name: 'Print logs on failure'
if: failure()
run: docker-compose --file docker-compose.ci.yml logs
test_population_statistics:
name: "Test population statistics"
name: 'Test population 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
- name: "Download images"
- name: 'Download images'
uses: actions/download-artifact@v2
with:
path: /tmp/
- name: "Load built images"
- name: 'Load built images'
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"
- name: 'Login to toska docker registry'
uses: docker/login-action@v1
with:
registry: registry.toska.cs.helsinki.fi
username: ${{ secrets.TOSKAREGISTRY_USERNAME }}
password: ${{ secrets.TOSKAREGISTRY_PASSWORD }}
- name: "Run services, pull rest of the needed images"
- name: 'Run services, pull rest of the needed images'
run: docker-compose --file docker-compose.ci.yml up --detach --no-recreate
- name: "Run cypress"
- name: 'Run cypress'
uses: cypress-io/github-action@v2
with:
browser: chrome
spec: cypress/integration/Population_statistics.js
env:
CYPRESS_baseUrl: http://localhost:1337
- name: "Upload video if tests fail"
- name: 'Upload video if tests fail'
uses: actions/upload-artifact@v2
if: failure()
with:
name: Cypress videos
path: ./cypress/videos/*
- name: 'Print logs on failure'
if: failure()
run: docker-compose --file docker-compose.ci.yml logs
test_filters:
name: "Test filters"
name: 'Test filters'
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
- name: "Download images"
- name: 'Download images'
uses: actions/download-artifact@v2
with:
path: /tmp/
- name: "Load built images"
- name: 'Load built images'
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"
- name: 'Login to toska docker registry'
uses: docker/login-action@v1
with:
registry: registry.toska.cs.helsinki.fi
username: ${{ secrets.TOSKAREGISTRY_USERNAME }}
password: ${{ secrets.TOSKAREGISTRY_PASSWORD }}
- name: "Run services, pull rest of the needed images"
- name: 'Run services, pull rest of the needed images'
run: docker-compose --file docker-compose.ci.yml up --detach --no-recreate
- name: "Run cypress"
- name: 'Run cypress'
uses: cypress-io/github-action@v2
with:
browser: chrome
spec: cypress/integration/Filters.js
env:
CYPRESS_baseUrl: http://localhost:1337
- name: "Upload video if tests fail"
- name: 'Upload video if tests fail'
uses: actions/upload-artifact@v2
if: failure()
with:
name: Cypress videos
path: ./cypress/videos/*
- name: 'Print logs on failure'
if: failure()
run: docker-compose --file docker-compose.ci.yml logs
test_student_statistics:
name: "Test student statistics"
name: 'Test student 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
- name: "Download images"
- name: 'Download images'
uses: actions/download-artifact@v2
with:
path: /tmp/
- name: "Load built images"
- name: 'Load built images'
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"
- name: 'Login to toska docker registry'
uses: docker/login-action@v1
with:
registry: registry.toska.cs.helsinki.fi
username: ${{ secrets.TOSKAREGISTRY_USERNAME }}
password: ${{ secrets.TOSKAREGISTRY_PASSWORD }}
- name: "Run services, pull rest of the needed images"
- name: 'Run services, pull rest of the needed images'
run: docker-compose --file docker-compose.ci.yml up --detach --no-recreate
- name: "Run cypress"
- name: 'Run cypress'
uses: cypress-io/github-action@v2
with:
browser: chrome
spec: cypress/integration/Student_statistics.js
env:
CYPRESS_baseUrl: http://localhost:1337
- name: "Upload video if tests fail"
- name: 'Upload video if tests fail'
uses: actions/upload-artifact@v2
if: failure()
with:
name: Cypress videos
path: ./cypress/videos/*
- name: 'Print logs on failure'
if: failure()
run: docker-compose --file docker-compose.ci.yml logs
test_studyprogramme_overview:
name: "Test studyprogramme overview"
name: 'Test studyprogramme overview'
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
- name: "Download images"
- name: 'Download images'
uses: actions/download-artifact@v2
with:
path: /tmp/
- name: "Load built images"
- name: 'Load built images'
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"
- name: 'Login to toska docker registry'
uses: docker/login-action@v1
with:
registry: registry.toska.cs.helsinki.fi
username: ${{ secrets.TOSKAREGISTRY_USERNAME }}
password: ${{ secrets.TOSKAREGISTRY_PASSWORD }}
- name: "Run services, pull rest of the needed images"
- name: 'Run services, pull rest of the needed images'
run: docker-compose --file docker-compose.ci.yml up --detach --no-recreate
- name: "Run cypress"
- name: 'Run cypress'
uses: cypress-io/github-action@v2
with:
browser: chrome
spec: cypress/integration/Studyprogramme_overview.js
env:
CYPRESS_baseUrl: http://localhost:1337
- name: "Upload video if tests fail"
- name: 'Upload video if tests fail'
uses: actions/upload-artifact@v2
if: failure()
with:
name: Cypress videos
path: ./cypress/videos/*
- name: 'Print logs on failure'
if: failure()
run: docker-compose --file docker-compose.ci.yml logs
test_teachers_and_users:
name: "Test teachers and users"
name: 'Test teachers and users'
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
- name: "Download images"
- name: 'Download images'
uses: actions/download-artifact@v2
with:
path: /tmp/
- name: "Load built images"
- name: 'Load built images'
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"
- name: 'Login to toska docker registry'
uses: docker/login-action@v1
with:
registry: registry.toska.cs.helsinki.fi
username: ${{ secrets.TOSKAREGISTRY_USERNAME }}
password: ${{ secrets.TOSKAREGISTRY_PASSWORD }}
- name: "Run services, pull rest of the needed images"
- name: 'Run services, pull rest of the needed images'
run: docker-compose --file docker-compose.ci.yml up --detach --no-recreate
- name: "Run cypress"
- name: 'Run cypress'
uses: cypress-io/github-action@v2
with:
browser: chrome
@@ -457,111 +494,143 @@ jobs:
cypress/integration/Users.js
env:
CYPRESS_baseUrl: http://localhost:1337
- name: "Upload video if tests fail"
- name: 'Upload video if tests fail'
uses: actions/upload-artifact@v2
if: failure()
with:
name: Cypress videos
path: ./cypress/videos/*
- name: 'Print logs on failure'
if: failure()
run: docker-compose --file docker-compose.ci.yml logs
# === Other tests ===
test_service_exit_statuses:
name: "Check services run correctly"
name: 'Check services run correctly'
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
- name: "Download images"
- name: 'Download images'
uses: actions/download-artifact@v2
with:
path: /tmp/
- name: "Load built images"
- name: 'Load built images'
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"
- name: 'Login to toska docker registry'
uses: docker/login-action@v1
with:
registry: registry.toska.cs.helsinki.fi
username: ${{ secrets.TOSKAREGISTRY_USERNAME }}
password: ${{ secrets.TOSKAREGISTRY_PASSWORD }}
- name: "Run services, pull rest of the needed images"
- name: 'Run services, pull rest of the needed images'
run: docker-compose --file docker-compose.ci.yml up --detach --no-recreate
- name: "Check that exit statuses are not errors"
- name: 'Check that exit statuses are not errors'
run: |
docker ps -a
if [[ $(docker ps --all | grep -Eo '(Exited|Restarting) \([0-9]+\)' | grep -Eo '[0-9]+' | awk 'BEGIN {sum=0} { sum += $1 } END { print sum }') != '0' ]]; then
echo 'Some process had nonzero exit code';
exit 1;
fi
- name: 'Print logs on failure'
if: failure()
run: docker-compose --file docker-compose.ci.yml logs
push_to_dockerhub:
name: "Push images to docker hub for caching"
needs:
- lint_backend
- lint_frontend
- lint_userservice
- run_userservice_integration_tests
- test_course_population
- test_course_statistics
- test_custom_population
- test_filters
- test_population_statistics
- test_student_statistics
- test_studyprogramme_overview
- test_teachers_and_users
- test_service_exit_statuses
# === Release ===
release_images:
name: 'Release built images'
runs-on: ubuntu-latest
needs:
- lint_backend
- lint_frontend
- test_course_statistics
- test_custom_population
- test_filters
- test_population_statistics
- test_student_statistics
- test_studyprogramme_overview
- test_teachers_and_users
- test_service_exit_statuses
steps:
- uses: actions/checkout@v2
- name: "Download images"
- name: 'Download images'
uses: actions/download-artifact@v2
with:
path: /tmp/
- name: "Load built images"
- name: 'Load built images'
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"
- name: 'Login to DockerHub'
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: "Push to docker hub"
run: docker-compose --file docker-compose.ci.yml push
- name: 'Create a container from frontend'
run: docker create --name frontend toska/oodikone2-frontend:staging
- name: 'Copy assets from the container'
run: docker cp frontend:/usr/src/app/build ./build
- name: 'Create Sentry release'
uses: getsentry/action-release@v1
env:
SENTRY_URL: ${{ secrets.SENTRY_URL }}
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
SENTRY_ORG: toska
SENTRY_PROJECT: oodikone
with:
environment: staging
set_commits: 'skip'
sourcemaps: './build/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
- name: 'Send deployment success notification to slack'
if: success()
uses: UniversityOfHelsinkiCS/slackbot-action@v1.4.2
with:
webhook-url: ${{ secrets.WEBHOOK_URL }}
message-type: deployment
softa-url: https://oodikone-staging.cs.helsinki.fi/
deployment-target: staging
- name: 'Send deployment failure notification to slack'
if: failure()
uses: UniversityOfHelsinkiCS/slackbot-action@v1.4.2
with:
webhook-url: ${{ secrets.WEBHOOK_URL }}
message-type: deployment-failure
deployment-target: staging
report_failure:
report_test_failure:
runs-on: ubuntu-latest
name: "Report test failure to oodikone notifications"
name: 'Report test failure to oodikone notifications'
needs:
- lint_backend
- lint_frontend
- lint_userservice
- run_userservice_integration_tests
- test_course_population
- test_course_statistics
- test_custom_population
- test_filters
- test_population_statistics
- test_student_statistics
- test_studyprogramme_overview
- test_teachers_and_users
- test_service_exit_statuses
- push_to_dockerhub
- lint_backend
- lint_frontend
- test_course_statistics
- test_custom_population
- test_filters
- test_population_statistics
- test_student_statistics
- test_studyprogramme_overview
- test_teachers_and_users
- test_service_exit_statuses
if: failure()
steps:
- uses: actions/checkout@v2
- 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: test-failure
- name: "Print logs on failure"
run: docker-compose --file docker-compose.ci.yml logs
+26
View File
@@ -0,0 +1,26 @@
const path = require('path')
const cwd = process.cwd()
const dockerCmdBase = `docker run --rm --volume ${cwd}:/oodikone --workdir /oodikone`
const relativeFilePaths = files => [...files.map(file => path.relative(cwd, file))].join(' ')
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 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 => {
if (['docker-compose.ci.yml', 'docker-compose.test.yml'].some(f => file.includes(f))) {
return `--file ${file}`
}
if (file.includes('docker-compose.real.yml')) {
return `--file ${cwd}/docker-compose.yml --file ${file}`
}
return ''
}
return files.map(file => `docker-compose ${composeFiles(file)} config --quiet`)
},
}
+72 -59
View File
@@ -7,17 +7,22 @@ An application for analyzing university data, running at [https://oodikone.cs.he
To run Oodikone locally, you will need the following:
1. Applications:
- [Docker](https://docs.docker.com/install/) (version 20.10+),
- [Docker Compose](https://docs.docker.com/compose/install/)(version 0.29+) and
- [npm](https://docs.npmjs.com/cli/v7) (version 7+)
2. Access to Toska Docker Hub, see Toska Gitlab for more information on how to login
3. (Optional) For real data setup, access to Toskas production servers.
- [Docker](https://docs.docker.com/install/) (version 20.10+),
- [Docker Compose](https://docs.docker.com/compose/install/)(version 0.29+) and
- [npm](https://docs.npmjs.com/cli/v7) (version 7+)
2. Access to Toska Docker Hub,
3. For real data setup, access to Toskas production servers.
4. For some features to work, access to Toskas service tokens
## 🚀 Installation
Launch the CLI and follow the instructions:
Pre-installation steps:
- Clone this repo
- Login to toskas docker hub (see Toskas Gitlab for more information)
- Populate `.env.template` file with correct tokens and rename it to `.env`
Then launch the CLI and follow the instructions:
```bash
npm run cli
@@ -28,26 +33,18 @@ _Please use a terminal at least 80 characters wide, the CLI is a bit rudimentary
What different CLI options do:
1. Set up oodikone from scratch:
- Cleans up any previous installations
- Installs all needed npm packages locally
- Sets up pre-commit linting hooks.
- Pulls and builds all needed Docker images and sets up dockerized development environment
- **Note:** Running this option cleans up all real data too, so please don't run option 2 before this.
- Cleans up any previous installations
- Installs all needed npm packages locally
- Sets up pre-commit linting hooks.
- Pulls and builds all needed Docker images and sets up dockerized development environment.
- Setup happens with anonymized data, which developers should use by default.
- **Note:** Running this option cleans up all real data too, so please don't run option 2 before this.
2. Reset all real data:
- 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.
4. Reset old oodi data:
- Like option 2, but for old oodi data, which is not refreshed anymore. Is only needed when setting up real databases after running option 1.
- 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 single database
- Like option 2, but only for single database. Leaves other databases untouched.
## ⌨️ Development
@@ -57,7 +54,7 @@ The development environment is entirely configured in the `docker-compose.yml` f
Running oodikone with real data requires separated databases and redis, which are defined in `docker-compose.real.yml` file. Otherwise real data development environment uses the configuration as anonymized data development environment.
Some useful commands are predefined in `package.json` and can be run with `npm run <command>` as follows:
Some useful commands are defined in `package.json` and can be run with `npm run <command>` as follows:
- `npm run oodikone`: starts oodikone with anonymized data
- `npm run oodikone:real`: starts oodikone with real data
@@ -67,11 +64,7 @@ Some useful commands are predefined in `package.json` and can be run with `npm r
- `npm run both:real`: starts oodikone and updater with real data
- `npm run docker:down`: stops the whole environment
If you're first timer, just run `npm run oodikone` after setting up oodikone according to [installation](#installation). After starting and waiting for a while for containers to compile, oodikone can be accessed at [http://localhost:3000/](http://localhost:3000/) and Adminer (database investigation tool) at [http://localhost:5050/](http://localhost:5050/). Adminer requires you to login with username `postgres` and with any password you choose (for example `p`).
### Anon / real data users
Some notes about mluukkai / tkl user here.
Once you have ran setup for oodikone , you can just execute the first one (`npm run oodikone`). After starting and waiting for a while for containers to compile, oodikone can be accessed at [http://localhost:3000/](http://localhost:3000/) and Adminer (database investigation tool) at [http://localhost:5050/](http://localhost:5050/). Adminer requires you to login with username `postgres` and with any password you choose (for example `p`).
### Run.sh script
@@ -86,51 +79,71 @@ 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.
### Linting and tests
### User types for development
Linting and other stuff here
By default, you're login as `mluukkai` dev user when running oodikone in development mode. If you want to debug with certain type of user, you can use mocking: go to "Users" -page, click on "edit" on user you want to use and then click icon on the right corner of user card. Another way is to mock user headers - see frontend's api configuration for how this is done.
The test suite is run in CI on every push to `trunk`. Take advantage of this as running the tests can take upwards of 15 minutes.
Anon data contains some preset user types, corresponding to most usual user types in real data. These are used in testing too (see "Testing" below for more info).
### Updater stuff
### Linting
Stop with HTTP GET
Cli script sets up pre-commit hooks that are used to lint and fix files before committing. If your `package.json` file doesn't include "lint-staged" -key, please set hooks manually with `npm run prepare`.
```bash
http://localhost:8082/v1/abort?token=dev
```
For more information on how files are tested, take a look at "lint-staged" in `package.json`. Some files (e.g. github action files, dockerfiles, shell scripts) are checked with external tools and may require you to install those tools in case you're modifying files in question.
Schedule students with HTTP POST
There's no separate "lintfix" / "format" / "lint" command available, since pre-commit hooks will fix formatting and basic linting errors for you. In case pre-commit fails, you should fix the erroring code manually.
```bash
http://localhost:8082/v1/students?token=dev
```
However, if you want to run linting / formatting manually from command line, `package.json` defines `npm run eslint`, `npm run prettier` and `npm run stylelint` which you can use as an entrypoint.
body
## 🔨 Testing & CI
```bash
{
"studentnumbers": [
"014598456"
]
}
```
There are three types of tests in this project: static tests with eslint/prettier/other tools, jest integration tests for single service and cypress end-to-end tests. Linting is described above and other do work as follows:
## 📖 Documentation
- Cypress
- can be launched in interactive mode with `npm run cypress open`. `package.json` defines entrypoint `npm run cypress` so you can basically run cypress with any arguments you want
- are defined in cypress -folder and cypress.json
- whole cypress test stack takes about 15 to 20 mins to run. Since tests are ran in our Github actions CI pipe, you're encouraged to take advantage of this instead of running all tests locally.
- There are some different user types and cypress commands defined for testing. Take a look at these when debugging tests.
- Jest
- are ran with `docker-compose.test.yml`
- test command and environment themselves are in Dockerfile for service in question
- can be debugged by overriding default command in `docker-compose.test.yml` and running docker container in interactive mode
Testing + CI docs link here.
Continuous integration (CI) works with Github actions and is defined in workflow files in `.github/workflows` folder:
##❓FAQ
- oodikone setup for cypress and other tests in CI is defined in `docker-compose.ci.yml`. Take a look at this too if debugging github action workflows.
- tests are run on every push.
- after successful test run, oodikone is deployed to staging
- after creating a release, oodikone is deployed to production
- updater is deployed to production on every push, no tests included
## ❓FAQ
### 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.
Just do clean install by launching cli with `npm cli` and running option 1: _Set up oodikone from scratch_.
## Maintainers and contribution
## ✌🏼 Maintainers and contribution
Toska of course.
University of Helsinki
## Tietosuoja / data protection
See [tietosuoja.md](tietosuoja.md).
Binary file not shown.
-1
View File
@@ -8,7 +8,6 @@ $driver = "pgsql";
return new AdminerLoginServers(
$servers = array(
"kone-db" => array("server" => "kone-db", "driver" => $driver),
"oodi-db" => array("server" => "oodi-db", "driver" => $driver),
"sis-db" => array("server" => "sis-db", "driver" => $driver),
"sis-importer-db" => array("server" => "sis-importer-db", "driver" => $driver),
"user-db" => array("server" => "user-db", "driver" => $driver)
+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 repository contains a guide and scripts to create docker images from database dumps and/or push database images to Toska Hub docker registry. Images are mostly used by oodikone.
## How to update database images
Updating images varies a bit depending on the db you're trying to update. Complete steps as defined below and run `./commit_and_push_to_toska_hub.sh <dbname>` to publish the image you've created.
### 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 data according to what's in `enrich_data.sql` file
### Sis-db:
The following steps are for building a clean sis-importer-db image from scratch using the sis-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'
```
(It might be a good idea to follow sis-updater-worker logs while updater is running)
- 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 user-db/kone-db -service in oodikone's docker-compose.yml with the same service from this repo's docker-compose.yml
- Ensure the newest user-db/kone-db image is pulled from the registry with `docker pull`
- Start oodikone, let it run migrations
- Modify db to wanted state. Tips:
- Easy way to create new users 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.
By default, previous user-db/kone-db images are used as a base. If you want to build the image from a clean Postgres image, just comment out toska hub image and uncomment the clean Postgres image.
## How to modify database images
If you need to clean up / modify / do whatever with dbs, either use `psql` directly in docker by running `docker exec -it <dbname> psql -U postgres <dbname>` or start adminer with oodikone's docker-compose.
## 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 -14
View File
@@ -44,21 +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."
"Reset old oodi 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
@@ -67,10 +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 old oodi data.")
reset_old_oodi_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
+4 -6
View File
@@ -1,9 +1,7 @@
{
"projectId": "c3jsph",
"baseUrl": "http://localhost:3000",
"defaultCommandTimeout": 60000,
"requestTimeout": 60000,
"responseTimeout": 60000,
"viewportHeight": 1080,
"viewportWidth": 1920
}
"defaultCommandTimeout": 30000,
"requestTimeout": 30000,
"videoCompression": false
}
-5
View File
@@ -1,5 +0,0 @@
{
"name": "Using fixtures to represent data",
"email": "hello@cypress.io",
"body": "Fixtures are a great way to mock data for responses to routes"
}
-29
View File
@@ -1,29 +0,0 @@
/// <reference types="Cypress" />
describe('Course population tests', () => {
beforeEach(() => {
cy.init()
cy.contains('Course statistics').click()
cy.contains('Search for courses')
})
it('Can find course population', () => {
cy.url().should('include', '/coursestatistics')
cy.contains('Search for courses')
cy.get("input[placeholder='Search by a course code']").type('TKT20003')
cy.contains('tr', 'TKT20003').click()
cy.contains('Fetch statistics').should('be.enabled').click()
cy.contains('Käyttöjärjestelmät')
cy.contains('TKT20003')
cy.get(':nth-child(3) > :nth-child(1) > div > .item > .level').click()
cy.contains('Population of course Käyttöjärjestelmät 2019-2020')
cy.contains('TKT20003')
// cy.contains("Students (all=24)");
cy.contains('Students (127)').click()
cy.contains('010135486')
cy.contains('010431753')
})
})
+294 -649
View File
@@ -1,695 +1,340 @@
/// <reference types="Cypress" />
describe('Course Statistics tests', () => {
beforeEach(() => {
cy.init()
cy.contains('Course statistics').click()
cy.contains('Search for courses')
})
it('Searching single course having substitution mappings shows course statistics', () => {
cy.url().should('include', '/coursestatistics')
cy.contains('Search for courses')
cy.get("input[placeholder='Search by a course code']").type('TKT20001')
// Click uni course, not avoin
cy.contains('td', /^TKT20001/).click()
cy.contains('Fetch statistics').should('be.enabled').click()
cy.contains('Search for courses').should('not.exist')
cy.contains('Tietorakenteet ja algoritmit')
cy.contains('TKT20001')
cy.contains('58131') // old mapped code
cy.contains('.tabular.menu a', 'Table').click()
cy.contains('All')
cy.contains('.modeSelectorRow a', 'Attempts').click()
cy.contains('.modeSelectorRow a', 'Students').click()
cy.contains('.tabular.menu a', 'Pass rate chart').click()
cy.get('div.modeSelectorContainer').click()
cy.contains('svg', 'Pass rate chart')
cy.contains('.tabular.menu a', 'Grade distribution chart').click()
cy.get('div.modeSelectorContainer').click()
cy.contains('svg', 'Grades')
cy.contains('a', 'New query').click()
cy.contains('Search for courses')
})
it('Searching multiple courses having substitution mappings shows course statistics', () => {
cy.url().should('include', '/coursestatistics')
cy.contains('Search for courses')
cy.get("input[placeholder='Search by a course code']").type('TKT')
cy.contains('td', /^TKT20001/).click()
cy.contains('td', /^TKT10002/).click()
cy.contains('Fetch statistics').should('be.enabled').click()
cy.contains('Search for courses').should('not.exist')
cy.contains('.courseNameCell', 'Tietorakenteet ja algoritmit').contains('TKT20001').click()
cy.contains('.courseNameCell', 'Ohjelmoinnin perusteet').should('not.exist')
cy.contains('TKT20001')
cy.contains('58131') // old mapped code
cy.contains('Summary').click()
cy.contains('.courseNameCell', 'Ohjelmoinnin perusteet').contains('TKT10002').click()
cy.contains('.courseNameCell', 'Käyttöjärjestelmät').should('not.exist')
cy.contains('TKT10002')
cy.contains('581325') // old mapped code
cy.contains('Summary').click()
cy.contains('a', 'New query').click()
cy.contains('Search for courses')
})
it('On consecutive searches should not crash and search should work', () => {
cy.url().should('include', '/coursestatistics')
cy.contains('Search for courses')
cy.get("input[placeholder='Search by a course code']").type('TKT20003')
cy.contains('td', 'TKT20003').click()
cy.contains('Fetch statistics').should('be.enabled').click()
cy.contains('Show population')
cy.contains('TKT20003')
cy.contains('Course statistics').click()
cy.contains('Search for courses')
//cy.wait(500)
cy.get("input[placeholder='Search by a course code']").type('TKT20001')
cy.contains('td', 'TKT20001').click()
cy.contains('Fetch statistics').should('be.enabled').click()
cy.contains('Show population')
cy.contains('TKT20001')
})
it('Shows statistics when separating by semester', () => {
const courseCode = 'DATA11002'
cy.url().should('include', '/coursestatistics')
cy.contains('Search for courses')
cy.get("input[placeholder='Search by a course code']").type(courseCode)
cy.contains('td', courseCode).click()
cy.contains('Separate statistics for Spring and Fall semesters').click()
cy.contains('Fetch statistics').should('be.enabled').click()
cy.contains('Syksy 2019')
cy.contains('Syksy 2017')
cy.get('tbody').contains('19')
cy.contains(courseCode)
})
it('Searching course by name displays right courses', () => {
cy.url().should('include', '/coursestatistics')
cy.contains('Search for courses')
cy.get("input[placeholder='Search by entering a course name']").type('tietokantojen perusteet')
cy.contains('Tietokantojen perusteet')
cy.contains('Avoin yo: Tietokantojen perusteet')
cy.contains('A581328')
cy.contains('AYTKT10004')
cy.contains('TKT10004, 581328')
cy.contains('td', /^TKT10004/).click()
cy.contains('Fetch statistics').should('be.enabled').click()
cy.contains('Search for courses').should('not.exist')
cy.contains('TKT10004, 581328 Tietokantojen perusteet')
cy.get('.right').click()
cy.contains('No results')
cy.get("input[placeholder='Search by entering a course name']").type('tietokantojen perusteet')
cy.contains('td', /^AYTKT10004/).click()
cy.contains('Fetch statistics').should('be.enabled').click()
cy.contains('Search for courses').should('not.exist')
cy.contains('AYTKT10004 Avoin yo: Tietokantojen perusteet')
})
describe('When searching unified course stats', () => {
describe('when using basic user', () => {
beforeEach(() => {
cy.init('/coursestatistics')
})
it('Searching single course having substitution mappings shows course statistics', () => {
cy.url().should('include', '/coursestatistics')
cy.contains('Search for courses')
cy.get("input[name='unifyOpenUniCourses']").parent().click()
cy.get("input[placeholder='Search by a course code']").type('TKT10002')
cy.contains('td', 'TKT10002, AYTKT10002').click()
cy.get("input[placeholder='Search by a course code']").type('TKT20001')
// Click uni course, not avoin
cy.contains('td', /^TKT20001/).click()
cy.contains('Fetch statistics').should('be.enabled').click()
cy.contains('Search for courses').should('not.exist')
cy.contains('TKT10002, 581325, AYTKT10002, A581325 Ohjelmoinnin perusteet')
cy.contains('Tietorakenteet ja algoritmit')
cy.contains('TKT20001')
cy.contains('58131') // old mapped code
cy.contains('.tabular.menu a', 'Table').click()
cy.contains('All')
cy.contains('.modeSelectorRow a', 'Attempts').click()
cy.contains('.modeSelectorRow a', 'Students').click()
cy.contains('.tabular.menu a', 'Pass rate chart').click()
cy.get('div.modeSelectorContainer').click()
cy.contains('svg', 'Pass rate chart')
cy.contains('.tabular.menu a', 'Grade distribution chart').click()
cy.get('div.modeSelectorContainer').click()
cy.contains('svg', 'Grades')
cy.contains('a', 'New query').click()
cy.contains('Search for courses')
})
// Statistics
const yearRange = { from: '2000-2001', to: '2020-2021' }
const attemptsTableContents = [
// [time, passed, failed, passrate]
['Total', 511, 486, 25],
]
/*
const studentTableContents = [
// time, students, passedfirsttry, passedeventually, pass%, neverpassed, fail%]
['Total', 289, 254, 13, '92.39 %', 22, '7.61 %'],
['2017-18', 157, 132, 5, '87.26 %', 20, '12.74 %'],
['2016-17', 62, 60, 1, '98.39 %', 1, '1.61 %'],
['2015-16', 27, 25, 2, '100.00 %', 0, '0.00 %'],
['2014-15', 16, 16, 0, '100.00 %', 0, '0.00 %'],
['2013-14', 5, 3, 2, '100.00 %', 0, '0.00 %'],
]
*/
it('Searching multiple courses having substitution mappings shows course statistics', () => {
cy.url().should('include', '/coursestatistics')
cy.contains('Search for courses')
cy.get("input[placeholder='Search by a course code']").type('TKT')
cy.contains('td', /^TKT20001/).click()
cy.contains('td', /^TKT10002/).click()
cy.contains('Fetch statistics').should('be.enabled').click()
cy.contains('Search for courses').should('not.exist')
const gradesTableContents = [
// [time, attempts, 0, 1, 2, 3, 4, 5, other passed]
['Total', 511, 25, 11, 17, 21, 59, 365, 13],
['2020-2021', 1, 0, 0, 0, 0, 0, 1, 0],
['2019-2020', 54, 4, 3, 1, 3, 4, 39, 0],
['2018-2019', 154, 7, 2, 6, 5, 15, 119, 0],
['2017-2018', 186, 9, 3, 5, 6, 16, 139, 8],
['2016-2017', 71, 2, 1, 2, 5, 13, 48, 0],
['2015-2016', 21, 1, 1, 1, 0, 7, 8, 3],
]
cy.contains('.courseNameCell', 'Tietorakenteet ja algoritmit').contains('TKT20001').click()
cy.contains('.courseNameCell', 'Ohjelmoinnin perusteet').should('not.exist')
cy.contains('TKT20001')
cy.contains('58131') // old mapped code
cy.contains('Summary').click()
it('shows stats', () => {
// Time range
cy.get("div[name='fromYear']").within(() => {
cy.get("div[role='option']").first().should('have.text', yearRange.to)
cy.contains("div[role='option']", yearRange.from).should('have.class', 'selected')
cy.get("div[role='option']").last().should('have.text', '2000-2001')
cy.get("div[role='option']").should('have.length', 21)
})
cy.get("div[name='toYear']").within(() => {
cy.get("div[role='option']").first().should('have.text', '2020-2021')
cy.contains("div[role='option']", yearRange.to).should('have.class', 'selected')
cy.get("div[role='option']").last().should('have.text', yearRange.from)
cy.get("div[role='option']").should('have.length', 21)
})
cy.contains('Show population').should('not.be.enabled')
cy.contains('.courseNameCell', 'Ohjelmoinnin perusteet').contains('TKT10002').click()
cy.contains('.courseNameCell', 'Käyttöjärjestelmät').should('not.exist')
cy.contains('TKT10002')
cy.contains('581325') // old mapped code
cy.contains('Summary').click()
/*
const timeRangeFilter =
timeRange =>
([time]) => {
return (timeRange.from <= time && time <= timeRange.to) || time === 'Total'
}
*/
cy.contains('#CourseStatPanes a.item', 'Table').click()
cy.contains('#CourseStatPanes a.item', 'Attempts').click()
cy.get('#CourseStatPanes table>tbody').within(() => {
attemptsTableContents.forEach((values, trIndex) => {
cy.get('tr')
.eq(trIndex)
.within(() => {
values.forEach((value, tdIndex) => {
cy.get('td').eq(tdIndex).contains(value)
})
})
})
cy.get('tr').should('have.length', 17)
})
cy.get('[data-cy=gradeToggle]', { force: true }).click({ force: true })
cy.get('#CourseStatPanes table>tbody').within(() => {
gradesTableContents.forEach((values, trIndex) => {
cy.get('tr')
.eq(trIndex)
.within(() => {
values.forEach((value, tdIndex) => {
cy.get('td').eq(tdIndex).contains(value)
})
})
})
cy.get('tr').should('have.length', 17)
})
cy.contains('a', 'New query').click()
cy.contains('Search for courses')
})
it('after changing time range shows same stats', () => {
const newYearRange = { from: '2016-2017', to: '2019-2020' }
cy.get("div[name='fromYear']")
.click()
.within(() => {
cy.contains(newYearRange.from).click()
})
cy.get("div[name='toYear']")
.click()
.within(() => {
cy.contains(newYearRange.to).click()
})
it('On consecutive searches should not crash and search should work', () => {
cy.url().should('include', '/coursestatistics')
cy.contains('Search for courses')
cy.get("input[placeholder='Search by a course code']").type('TKT20003')
cy.contains('td', 'TKT20003').click()
cy.contains('Fetch statistics').should('be.enabled').click()
cy.contains('Show population')
cy.contains('TKT20003')
// Time range
cy.get("div[name='fromYear']").within(() => {
cy.get("div[role='option']").first().should('have.text', newYearRange.to)
cy.contains("div[role='option']", newYearRange.from).should('have.class', 'selected')
cy.get("div[role='option']").last().should('have.text', '2000-2001')
cy.get("div[role='option']").should('have.length', 20)
})
cy.get("div[name='toYear']").within(() => {
cy.get("div[role='option']").first().should('have.text', '2020-2021')
cy.contains("div[role='option']", newYearRange.to).should('have.class', 'selected')
cy.get("div[role='option']").last().should('have.text', newYearRange.from)
cy.get("div[role='option']").should('have.length', 5)
})
cy.contains('Show population').should('be.enabled')
cy.contains('Course statistics').click()
cy.contains('Search for courses')
cy.get("input[placeholder='Search by a course code']").type('TKT20001')
cy.contains('td', 'TKT20001').click()
cy.contains('Fetch statistics').should('be.enabled').click()
cy.contains('Show population')
cy.contains('TKT20001')
})
/*
// Filters
// Data currently has only "Muu" so we can't test these yet! See
// https://github.com/UniversityOfHelsinkiCS/oodikone/issues/3058
// const primaryContents = [
// // [studentcount, label, code]
// [179, "Tietojenkäsittelytieteen kandiohjelma", "KH50_005"],
// [64, "Matemaattisten tieteiden kandiohjelma", "KH50_001"],
// [24, "Tietojenkäsittelytieteen koulutusohjelma", "520080"],
// [16, "Tietojenkäsittelytieteen maisteriohjelma", "MH50_009"],
// [9, "Matematiikan koulutusohjelma", "520070"],
// ];
// const totalStudyProgrammes = 20;
// cy.get("div[name='primary']").within(() => {
// primaryContents.forEach(([count, label, code], i) => {
// cy.get("div[role='option']")
// .eq(i)
// .within(() => {
// cy.contains(label);
// cy.contains("div>div:last-child>div.label", code);
// cy.contains("div>div:first-child>div.label", count);
// });
// });
// cy.get("div[role='option']")
// .eq(10)
// .within(() => {
// cy.contains("Muu");
// cy.contains("div>div:last-child>div.label", "OTHER");
// cy.contains("div>div:first-child>div.label", "2");
// });
// cy.get("div[role='option']").should(
// "have.length",
// totalStudyProgrammes
// );
// });
// const comparisonContents = [[289, "All", "ALL"], ...primaryContents];
// cy.get("div[name='comparison']").within(() => {
// comparisonContents.forEach(([count, label, code], i) => {
// cy.get("div[role='option']")
// .eq(i)
// .within(() => {
// cy.contains(label);
// cy.contains("div>div:last-child>div.label", code);
// cy.contains("div>div:first-child>div.label", count);
// });
// });
// cy.get("div[role='option']")
// .eq(11)
// .within(() => {
// cy.contains("Muu");
// cy.contains("div>div:last-child>div.label", "OTHER");
// cy.contains("div>div:first-child>div.label", "2");
// });
// cy.get("div[role='option']").should(
// "have.length",
// totalStudyProgrammes + 1
// ); // +1 comes from already selected option
// });
// cy.contains("Select excluded study programmes").should("be.disabled");
it('Shows statistics when separating by semester', () => {
const courseCode = 'DATA11002'
cy.url().should('include', '/coursestatistics')
cy.contains('Search for courses')
cy.get("input[placeholder='Search by a course code']").type(courseCode)
cy.contains('td', courseCode).click()
cy.contains('Separate statistics for Spring and Fall semesters').click()
cy.contains('Fetch statistics').should('be.enabled').click()
cy.contains('Syksy 2019')
cy.contains('Syksy 2017')
cy.get('tbody').contains('19')
cy.contains(courseCode)
})
// Filters
// Data currently has only "Muu" so we can't test these yet! See
// https://github.com/UniversityOfHelsinkiCS/oodikone/issues/3058
//const primaryContentsAfterTimeChange = [
// // [studentcount, label, code]
// [79, "Tietojenkäsittelytieteen kandiohjelma", "KH50_005"],
// [15, "Tietojenkäsittelytieteen koulutusohjelma", "520080"],
// [10, "Tietojenkäsittelytieteen maisteriohjelma", "MH50_009"],
// [7, "Matemaattisten tieteiden kandiohjelma", "KH50_001"],
//];
//const totalStudyProgrammesAfterTimeChange = 13;
//cy.get("div[name='primary']").within(() => {
// primaryContentsAfterTimeChange.forEach(([count, label, code], i) => {
// cy.get("div[role='option']")
// .eq(i)
// .within(() => {
// cy.contains(label);
// cy.contains("div>div:last-child>div.label", code);
// cy.contains("div>div:first-child>div.label", count);
// });
// });
// cy.get("div[role='option']")
// .eq(5)
// .within(() => {
// cy.contains("Muu");
// cy.contains("div>div:last-child>div.label", "OTHER");
// cy.contains("div>div:first-child>div.label", "2");
// });
// cy.get("div[role='option']").should(
// "have.length",
// totalStudyProgrammesAfterTimeChange
// );
//});
//const comparisonContentsAfterTimeChange = [
// [107, "All", "ALL"],
// ...primaryContentsAfterTimeChange,
//];
//cy.get("div[name='comparison']").within(() => {
// comparisonContentsAfterTimeChange.forEach(([count, label, code], i) => {
// cy.get("div[role='option']")
// .eq(i)
// .within(() => {
// cy.contains(label);
// cy.contains("div>div:last-child>div.label", code);
// cy.contains("div>div:first-child>div.label", count);
// });
// });
// cy.get("div[role='option']")
// .eq(6)
// .within(() => {
// cy.contains("Muu");
// cy.contains("div>div:last-child>div.label", "OTHER");
// cy.contains("div>div:first-child>div.label", "2");
// });
// cy.get("div[role='option']").should(
// "have.length",
// totalStudyProgrammesAfterTimeChange + 1
// ); // +1 comes from already selected option
//});
//cy.contains("Select excluded study programmes").should("be.disabled");
it('Searching course by name displays right courses', () => {
cy.url().should('include', '/coursestatistics')
cy.contains('Search for courses')
cy.get("input[placeholder='Search by entering a course name']").type('tietokantojen perusteet')
const timeAttemptsTableContents = [
// [time, passed, failed, passrate]
["Total", 113, 109, 4, "96.46 %"],
["2017-18", 204, 175, 29, "85.78 %"],
["2016-17", 68, 66, 2, "97.06 %"],
["2015-16", 29, 27, 2, "93.10 %"],
["2014-15", 16, 16, 0, "100.00 %"],
["2013-14", 8, 6, 2, "75.00 %"],
];
cy.contains('Tietokantojen perusteet')
cy.contains('Avoin yo: Tietokantojen perusteet')
cy.contains('A581328')
cy.contains('AYTKT10004')
cy.contains('TKT10004, 581328')
cy.contains('td', /^TKT10004/).click()
const timeStudentTableContents = [
// time, students, passedfirsttry, passedeventually, pass%, neverpassed, fail%]
["Total", 105, 101, 3, "99.05 %", 1, "0.95 %"],
["2017-18", 195, 131, 43, "89.23 %", 1, "10.77 %"],
["2016-17", 62, 60, 1, "98.39 %", 1, "1.61 %"],
["2015-16", 27, 25, 2, "100.00 %", 0, "0.00 %"],
["2014-15", 16, 16, 0, "100.00 %", 0, "0.00 %"],
["2013-14", 5, 2, 3, "100.00 %", 0, "0.00 %"],
];
cy.contains('Fetch statistics').should('be.enabled').click()
cy.contains('Search for courses').should('not.exist')
const timeGradesTableContents = [
// [time, attempts, 0, 1, 2, 3, 4, 5, other passed]
["Total", 113, 4, 1, 2, 7, 15, 76, 8],
["2017-18", 204, 29, 5, 8, 10, 16, 127, 9],
["2016-17", 68, 2, 0, 1, 6, 10, 49, 0],
["2015-16", 29, 2, 1, 1, 0, 5, 16, 4],
["2014-15", 16, 0, 0, 0, 1, 0, 11, 4],
["2013-14", 8, 2, 0, 0, 2, 0, 4, 0],
];
cy.contains('TKT10004, 581328 Tietokantojen perusteet')
cy.get('.right').click()
cy.contains('No results')
cy.get("input[placeholder='Search by entering a course name']").type('tietokantojen perusteet')
cy.contains('td', /^AYTKT10004/).click()
// Not working since only "Muu" students
it("after changing filters shows different stats", () => {
cy.get("div[name='primary']")
.click()
.within(() => {
cy.contains(
"div[role='option']",
"Tietojenkäsittelytieteen kandiohjelma"
).click();
});
cy.get("body").type("{esc}"); // close dropdown
cy.contains('Fetch statistics').should('be.enabled').click()
cy.contains('Search for courses').should('not.exist')
cy.contains('AYTKT10004 Avoin yo: Tietokantojen perusteet')
})
// Time range (should not have changed)
cy.get("div[name='fromYear']").within(() => {
cy.get("div[role='option']").first().should("have.text", yearRange.to);
cy.contains("div[role='option']", yearRange.from).should(
"have.class",
"selected"
);
cy.get("div[role='option']").last().should("have.text", "2000-2001");
cy.get("div[role='option']").should("have.length", 21);
});
cy.get("div[name='toYear']").within(() => {
cy.get("div[role='option']").first().should("have.text", "2020-2021");
cy.contains("div[role='option']", yearRange.to).should(
"have.class",
"selected"
);
cy.get("div[role='option']").last().should("have.text", yearRange.from);
cy.get("div[role='option']").should("have.length", );
});
cy.contains("Show population").should("be.enabled");
it('Searching course by name displays right courses, 10 credit courses', () => {
cy.url().should('include', '/coursestatistics')
cy.contains('Search for courses')
cy.get("input[placeholder='Search by entering a course name']").type('tietorakenteet ja algoritmit')
// Filters
// Data currently has only "Muu" so we can't test these yet! See
// https://github.com/UniversityOfHelsinkiCS/oodikone/issues/3058
const primaryContentsAfterFilterChange = [
// [studentcount, label, code]
[289, "All", "ALL"],
[64, "Matemaattisten tieteiden kandiohjelma", "KH50_001"],
[24, "Tietojenkäsittelytieteen koulutusohjelma", "520080"],
[16, "Tietojenkäsittelytieteen maisteriohjelma", "MH50_009"],
[9, "Matematiikan koulutusohjelma", "520070"],
];
const totalStudyProgrammesAfterFilterChange = 20;
cy.get("div[name='primary']").within(() => {
primaryContentsAfterFilterChange.forEach(([count, label, code], i) => {
cy.get("div[role='option']")
.eq(i)
.within(() => {
cy.contains(label);
cy.contains("div>div:last-child>div.label", code);
cy.contains("div>div:first-child>div.label", count);
});
});
cy.get("div[role='option']").should(
"have.length",
totalStudyProgrammesAfterFilterChange
);
});
const comparisonContentsAfterFilterChange = [
// [studentcount, label, code]
[289, "All", "ALL"],
[179, "Tietojenkäsittelytieteen kandiohjelma", "KH50_005"],
[122, "Excluded", "EXCLUDED"],
[64, "Matemaattisten tieteiden kandiohjelma", "KH50_001"],
[24, "Tietojenkäsittelytieteen koulutusohjelma", "520080"],
[16, "Tietojenkäsittelytieteen maisteriohjelma", "MH50_009"],
[9, "Matematiikan koulutusohjelma", "520070"],
];
cy.get("div[name='comparison']").within(() => {
comparisonContentsAfterFilterChange.forEach(
([count, label, code], i) => {
cy.get("div[role='option']")
.eq(i)
.within(() => {
cy.contains(label);
cy.contains("div>div:last-child>div.label", code);
cy.contains("div>div:first-child>div.label", count);
});
}
);
cy.get("div[role='option']").should(
"have.length",
totalStudyProgrammesAfterFilterChange + 2
); // +2 comes from EXCLUDED and the filtered programme
});
cy.contains("Select excluded study programmes")
.should("be.enabled")
.click();
cy.contains('Tietorakenteet ja algoritmit')
cy.contains('Avoin yo: Tietorakenteet ja algoritmit')
cy.contains('AYTKT20001')
cy.contains('TKT20001, 58131')
cy.contains('td', /^TKT20001/).click()
// Statistics
cy.contains("#CourseStatPanes a.item", "Table").click();
cy.contains("#CourseStatPanes a.item", "Attempts").click();
cy.get("#PrimaryDataTable table>tbody").within(() => {
const attemptsTableContents = [
// [time, passed, failed, passrate]
["Total", 227, 215, 12, "94.71 %"],
["2017-18", 136, 126, 10, "92.65 %"],
["2016-17", 57, 56, 1, "98.25 %"],
["2015-16", 21, 20, 1, "95.24 %"],
["2014-15", 6, 6, 0, "100.00 %"],
];
attemptsTableContents.forEach((values, trIndex) => {
cy.get("tr")
.eq(trIndex)
.within(() => {
values.forEach((value, tdIndex) => {
cy.get("td").eq(tdIndex).contains(value);
});
});
});
cy.get("tr").should("have.length", 14);
});
cy.get("#ComparisonDataTable table>tbody").within(() => {
const attemptsTableContents = [
// [time, passed, failed, passrate]
["Total", 140, 110, 30, "78.57 %"],
["2017-18", 71, 52, 19, "73.24 %"],
["2016-17", 16, 15, 1, "93.75 %"],
["2015-16", 10, 9, 1, "90.00 %"],
["2014-15", 11, 11, 0, "100.00 %"],
];
attemptsTableContents.forEach((values, trIndex) => {
cy.get("tr")
.eq(trIndex)
.within(() => {
values.forEach((value, tdIndex) => {
cy.get("td").eq(tdIndex).contains(value);
});
});
});
cy.get("tr").should("have.length", 14);
});
cy.contains('Fetch statistics').should('be.enabled').click()
cy.contains('Search for courses').should('not.exist')
cy.contains("#CourseStatPanes a.item", "Table").click();
cy.contains("#CourseStatPanes a.item", "Students").click();
cy.get("#PrimaryDataTable table>tbody").within(() => {
const studentTableContents = [
// time, students, passedfirsttry, eventuallypassed, pass%, neverpassed, fail%]
["Total", 179, 168, 6, "97.21 %", 5, "2.79 %"],
["2017-18", 95, 86, 4, "94.74 %", 5, "5.26 %"],
["2016-17", 53, 52, 1, "100.00 %", 0, "0.00 %"],
["2015-16", 19, 18, 1, "100.00 %", 0, "0.00 %"],
["2014-15", 6, 6, 0, "100.00 %", 0, "0.00 %"],
];
studentTableContents.forEach((values, trIndex) => {
cy.get("tr")
.eq(trIndex)
.within(() => {
values.forEach((value, tdIndex) => {
cy.get("td").eq(tdIndex).contains(value);
});
});
});
cy.get("tr").should("have.length", 14);
});
cy.get("#ComparisonDataTable table>tbody").within(() => {
const studentTableContents = [
// time, students, passedfirsttry, passedeventually, pass%, neverpassed, fail%]
["Total", 122, 98, 7, "86.07 %", 17, "13.93 %"],
["2017-18", 65, 49, 1, "76.92 %", 15, "23.08 %"],
["2016-17", 13, 12, 0, "92.31 %", 1, "7.69 %"],
["2015-16", 10, 9, 1, "100.00 %", 0, "0.00 %"],
["2014-15", 11, 11, 0, "100.00 %", 0, "0.00 %"],
];
studentTableContents.forEach((values, trIndex) => {
cy.get("tr")
.eq(trIndex)
.within(() => {
values.forEach((value, tdIndex) => {
cy.get("td").eq(tdIndex).contains(value);
});
});
});
cy.get("tr").should("have.length", 14);
});
cy.contains('TKT20001, 58131 Tietorakenteet ja algoritmit')
cy.get('.right').click()
cy.contains('No results')
cy.contains("#CourseStatPanes a.item", "Table").click();
cy.contains("#CourseStatPanes a.item", "Attempts").click();
cy.get("[data-cy=gradeToggle]", { force: true }).click({ force: true });
cy.get("input[placeholder='Search by entering a course name']").type('tietorakenteet ja algoritmit')
cy.contains('td', /^AYTKT20001/).click()
cy.get("#PrimaryDataTable table>tbody").within(() => {
const gradesTableContents = [
// [time, attempts, 0, 1, 2, 3, 4, 5, other passed]
["Total", 227, 12, 4, 7, 11, 18, 161, 14],
["2017-18", 136, 10, 3, 4, 4, 4, 102, 9],
["2016-17", 57, 1, 0, 1, 6, 8, 41, 0],
["2015-16", 21, 1, 1, 1, 0, 5, 11, 2],
["2014-15", 6, 0, 0, 0, 0, 0, 3, 3],
["2013-14", 1, 0, 0, 0, 0, 0, 1, 0],
];
gradesTableContents.forEach((values, trIndex) => {
cy.get("tr")
.eq(trIndex)
.within(() => {
values.forEach((value, tdIndex) => {
cy.get("td").eq(tdIndex).contains(value);
});
});
});
cy.get("tr").should("have.length", 14);
});
cy.get("#ComparisonDataTable table>tbody").within(() => {
const gradesTableContents = [
// [time, attempts, 0, 1, 2, 3, 4, 5, other passed]
["Total", 140, 30, 2, 5, 13, 20, 67, 3],
["2017-18", 71, 19, 2, 4, 6, 13, 27, 0],
["2016-17", 16, 1, 0, 0, 0, 3, 12, 0],
["2015-16", 10, 1, 0, 0, 0, 0, 7, 2],
["2014-15", 11, 0, 0, 0, 1, 0, 9, 1],
["2013-14", 8, 2, 0, 0, 2, 0, 4, 0],
];
gradesTableContents.forEach((values, trIndex) => {
cy.get("tr")
.eq(trIndex)
.within(() => {
values.forEach((value, tdIndex) => {
cy.get("td").eq(tdIndex).contains(value);
});
});
});
cy.get("tr").should("have.length", 14);
});
});
});
*/
cy.contains('Fetch statistics').should('be.enabled').click()
cy.contains('Search for courses').should('not.exist')
cy.contains('AYTKT20001 Avoin yo: Tietorakenteet ja algoritmit')
it('Some features of Course Statistics are hidden for courseStatistics-users', () => {
cy.contains('Users').click()
cy.contains('Oodikone users')
cy.contains('mocking').should('not.exist')
cy.contains('tr', 'User manager').within(() => {
cy.contains('.button', 'Edit').click()
})
cy.get('.right').click()
cy.contains('No results')
cy.get("input[name='unifyOpenUniCourses']").parent().click()
cy.get("input[placeholder='Search by entering a course name']").type('tietorakenteet ja algoritmit')
cy.contains('td', 'TKT20001, 58131, AYTKT20001').click()
cy.get('[data-cy=access-groups-form]').click().contains('courseStatistics').click()
cy.get('[data-cy=access-groups-save]').click()
cy.contains('Fetch statistics').should('be.enabled').click()
cy.contains('Search for courses').should('not.exist')
cy.contains('TKT20001, 58131, AYTKT20001 Tietorakenteet ja algoritmit')
})
cy.route('POST', '/api/superlogin/usermk').as('superlogin')
cy.get('i.spy').click()
cy.wait('@superlogin')
cy.contains('mocking as usermk')
// eslint-disable-next-line cypress/no-unnecessary-waiting
cy.wait(1000)
cy.get('[data-cy=navbar-courseStatistics]').click()
cy.get('[data-cy=course-code-input]').type('TKT20003')
it('Can find course population', () => {
cy.url().should('include', '/coursestatistics')
cy.contains('Search for courses')
cy.get("input[placeholder='Search by a course code']").type('TKT20003')
cy.contains('tr', 'TKT20003').click()
cy.contains('Fetch statistics').should('be.enabled').click()
cy.contains('Filter statistics by study programmes').should('not.exist')
cy.contains('Faculty statistics').should('not.exist')
cy.contains('Show population').should('not.exist')
cy.contains('Käyttöjärjestelmät')
cy.contains('TKT20003')
cy.get(':nth-child(3) > :nth-child(1) > div > .item > .level').click()
cy.contains('Population of course Käyttöjärjestelmät 2019-2020')
cy.contains('TKT20003')
cy.contains('Students (127)').click()
cy.contains('010135486')
cy.contains('010431753')
})
it('Population of course shows grades for each student', () => {
cy.get("input[placeholder='Search by a course code']").type('TKT20001')
cy.contains(/^TKT20001$/).click()
cy.contains('Fetch statistics').should('be.enabled').click()
cy.contains('TKT20001, 58131 Tietorakenteet ja algoritmit')
cy.get(':nth-child(3) > :nth-child(1) > div > .item > .level').click()
cy.contains('Population of course Tietorakenteet ja algoritmit 2018-2019')
cy.contains('Students (197)').click()
cy.contains('td', '010262566').siblings().contains('4')
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')
cy.contains('Search for courses')
cy.get("input[name='unifyOpenUniCourses']").parent().click()
cy.get("input[placeholder='Search by a course code']").type('TKT10002')
cy.contains('td', 'TKT10002, AYTKT10002').click()
cy.contains('Fetch statistics').should('be.enabled').click()
cy.contains('Search for courses').should('not.exist')
cy.contains('TKT10002, 581325, AYTKT10002, A581325 Ohjelmoinnin perusteet')
})
// Statistics
const yearRange = { from: '2000-2001', to: '2020-2021' }
const attemptsTableContents = [
// [time, passed, failed, passrate]
['Total*', 288, 213, 75, '73.96 %'],
['2020-2021', '5 or less students', 'NA', 'NA', 'NA'],
['2019-2020', 164, 121, 43, '73.78 %'],
['2018-2019', 85, 60, 25, '70.59 %'],
['2017-2018', 39, 32, 7, '82.05 %'],
['Total', 511, 486, 25],
]
cy.contains('#CourseStatPanes a.item', 'Attempts').click()
cy.get('#CourseStatPanes table>tbody').within(() => {
attemptsTableContents.forEach((values, trIndex) => {
cy.get('tr')
.eq(trIndex)
.within(() => {
values.forEach((value, tdIndex) => {
cy.get('td').eq(tdIndex).contains(value)
const gradesTableContents = [
// [time, attempts, 0, 1, 2, 3, 4, 5, other passed]
['Total', 511, 25, 11, 17, 21, 59, 365, 13],
['2020-2021', 1, 0, 0, 0, 0, 0, 1, 0],
['2019-2020', 54, 4, 3, 1, 3, 4, 39, 0],
['2018-2019', 154, 7, 2, 6, 5, 15, 119, 0],
['2017-2018', 186, 9, 3, 5, 6, 16, 139, 8],
['2016-2017', 71, 2, 1, 2, 5, 13, 48, 0],
['2015-2016', 21, 1, 1, 1, 0, 7, 8, 3],
]
it('shows stats', () => {
// Time range
cy.get("div[name='fromYear']").within(() => {
cy.get("div[role='option']").first().should('have.text', yearRange.to)
cy.contains("div[role='option']", yearRange.from).should('have.class', 'selected')
cy.get("div[role='option']").last().should('have.text', '2000-2001')
cy.get("div[role='option']").should('have.length', 21)
})
cy.get("div[name='toYear']").within(() => {
cy.get("div[role='option']").first().should('have.text', '2020-2021')
cy.contains("div[role='option']", yearRange.to).should('have.class', 'selected')
cy.get("div[role='option']").last().should('have.text', yearRange.from)
cy.get("div[role='option']").should('have.length', 21)
})
cy.contains('Show population').should('not.be.enabled')
cy.contains('#CourseStatPanes a.item', 'Table').click()
cy.contains('#CourseStatPanes a.item', 'Attempts').click()
cy.get('#CourseStatPanes table>tbody').within(() => {
attemptsTableContents.forEach((values, trIndex) => {
cy.get('tr')
.eq(trIndex)
.within(() => {
values.forEach((value, tdIndex) => {
cy.get('td').eq(tdIndex).contains(value)
})
})
})
cy.get('tr').should('have.length', 17)
})
cy.get('[data-cy=gradeToggle]', { force: true }).click({ force: true })
cy.get('#CourseStatPanes table>tbody').within(() => {
gradesTableContents.forEach((values, trIndex) => {
cy.get('tr')
.eq(trIndex)
.within(() => {
values.forEach((value, tdIndex) => {
cy.get('td').eq(tdIndex).contains(value)
})
})
})
cy.get('tr').should('have.length', 17)
})
})
it('after changing time range shows same stats', () => {
const newYearRange = { from: '2016-2017', to: '2019-2020' }
cy.get("div[name='fromYear']")
.click()
.within(() => {
cy.contains(newYearRange.from).click()
})
cy.get("div[name='toYear']")
.click()
.within(() => {
cy.contains(newYearRange.to).click()
})
// Time range
cy.get("div[name='fromYear']").within(() => {
cy.get("div[role='option']").first().should('have.text', newYearRange.to)
cy.contains("div[role='option']", newYearRange.from).should('have.class', 'selected')
cy.get("div[role='option']").last().should('have.text', '2000-2001')
cy.get("div[role='option']").should('have.length', 20)
})
cy.get("div[name='toYear']").within(() => {
cy.get("div[role='option']").first().should('have.text', '2020-2021')
cy.contains("div[role='option']", newYearRange.to).should('have.class', 'selected')
cy.get("div[role='option']").last().should('have.text', newYearRange.from)
cy.get("div[role='option']").should('have.length', 5)
})
cy.contains('Show population').should('be.enabled')
})
})
})
it('Some features of Course Statistics are hidden for courseStatistics-users without other rights', () => {
cy.init('/coursestatistics', 'onlycoursestatistics')
cy.get('[data-cy=navbar-courseStatistics]').click()
cy.get('[data-cy=course-code-input]').type('TKT20003')
cy.contains('tr', 'TKT20003').click()
cy.contains('Fetch statistics').should('be.enabled').click()
cy.contains('Filter statistics by study programmes').should('not.exist')
cy.contains('Faculty statistics').should('not.exist')
cy.contains('Show population').should('not.exist')
const attemptsTableContents = [
// [time, passed, failed, passrate]
['Total*', 288, 213, 75, '73.96 %'],
['2020-2021', '5 or less students', 'NA', 'NA', 'NA'],
['2019-2020', 164, 121, 43, '73.78 %'],
['2018-2019', 85, 60, 25, '70.59 %'],
['2017-2018', 39, 32, 7, '82.05 %'],
]
cy.contains('#CourseStatPanes a.item', 'Attempts').click()
cy.get('#CourseStatPanes table>tbody').within(() => {
attemptsTableContents.forEach((values, trIndex) => {
cy.get('tr')
.eq(trIndex)
.within(() => {
values.forEach((value, tdIndex) => {
cy.get('td').eq(tdIndex).contains(value)
})
})
cy.get('tr').should('have.length', 7)
})
})
cy.contains('Stop mocking').click()
// eslint-disable-next-line cypress/no-unnecessary-waiting
cy.wait(1000)
cy.contains('Users').click()
cy.contains('Oodikone users')
cy.contains('tr', 'User manager').within(() => {
cy.contains('.button', 'Edit').click()
})
cy.contains('Access Groups')
.siblings()
.within(() => {
cy.get("i[class='dropdown icon clear']").click()
})
cy.get('[data-cy=access-groups-save]').click()
cy.get('tr').should('have.length', 7)
})
})
})
+10 -25
View File
@@ -74,15 +74,6 @@ const containsSpecificStudents = (studentnumbers = []) => {
studentnumbers.forEach(s => cy.contains(s))
}
const loginAs = username => {
cy.contains('Users').click()
cy.contains(username).siblings().contains('Edit').click()
cy.get('.spy').click()
cy.visit(`${Cypress.config().baseUrl}/custompopulation`)
cy.url().should('include', '/custompopulation')
cy.contains('Custom population')
}
describe('Custom population tests', () => {
const nonExistentStudentNumbers = ['123', 'X', '-', ' ']
beforeEach(() => {
@@ -128,22 +119,16 @@ describe('Custom population tests', () => {
cy.get('button').contains('Custom population')
})
// Check login as when anon user database rights have been corrected
// it("Doesn't return students to whom the user doesn't have rights to", () => {
// const kasvatusStudents = ["010102273", "010112616", "010116531"];
// const kapistelyStudents = ["010623419", "010942404", "010484975"];
// loginAs("Normaalikäyttäjä");
// searchFor(kasvatusStudents);
// cy.contains("Credit accumulation").should("not.exist");
// cy.contains("Programme distribution").should("not.exist");
// cy.contains("Courses of population").should("not.exist");
// searchFor(kapistelyStudents.concat(kasvatusStudents));
// hasLanded();
// containsAmountOfStudents(3);
// containsSpecificStudents(kapistelyStudents);
// });
it("Doesn't return students user has no right to", () => {
// TODO: check that this doesn't create false positives, since this test goes
// through even if following students are not in anon sis-db.
const studentsForEduBachStudents = ['014990067', '013069465', '014853890']
searchFor(studentsForEduBachStudents)
cy.contains('Credit accumulation').should('not.exist')
cy.contains('Programme distribution').should('not.exist')
cy.contains('Courses of population').should('not.exist')
cy.get('button').contains('Custom population')
})
})
describe('Custom population search saving', () => {
+105 -76
View File
@@ -1,6 +1,6 @@
/// <reference types="Cypress" />
const baseUrl = Cypress.config().baseUrl
// const baseUrl = Cypress.config().baseUrl
const checkFilteringResult = studentCount => {
cy.contains(`Students (${studentCount})`)
@@ -9,23 +9,24 @@ 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()
return (filterName, testStepFunctionToRun) => {
cy.get(`[data-cy="${filterName}-header"]`).click()
checkFilteringResult(amountWithoutFiltering)
testStepFunctionToRun()
checkFilteringResult(amountWithoutFiltering)
cy.cs(dataCyAttributeOfHeaderToClick).click()
cy.get(`[data-cy="${filterName}-header"]`).click({ force: true })
}
}
@@ -36,116 +37,136 @@ describe('Population Statistics', () => {
const runTestStepWithPreAndPostParts = createRunTestStepWithPreAndPostPartsFunction(defaultAmountOfStudents)
before(() => {
cy.init()
cy.visit(baseUrl.concat(pathToCSBach2018))
cy.init(pathToCSBach2018)
})
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')
const card = cy.cs('TransferredToProgramme-filter-card')
card.cs('TransferredToProgramme-header').click()
card.get('[data-cy="option-havenot"] input').should('be.checked')
card.cs('TransferredToProgramme-header').click()
})
it('Graduation filter works', () => {
runTestStepWithPreAndPostParts('graduatedFromProgrammeFilter-header', () => {
runTestStepWithPreAndPostParts('GraduatedFromProgramme', () => {
const card = cy.cs('GraduatedFromProgramme-filter-card')
const graduated = 11
cy.cs('graduatedFromProgrammeFilter-graduated-true').click()
card.cs('option-graduated-true').click()
checkFilteringResult(graduated)
cy.cs('graduatedFromProgrammeFilter-graduated-false').click()
card.cs('option-graduated-false').click()
checkFilteringResult(defaultAmountOfStudents - graduated)
cy.cs('graduatedFromProgrammeFilter-all').click()
card.get('[data-cy="GraduatedFromProgramme-filter-card"] [data-cy="option-all"]').click()
})
})
it('Transfer filter works', () => {
runTestStepWithPreAndPostParts('transferredToProgrammeFilter-header', () => {
runTestStepWithPreAndPostParts('TransferredToProgramme', () => {
const transferred = 1
cy.cs('transferredToProgrammeFilter-have').click()
cy.cs('option-have').click()
checkFilteringResult(transferred)
cy.cs('transferredToProgrammeFilter-all').click()
cy.cs('option-all').click()
checkFilteringResult(defaultAmountOfStudents + transferred)
cy.cs('transferredToProgrammeFilter-havenot').click()
cy.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(41)
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('Courses filter works', () => {
runTestStepWithPreAndPostParts('courseFilter-header', () => {
// 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.cs('admissionTypeFilter-dropdown').selectFromDropdown(2)
checkFilteringResult(10)
clearSingleDropdownSelection('admissionTypeFilter-dropdown')
})
})
// plz fix
it.skip('Courses filter works', () => {
// courses takes some time to load, wait for it to complete
cy.wait(10000)
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 card = cy.cs('GraduatedFromProgramme-filter-card')
card.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()
card.cs('option-all').click()
})
})
})
@@ -158,12 +179,11 @@ describe('Course Statistics', () => {
const runTestStepWithPreAndPostParts = createRunTestStepWithPreAndPostPartsFunction(defaultAmountOfStudents)
before(() => {
cy.init()
cy.visit(baseUrl.concat(pathToDSAndAlgoSpring2019))
cy.init(pathToDSAndAlgoSpring2019)
})
it('Grade filter works', () => {
runTestStepWithPreAndPostParts('gradeFilter-header', () => {
runTestStepWithPreAndPostParts('Grade', () => {
cy.cs('gradeFilter-5').click()
checkFilteringResult(20)
cy.cs('gradeFilter-3').click()
@@ -174,43 +194,43 @@ describe('Course Statistics', () => {
})
it('Age filter works', () => {
runTestStepWithPreAndPostParts('ageFilter-header', () => {
runTestStepWithPreAndPostParts('Age', () => {
cy.cs('ageFilter-min').type('20')
cy.cs('ageFilter-max').type('40')
checkFilteringResult(33)
checkFilteringResult(32)
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')
@@ -232,13 +252,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)
@@ -248,35 +269,43 @@ 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)
runTestStepWithPreAndPostParts('Gender', () => {
cy.cs('genderFilter-dropdown').selectFromDropdown(0)
checkFilteringResult(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-header', () => {
cy.selectFromDropdown('startYearAtUni-dropdown', [0])
runTestStepWithPreAndPostParts('StartYearAtUni', () => {
cy.cs('startYearAtUni-dropdown').selectFromDropdown([0])
checkFilteringResult(1)
clearSemanticUIMultipleDropDownSelection('startYearAtUni-dropdown')
cy.cs('startYearAtUni-dropdown').click()
})
})
it('Courses filter works', () => {
runTestStepWithPreAndPostParts('courseFilter-header', () => {
runTestStepWithPreAndPostParts('Courses', () => {
const courses = ['MAT11001', 'TKT20004']
cy.cs('courseFilter-course-dropdown').click().contains(courses[0]).click()
checkFilteringResult(4)
cy.selectFromDropdown(`courseFilter-${courses[0]}-dropdown`, 1)
checkFilteringResult(4)
cy.cs('courseFilter-course-dropdown').click().contains(courses[1]).click()
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.selectFromDropdown(`courseFilter-${courses[1]}-dropdown`, 3)
cy.cs(`courseFilter-${courses[1]}-dropdown`).selectFromDropdown(3)
checkFilteringResult(1)
courses.forEach(course => cy.cs(`courseFilter-${course}-clear`).click())
})
+191 -150
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 => {
@@ -11,167 +14,205 @@ const setPopStatsUntil = (until, includeSettings = []) => {
}
describe('Population Statistics tests', () => {
beforeEach(() => {
cy.init()
})
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')
})
/*
const checkAmountOfStudents = assertion => {
let students = 0
cy.contains('Credit accumulation')
.click()
.invoke('text')
.then(text => {
students = Number(text.match(/\d+/g)[0])
expect(students).to.equal(assertion)
})
}
*/
it('Population statistics search form is usable', () => {
cy.contains('See population').should('be.disabled')
cy.contains('Search for population')
cy.contains('Class of')
.parent()
.within(() => {
cy.get('.form-control').as('enrollmentSelect')
})
it('Population statistics search form is usable', () => {
cy.cs('navbar-studyProgramme').click()
cy.cs('navbar-class').click()
cy.contains('See population').should('be.disabled')
cy.url().should('include', '/populations')
cy.contains('Search for population')
cy.contains('Class of')
.parent()
.within(() => {
cy.get('.form-control').as('enrollmentSelect')
})
cy.get('@enrollmentSelect')
.its(`${[0]}.value`)
.then(beforeVal => {
cy.get('@enrollmentSelect').click()
cy.get('.yearSelectInput .rdtPrev').click({ force: true })
cy.get('.yearSelectInput table').contains('2018-2019').click({ force: true })
cy.get('@enrollmentSelect').should('not.have.value', beforeVal)
})
cy.get('@enrollmentSelect')
.its(`${[0]}.value`)
.then(beforeVal => {
cy.get('@enrollmentSelect').click()
cy.get('.yearSelectInput .rdtPrev').click({ force: true })
cy.get('.yearSelectInput table').contains('2018-2019').click({ force: true })
cy.get('@enrollmentSelect').should('not.have.value', beforeVal)
})
cy.contains('Select study programme')
cy.get('[data-cy=select-study-programme]')
.click()
.children()
.contains('Tietojenkäsittelytieteen maisteriohjelma')
.click()
cy.contains('See population').should('be.enabled')
})
cy.contains('Select study programme')
cy.get('[data-cy=select-study-programme]')
.click()
.children()
.contains('Tietojenkäsittelytieteen maisteriohjelma')
.click()
})
it('Population statistics is usable on general level', () => {
cy.selectStudyProgramme('Tietojenkäsittelytieteen kandiohjelma')
setPopStatsUntil('toukokuu 2020')
cy.get('.card').within(() => {
it('Searching for population really shows population', () => {
cy.contains('Select study programme')
cy.get('[data-cy=select-study-programme]')
.click()
.children()
.contains('Tietojenkäsittelytieteen kandiohjelma')
.click()
cy.contains('See population').click()
cy.contains('Population statistics')
cy.contains('Tietojenkäsittelytieteen kandiohjelma')
cy.contains('Sample size: 149 students')
cy.contains('Excludes exchange students')
cy.contains("Excludes students who haven't enrolled present nor absent")
cy.contains('Excludes students with non-degree study right')
cy.contains('Excludes students who have transferred out of this programme')
})
cy.contains('Courses of population').click({ force: true })
cy.intercept('/api/v3/courseyearlystats**').as('coursePage')
// eslint-disable-next-line cypress/no-unnecessary-waiting
cy.wait(3000) // a bit hacky way, wait until ui is ready
cy.cs('expand-TKT1').click()
cy.cs('coursestats-link-TKT10002').click()
cy.wait('@coursePage')
cy.url().should('include', '/coursestatistics')
cy.contains('TKT10002, 581325 Ohjelmoinnin perusteet')
})
it('Population statistics is usable on general level', () => {
cy.visit(pathToCSBach2017)
cy.cs('filtered-students')
setPopStatsUntil('toukokuu 2020')
it('Student list checking works as intended', () => {
const existing = '010113437'
const nonExisting = '66666666'
cy.selectStudyProgramme('Tietojenkäsittelytieteen kandiohjelma')
cy.contains('Students (149)').click()
cy.contains(existing)
cy.contains(nonExisting).should('not.exist')
cy.contains('button', 'Check studentnumbers').click()
cy.contains('Check for studentnumbers')
cy.get('textarea').type(existing).type('{enter}').type(nonExisting)
cy.contains('button', 'check students').click()
cy.contains('#checkstudentsresults', 'Results').within(() => {
cy.contains('Student numbers in list and in oodi').click()
cy.contains('#found', existing)
cy.contains('Student numbers in list but not in oodi').click()
cy.contains('#notfound', nonExisting)
cy.contains('Student numbers in oodi but not in list').click()
cy.contains('#notsearched', '010614509')
cy.get('.card').within(() => {
cy.contains('Tietojenkäsittelytieteen kandiohjelma')
cy.contains('Sample size: 149 students')
cy.contains('Excludes exchange students')
cy.contains("Excludes students who haven't enrolled present nor absent")
cy.contains('Excludes students with non-degree study right')
cy.contains('Excludes students who have transferred out of this programme')
})
cy.cs('filtered-students')
cy.contains('Courses of population').click()
cy.intercept('/api/v3/courseyearlystats**').as('coursePage')
// eslint-disable-next-line cypress/no-unnecessary-waiting
cy.wait(10000) // a bit hacky way, wait until ui is ready
cy.get('[data-cy=expand-TKT1]').click()
cy.get('[data-cy=coursestats-link-TKT10002]').click()
cy.wait('@coursePage')
cy.url().should('include', '/coursestatistics')
cy.contains('TKT10002, 581325 Ohjelmoinnin perusteet')
})
it("Empty 'tags' tab has a link to the page where tags can be created", () => {
cy.visit(pathToCSBach2017)
cy.contains('Students (149)').click()
cy.get('[data-cy=student-table-tabs]').contains('Tags').click()
cy.contains('No tags defined. You can define them here.').find('a').click()
cy.contains('Tags').click()
cy.contains('Tietojenkäsittelytieteen kandiohjelma')
cy.contains('Create new tag')
})
// TODO: this fails in CI, but not locally. Investigate and fix
it.skip('Advanced settings work', () => {
cy.visit(pathToCSBach2017)
cy.get('[data-cy=advanced-toggle]').click()
cy.contains('Statistics until')
// only spring
cy.cs('toggle-fall').click()
cy.contains('Fetch population').click()
// moar waiting hack
cy.wait(10000)
cy.contains('No statistics found for the given query.')
// only fall
cy.get('[data-cy=advanced-toggle]').click()
cy.cs('toggle-fall').click()
cy.cs('toggle-spring').click()
cy.contains('Fetch population').click()
cy.contains('Credit accumulation (for 149 students)')
// spring + fall and include cancelled
cy.get('[data-cy=advanced-toggle]').click()
cy.cs('toggle-spring').click()
cy.cs('toggle-cancelled').click()
cy.contains('Fetch population').click()
cy.contains('Credit accumulation (for 170 students)')
})
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-max']").should('contain', '314')
})
})
it("Empty 'tags' tab has a link to the page where tags can be created", () => {
cy.cs('navbar-studyProgramme').click()
cy.cs('navbar-class').click()
cy.contains('See population').should('be.disabled')
cy.url().should('include', '/populations')
cy.contains('Search for population')
cy.contains('Class of')
.parent()
.within(() => {
cy.get('.form-control').as('enrollmentSelect')
describe('when using admin', () => {
beforeEach(() => {
cy.init(pathToCSBach2017, 'admin')
})
it('Student list checking works as intended', () => {
const existing = '010113437'
const nonExisting = '66666666'
cy.visit(pathToCSBach2017)
cy.contains('Students (149)').click()
cy.contains(existing)
cy.contains(nonExisting).should('not.exist')
cy.contains('button', 'Check studentnumbers').click()
cy.contains('Check for studentnumbers')
cy.get('textarea').type(existing).type('{enter}').type(nonExisting)
cy.contains('button', 'check students').click()
cy.contains('#checkstudentsresults', 'Results').within(() => {
cy.contains('Student numbers in list and in oodi').click()
cy.contains('#found', existing)
cy.contains('Student numbers in list but not in oodi').click()
cy.contains('#notfound', nonExisting)
cy.contains('Student numbers in oodi but not in list').click()
cy.contains('#notsearched', '010614509')
})
cy.get('@enrollmentSelect')
.its(`${[0]}.value`)
.then(beforeVal => {
cy.get('@enrollmentSelect').click()
cy.get('.yearSelectInput .rdtPrev').click({ force: true })
cy.get('.yearSelectInput table').contains('2019-2020').click({ force: true })
cy.get('@enrollmentSelect').should('not.have.value', beforeVal)
})
cy.selectStudyProgramme('Datatieteen maisteriohjelma')
cy.contains('Students (13)').click()
cy.get('[data-cy=student-table-tabs]').contains('Tags').click()
cy.contains('No tags defined. You can define them here.').find('a').click()
cy.contains('Tags').click()
cy.contains('Datatieteen maisteriohjelma')
cy.contains('Create new tag')
})
it.skip('Advanced settings work', () => {
cy.selectStudyProgramme('Tietojenkäsittelytieteen kandiohjelma')
cy.get('[data-cy=advanced-toggle]').click()
cy.contains('Statistics until')
// only spring
cy.cs('toggle-fall').click()
cy.contains('Fetch population').click()
cy.contains('Credit accumulation (for 0 students)')
// only fall
cy.get('[data-cy=advanced-toggle]').click()
cy.cs('toggle-fall').click()
cy.cs('toggle-spring').click()
cy.contains('Fetch population').click()
// FIXME: This fails randomly both locally and on CI.
cy.contains('Credit accumulation (for 194 students)')
// spring + fall and include cancelled
cy.get('[data-cy=advanced-toggle]').click()
cy.cs('toggle-spring').click()
cy.cs('toggle-cancelled').click()
cy.contains('Fetch population').click()
cy.contains('Credit accumulation (for 202 students)')
})
it('Credit Statistics, Statistics pane works', () => {
cy.selectStudyProgramme('Tietojenkäsittelytieteen kandiohjelma')
cy.contains('Credit statistics').click()
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')
})
})
+196 -161
View File
@@ -8,53 +8,51 @@ const deleteTag = name => {
}
describe('Studyprogramme overview', () => {
beforeEach(() => {
cy.init()
cy.contains('Study programme').click().siblings().contains('Overview').click()
cy.contains('Study Programme', { timeout: 100000 })
})
// check is used by couple of tests
const checkProgressAndProductivity = (user = 'basic') => {
// Column amount depends on user. Note that with dev user, you'll see even more
// stuff than with admin or basic.
const CSBachPopulationProgress2018ForAdmin = [
'162',
'44 (27%)',
'118 (72%)',
'161 (99%)',
'161',
'1',
'11',
'11',
'152',
'125',
'78',
'42',
'16',
]
const CSBachPopulationProgress2018ForBasic = [
'162',
'44 (27%)',
'118 (72%)',
'161 (99%)',
'161',
'1',
'11',
'11',
'152',
'125',
'78',
'42',
'16',
] // cancelled and transferred from are missing
// Taken from https://docs.cypress.io/api/commands/should#Compare-text-values-of-two-elements
const normalizeText = s => s.replace(/\s/g, '').toLowerCase()
let originalProgressCalculatedText
let originalProductivityCalculatedText
// Following function used by two tests
const testProgressAndProductivity = () => {
cy.contains('Tietojenkäsittelytieteen kandiohjelma').click()
cy.contains('Admin').click()
cy.contains('productivity').click()
cy.contains('throughput').click()
cy.wait(1000)
cy.get('.attached > :nth-child(1)').click()
cy.get('table').should('have.length', 3)
const population = user === 'basic' ? CSBachPopulationProgress2018ForBasic : CSBachPopulationProgress2018ForAdmin
cy.contains('Population progress')
cy.contains('Yearly productivity')
// Graduation feature is shown for only for devs, these are the values for normal user
const populationprogress2018 = [
'160',
'42 (26%)',
'118 (73%)',
'159 (99%)',
'159',
'11',
'11',
'1',
'0',
'150',
'123',
'76',
'41',
'16',
]
cy.contains('2018-2019')
.siblings()
.each((elem, index) => {
cy.wrap(elem).contains(populationprogress2018[index])
cy.wrap(elem).contains(population[index])
})
const populationproductivity2019 = ['7388.0', '17', '7329.00', '59.00', '349.00']
const populationproductivity2019 = ['7388.0', '17', '7388.00', '0.00', '349.00']
cy.get('table')
.eq(1)
.contains('2019')
@@ -64,130 +62,167 @@ describe('Studyprogramme overview', () => {
})
}
it('progress should not be recalculating when opened for the first time', () => {
cy.contains('Tietojenkäsittelytieteen kandiohjelma').click()
cy.wait(1000)
cy.contains('Recalculating').should('not.exist')
})
it('renders progress and productivity tables with calculated status', () => {
testProgressAndProductivity()
// Wait to "recalculating" to disappear
cy.wait(1000)
// Grab update dates to be compared later
cy.cs('throughputUpdateStatus')
.invoke('text')
.then(text => {
originalProgressCalculatedText = normalizeText(text)
expect(originalProgressCalculatedText).not.to.contain('recalculating')
expect(originalProgressCalculatedText).not.to.contain('refresh')
})
cy.cs('productivityUpdateStatus')
.invoke('text')
.then(text => {
originalProductivityCalculatedText = normalizeText(text)
expect(originalProductivityCalculatedText).not.to.contain('recalculating')
expect(originalProgressCalculatedText).not.to.contain('refresh')
})
})
it('can open Thesis page', () => {
cy.contains('Tietojenkäsittelytieteen kandiohjelma').click()
cy.contains('Thesis Courses').click()
cy.contains('Add thesis course').click()
cy.contains('No results')
})
it('can move to Population statistics page by clickin', () => {
cy.contains('Lääketieteen koulutusohjelma').click()
cy.get('i.level.up.alternate.icon').eq(0).click()
cy.contains('Students (121)')
})
it('can create and delete tags for population', () => {
const name = `tag-${new Date().getTime()}`
cy.contains('Tietojenkäsittelytieteen kandiohjelma').click()
cy.get('.attached').contains('Tags').click()
cy.get('.tagNameSelectInput > .ui > input').type(name)
cy.get('.yearSelectInput').type('2018')
cy.contains('Create new tag').click()
cy.contains(name)
cy.contains('2018')
deleteTag(name)
})
it('can create personal tags', () => {
const name = `tag-${new Date().getTime()}`
cy.contains('Tietojenkäsittelytieteen kandiohjelma').click()
cy.get('.attached').contains('Tags').click()
cy.get('.tagNameSelectInput > .ui > input').type(name)
cy.get('.yearSelectInput').type('2018')
cy.get('.ui > label').click()
cy.contains('Create new tag').click()
cy.get('.purple')
cy.contains(name)
deleteTag(name)
})
it('can add tags to students', () => {
const name = `tag-${new Date().getTime()}`
const student = '010113437'
cy.contains('Tietojenkäsittelytieteen kandiohjelma').click()
cy.get('.attached').contains('Tags').click()
cy.get('.tagNameSelectInput > .ui > input').type(name)
cy.get('.yearSelectInput').type('2018')
cy.contains('Create new tag').click()
cy.contains(name)
cy.contains('Add tags to students').click()
cy.get('.form > .field > .dropdown').click().get('.ui > .search').type(name).click()
cy.get('.form > .field > .dropdown > .visible').contains(name).click()
cy.get('textarea').type('010113437')
cy.get('.positive').click()
cy.contains('Student statistics').click()
cy.get('.prompt').type(student)
cy.contains(student).click()
cy.contains(name)
cy.go('back')
cy.go('back')
deleteTag(name)
cy.contains('Student statistics').click()
cy.get('.prompt').type(student)
cy.contains(student).click()
cy.contains(name).should('not.exist')
})
it('renders progress and productivity tables with calculated status after recalculating stats again', () => {
testProgressAndProductivity()
// Wait to "recalculating" to disappear
cy.wait(1000)
// Check new calculation statuses are reported
const newProgressCalculatedTextElement = cy.cs('throughputUpdateStatus').invoke('text')
const newProductivityCalculatedTextElement = cy.cs('productivityUpdateStatus').invoke('text')
newProgressCalculatedTextElement.should(text => {
const newProgressCalculatedText = normalizeText(text)
expect(newProgressCalculatedText).not.to.contain('recalculating')
expect(newProgressCalculatedText).not.to.contain('refresh')
expect(newProgressCalculatedText).not.to.contain(originalProgressCalculatedText)
describe('when opening programme page with admin user', () => {
beforeEach(() => {
cy.init('/study-programme/KH50_005', 'admin')
})
newProductivityCalculatedTextElement.should(text => {
const newProductivityCalculatedText = normalizeText(text)
expect(newProductivityCalculatedText).not.to.contain('recalculating')
expect(newProductivityCalculatedText).not.to.contain('refresh')
expect(newProductivityCalculatedText).not.to.contain(originalProgressCalculatedText)
it('progress should not be recalculating when opened for the first time', () => {
cy.contains('Recalculating').should('not.exist')
cy.contains('Last updated').should('exist')
})
// Following vars are used to compare values before and after stats calculation
// Taken from https://docs.cypress.io/api/commands/should#Compare-text-values-of-two-elements
const normalizeText = s => s.replace(/\s/g, '').toLowerCase()
let originalProgressCalculatedText
let originalProductivityCalculatedText
const doRecalculation = () => {
// Wait for page to load before clicking to admin page, TODO: change to cy.intercept
cy.wait(5000)
cy.get('.attached').contains('Admin').click()
cy.contains('recalculate productivity').click()
cy.contains('recalculate throughput').click()
cy.get('.attached').contains('Overview').click() // TODO: add data-cy tags for tabs
// Wait to "recalculating" to disappear, TODO: change this to cy.intercept
cy.wait(5000)
cy.reload()
}
it('renders progress and productivity tables with calculated status', () => {
doRecalculation()
// check contains also study programme options for admin
cy.contains('Study programme options')
cy.get('table').should('have.length', 3)
checkProgressAndProductivity('admin')
// Grab update dates to be compared later
cy.cs('throughputUpdateStatus')
.invoke('text')
.then(text => {
originalProgressCalculatedText = normalizeText(text)
expect(originalProgressCalculatedText).not.to.contain('recalculating')
expect(originalProgressCalculatedText).not.to.contain('refresh')
})
cy.cs('productivityUpdateStatus')
.invoke('text')
.then(text => {
originalProductivityCalculatedText = normalizeText(text)
expect(originalProductivityCalculatedText).not.to.contain('recalculating')
expect(originalProgressCalculatedText).not.to.contain('refresh')
})
})
it('renders progress and productivity tables with calculated status after recalculating stats again', () => {
doRecalculation()
checkProgressAndProductivity('admin')
// Check new calculation statuses are reported
const newProgressCalculatedTextElement = cy.cs('throughputUpdateStatus').invoke('text')
const newProductivityCalculatedTextElement = cy.cs('productivityUpdateStatus').invoke('text')
newProgressCalculatedTextElement.should(text => {
const newProgressCalculatedText = normalizeText(text)
expect(newProgressCalculatedText).not.to.contain('recalculating')
expect(newProgressCalculatedText).not.to.contain('refresh')
expect(newProgressCalculatedText).not.to.contain(originalProgressCalculatedText)
})
newProductivityCalculatedTextElement.should(text => {
const newProductivityCalculatedText = normalizeText(text)
expect(newProductivityCalculatedText).not.to.contain('recalculating')
expect(newProductivityCalculatedText).not.to.contain('refresh')
expect(newProductivityCalculatedText).not.to.contain(originalProgressCalculatedText)
})
})
})
describe('when opening programme page with basic user', () => {
beforeEach(() => {
cy.init('/study-programme/KH50_005')
})
it('renders progress and productivity tables with previosly calculated values', () => {
// check doesn't contain study programme options for admin
cy.contains('Study programme options').should('not.exist')
cy.get('table').should('have.length', 2)
checkProgressAndProductivity()
})
it('can open Thesis page', () => {
cy.contains('Tietojenkäsittelytieteen kandiohjelma').click()
cy.contains('Thesis Courses').click()
cy.contains('Add thesis course').click()
cy.contains('No results')
})
it('can move to Population statistics page by clickin', () => {
cy.get('i.level.up.alternate.icon').eq(0).click()
cy.contains('Students (10)')
})
it('can create and delete tags for population', () => {
const name = `tag-${new Date().getTime()}`
cy.contains('Tietojenkäsittelytieteen kandiohjelma').click()
cy.get('.attached').contains('Tags').click()
cy.get('.tagNameSelectInput > .ui > input').type(name)
cy.get('.yearSelectInput').type('2018')
cy.contains('Create new tag').click()
cy.contains(name)
cy.contains('2018')
deleteTag(name)
})
it('can create personal tags', () => {
const name = `tag-${new Date().getTime()}`
cy.contains('Tietojenkäsittelytieteen kandiohjelma').click()
cy.get('.attached').contains('Tags').click()
cy.get('.tagNameSelectInput > .ui > input').type(name)
cy.get('.yearSelectInput').type('2018')
cy.get('.ui > label').click()
cy.contains('Create new tag').click()
cy.contains(name)
deleteTag(name)
})
it('can add tags to students', () => {
const name = `tag-${new Date().getTime()}`
const student = '010113437'
cy.contains('Tietojenkäsittelytieteen kandiohjelma').click()
cy.get('.attached').contains('Tags').click()
cy.get('.tagNameSelectInput > .ui > input').type(name)
cy.get('.yearSelectInput').type('2018')
cy.contains('Create new tag').click()
cy.contains(name)
cy.contains('Add tags to students').click()
cy.get('.form > .field > .dropdown').click().get('.ui > .search').type(name).click()
cy.get('.form > .field > .dropdown > .visible').contains(name).click()
cy.get('textarea').type('010113437')
cy.get('.positive').click()
cy.contains('Student statistics').click()
cy.get('.prompt').type(student)
cy.contains(student).click()
cy.contains(name)
cy.go('back')
cy.go('back')
deleteTag(name)
cy.contains('Student statistics').click()
cy.get('.prompt').type(student)
cy.contains(student).click()
cy.contains(name).should('not.exist')
})
})
})
+2 -3
View File
@@ -2,9 +2,8 @@
describe('Teachers page tests', () => {
beforeEach(() => {
cy.init()
cy.contains('Teachers').click()
cy.contains('Teacher statistics by course providers')
// login as admin = has teacher rights
cy.init('/teachers', 'admin')
})
const teacher1 = 'Landgraf Leo'
+45 -58
View File
@@ -1,70 +1,57 @@
/// <reference types="Cypress" />
describe('Users tests', () => {
beforeEach(() => {
cy.init()
cy.contains('Users').click()
cy.contains('Oodikone users')
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('[data-cy="navBar"]').contains('Trends').should('exist')
cy.get('[data-cy="navBar"]').contains('Give feedback').should('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')
})
})
it("Admin mocking normal user shows only the mocked user's programmes", () => {
cy.contains('mocking').should('not.exist')
cy.contains('tr', 'Normaalikäyttäjä').within($row => {
cy.contains('.button', 'Edit').click()
})
cy.contains('Access rights')
.siblings()
.within($row => {
cy.get("div[role='listitem']").should('have.length', 1).contains('Tietojenkäsittelytieteen kandiohjelma')
})
// TODO: Add user right checks for onlycoursestats user (should be like above + should
// show course stats
//
// TODO: Add user right checks for basic user (should show also study programme,
// student stats, course stats
cy.route('POST', '/api/superlogin/normk').as('superlogin')
cy.get('i.spy').click()
cy.wait('@superlogin')
cy.contains('mocking as normk')
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 => {
cy.get("div[role='option']").should('have.length', 1).contains('Tietojenkäsittelytieteen kandiohjelma')
})
})
it('Admin mocking courseStatistics-user can see only Trends and Course Statistics', () => {
cy.contains('mocking').should('not.exist')
cy.contains('tr', 'User manager').within($row => {
cy.contains('.button', 'Edit').click()
describe('Using as admin', () => {
beforeEach(() => {
cy.init('/users', 'admin')
})
cy.get('[data-cy=access-groups-form]').click().contains('courseStatistics').click()
cy.get('[data-cy=access-groups-save]').click()
cy.route('POST', '/api/superlogin/usermk').as('superlogin')
cy.get('i.spy').click()
cy.wait('@superlogin')
cy.contains('mocking as usermk')
cy.wait(1000)
cy.get('[data-cy=navbar-studyProgramme').should('not.exist')
cy.get('[data-cy=navbar-students]').should('not.exist')
cy.get('[data-cy=navbar-teachers]').should('not.exist')
cy.get('[data-cy=navbar-users').should('not.exist')
cy.contains('Stop mocking').click()
cy.wait(1000)
cy.contains('Users').click()
cy.contains('Oodikone users')
cy.contains('tr', 'User manager').within($row => {
cy.contains('.button', 'Edit').click()
it('should see more stuff than others', () => {
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')
})
cy.contains('Access Groups')
.siblings()
.within($row => {
cy.get("i[class='dropdown icon clear']").click()
it("mocking normal user shows only the mocked user's programmes", () => {
cy.contains('mocking').should('not.exist')
cy.contains('tr', 'basic').within(() => {
cy.contains('.button', 'Edit').click()
})
cy.get('[data-cy=access-groups-save]').click()
cy.get('i.spy').click()
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(() => {
cy.get("div[role='option']").should('have.length', 2).contains('Tietojenkäsittelytieteen kandiohjelma')
})
})
})
})
+60 -22
View File
@@ -1,25 +1,59 @@
/// <reference types="Cypress" />
//
// Note: here we need to set keys to be all lowercase, since
// we're replacing headers after they've left browser / frontend.
// All of these users are available in anon user-db
const adminUserHeaders = {
uid: 'admin',
displayname: 'Admin User',
'shib-session-id': 'mock-cypress-session',
hygroupcn: 'grp-oodikone-users;grp-oodikone-basic-users',
edupersonaffiliation: 'member;employee;faculty',
mail: 'grp-toska+mockadminuser@helsinki.fi',
hypersonsisuid: 'hy-hlo-6666666',
}
const basicUserHeaders = {
uid: 'basic',
displayname: 'Basic User',
'shib-session-id': 'mock-cypress-session',
hygroupcn: 'grp-oodikone-users;grp-oodikone-basic-users',
edupersonaffiliation: 'member',
mail: 'grp-toska+mockbasicuser@helsinki.fi',
hypersonsisuid: 'hy-hlo-1234567',
}
const onlycoursestatisticsUserHeaders = {
uid: 'onlycoursestatistics',
displayName: 'Onlycoursestatistics User',
'shib-session-id': 'mock-session',
hyGroupCn: 'grp-oodikone-basic-users',
eduPersonAffiliation: 'member',
mail: 'grp-toska+mockonlycoursestatisticsuser@helsinki.fi',
hypersonsisuid: 'hy-hlo-7654321',
}
const norightsUserHeaders = {
uid: 'norights',
displayname: 'Norights User',
'shib-session-id': 'mock-cypress-session',
hygroupcn: 'grp-oodikone-users',
edupersonaffiliation: 'member',
mail: 'grp-toska+mocknorightuser@helsinki.fi',
hypersonsisuid: 'hy-hlo-0000000',
}
const userHeaders = [adminUserHeaders, basicUserHeaders, onlycoursestatisticsUserHeaders, norightsUserHeaders]
/**
* Initialize headers and load the base URL or optional path.
Set up headers to login, set up correct user (admin / basic / etc.) and open given path.
*/
Cypress.Commands.add('init', (path = '') => {
cy.server({
onAnyRequest: function (route, proxy) {
proxy.xhr.setRequestHeader('uid', 'tktl')
proxy.xhr.setRequestHeader('shib-session-id', 'mock-shibboleth')
proxy.xhr.setRequestHeader('hygroupcn', 'grp-oodikone-users')
proxy.xhr.setRequestHeader('edupersonaffiliation', 'asdasd')
},
Cypress.Commands.add('init', (path = '', userId = 'basic') => {
cy.intercept('', req => {
const headersToUse = userHeaders.find(({ uid }) => uid === userId)
if (!headersToUse) throw Error(`${userId} is not valid user id!`)
req.headers = headersToUse
})
// Babel throws an error probably because of markdown files. I'm sorry about this :lul:.
cy.on('uncaught:exception', (err, runnable) => {
expect(err.message).to.include("Cannot read property 'helpers' of undefined")
done()
return false
})
const baseUrl = Cypress.config().baseUrl
cy.visit(baseUrl.concat(path))
})
@@ -27,24 +61,28 @@ Cypress.Commands.add('init', (path = '') => {
/**
* 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 p = subject === undefined ? cy : cy.wrap(subject)
return p.get(`[data-cy='${name}']`)
})
/**
* 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()
subject.children('.menu').children().eq(i).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 })
}
})
+58 -31
View File
@@ -1,51 +1,43 @@
version: "3.9"
x-defaults: &defaults
restart: unless-stopped
services:
backend:
<<: *defaults
container_name: backend
depends_on:
- kone-db
- oodi-db
- redis
- sis-db
- user-db
environment:
DB_URL: postgres://postgres@oodi-db:5432/oodi-db
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:trunk
image: toska/oodikone2-backend:staging
frontend:
<<: *defaults
container_name: frontend
environment:
BACKEND_ADDR: backend
image: toska/oodikone2-frontend:trunk
image: toska/oodikone2-frontend:staging
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
oodi-db: # TODO: Remove when oodi stuff has been removed from oodikone
<<: *defaults
container_name: oodi-db
image: registry.toska.cs.helsinki.fi/oodi-db
redis:
<<: *defaults
command: "redis-server --appendonly yes"
command: 'redis-server --appendonly yes'
container_name: redis
image: redis
ports:
@@ -55,22 +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:
DB_URL: postgres://postgres@user-db:5432/user-db
SECRET: Luukk4inenApprov3dVitunMinttuSalainenAvain666NurmivaaraPJ2019
TOKEN_SECRET: IWannaBeTheVeryBest
image: toska/oodikone2-userservice:trunk
PGDATA: /data
POSTGRES_DB: user-db
POSTGRES_HOST_AUTH_METHOD: trust
image: registry.toska.cs.helsinki.fi/user-db
nginx:
image: nginx:latest
@@ -78,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 -6
View File
@@ -1,8 +1,7 @@
services:
backend:
environment:
DB_URL: postgres://postgres@oodi-db:5432/oodi-db-real
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
@@ -19,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
+8 -47
View File
@@ -5,7 +5,6 @@ x-build_defaults: &build_defaults
target: development
services:
adminer:
<<: *defaults
container_name: adminer
@@ -25,20 +24,19 @@ services:
container_name: backend
depends_on:
- kone-db
- oodi-db
- redis
- sis-db
- user-db
env_file:
- .env
environment:
DB_URL: postgres://postgres@oodi-db:5432/oodi-db
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:
@@ -53,13 +51,6 @@ services:
<<: *build_defaults
context: services/frontend
container_name: frontend
environment:
ADMINER_URL: http://localhost:5050/?pgsql=oodi-db&username=postgres
BACKEND_ADDR: backend
KONE_ADMINER_URL: http://localhost:5050/?pgsql=kone-db&username=postgres
SIS_ADMINER_URL: http://localhost:5050/?pgsql=sis-db&username=postgres
SIS_IMPORTER_ADMINER_URL: http://localhost:5050/?pgsql=sis-importer-db&username=postgres
USER_ADMINER_URL: http://localhost:5050/?pgsql=user-db&username=postgres
ports:
- 3000:3000
profiles:
@@ -77,22 +68,13 @@ services:
volumes:
- kone-db-data:/data
oodi-db: # TODO: Remove when oodi stuff has been removed from oodikone
<<: *defaults
container_name: oodi-db
image: registry.toska.cs.helsinki.fi/oodi-db
profiles:
- oodikone
volumes:
- oodi-db-data:/data
redis:
<<: *defaults
command: "redis-server --appendonly yes"
command: 'redis-server --appendonly yes'
container_name: redis
image: redis
ports:
- "6379:6379"
- '6379:6379'
volumes:
- redis-data:/data
@@ -102,6 +84,7 @@ services:
image: registry.toska.cs.helsinki.fi/sis-db
volumes:
- sis-db-data:/data
shm_size: 2gb
sis-importer-db:
<<: *defaults
@@ -114,7 +97,7 @@ services:
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"
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:
@@ -181,35 +164,13 @@ 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:
kone-db-data:
oodi-db-data: # TODO: Remove when oodi stuff has been removed from oodikone
redis-data:
sis-db-data:
sis-importer-db-data:
sis-updater-scheduler-nodemod:
sis-updater-worker-nodemod:
user-db-data:
userservice-nodemod:
@@ -1,197 +0,0 @@
const { CourseStatsCounter } = require('./course_stats_counter')
describe('CourseStatsCounter tests', () => {
const studentnumbers = ['1', '2', '3', '4', '5']
const coursecode = 'COURSE_01'
const coursename = 'COURSE NAME'
const createCounter = () => new CourseStatsCounter(coursecode, coursename, studentnumbers.length)
describe('counter constructor should set course values correctly', () => {
const counter = createCounter()
test('course name should be set after creation', () => {
expect(counter.course.name).toBe(coursename)
})
test('course code should be set after creation', () => {
expect(counter.course.code).toBe(coursecode)
})
})
describe('mark passed credit for student', () => {
const student = studentnumbers[0]
const grade = '5'
const counter = createCounter()
counter.markCredit(student, grade, true, false, false)
const { all, failed, passed } = counter.students
test('all should have studentnumber as key', () => {
expect(all).toHaveProperty(student)
})
test('passed students should have studentnumber as key', () => {
expect(passed).toHaveProperty(student)
})
test('failed students should not have studentnumber as key', () => {
expect(failed).not.toHaveProperty(student)
})
})
describe('mark failed credit for student', () => {
const student = studentnumbers[0]
const grade = '0'
const counter = createCounter()
counter.markCredit(student, grade, false, true, false)
const { all, failed, passed } = counter.students
test('all should have studentnumber as key', () => {
expect(all).toHaveProperty(student)
})
test('failed students should have studentnumber as key', () => {
expect(failed).toHaveProperty(student)
})
test('passed students should not have studentnumber as key', () => {
expect(passed).not.toHaveProperty(student)
})
})
describe('mark failed credit then passed credit for student', () => {
const student = studentnumbers[0]
const counter = createCounter()
counter.markCredit(student, '0', false, true, false)
counter.markCredit(student, '5', true, false, false)
const { passed, failed, retryPassed } = counter.students
test('failed students should not have studentnumber as key', () => {
expect(failed).not.toHaveProperty(student)
})
test('passed students should have studentnumber as key', () => {
expect(passed).toHaveProperty(student)
})
test('retryPassed should have studentnumber as key', () => {
expect(retryPassed).toHaveProperty(student)
})
})
describe('mark passed credit then failed credit for student', () => {
const student = studentnumbers[0]
const counter = createCounter()
counter.markCredit(student, '5', true, false, false)
counter.markCredit(student, '0', false, true, false)
const { passed, failed, retryPassed } = counter.students
test('failed students should not have studentnumber as key', () => {
expect(failed).not.toHaveProperty(student)
})
test('passed students should have studentnumber', () => {
expect(passed).toHaveProperty(student)
})
test('retryPassed should have student', () => {
expect(retryPassed).toHaveProperty(student)
})
})
describe('mark failed credit then failed credit for student', () => {
const student = studentnumbers[0]
const counter = createCounter()
counter.markCredit(student, '0', false, true, false)
counter.markCredit(student, '0', false, true, false)
const { failed, passed, failedMany } = counter.students
test('failed students should have studentnumber', () => {
expect(failed).toHaveProperty(student)
})
test('passed students should not have studentnumber', () => {
expect(passed).not.toHaveProperty(student)
})
test('failed many students should have students', () => {
expect(failedMany).toHaveProperty(student)
})
})
describe('mark passed then failed then failed for student', () => {
const student = studentnumbers[0]
const counter = createCounter()
counter.markCredit(student, '5', true, false, false)
counter.markCredit(student, '0', false, true, false)
counter.markCredit(student, '0', false, true, false)
const { passed, failed, failedMany, retryPassed } = counter.students
test('passed students should have studentnumber', () => {
expect(passed).toHaveProperty(student)
})
test('failed students should not have studentnumber', () => {
expect(failed).not.toHaveProperty(student)
})
test('failedMany should not have studentnumber', () => {
expect(failedMany).not.toHaveProperty(student)
})
test('retryPassed should have studentnumber', () => {
expect(retryPassed).toHaveProperty(student)
})
})
describe('mark passed then improved grade for student', () => {
const student = studentnumbers[0]
const counter = createCounter()
counter.markCredit(student, '3', true, false, false)
counter.markCredit(student, '5', false, false, true)
const { passed, improvedPassedGrade } = counter.students
test('passed should contain student', () => {
expect(passed).toHaveProperty(student)
})
test('improvedPassedGrade should have student', () => {
expect(improvedPassedGrade).toHaveProperty(student)
})
})
describe('mark improved grade then failed for student', () => {
const student = studentnumbers[0]
const counter = createCounter()
counter.markCredit(student, '3', false, false, true)
counter.markCredit(student, '0', false, true, false)
const { passed, improvedPassedGrade, failed } = counter.students
test('passed should contain student', () => {
expect(passed).toHaveProperty(student)
})
test('improvedPassedGrade should contain student', () => {
expect(improvedPassedGrade).toHaveProperty(student)
})
test('failed should not contain student', () => {
expect(failed).not.toHaveProperty(student)
})
})
})
@@ -1,17 +0,0 @@
const { sequelize } = require('../../src/models/index')
const { sequelizeKone } = require('../../src/models/models_kone')
const { redisClient } = require('../../src/services/redis')
const { forceSyncDatabase } = require('../../src/database/connection')
const { seedAllMigrations } = require('../../src/database/seed_migrations')
beforeAll(async () => {
await forceSyncDatabase()
await seedAllMigrations()
})
afterAll(async () => {
await sequelize.close()
await sequelizeKone.close()
// https://stackoverflow.com/a/54560610
await new Promise(res => redisClient.quit(() => setImmediate(res)))
})
@@ -1,227 +0,0 @@
const {
Student,
Course,
ElementDetails,
StudyrightElement,
Studyright,
Credit,
StudyrightExtent,
Semester,
SemesterEnrollment
} = require('../../../src/models/index')
const { optimizedStatisticsOf } = require('../../../src/services/populations')
jest.mock('../../../src/services/semesters')
const semesterService = require('../../../src/services/semesters')
jest.setTimeout(10000)
const langify = name => ({
en: `${name}_en`,
fi: `${name}_fi`,
sv: `${name}_sv`
})
const SEMESTER = { SPRING: 'SPRING', FALL: 'FALL' }
const elementdetails = {
bsc: {
code: 'Element_BSC',
type: 10,
name: langify('Bachelor of Science')
},
maths: {
code: 'Element_MATH',
type: 20,
name: langify('Mathematics')
},
cs: {
code: 'ELEMENT_CS',
type: 20,
name: langify('Computer Science')
}
}
const courses = {
elements_of_ai: {
code: 'COURSE-1',
name: langify('ELEMENTS_OF_AI')
}
}
const semesters = {
fall: {
semestercode: 123,
name: { en: 'Autumn 2011', fi: 'Syksy 2011', sv: 'Hösten 2011' },
startdate: new Date('2011-07-31'),
enddate: new Date('2012-07-30'),
yearcode: 62,
yearname: '2011-12'
},
spring: {
semestercode: 124,
name: { en: 'Spring 2012', fi: 'Kevät 2012', sv: 'Våren 2012' },
startdate: new Date('2011-12-31'),
enddate: new Date('2012-07-30'),
yearcode: 62,
yearname: '2011-12'
},
// used for mocking current
current: {
semestercode: 125,
name: { en: 'Autumn 2012', fi: 'Syksy 2012', sv: 'Hösten 2012' },
startdate: new Date('2012-08-01'),
enddate: new Date('2012-12-31'),
yearcode: 63,
yearname: '2012-13'
}
}
const createQueryObject = (year, semester, codes, months) => ({
studyRights: codes,
year,
semesters: [semester],
months
})
afterAll(async () => {})
describe('optimizedStatisticsOf tests', () => {
const courseinstanceFall = {
id: 1,
course_code: courses.elements_of_ai.code,
coursedate: new Date('2011-09-31 21:00:00+00')
}
const courseinstanceSpring = {
id: 2,
course_code: courses.elements_of_ai.code,
coursedate: new Date('2012-02-31 21:00:00+00')
}
const student = {
studentnumber: '1234'
}
const studyrightextents = {
bachelors: {
extentcode: 1,
name: langify("bachelor's degree extent")
}
}
const studyright = {
studyrightid: 1,
studystartdate: new Date('2011-07-31 21:00:00+00'),
student_studentnumber: student.studentnumber,
extentcode: studyrightextents.bachelors.extentcode
}
const creditFall = {
id: 'CREDIT-1',
student_studentnumber: student.studentnumber,
course_code: courseinstanceFall.course_code,
attainment_date: courseinstanceFall.coursedate,
semestercode: 123
}
const creditSpring = {
id: 'CREDIT-2',
student_studentnumber: student.studentnumber,
course_code: courseinstanceSpring.course_code,
attainment_date: courseinstanceSpring.coursedate,
semestercode: 124
}
const semesterEnrollments = {
fall: {
enrollmenttype: 1,
studentnumber: student.studentnumber,
semestercode: semesters.fall.semestercode,
enrollment_date: semesters.fall.startdate
},
spring: {
enrollmenttype: 1,
studentnumber: student.studentnumber,
semestercode: semesters.spring.semestercode,
enrollment_date: semesters.spring.startdate
},
current: {
enrollmenttype: 1,
studentnumber: student.studentnumber,
semestercode: semesters.current.semestercode,
enrollment_date: semesters.current.startdate
}
}
const studyrightelements = {
bsc: {
startdate: new Date('2011-07-31 21:00:00+00'),
enddate: Date('2016-12-20 22:00:00+00'),
studyrightid: studyright.studyrightid,
code: elementdetails.bsc.code,
studentnumber: student.studentnumber
},
maths: {
startdate: new Date('2011-07-31 21:00:00+00'),
enddate: Date('2016-12-20 22:00:00+00'),
studyrightid: studyright.studyrightid,
code: elementdetails.maths.code,
studentnumber: student.studentnumber
}
}
describe(`
Student with:
- BSc studyright (2011-07-31 – 2016-12-20),
- Mathematics studyright (2011-07-31 – 2016-12-20),
- Two credits in 2011-09-31 and 2012-02-31.
`, () => {
beforeAll(async () => {
await Semester.bulkCreate([semesters.fall, semesters.spring, semesters.current])
await Student.create(student)
await Course.create(courses.elements_of_ai)
await Credit.bulkCreate([creditFall, creditSpring])
await ElementDetails.bulkCreate([elementdetails.bsc, elementdetails.maths, elementdetails.cs])
await StudyrightExtent.create(studyrightextents.bachelors)
await Studyright.create(studyright)
await StudyrightElement.bulkCreate([studyrightelements.bsc, studyrightelements.maths])
await SemesterEnrollment.bulkCreate([
semesterEnrollments.fall,
semesterEnrollments.spring,
semesterEnrollments.current
])
// Mock current semester implementation
semesterService.getCurrentSemester.mockImplementation(async () => {
const currentSemester = await Semester.findOne({
where: {
semestercode: semesters.current.semestercode
}
})
return currentSemester
})
})
test('Query result for Mathematics, Fall 2010 for 12 months should not contain the student.', async () => {
const query = createQueryObject('2010', SEMESTER.FALL, [elementdetails.maths.code], 12)
const { students } = await optimizedStatisticsOf(query)
expect(students.some(s => s.studentNumber === student.studentnumber)).toBe(false)
})
test('Query result for Mathematics, Fall 2012 for 12 months should not contain the student.', async () => {
const query = createQueryObject('2012', SEMESTER.FALL, [elementdetails.maths.code], 12)
const { students } = await optimizedStatisticsOf(query)
expect(students.some(s => s.studentNumber === student.studentnumber)).toBe(false)
})
test('Query result for BSc and Computer Science, Fall 2011 for 12 months should not contain the student.', async () => {
const query = createQueryObject('2011', SEMESTER.FALL, [elementdetails.bsc.code, elementdetails.cs.code], 12)
const { students } = await optimizedStatisticsOf(query)
expect(students.some(s => s.studentNumber === student.student_studentnumber)).toBe(false)
})
test('Query result for Mathematics, Spring 2012 for 12 months should not contain the student', async () => {
const query = createQueryObject('2012', SEMESTER.SPRING, [elementdetails.maths.code], 12)
const { students } = await optimizedStatisticsOf(query)
expect(students.some(s => s.studentNumber === student.studentnumber)).toBe(false)
})
})
})
@@ -1,42 +0,0 @@
const faculties = [
{
code: 'FAC1',
organisation_type_code: 2,
name: [
{
langcode: 'fi',
text: 'FAC 1 FIN'
},
{
langcode: 'sv',
text: 'FAC 1 SV'
},
{
langcode: 'en',
text: 'FAC 1 SV'
}
]
},
{
code: 'FAC2',
organisation_type_code: 2,
name: [
{
langcode: 'fi',
text: 'FAC 2 FIN'
},
{
langcode: 'sv',
text: 'FAC 2 SV'
},
{
langcode: 'en',
text: 'FAC 3 SV'
}
]
}
]
module.exports = {
faculties
}
@@ -1,264 +0,0 @@
const {
getCreditsForProvider,
productivityStatsFromCredits,
productivityStatsForProvider,
findGraduated,
graduatedStatsFromStudyrights,
combineStatistics,
productivityStatsForCode,
findProgrammeThesisCredits,
thesisProductivityFromCredits,
thesisProductivityForStudytrack
} = require('./studyprogramme')
const { sequelize } = require('../models')
const { ThesisTypeEnums, ThesisCourse } = require('../models/models_kone')
const { readFileSync } = require('fs')
const { mapToProviders } = require('../util/utils')
const { MASTER, BACHELOR } = ThesisTypeEnums
const provider = '500-M010'
const studytrack = 'MH50_010'
const thesisCourse = {
// from studyprogramme.test.sql
programmeCode: 'MH50_010',
courseCode: 'THESIS_01',
thesisType: MASTER
}
beforeAll(async () => {
const query = readFileSync('./src/services/studyprogramme.test.sql', 'utf8')
await sequelize.query(query)
await ThesisCourse.create(thesisCourse)
})
test('Credits for provider should contain passed course credit', async () => {
const credits = await getCreditsForProvider(provider, '2001-01-01')
const credit = await credits.find(cr => cr.id === 'CREDIT_01')
expect(credit).toMatchObject({ id: 'CREDIT_01' })
})
test('Credits for provider should return object in correct format', async () => {
const credits = await getCreditsForProvider(provider, '2001-01-01')
const credit = await credits.find(cr => cr.id === 'CREDIT_01')
expect(credit).toMatchObject({
credits: 5,
year: 2016,
id: 'CREDIT_01'
})
})
test('Credits for provider should not contain passed credits from other providers', async () => {
const credits = await getCreditsForProvider(provider)
const credit = await credits.find(cr => cr.id === 'CREDIT_04')
expect(credit).toBeFalsy()
})
test('Credits for provider should not contain credit with failed credit typecode 10', async () => {
const credits = await getCreditsForProvider(provider)
const credit = await credits.find(cr => cr.id === 'CREDIT_02')
expect(credit).toBeFalsy()
})
test('Credits for provider should not contain credit for passed study module', async () => {
const credits = await getCreditsForProvider(provider)
const credit = await credits.find(cr => cr.id === 'CREDIT_03')
expect(credit).toBeFalsy()
})
test('KH50_005 track code maps to 500-K005 provider code', () => {
const track = 'KH50_005'
const provider = mapToProviders([track])[0]
expect(provider).toBe('500-K005')
})
test('MH50_010 track code maps to 500-M010 provider code', () => {
const track = 'MH50_010'
const provider = mapToProviders([track])[0]
expect(provider).toBe('500-M010')
})
test('productivityStatsFromCredits calculates stats correctly', () => {
const credits = [
{ year: 2016, credits: 5 },
{ year: 2016, credits: 5 },
{ year: 2016, credits: 5 },
{ year: 2015, credits: 5 },
{ year: 2015, credits: 5 },
{ year: 2015, credits: 10 }
]
const stats = productivityStatsFromCredits(credits)
expect(stats).toMatchObject({
2016: { year: 2016, credits: 15 },
2015: { year: 2015, credits: 20 }
})
})
test('productivityStats integrates correctly', async () => {
const stats = await productivityStatsForProvider(provider, '2001-01-01')
expect(stats).toMatchObject({
2015: { year: 2015, credits: 40 },
2016: { year: 2016, credits: 5 }
})
})
test('findGraduated finds graduated studyright', async () => {
const studyrights = await findGraduated(studytrack, '2001-01-01')
const match = studyrights.find(sr => sr.studyrightid === '10')
expect(match).toBeTruthy()
})
test('findGraduated formats graduated studyrights correctly', async () => {
const studyrights = await findGraduated(studytrack, '2001-01-01')
expect(studyrights).toContainEqual({ studyrightid: '10', year: 2016, timeToGraduation: NaN })
})
test('findGraduated does not return studyrights that are not graduated', async () => {
const studyrights = await findGraduated(studytrack, '2001-01-01')
const match = studyrights.find(sr => sr.id === '11')
expect(match).toBeFalsy()
})
test('findGraduated does return graduated studyrights from other studytracks', async () => {
const studyrights = await findGraduated(studytrack, '2001-01-01')
const match = studyrights.find(sr => sr.id === '12')
expect(match).toBeFalsy()
})
test('graduatedStatsFromStudyrights calculates stats correctly', () => {
const studyrights = [{ year: 2015 }, { year: 2015 }, { year: 2014 }]
const stats = graduatedStatsFromStudyrights(studyrights)
expect(stats).toMatchObject({
2015: { graduated: 2 },
2014: { graduated: 1 }
})
})
test('combineStatistics returns correctly formatted array', () => {
const creditStats = {
2015: { year: 2015, credits: 40 },
2016: { year: 2016, credits: 5 },
2014: { year: 2014, credits: 20 }
}
const studyrightStats = {
2015: { graduated: 2 },
2016: { graduated: 1 }
}
const thesisStats = {
2014: { mThesis: 1 },
2015: { mThesis: 2, bThesis: 1 }
}
const creditsForMajors = {
2014: 10,
2015: 22
}
const transferredCredits = {
2014: 2,
2015: 4
}
const stats = combineStatistics(creditStats, studyrightStats, thesisStats, creditsForMajors, transferredCredits)
expect(stats).toContainEqual({
year: 2015,
mThesis: 2,
bThesis: 1,
credits: 40,
graduated: 2,
creditsForMajors: 22,
transferredCredits: 4
})
expect(stats).toContainEqual({
year: 2014,
mThesis: 1,
bThesis: 0,
credits: 20,
graduated: 0,
creditsForMajors: 10,
transferredCredits: 2
})
expect(stats).toContainEqual({
year: 2016,
mThesis: 0,
bThesis: 0,
credits: 5,
graduated: 1,
creditsForMajors: 0,
transferredCredits: 0
})
})
test('productivityStatsForCode integrates', async () => {
const stats = await productivityStatsForCode(studytrack, '2001-01-01')
expect(stats.data).toContainEqual({
year: 2015,
graduated: 0,
bThesis: 0,
mThesis: 1,
credits: 40,
creditsForMajors: 0,
transferredCredits: 0
})
expect(stats.data).toContainEqual({
year: 2016,
graduated: 1,
mThesis: 0,
bThesis: 0,
credits: 5,
creditsForMajors: 0,
transferredCredits: 0
})
})
test('findProgrammeThesisCredits returns correct credit', async () => {
const credits = await findProgrammeThesisCredits(studytrack)
expect(credits).toBeTruthy()
expect(credits.length).toBe(1)
expect(credits.find(c => c.id === 'CREDIT_07')).toBeTruthy()
})
test('findProgrammeThesisCredits format credit correctly', async () => {
const credits = await findProgrammeThesisCredits(studytrack)
expect(credits).toContainEqual({
id: 'CREDIT_07',
code: 'THESIS_01',
type: MASTER,
year: 2015
})
})
test('thesisProductivityFromCredits', async () => {
const credits = [
{ type: MASTER, year: 2015 },
{ type: MASTER, year: 2015 },
{ type: BACHELOR, year: 2015 },
{ type: BACHELOR, year: 2014 },
{ type: MASTER, year: 2013 }
]
const stats = thesisProductivityFromCredits(credits)
expect(stats).toHaveProperty('2015', '2014', '2013')
expect(stats).toMatchObject({
'2015': {
mThesis: 2,
bThesis: 1
},
'2014': {
mThesis: 0,
bThesis: 1
},
'2013': {
mThesis: 1,
bThesis: 0
}
})
})
test('thesisProductivityForStudytrack integrates', async () => {
const stats = await thesisProductivityForStudytrack(studytrack)
expect(stats).toMatchObject({
2015: {
mThesis: 1,
bThesis: 0
}
})
})
@@ -1,93 +0,0 @@
DO $$
DECLARE
ts TIMESTAMP = CURRENT_TIMESTAMP;
studytrk_01 VARCHAR(50) = 'MH50_010';
studytrk_02 VARCHAR(50) = 'KH50_005';
provider_01 VARCHAR(50) = '500-M010';
provider_02 VARCHAR(50) = '500-K005';
course_01 VARCHAR(50) = 'M010_COURSE';
module_01 VARCHAR(50) = 'M010_MODULE';
course_02 VARCHAR(50) = 'K005_COURSE';
thesis VARCHAR(50) = 'THESIS_01';
date_2016 TIMESTAMPTZ = '2016-09-28 00:00:00+00';
date_2015 TIMESTAMPTZ = '2015-09-28 00:00:00+00';
BEGIN
INSERT INTO semesters ("semestercode", "createdAt", "updatedAt")
VALUES
(5, ts, ts)
;
INSERT INTO course
("code", "name", "is_study_module")
VALUES
(course_01, '{ "en": "COURSE_EN" }', 'false'),
(module_01, '{ "en": "MODULE_EN" }', 'true'),
(course_02, '{ "en": "COURSE_EN" }', 'false'),
(thesis, '{ "en": "Bachelor thesis" }', 'false')
;
INSERT INTO student
("studentnumber", "createdAt", "updatedAt")
VALUES
('012111517', ts, ts)
;
INSERT INTO credit_types
("credittypecode", "createdAt", "updatedAt")
VALUES
(10, ts, ts),
(4, ts, ts)
;
INSERT INTO credit
("id", "createddate", "lastmodifieddate", "semestercode", "course_code", "credittypecode", "attainment_date", "credits", "student_studentnumber")
VALUES
('CREDIT_01', ts, ts, 5, course_01, 4, date_2016, 5, '012111517'),
('CREDIT_02', ts, ts, 5, course_01, 10, date_2016, 5, '012111517'),
('CREDIT_03', ts, ts, 5, module_01, 4, date_2016, 20, '012111517'),
('CREDIT_04', ts, ts, 5, course_02, 4, date_2016, 5, '012111517'),
('CREDIT_05', ts, ts, 5, course_01, 4, date_2015, 5, '012111517'),
('CREDIT_06', ts, ts, 5, course_01, 4, date_2015, 5, '012111517'),
('CREDIT_07', ts, ts, 5, thesis, 4, date_2015, 30, '012111517')
;
INSERT INTO providers
("providercode", "createdAt", "updatedAt")
VALUES
(provider_01, ts, ts),
(provider_02, ts, ts)
;
INSERT INTO course_providers
("coursecode", "providercode", "createdAt", "updatedAt")
VALUES
(course_01, provider_01, ts, ts),
(module_01, provider_01, ts, ts),
(course_02, provider_02, ts, ts),
(thesis, provider_01, ts, ts)
;
INSERT INTO element_details
("code", "createdAt", "updatedAt")
VALUES
(studytrk_01, ts, ts),
(studytrk_02, ts, ts)
;
INSERT INTO studyright
("studyrightid", "graduated", "enddate")
VALUES
(10, 1, date_2016),
(11, 0, date_2016),
(12, 1, date_2016)
;
INSERT INTO studyright_elements
("studyrightid", "code", "createdAt", "updatedAt", "studentnumber")
VALUES
(10, studytrk_01, ts, ts, '012111517'),
(11, studytrk_01, ts, ts, '012111517'),
(12, studytrk_02, ts, ts, '012111517')
;
END $$;
@@ -1,53 +0,0 @@
const { Teacher } = require('../models/index')
const service = require('./teachers')
const teacher = {
id: 'TID_001',
code: 'usercode',
name: 'Lastname Firstname Middlename'
}
beforeAll(async () => {
await Teacher.create(teacher)
})
describe('teacher service tests', () => {
describe('teacher by search term tests', () => {
const teacherIsInResults = (id, matches) => matches.some(teacher => teacher.id === 'TID_001')
test('Search by `usercode` finds teacher', async () => {
const matches = await service.bySearchTerm('usercode')
expect(teacherIsInResults('TID_001', matches)).toBe(true)
})
test('Search by `usercodeEXTRA` does not find teacher', async () => {
const matches = await service.bySearchTerm('usercodeEXTRA')
expect(teacherIsInResults(teacher.id, matches)).toBe(false)
})
test('Search by just `Lastname` should find the teacher', async () => {
const matches = await service.bySearchTerm('Lastname')
expect(teacherIsInResults(teacher.id, matches)).toBe(true)
})
test('Search by `Firstname` should find the teacher', async () => {
const matches = await service.bySearchTerm('Firstname')
expect(teacherIsInResults(teacher.id, matches)).toBe(true)
})
test('Search by `Lastname Firstname` should find the teacher', async () => {
const matches = await service.bySearchTerm('Lastname Firstname')
expect(teacherIsInResults(teacher.id, matches)).toBe(true)
})
test('Search by `Firstname Lastname` should find the teacher', async () => {
const matches = await service.bySearchTerm('Firstname Lastname')
expect(teacherIsInResults(teacher.id, matches)).toBe(true)
})
test('Search by `id` should find the teacher', async () => {
const matches = await service.bySearchTerm('TID_001')
expect(teacherIsInResults(teacher.id, matches)).toBe(true)
})
})
})
@@ -1,85 +0,0 @@
const { forceSyncDatabase } = require('../database/connection')
const { Course, ElementDetails } = require('../models')
const { ThesisTypeEnums, ThesisCourse } = require('../models/models_kone')
const { createThesisCourse, deleteThesisCourse, findProgrammeTheses } = require('./thesis')
const courseOne = {
code: 'COURSE',
name: { en: 'Course_EN' }
}
const courseTwo = {
code: 'COURSE_2',
name: { en: 'Course_EN_2' }
}
const progOne = {
code: 'CS_BSC',
name: { en: 'CS_BSC' }
}
const progTwo = {
code: 'CS_MSC',
name: { en: 'CS_MSC' }
}
beforeEach(async () => {
await forceSyncDatabase()
await Course.create(courseOne)
await Course.create(courseTwo)
await ElementDetails.create(progOne)
await ElementDetails.create(progTwo)
})
test('createThesisCourse should save thesis correctly', async () => {
await createThesisCourse(progOne.code, courseOne.code, ThesisTypeEnums.MASTER)
const courses = await ThesisCourse.findAll()
expect(courses.length).toBe(1)
expect(courses[0]).toMatchObject({ programmeCode: 'CS_BSC', courseCode: 'COURSE', thesisType: 'MASTER' })
})
test('createThesisCourse should create separate bachelor and thesis for same programme and course', async () => {
await createThesisCourse(progOne.code, courseOne.code, ThesisTypeEnums.MASTER)
await createThesisCourse(progOne.code, courseTwo.code, ThesisTypeEnums.BACHELOR)
const courses = await ThesisCourse.findAll()
expect(courses.length).toBe(2)
const master = courses.find(c => c.thesisType === 'MASTER')
const bachelor = courses.find(c => c.thesisType === 'BACHELOR')
expect(master).toBeTruthy()
expect(master).toMatchObject({ programmeCode: 'CS_BSC', courseCode: 'COURSE', thesisType: 'MASTER' })
expect(bachelor).toBeTruthy()
expect(bachelor).toMatchObject({ programmeCode: 'CS_BSC', courseCode: 'COURSE_2', thesisType: 'BACHELOR' })
})
test('createThesisCourse should error for creating a thesis for existing (course, prog) pair', async () => {
await createThesisCourse(progOne.code, courseOne.code, ThesisTypeEnums.MASTER)
const promise = createThesisCourse(progOne.code, courseOne.code, ThesisTypeEnums.BACHELOR)
await expect(promise).rejects.toThrow('Validation error')
})
test('deleteThesisCourse removes thesis from table', async () => {
let courses = await ThesisCourse.findAll()
expect(courses.length).toBe(0)
await createThesisCourse(progOne.code, courseOne.code, ThesisTypeEnums.MASTER)
await createThesisCourse(progOne.code, courseTwo.code, ThesisTypeEnums.BACHELOR)
courses = await ThesisCourse.findAll()
expect(courses.length).toBe(2)
await deleteThesisCourse(progOne.code, courseOne.code)
courses = await ThesisCourse.findAll()
expect(courses.length).toBe(1)
expect(courses[0]).toMatchObject({ thesisType: 'BACHELOR' })
})
test('findProgrammeTheses returns correct courses', async () => {
await createThesisCourse(progOne.code, courseOne.code, ThesisTypeEnums.MASTER)
await createThesisCourse(progOne.code, courseTwo.code, ThesisTypeEnums.BACHELOR)
await createThesisCourse(progTwo.code, courseOne.code, ThesisTypeEnums.MASTER)
const theses = await findProgrammeTheses(progOne.code)
expect(theses.length).toBe(2)
})
test('createThesisCourse should return created thesis', async () => {
const thesis = await createThesisCourse(progOne.code, courseOne.code, ThesisTypeEnums.MASTER)
expect(thesis).toBeTruthy()
expect(thesis).toMatchObject({ programmeCode: progOne.code, courseCode: courseOne.code })
})
@@ -1,248 +0,0 @@
const faker = require('faker')
const numberFromTo = (from, to) => Math.round(Math.random() * (to - from)) + from
const daysAgo = daysAgo => new Date(new Date() - 1000 * 60 * 60 * 24 * daysAgo)
const idFromTwoIds = (a, b) => (1 / 2) * (a + b) * (a + b + 1) + b
const gradeArray = [
'2',
'CL',
'Eisa',
'L',
'Hyv.',
'TT',
'LUB',
'HT',
'MCLA',
'4',
'0',
'A',
'NSLA',
'Luop',
'5',
'3',
'Hyl.',
'ECLA',
'1',
'KH'
]
const statusArray = [
{ status: 'Suoritettu', statuscode: 4 },
{ status: 'Korotettu', statuscode: 7 },
{ status: 'Hyväksiluettu', statuscode: 9 },
{ status: 'Hylätty', statuscode: 10 }
]
const teacherRoleArray = ['Responsible', 'Approver', 'Teacher']
const generateCourses = async amount => {
const number = amount || numberFromTo(10, 100)
const courses = []
for (let i = 0; i < number; i++) {
courses.push({
code: i.toString(10),
name: faker.lorem.words(3)
})
}
return courses
}
const generateCourseInstances = async (courses, amount) => {
const instances = []
let id = 0
courses.forEach(course => {
const number = amount || numberFromTo(5, 40)
for (let i = 0; i < number; i++) {
instances.push({
id,
coursedate: daysAgo(numberFromTo(10, 365)).toUTCString(),
course_code: course.code
})
id++
}
})
return instances
}
const generateStudents = async amount => {
const students = []
const number = amount || numberFromTo(5, 40)
const now = new Date()
for (let i = 0; i < number; i++) {
const birthdate = daysAgo(numberFromTo(365 * 18, 365 * 70))
const dateoffirstcredit = faker.date.between(birthdate, now)
const dateoflastcredit = faker.date.between(dateoffirstcredit, now)
const dateofuniversityenrollment = faker.date.between(birthdate, dateoflastcredit)
const lastname = faker.name.lastName()
const firstnames = faker.name.firstName(2)
students.push({
studentnumber: `0${numberFromTo(10000000, 15000000)}`,
lastname,
firstnames,
abbreviatedname: `${lastname} ${firstnames}`,
birthdate,
communicationlanguage: ['fi', 'sv', 'en'][numberFromTo(0, 2)],
country: faker.address.country(),
creditcount: numberFromTo(0, 600),
dateoffirstcredit,
dateoflastcredit,
dateofuniversityenrollment,
gradestudent: `${numberFromTo(0, 1)}`,
nationalities: ['fi', 'sv', 'en'][numberFromTo(0, 2)],
semesterenrollmenttypecode: [null, '1.0', '2.0'][numberFromTo(0, 2)],
sex: ['female', 'male'][numberFromTo(0, 1)],
studentstatuscode: numberFromTo(2, 8)
})
}
return students
}
const generateCredits = async (courseInstances, students, amount) => {
const credits = []
students.forEach((student, studentIdx) => {
courseInstances.forEach(courseInstance => {
const number = amount || numberFromTo(5, 40)
for (let i = 0; i < number; i++) {
const statusObject = statusArray[numberFromTo(0, statusArray.length - 1)]
credits.push({
id: idFromTwoIds(idFromTwoIds(studentIdx, courseInstance.id), i),
grade: gradeArray[numberFromTo(0, gradeArray.length - 1)],
student_studentnumber: student.studentnumber,
credits: [5, 10][numberFromTo(0, 1)],
ordering: faker.date.between(student.dateoffirstcredit, student.dateoflastcredit),
status: statusObject.status,
statuscode: statusObject.statuscode,
courseinstance_id: courseInstance.id
})
}
})
})
return credits
}
const generateTeachers = async amount => {
const teachers = []
const number = amount || numberFromTo(5, 40)
for (let i = 0; i < number; i++) {
teachers.push({
id: i,
code: faker.lorem.word(),
name: `${faker.name.lastName()} ${faker.name.firstName()}`
})
}
return teachers
}
const generateCourseTeachers = async (courseInstances, teachers, maximumAmount) => {
const courseTeachers = []
courseInstances.forEach((courseInstance, index) => {
const number = numberFromTo(0, maximumAmount)
for (let i = 0; i < number; i++) {
const teacher = teachers[numberFromTo(0, teachers.length - 1)]
courseTeachers.push({
id: idFromTwoIds(index, i),
teacherrole: teacherRoleArray[numberFromTo(0, teacherRoleArray.length - 1)],
courseinstance_id: courseInstance.id,
teacher_id: teacher.id
})
}
})
return courseTeachers
}
const generateOrganizations = async amount => {
const organizations = []
const number = amount || numberFromTo(5, 40)
for (let i = 0; i < number; i++) {
organizations.push({
code: faker.lorem.words(3),
name: faker.company.companyName()
})
}
return organizations
}
const generateStudyrights = async (students, organization, amount, date, prioritycode) => {
const studyrights = []
const possibleStudyrightNames = []
if (amount) {
for (let i = 0; i < amount; i++) {
possibleStudyrightNames.push(faker.company.catchPhrase())
}
}
students.forEach(student => {
studyrights.push({
studyrightid: student.studentnumber,
canceldate: null,
cancelorganisation: null,
enddate: new Date().toUTCString(),
extentcode: numberFromTo(2, 99),
givendate: daysAgo(365 * numberFromTo(1, 100)),
graduated: null,
prioritycode: prioritycode || numberFromTo(1, 30),
startdate: date,
studystartdate: date,
organization_code: organization.code,
student_studentnumber: student.studentnumber
})
})
return studyrights
}
const generateUsers = amount => {
const number = amount || numberFromTo(10, 100)
const users = []
for (let i = 0; i < number; i++) {
users.push({
full_name: `${faker.name.lastName()}, ${faker.name.firstName(2)}`,
username: faker.lorem.word(),
is_enabled: numberFromTo(0, 1) === 1
})
}
return users
}
const generateTags = amount => {
const number = amount || numberFromTo(10, 100)
const tags = []
for (let i = 0; i < number; i++) {
tags.push({
tagname: `${faker.company.catchPhraseAdjective()} ${i}` //tagname should be unique, add i just in case
})
}
return tags
}
// adds all tags to all students
const generateTagStudents = (students, tags) => {
let tag_students = []
students.forEach(student => {
tags.forEach(tag => {
tag_students.push({
taggedstudents_studentnumber: student.studentnumber,
tags_tagname: tag.tagname
})
})
})
return tag_students
}
module.exports = {
generateCourses,
generateCourseInstances,
generateStudents,
generateStudyrights,
generateCredits,
generateTeachers,
generateCourseTeachers,
generateOrganizations,
generateUsers,
generateTags,
generateTagStudents
}
@@ -1,14 +0,0 @@
const moment = require('moment')
const semesterUtils = require('../../../src/util/semester')
describe('Semester utils tests', () => {
test('Get passing semester', () => {
expect(semesterUtils.getPassingSemester(2018, moment('2017-09-09'))).toBe('BEFORE')
expect(semesterUtils.getPassingSemester(2018, moment('2018-09-09'))).toBe('0-FALL')
expect(semesterUtils.getPassingSemester(2018, moment('2019-05-01'))).toBe('0-SPRING')
expect(semesterUtils.getPassingSemester(2018, moment('2019-09-01'))).toBe('1-FALL')
expect(semesterUtils.getPassingSemester(2018, moment('2020-05-01'))).toBe('1-SPRING')
expect(semesterUtils.getPassingSemester(2018, moment('2026-05-01'))).toBe('LATER')
expect(semesterUtils.getPassingSemester(2018, moment('2018-05-01'))).toBe('BEFORE')
})
})
@@ -1 +0,0 @@
node_modules
@@ -1 +0,0 @@
*.sql
@@ -1,13 +0,0 @@
{
"env": {
"es6": true,
"node": true,
"jest": true
},
"parser": "babel-eslint",
"extends": ["eslint:recommended", "plugin:prettier/recommended", "prettier/babel"],
"rules": {
"no-console": 0,
"prettier/prettier": "error"
}
}
@@ -1 +0,0 @@
node_modules/
@@ -1,9 +0,0 @@
{
"semi": false,
"arrowParens": "avoid",
"singleQuote": true,
"printWidth": 120,
"tabWidth": 2,
"useTabs": false,
"jsxSingleQuote": false
}
-13
View File
@@ -1,13 +0,0 @@
FROM node:14-alpine
WORKDIR /usr/src/app
CMD npm start
COPY package* ./
RUN npm ci
ARG NODE_ENV=production
ENV NODE_ENV=${NODE_ENV}
COPY . .
@@ -1,15 +0,0 @@
const fs = require('fs')
const { Student } = require('../../models/index')
const getStudentNumbers = async (writefilepath = './studentnumbers.txt') => {
const file = fs.openSync(writefilepath, 'w')
const students = await Student.findAll()
for (let student of students) {
const data = `${student.studentnumber} \n`
fs.appendFileSync(file, data)
}
fs.closeSync(file)
process.exit(0)
}
getStudentNumbers()
@@ -1,350 +0,0 @@
const moment = require('moment')
const _ = require('lodash')
const getStudyRightIdStrings = data => data['data'].map(elements => elements[0])
const getTextsByLanguage = texts => {
const names = {}
texts.forEach(text => (names[text.langcode] = text.text))
return { fi: null, sv: null, en: null, ...names }
}
const defaultNameFromTexts = texts => {
const names = getTextsByLanguage(texts)
return names.fi || names.en || names.sv
}
const jsonNamesFromTexts = texts => {
const names = getTextsByLanguage(texts)
return names
}
const universityEnrollmentDateFromStudyRights = studyRightArray => {
return _.sortBy(studyRightArray.map(s => s.start_date), n => moment(n).valueOf())[0]
}
const getStudentFromData = (student, studyrights) => {
const city = getTextsByLanguage(student.city)
const language = getTextsByLanguage(student.language)
const country = getTextsByLanguage(student.country)
const gender = getTextsByLanguage(student.gender)
const home_country = getTextsByLanguage(student.home_country)
return {
studentnumber: student.student_number,
email: student.email,
phone: student.phone,
city_fi: city.fi,
city_sv: city.sv,
national_student_number: student.national_student_number,
zipcode: student.zipcode,
address: student.address1,
creditcount: student.studyattainments,
address2: student.address2,
birthdate: getDate(student.birth_date),
language_fi: language.fi,
language_sv: language.sv,
language_en: language.en,
age: student.age_years,
lastname: student.last_name,
mobile: student.mobile_phone,
home_county_id: student.home_county_id,
country_fi: country.fi,
country_sv: country.sv,
country_en: country.en,
home_country_fi: home_country.fi,
home_country_sv: home_country.sv,
home_country_en: home_country.en,
firstnames: student.first_names,
communicationlanguage: language.en || language.fi || language.sv,
dateofuniversityenrollment: universityEnrollmentDateFromStudyRights(studyrights),
abbreviatedname: [student.last_name, student.first_names].join(' '),
gender_code: student.gender_code,
gender_fi: gender.fi,
gender_sv: gender.sv,
gender_en: gender.en
}
}
const getSemesterCode = attainment_date => moment(attainment_date).diff(moment('1950', 'YYYY'), 'months') / 6
const getDate = (date, format = 'DD.MM.YYYY') => {
if (!date) return null
return moment(date, format).format('YYYY-MM-DD')
}
const getOrganisationFromData = ({ name, code }) => {
return {
code,
name: jsonNamesFromTexts(name)
}
}
const attainmentDataToCredit = (attainment, studentnumber) => {
return {
id: String(attainment.studyattainment_id),
grade: defaultNameFromTexts(attainment.grade),
credits: attainment.credits,
ordering: getDate(attainment.attainment_date, null),
credittypecode: attainment.attainment_status_code,
student_studentnumber: studentnumber,
attainment_date: parseDate(attainment.attainment_date),
course_code: `${attainment.learningopportunity_id}`,
semestercode: attainment.semester_code,
isStudyModule: attainment.is_leaf === 'false'
}
}
const attainmentDataToCourse = attainment => {
const { learningopportunity_name, attainment_date } = attainment
return {
code: attainment.learningopportunity_id,
name: jsonNamesFromTexts(learningopportunity_name),
latest_instance_date: parseDate(attainment_date),
is_study_module: attainment.is_leaf === 'false'
}
}
const getTeacherFromData = teacher => {
if (teacher) {
return {
id: teacher.teacher_id,
code: teacher.userid,
name: teacher.full_name
}
}
}
const parseDate = (date, format = 'YYYY-MM-DD HH:mm') => date && moment.utc(date, format).toDate()
const getStudyRightFromData = (data, studentNumber) => {
return {
studyrightid: `${data.studyright_id}`,
canceldate: data.cancel_date,
cancelorganisation: data.organisation_code,
enddate: parseDate(data.degree_date) || parseDate(data.end_date),
extentcode: data.extent_code,
givendate: parseDate(data.admission_date),
graduated: Number(!!data.degree_date),
graduation_date: data.degree_date,
prioritycode: data.priority,
startdate: parseDate(data.start_date),
studystartdate: parseDate(data.study_start_date),
organization_code: data.organisation_code,
student_studentnumber: studentNumber,
faculty_code: data.faculty_code
}
}
const elementDetailFromData = element => {
const { code, name, element_id } = element
const names = getTextsByLanguage(name)
return {
code,
name: names,
type: element_id
}
}
const studyrightElementFromData = (element, studyrightid, studentnumber, degree_date) => {
const { start_date, end_date, code } = element
return {
code,
studyrightid,
studentnumber,
startdate: start_date,
enddate: degree_date || end_date
}
}
const attainmentDataToTeachers = data =>
data.teachers.map(teacherdata => ({
id: teacherdata.teacher_id
}))
const studyrightDataToExtent = data => ({
extentcode: data.extent_code,
name: getTextsByLanguage(data.extent)
})
const courseTypeFromData = data => ({
coursetypecode: data.code,
name: getTextsByLanguage(data.name)
})
const disciplineFromData = data => ({
discipline_id: data.discipline_id,
name: getTextsByLanguage(data.name)
})
const learningOpportunityDataToCourseDisciplines = data => {
if (data) {
return data.disciplines.map(discipline => ({
discipline_id: discipline.discipline_id,
course_id: data.learningopportunity_id
}))
}
}
const learningOpportunityDataToCourse = data => {
if (data) {
return {
code: data.learningopportunity_id,
coursetypecode: data.learningopportunity_type_code,
disciplines: data.disciplines,
name: getTextsByLanguage(data.names),
startdate: parseDate(data.start_date),
enddate: parseDate(data.end_date)
}
}
}
const learningOpportunityDataToCourseProviders = data => {
if (!data) {
return { providers: [], courseproviders: [] }
}
const providers = data.organisations.map(organisation => ({
providercode: organisation.code,
name: getTextsByLanguage(organisation.name)
}))
const courseproviders = providers.map(provider => ({
providercode: provider.providercode,
coursecode: data.learningopportunity_id
}))
return {
providers,
courseproviders
}
}
const studyattainmentStatusCodeToCreditType = data => ({
credittypecode: data.code,
name: getTextsByLanguage(data.name)
})
const semesterFromData = data => ({
semestercode: data.semester_code,
name: getTextsByLanguage(data.name),
startdate: parseDate(data.start_date),
enddate: parseDate(data.end_date),
yearcode: data.year_code,
yearname: data.year_name
})
const semesterEnrollmentFromData = (data, studentnumber) => ({
enrollmenttype: data.semester_enrollment_type_code,
semestercode: data.semester_code,
enrollment_date: parseDate(data.semester_enrollment_date),
studentnumber
})
const getTransfersFromData = (data, studentnumber) => {
const studytracks = data.elements.filter(element => element.element_id === 20)
const sorted = _.sortBy(studytracks, 'end_date')
let transfers = []
let i = 0
while (i < sorted.length) {
if (i === 0) {
i++
continue
}
let target = sorted[i].code
let source = sorted[i - 1].code
let transferdate = sorted[i - 1].end_date
transfers = transfers.concat({
sourcecode: source,
targetcode: target,
transferdate,
studentnumber,
studyrightid: `${data.studyright_id}`
})
i++
}
return transfers
}
const courseRealisationTypeFromData = data => ({
realisationtypecode: `${data.code}`,
name: getTextsByLanguage(data.name)
})
const studentEnrollmentToModels = (data, studentnumber) => {
const courserealisation = {
courserealisation_id: `${data.course_id}`,
startdate: parseDate(data.start_date),
enddate: parseDate(data.end_date),
coursecode: `${data.learningopportunity_id}`,
name: getTextsByLanguage(data.name),
realisationtypecode: `${data.type_code}`,
parent: `${data.parent_id}`,
root: `${data.root_id}`
}
const courseenrollment = {
studentnumber,
courserealisation_id: courserealisation.courserealisation_id
}
const course = {
code: data.learningopportunity_id
}
return {
courserealisation,
courseenrollment,
course
}
}
const courseUnitRealisationDataToModels = data => {
const courserealisation = {
courserealisation_id: `${data.course_id}`,
startdate: parseDate(data.start_date),
enddate: parseDate(data.end_date),
coursecode: `${data.learningopportunity_id}`,
name: getTextsByLanguage(data.realisation_name),
realisationtypecode: `${data.realisation_type_code}`,
parent: `${data.parent_id}`
}
const course = {
code: courserealisation.coursecode
}
const students = data.students.map(({ student_number }) => ({
studentnumber: student_number
}))
const courseenrollments = students.map(student => ({
studentnumber: student.studentnumber,
courserealisation_id: courserealisation.courserealisation_id
}))
return {
courserealisation,
course,
students,
courseenrollments
}
}
module.exports = {
getStudentFromData,
getStudyRightIdStrings,
getStudyRightFromData,
getDate,
getTeacherFromData,
elementDetailFromData,
studyrightElementFromData,
attainmentDataToCourse,
attainmentDataToCredit,
getOrganisationFromData,
attainmentDataToTeachers,
studyrightDataToExtent,
courseTypeFromData,
learningOpportunityDataToCourse,
studyattainmentStatusCodeToCreditType,
disciplineFromData,
learningOpportunityDataToCourseDisciplines,
semesterFromData,
semesterEnrollmentFromData,
learningOpportunityDataToCourseProviders,
getTransfersFromData,
courseRealisationTypeFromData,
studentEnrollmentToModels,
courseUnitRealisationDataToModels,
getSemesterCode
}
@@ -1,199 +0,0 @@
const mapper = require('./oodi_data_mapper')
describe('studyattainment data mapping tests', () => {
const data = {
attainment_status_code: 4,
attainment_date: '2018-05-30T21:00:00.000Z',
studyattainment_id: 123456789,
attainment_lang: null,
person_id: 1000002,
textual_assessment: null,
teachers: [
{
teacher_id: 'TID_01',
person_id: 1000001,
short_name: 'Matti Meikäläinen',
last_names: 'Meikäläinen',
first_names: 'Matti Teppo'
}
],
studyright_id: null,
semester_code: 136,
grade: [
{
langcode: 'fi',
text: 'Hyv.'
},
{
langcode: 'sv',
text: 'Godk'
},
{
langcode: 'en',
text: 'Pass'
}
],
credits: 10,
expiry_date: null,
learningopportunity_name: [
{
langcode: 'fi',
text: 'learningopportunity_name FI'
},
{
langcode: 'sv',
text: 'learningopportunity_name SV'
},
{
langcode: 'en',
text: 'learningopportunity_name EN'
}
],
learningopportunity_id: 'LID_001'
}
describe('credit mapping tests', () => {
const studentnumber = '0876654321'
const credit = mapper.attainmentDataToCredit(data, studentnumber)
describe('studyattainment date tests', () => {
const { attainment_date } = credit
const MAY = 4
test('attainment date is truthy', () => {
expect(attainment_date).toBeTruthy()
})
test('attainment date is instance of Date', () => {
expect(attainment_date instanceof Date).toBe(true)
})
test('attainment date has correct month, year and date', () => {
expect(attainment_date.getMonth()).toBe(MAY)
expect(attainment_date.getFullYear()).toBe(2018)
expect(attainment_date.getDate()).toBe(30)
})
})
test('course code matches learningopportunity_id', () => {
expect(credit.course_code).toBe('LID_001')
})
test('credit id should attainment id as string', () => {
expect(credit.id).toBe('123456789')
})
test('studentnumber is correct', () => {
expect(credit.student_studentnumber).toBe(studentnumber)
})
test('semestercode is correct', () => {
expect(credit.semestercode).toBe(136)
})
})
describe('teacher mapping tests', () => {
const teachers = mapper.attainmentDataToTeachers(data)
test('amount of mapped teachers is correct (1)', () => {
expect(teachers.length).toBe(1)
})
test('teacher should have correct id', () => {
expect(teachers[0].id).toBe('TID_01')
})
})
})
describe('semester enrollment data mapping tests', () => {
const studentnumber = '12345678'
const data = {
full_time_student: 'true',
yths_code: null,
semester_enrollment_type_code: 1,
absence_reason_code: null,
payment_amount: null,
yths_description: null,
student_union_member: 'true',
semester_enrollment_date: '2011-08-30T21:00:00.000Z',
payment_date: null,
semester_code: 123,
doctoral_current_year_estimate: null,
doctoral_previous_year: null
}
describe('mapping of enrolled data', () => {
const mapping = mapper.semesterEnrollmentFromData(data, studentnumber)
test('mapped semestercode should be integer 123', () => {
expect(mapping.semestercode).toBe(123)
})
test('mapped enrollmenttype code should be integer 1', () => {
expect(mapping.enrollmenttype).toBe(1)
})
test('mapped studentnumber should be correct', () => {
expect(mapping.studentnumber).toBe(studentnumber)
})
test('mapped enrollment_date should be instance of Date', () => {
expect(mapping.enrollment_date instanceof Date).toBe(true)
})
test('mapped enrollment_date should be equal to Date object with same day, month and year', () => {
expect(mapping.enrollment_date).toEqual(new Date('2011-08-30'))
})
})
})
describe('semester code data mapping tests', () => {
const data = {
teaching_start_date: '1950-09-14T22:00:00.000Z',
name: [
{
langcode: 'fi',
text: 'Syksy 1950'
},
{
langcode: 'sv',
text: 'Hösten 1950'
},
{
langcode: 'en',
text: 'Autumn 1950'
}
],
year_name: '1950-51',
enroll_end_date: '1950-09-29T22:00:00.000Z',
enroll_start_date: '1950-05-31T22:00:00.000Z',
end_date: '1950-12-30T22:00:00.000Z',
semester_code: 1,
start_date: '1900-07-31T22:20:11.000Z',
teaching_end_date: '1950-12-20T22:00:00.000Z',
year_code: 1
}
const semester = mapper.semesterFromData(data)
test('semester code should be correct', () => {
expect(semester.semestercode).toBe(1)
})
test('semester name should have all language keys set as properties', () => {
expect(semester.name).toHaveProperty('fi')
expect(semester.name).toHaveProperty('en')
expect(semester.name).toHaveProperty('sv')
})
test('semester start and end date should be instances of Date', () => {
expect(semester.startdate instanceof Date).toBe(true)
expect(semester.enddate instanceof Date).toBe(true)
})
test('semester yearcode and yearname should be correct', () => {
expect(semester.yearcode).toBe(1)
expect(semester.yearname).toBe('1950-51')
})
})
@@ -1,253 +0,0 @@
require('dotenv').config()
const axios = require('axios')
const LRU = require('lru-cache')
const https = require('https')
const fs = require('fs')
const { OODI_ADDR, KEY_PATH, CERT_PATH } = process.env
const base_url = OODI_ADDR
const requestCache = new LRU({
max: 25000,
length: () => 1,
maxAge: 60 * 60 * 1000
})
const agent =
KEY_PATH && CERT_PATH
? new https.Agent({
cert: fs.readFileSync(process.env.CERT_PATH, 'utf8'),
key: fs.readFileSync(process.env.KEY_PATH, 'utf8')
})
: new https.Agent({
rejectUnauthorized: false
})
const instance = axios.create({
httpsAgent: agent
})
const getUrl = async url => {
const cacheHit = requestCache.get(url)
if (cacheHit) return cacheHit
const res = await instance.get(url)
// Sometimes oodi seems to randomly return a response with status 200
// that in reality contains an error. Check this out:
// https://github.com/UniversityOfHelsinkiCS/oodikone/issues/1547#issuecomment-550167612
if (res.data.exception) {
throw new Error(res.data.exception.message)
}
if (!url.includes('/students/')) requestCache.set(url, res.data.data)
return res.data.data
}
const getOodiApi = async relative => {
const route = base_url + relative
const stuff = await getUrl(route)
return stuff
}
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms))
const attemptGetFor = async (url, attempts = 6) => {
for (let attempt = 1; attempt <= attempts; ++attempt) {
try {
return await getUrl(url)
} catch (error) {
if (attempt === attempts) {
console.log('ATTEMPT GET FOR FAILURE')
throw error
}
await sleep(attempt * 5000)
}
}
}
if (process.env.NODE_ENV === 'development') {
// axios.defaults.auth = {
// username: 'tktl',
// password: process.env.OODI_PW
// }
instance.interceptors.request.use(config => {
config.params = { token: process.env.TOKEN }
return config
})
}
console.log(process.env.NODE_ENV)
const getStudent = async studentNumber => {
const url = `${base_url}/students/${studentNumber}/info`
try {
return await attemptGetFor(url)
} catch (e) {
console.log('GET STUDENT FAILED')
throw e
}
}
const getStudentStudyRights = async studentNumber => {
const url = `${base_url}/students/${studentNumber}/studyrights`
try {
return await attemptGetFor(url)
} catch (e) {
console.log('GET STUDYRIGHT FAIELD')
throw e
}
}
const getFaculties = async () => {
const url = `${base_url}/codes/faculties`
try {
return await attemptGetFor(url)
} catch (e) {
console.log(' GET FACULTIE S FAIELD')
throw e
}
}
const getStudyAttainments = async studentNumber => {
const url = `${base_url}/students/${studentNumber}/studyattainments`
try {
return await attemptGetFor(url)
} catch (e) {
console.log('GET STSDU ATTAINEMNT FIALE')
throw e
}
}
const getTeacherInfo = async id => {
const url = `${base_url}/teachers/${id}/info`
try {
return await attemptGetFor(url)
} catch (e) {
console.log('GET TCHR FIAIELd', id)
throw e
}
}
const getCourseTypeCodes = async () => {
const url = `${base_url}/codes/learningopportunities/types`
try {
return await attemptGetFor(url)
} catch (e) {
console.log('CORUSE TTYPE CODE FAIEL')
throw e
}
}
const getLearningOpportunity = async id => {
const url = `${base_url}/learningopportunities/${id}`
try {
return await attemptGetFor(url)
} catch (e) {
console.log('GET LEARNIGN OEPPRITUNCITY FAIELD', id)
throw e
}
}
const getStudyattainmentStatusCodes = async () => {
const url = `${base_url}/codes/studyattainments/statuses`
return await attemptGetFor(url)
}
const getCourseDisciplines = async () => {
const url = `${base_url}/codes/learningopportunities/disciplines`
try {
return await attemptGetFor(url)
} catch (e) {
console.log(' GET COURS EDISCIIPLINES FIALED')
throw e
}
}
const getSemesters = async () => {
const url = `${base_url}/codes/semesters`
try {
return await attemptGetFor(url)
} catch (e) {
console.log('GET SEMESTERS FAIELD')
throw e
}
}
const getSemesterEnrollments = async studentnumber => {
const url = `${base_url}/students/${studentnumber}/semesterenrollments`
try {
return await attemptGetFor(url)
} catch (e) {
console.log('GEST EMESTER ENROLLMENTS FAIELD')
throw e
}
}
const getCourseEnrollments = async studentnumber => {
const url = `${base_url}/students/${studentnumber}/enrollments`
try {
return await attemptGetFor(url)
} catch (e) {
console.log('GET COURS EENREOLMETNS FAIELD')
throw e
}
}
const getCourseRealisationTypes = async () => {
const url = `${base_url}/codes/courseunitrealisations/types`
try {
return await attemptGetFor(url)
} catch (e) {
console.log('GET COURSE REALSITASITON TYPES')
throw e
}
}
const courseUnitRealisationsSince = async sinceDate => {
const url = `${base_url}/courseunitrealisations/changes/ids/${sinceDate}`
try {
return await attemptGetFor(url)
} catch (e) {
console.log('COURSEU UNTI REALISTIATION')
throw e
}
}
const learningOpportunitiesSince = async (sinceDate = '0000-01-01') => {
const url = `${base_url}/learningopportunities/changes/ids/${sinceDate}`
try {
return await attemptGetFor(url)
} catch (e) {
console.log('LRNG OPRRTN SINCE')
throw e
}
}
const getCourseUnitRealisation = async id => {
const url = `${base_url}/courseunitrealisations/${id}`
try {
return await attemptGetFor(url)
} catch (e) {
console.log('GET COURS EUNTI GREALSISTAITOn', id)
throw e
}
}
module.exports = {
getStudentStudyRights,
getStudent,
getFaculties,
getStudyAttainments,
getTeacherInfo,
getCourseTypeCodes,
getLearningOpportunity,
getStudyattainmentStatusCodes,
getCourseDisciplines,
getSemesters,
getSemesterEnrollments,
getCourseEnrollments,
getCourseRealisationTypes,
courseUnitRealisationsSince,
getCourseUnitRealisation,
learningOpportunitiesSince,
getOodiApi
}
@@ -1,158 +0,0 @@
const Oodi = require('./oodi_interface')
const mapper = require('./oodi_data_mapper')
const getAllStudentInformationFromApi = async studentnumber => {
const [student, studyrights, studyattainments, semesterEnrollments, courseEnrollments] = await Promise.all([
Oodi.getStudent(studentnumber),
Oodi.getStudentStudyRights(studentnumber),
Oodi.getStudyAttainments(studentnumber),
Oodi.getSemesterEnrollments(studentnumber),
Oodi.getCourseEnrollments(studentnumber)
])
return {
student,
studyrights,
studyattainments,
studentnumber,
semesterEnrollments,
courseEnrollments
}
}
const formatStudyrights = async (api, studentnumber) => {
if (api.studyrights.length === 0) {
console.log(`No studyrights for ${studentnumber}`)
return []
}
return api.studyrights.map(studyrightData => {
const studyRightExtent = mapper.studyrightDataToExtent(studyrightData)
const studyright = mapper.getStudyRightFromData(studyrightData, studentnumber)
const elementDetails = studyrightData.elements.map(element => mapper.elementDetailFromData(element))
const studyRightElements = studyrightData.elements.map(element =>
mapper.studyrightElementFromData(element, studyright.studyrightid, studentnumber, studyrightData.degree_date)
)
const transfers = mapper.getTransfersFromData(studyrightData, studentnumber)
return { studyRightExtent, studyright, elementDetails, studyRightElements, transfers }
})
}
const formatCourseEnrollments = async (apidata, studentnumber) =>
await Promise.all(
apidata.courseEnrollments.map(enrollment => mapper.studentEnrollmentToModels(enrollment, studentnumber))
)
const parseAttainmentData = (data, studentnumber) => {
return {
credit: mapper.attainmentDataToCredit(data, studentnumber),
teachers: mapper.attainmentDataToTeachers(data),
course: mapper.attainmentDataToCourse(data)
}
}
const createCreditTeachers = async (credit, teachers) => {
const creditTeachers = teachers.map(teacher => ({
credit_id: credit.id,
teacher_id: teacher.id
}))
return creditTeachers
}
const formatStudyattainments = async (api, studentnumber) => {
let studyAttainments = []
for (let data of api.studyattainments) {
const { credit, teachers, course } = parseAttainmentData(data, studentnumber)
let learningOpportunity
// For some learning opportunities oodi returns an SQL error.
// F.ex. /learningopportunities/590202B. Therefore, until the
// bug is fixed on Oodi's side, this is what we need to do.
try {
learningOpportunity = await Oodi.getLearningOpportunity(course.code)
} catch (e) {
if (e.message && e.message.includes('PreparedStatementCallback')) continue
throw e
}
const { providers, courseproviders } = mapper.learningOpportunityDataToCourseProviders(learningOpportunity)
studyAttainments = [
...studyAttainments,
{
credit: credit.semestercode
? credit
: { ...credit, semestercode: mapper.getSemesterCode(credit.attainment_date) },
creditTeachers: await createCreditTeachers(credit, teachers),
teachers: (await Promise.all(
teachers.map(async t => {
const teacher = await Oodi.getTeacherInfo(t.id)
// https://github.com/UniversityOfHelsinkiCS/oodikone/issues/1547#issuecomment-550184131
// Sometimes Oodi returns a null teacher... We need to figure out why.
if (!teacher) {
console.log('TEACHER MISSING!', t.id)
return null
}
return mapper.getTeacherFromData(teacher)
})
)).filter(t => t),
course: {
...course,
...mapper.learningOpportunityDataToCourse(learningOpportunity),
disciplines: mapper.learningOpportunityDataToCourseDisciplines(learningOpportunity),
providers,
courseproviders
}
}
]
}
return studyAttainments
}
const formatSemesterEnrollments = async (apidata, studentnumber) =>
await Promise.all(
apidata.semesterEnrollments.map(apiEnrollment => mapper.semesterEnrollmentFromData(apiEnrollment, studentnumber))
)
const getStudent = async studentnumber => {
const api = await getAllStudentInformationFromApi(studentnumber)
if (api.student == null) {
const error = new Error(`API returned ${api.student} for studentnumber ${studentnumber}.`)
error.name = 'NO_STUDENT'
throw error
}
try {
const studentInfo = await mapper.getStudentFromData(api.student, api.studyrights)
const [studyRights, studyAttainments, semesterEnrollments, courseEnrollments] = await Promise.all([
formatStudyrights(api, studentnumber),
formatStudyattainments(api, studentnumber),
formatSemesterEnrollments(api, studentnumber),
formatCourseEnrollments(api, studentnumber)
])
return { studentInfo, studyRights, studyAttainments, semesterEnrollments, courseEnrollments }
} catch (e) {
console.error('getStudent failed', api)
throw e
}
}
const getMeta = async () => {
const [
faculties,
courseRealisationsTypes,
semesters,
creditTypeCodes,
courseTypeCodes,
disciplines
] = await Promise.all([
(await Oodi.getFaculties()).map(mapper.getOrganisationFromData),
(await Oodi.getCourseRealisationTypes()).map(mapper.courseRealisationTypeFromData),
(await Oodi.getSemesters()).map(mapper.semesterFromData),
(await Oodi.getStudyattainmentStatusCodes()).map(mapper.studyattainmentStatusCodeToCreditType),
(await Oodi.getCourseTypeCodes()).map(mapper.courseTypeFromData),
(await Oodi.getCourseDisciplines()).map(mapper.disciplineFromData)
])
return { faculties, courseRealisationsTypes, semesters, creditTypeCodes, courseTypeCodes, disciplines }
}
module.exports = {
getStudent,
getMeta
}
-72
View File
@@ -1,72 +0,0 @@
const { stan, opts } = require('./stan')
const { getStudent, getMeta } = require('./doo_api_database_updater/updater_formatter')
const logger = require('./logger')
const moment = require('moment')
const hasActiveEnrollment = semesterEnrollments => {
if (semesterEnrollments) {
const currentSemester = Math.floor(moment().diff(moment('1950', 'YYYY'), 'months') / 6)
const active = semesterEnrollments.find(e => e.semestercode === currentSemester && e.enrollmenttype === 1)
return active ? true : false
}
return false
}
const fetchData = async (priority, msg) => {
try {
const task = JSON.parse(msg.getData())
if (task.task === 'meta') {
const data = await getMeta()
stan.publish('status', JSON.stringify({ task: task.task, status: 'FETCHED', priority }), err => {
if (err) console.log('STATUS PUBLISH FAILED', err)
})
stan.publish(priority ? 'PriorityWrite' : 'UpdateWrite', JSON.stringify({ task: task.task, data }))
} else {
try {
const data = await getStudent(task.task)
// TODO: check that data is properly structured(?)
const active = hasActiveEnrollment(data.semesterEnrollments)
stan.publish('status', JSON.stringify({ task: task.task, status: 'FETCHED', active, priority }), err => {
if (err) console.log('STATUS PUBLISH FAIELD', err)
})
stan.publish(
priority ? 'PriorityWrite' : 'UpdateWrite',
JSON.stringify({ task: task.task, data, active }),
err => {
if (err) console.log('STATUS PUBLISH FAILED', err)
}
)
} catch (e) {
if (e.name === 'NO_STUDENT') {
stan.publish(
'status',
JSON.stringify({ task: task.task, status: 'NO_STUDENT', active: false, priority }),
err => {
if (err) console.log('STATUS PUBLISH FAILED', err)
}
)
return
} else {
console.error('Task failed', task)
throw e
}
}
}
} catch (e) {
console.log(e)
logger.info('failure', { service: 'API' })
}
}
const handleMessage = priority => async msg => {
await fetchData(priority, msg)
msg.ack()
}
stan.on('connect', async () => {
const sub = stan.subscribe('UpdateApi', 'updater.workers', opts)
const prioSub = stan.subscribe('PriorityApi', 'updater.workers.prio', opts)
sub.on('message', handleMessage(false))
prioSub.on('message', handleMessage(true))
})
-27
View File
@@ -1,27 +0,0 @@
const winston = require('winston')
const Log2gelf = require('winston-log2gelf')
const transports = []
if (process.env.LOG_PORT && process.env.LOG_HOST) {
transports.push(
new Log2gelf({
hostname: process.env.LOG_HOSTNAME || 'UPDATER',
host: process.env.LOG_HOST,
port: process.env.LOG_PORT,
protocol: process.env.LOG_PROTOCOL || 'https',
environment: process.env.NODE_ENV,
protocolOptions: {
path: process.env.LOG_PATH || '/gelf'
}
})
)
}
if (process.env.NODE_ENV !== 'test') {
transports.push(new winston.transports.File({ filename: 'debug.log' }))
}
const logger = winston.createLogger({ transports })
module.exports = logger
File diff suppressed because it is too large Load Diff
@@ -1,34 +0,0 @@
{
"name": "updater_api",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "npm run lint",
"lint": "eslint .",
"lintfix": "prettier --write **/*.{js,json} && eslint --fix .",
"start": "node index.js",
"dev": "nodemon index.js"
},
"author": "",
"license": "ISC",
"dependencies": {
"axios": "^0.21.1",
"dotenv": "^7.0.0",
"lodash": "^4.17.21",
"lru-cache": "^5.1.1",
"moment": "^2.29.1",
"node-nats-streaming": "^0.2.6",
"nodemon": "^1.19.4",
"winston": "^3.3.3",
"winston-log2gelf": "^2.4.0"
},
"devDependencies": {
"babel-eslint": "^10.1.0",
"eslint": "^6.8.0",
"eslint-config-prettier": "^6.15.0",
"eslint-plugin-prettier": "^3.4.0",
"prettier": "^1.19.1",
"prettier-eslint": "^9.0.2"
}
}
-13
View File
@@ -1,13 +0,0 @@
const stan = require('node-nats-streaming').connect('updaterNATS', process.env.HOSTNAME, process.env.NATS_URI)
console.log(`STARTING WITH ${process.env.HOSTNAME} as id`)
const opts = stan.subscriptionOptions()
opts.setManualAckMode(true)
opts.setAckWait(10 * 60 * 1000)
opts.setMaxInFlight(1)
module.exports = {
stan,
opts
}
@@ -1,32 +0,0 @@
var stan = require('node-nats-streaming').connect('updaterNATS', 'producer', process.env.NATS_URI)
stan.on('connect', () => {
stan.publish('UpdateApi', 'meta', (err, guid) => {
if (err) {
console.log('publish failed')
} else {
console.log('published', guid)
}
})
stan.publish('UpdateApi', '011120775', (err, guid) => {
if (err) {
console.log('publish failed')
} else {
console.log('published', guid)
}
})
stan.publish('UpdateApi', '014441008', (err, guid) => {
if (err) {
console.log('publish failed')
} else {
console.log('published', guid)
}
})
stan.publish('UpdateApi', '014272112', (err, guid) => {
if (err) {
console.log('publish failed')
} else {
console.log('published', guid)
}
})
})
@@ -1 +0,0 @@
node_modules
@@ -1 +0,0 @@
*.sql
@@ -1,13 +0,0 @@
{
"env": {
"es6": true,
"node": true,
"jest": true
},
"parser": "babel-eslint",
"extends": ["eslint:recommended", "plugin:prettier/recommended", "prettier/babel"],
"rules": {
"no-console": 0,
"prettier/prettier": "error"
}
}
@@ -1,9 +0,0 @@
{
"semi": false,
"arrowParens": "avoid",
"singleQuote": true,
"printWidth": 120,
"tabWidth": 2,
"useTabs": false,
"jsxSingleQuote": false
}
@@ -1,13 +0,0 @@
FROM node:14-alpine
WORKDIR /usr/src/app
CMD npm start
COPY package* ./
RUN npm ci
ARG NODE_ENV=production
ENV NODE_ENV=${NODE_ENV}
COPY . .
@@ -1,126 +0,0 @@
const { stan } = require('./src/nats_connection')
const { CronJob } = require('cron')
const Schedule = require('./models')
const logger = require('./logger')
const {
scheduleDaily,
scheduleAllStudents,
scheduleAttainmentUpdate,
scheduleMeta
} = require('./src/schedule_students')
const { isValidStudentId } = require('./src/util')
require('./src/api')
const schedule = (cronTime, func) => new CronJob({ cronTime, onTick: func, start: true, timeZone: 'Europe/Helsinki' })
let updatedCount = 0
let scheduledCount = 0
let fetchedCount = 0
const updateTask = async ({ task, status, type, updatetime, active }) => {
const doc = { task, status, type }
if (updatetime) doc.updatedAt = new Date()
if (active != null) doc.active = active
await Schedule.findOneAndUpdate({ task }, doc, { upsert: true })
}
stan.on('connect', async () => {
if (process.env.NODE_ENV === 'production') {
schedule('0 0 * * 6', async () => {
// Every Friday-Saturday midnight
try {
console.log('SCHEDULING ALL STUDENTS')
await scheduleAllStudents()
} catch (err) {
console.log('SCHEDULING ALL STUDENTS FAILED')
console.log(err)
logger.info('failure', { service: 'SCHEDULER' })
}
})
schedule('0 6 * * 1-5', async () => {
// Every Monday through Friday at 06:00
try {
console.log('SCHEDULING META AND ATTAINMENT')
await scheduleMeta()
await scheduleAttainmentUpdate()
} catch (err) {
console.log('SCHEDULING META AND ATTAINMENT')
console.log(err)
logger.info('failure', { service: 'SCHEDULER' })
}
})
schedule('0 20 * * 1-4', async () => {
// Every Monday through Thursday at 20:00
try {
console.log('SCHEDULING DAILY TASKS')
await scheduleDaily()
} catch (err) {
console.log('SCHEDULING DAILY TASKS FAILED')
console.log(err)
logger.info('failure', { service: 'SCHEDULER' })
}
})
}
schedule('0 * * * *', async () => {
// Every hour
console.log(
`${updatedCount} TASKS DONE IN LAST HOUR. ${scheduledCount} TASKS SCHEDULED IN LAST HOUR. ${fetchedCount} TASKS FETCHED FROM API IN LAST HOUR`
)
updatedCount = 0
fetchedCount = 0
scheduledCount = 0
})
const opts = stan.subscriptionOptions()
opts.setManualAckMode(true)
opts.setAckWait(5 * 60 * 1000)
// opts.setDeliverAllAvailable()
// opts.setDurableName('durable')
opts.setMaxInFlight(20)
const statusSub = stan.subscribe('status', opts)
const handleStatusMessage = async msg => {
const data = JSON.parse(msg.getData())
const { task, status, timems, active, priority } = data
let updatetime = false
switch (status) {
case 'DONE':
case 'NO_STUDENT':
updatedCount = updatedCount + 1
updatetime = true
break
case 'FETCHED':
fetchedCount = fetchedCount + 1
break
case 'SCHEDULED':
scheduledCount = scheduledCount + 1
break
default:
throw 'unknown status: ' + msg.getData()
}
const isStudent = !!isValidStudentId(task)
if (status === 'DONE')
logger.info(`Status changed for ${task} to ${status}`, {
task,
status,
student: isStudent,
timems,
active,
priority
})
await updateTask({ task, status, type: isStudent ? 'student' : 'other', updatetime, active })
}
statusSub.on('message', async msg => {
try {
await handleStatusMessage(msg)
} catch (err) {
console.log(err)
logger.info('failure', { service: 'SCHEDULER' })
}
msg.ack()
})
})
@@ -1,29 +0,0 @@
const winston = require('winston')
const Log2gelf = require('winston-log2gelf')
const transports = []
if (process.env.LOG_PORT && process.env.LOG_HOST) {
transports.push(
new Log2gelf({
hostname: process.env.LOG_HOSTNAME || 'UPDATER',
host: process.env.LOG_HOST,
port: process.env.LOG_PORT,
protocol: process.env.LOG_PROTOCOL || 'https',
environment: process.env.NODE_ENV,
protocolOptions: {
path: process.env.LOG_PATH || '/gelf'
}
})
)
}
if (process.env.NODE_ENV !== 'test') {
transports.push(new winston.transports.File({ filename: 'debug.log' }))
}
transports.push(new winston.transports.Console({ level: 'debug' }))
const logger = winston.createLogger({ transports })
module.exports = logger
@@ -1,18 +0,0 @@
const mongoose = require('mongoose')
mongoose.connect(process.env.MONGO_URI, { useNewUrlParser: false, useFindAndModify: false })
const scheduleSchema = new mongoose.Schema({
task: {
type: String,
unique: true
},
status: String,
type: String,
active: Boolean,
updatedAt: {
type: Date,
index: true
}
})
module.exports = mongoose.model('Schedule', scheduleSchema)
File diff suppressed because it is too large Load Diff
@@ -1,35 +0,0 @@
{
"name": "updater_scheduler",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "npm run lint",
"lint": "eslint .",
"lintfix": "prettier --write **/*.{js,json} && eslint --fix .",
"start": "node --max_old_space_size=4096 index.js",
"dev": "nodemon --max_old_space_size=4096 index.js",
"scheduleActiveStudents": "NATS_CLIENTID=manualActiveSchedule node --max_old_space_size=4096 -e 'require(\"./src/schedule_students.js\").scheduleActiveStudents()'",
"scheduleMeta": "NATS_CLIENTID=manualAllSchedule node --max_old_space_size=4096 -e 'require(\"./src/schedule_students.js\").scheduleMeta()'",
"getOldestTasks": "node --max_old_space_size=4096 -e 'require(\"./src/SchedulingStatistics.js\").getOldestTasks()'"
},
"author": "",
"license": "ISC",
"dependencies": {
"cron": "^1.8.2",
"express": "^4.17.1",
"mongoose": "^5.12.3",
"node-nats-streaming": "^0.2.6",
"nodemon": "^1.19.4",
"winston": "^3.3.3",
"winston-log2gelf": "^2.4.0"
},
"devDependencies": {
"babel-eslint": "^10.1.0",
"eslint": "^6.8.0",
"eslint-config-prettier": "^6.15.0",
"eslint-plugin-prettier": "^3.4.0",
"prettier": "^1.19.1",
"prettier-eslint": "^9.0.2"
}
}
@@ -1,48 +0,0 @@
const Schedule = require('../models')
const getOldestTasks = async () => {
const oldestTask = (await Schedule.find({ type: 'student' })
.sort({ updatedAt: 1 })
.limit(1))[0]
const oldestActiveTask = (await Schedule.find({ type: 'student', active: true })
.sort({ updatedAt: 1 })
.limit(1))[0]
const oldestMetaTask = (await Schedule.find({ type: 'other', task: 'meta' })
.sort({ updatedAt: 1 })
.limit(1))[0]
const oldestAttainmentTask = (await Schedule.find({ type: 'other', task: 'attainment' })
.sort({ updatedAt: 1 })
.limit(1))[0]
return {
oldestChangedStatus: {
studentnumber: oldestTask ? oldestTask.task : null,
updatedAt: oldestTask ? oldestTask.updatedAt : null
},
oldestActiveStudentDone: {
studentnumber: oldestActiveTask ? oldestActiveTask.task : null,
updatedAt: oldestActiveTask ? oldestActiveTask.updatedAt : null
},
oldestMetaTask: {
updatedAt: oldestMetaTask ? oldestMetaTask.updatedAt : null,
status: oldestMetaTask ? oldestMetaTask.status : null
},
oldestAttainmentTask: {
updatedAt: oldestAttainmentTask ? oldestAttainmentTask.updatedAt : null,
status: oldestAttainmentTask ? oldestAttainmentTask.status : null
}
}
}
const getCurrentStatus = async () => {
const allTasksScheduled = await Schedule.count({ type: 'student', status: 'SCHEDULED' })
const allTasksFetched = await Schedule.count({ type: 'student', status: 'FETCHED' })
const allTasksDone = await Schedule.count({ type: 'student', status: 'DONE' })
const allTasksCreated = await Schedule.count({ type: 'student', status: 'CREATED' })
const allTasksNoStudent = await Schedule.count({ type: 'student', status: 'NO_STUDENT' })
const allTasksActive = await Schedule.count({ type: 'student', active: true })
return { allTasksScheduled, allTasksFetched, allTasksDone, allTasksCreated, allTasksActive, allTasksNoStudent }
}
module.exports = { getOldestTasks, getCurrentStatus }
@@ -1,127 +0,0 @@
const express = require('express')
const app = express()
const port = process.env.PORT
const bodyParser = require('body-parser')
const {
scheduleStudentsByArray,
scheduleOldestNStudents,
scheduleAllStudents,
scheduleActiveStudents,
scheduleMeta,
scheduleAttainmentUpdate,
scheduleStudentCheck,
rescheduleScheduled,
rescheduleFetched,
rescheduleCreated,
scheduleDaily
} = require('./schedule_students')
const { getOldestTasks, getCurrentStatus } = require('./SchedulingStatistics')
const { createTasks } = require('./student_list_updater')
app.use(bodyParser.json())
app.get('/ping', (req, res) => res.json({ message: 'pong ' }))
app.post('/update', async (req, res) => {
await createTasks(req.body)
const scheduled = await scheduleStudentsByArray(req.body)
res.json({ message: `scheduled ${scheduled} updates` })
})
app.post('/update/oldest', async (req, res) => {
const { amount } = req.body
const scheduled = await scheduleOldestNStudents(amount)
res.json({ message: `scheduled ${scheduled} updates` })
})
app.post('/update/all', async (req, res) => {
const scheduled = await scheduleAllStudents()
res.json({ message: `scheduled ${scheduled} updates` })
})
app.post('/update/active', async (req, res) => {
const scheduled = await scheduleActiveStudents()
res.json({ message: `scheduled ${scheduled} updates` })
})
app.post('/update/attainment', async (req, res) => {
scheduleAttainmentUpdate()
res.json({ message: `scheduled attainments` })
})
app.post('/update/meta', async (req, res) => {
scheduleMeta()
res.json({ message: 'scheduled meta' })
})
app.post('/update/no_student', async (req, res) => {
const scheduled = await scheduleStudentCheck()
res.json({ message: `scheduled ${scheduled} updates` })
})
app.post('/update/studentlist', async (req, res) => {
createTasks()
res.json({ message: `tasks created` })
})
app.post('/update/daily', async (req, res) => {
const scheduled = await scheduleDaily()
res.json({ message: `scheduled ${scheduled} updates` })
})
app.post('/reschedule/created', async (req, res) => {
const scheduled = await rescheduleCreated()
res.json({ message: `scheduled ${scheduled} updates` })
})
app.post('/reschedule/scheduled', async (req, res) => {
const scheduled = await rescheduleScheduled()
res.json({ message: `scheduled ${scheduled} updates` })
})
app.post('/reschedule/fetched', async (req, res) => {
const scheduled = await rescheduleFetched()
res.json({ message: `scheduled ${scheduled} updates` })
})
app.get('/statuses', async (req, res) => {
const statuses = []
const {
oldestChangedStatus: { studentnumber, updatedAt },
oldestActiveStudentDone: { studentnumber: studentnumberActive, updatedAt: updatedAtActive },
oldestMetaTask,
oldestAttainmentTask
} = await getOldestTasks()
const {
allTasksScheduled,
allTasksFetched,
allTasksDone,
allTasksCreated,
allTasksActive,
allTasksNoStudent
} = await getCurrentStatus()
statuses.push(
{ label: 'CREATED', value: allTasksCreated },
{ label: 'SCHEDULED', value: allTasksScheduled },
{ label: 'FETCHED', value: allTasksFetched },
{ label: 'DONE', value: allTasksDone },
{ label: 'NO_STUDENT', value: allTasksNoStudent },
{ label: 'ACTIVE', value: allTasksActive },
{ label: 'oldest student studentnumber', value: studentnumber },
{ label: 'oldest student timestamp', value: updatedAt },
{ label: 'oldest active student studentnumber', value: studentnumberActive },
{ label: 'oldest active student timestamp', value: updatedAtActive },
{ label: 'oldest metadata update', value: oldestMetaTask.updatedAt },
{ label: 'current metadata status', value: oldestMetaTask.status },
{ label: 'oldest attainment update', value: oldestAttainmentTask.updatedAt },
{ label: 'current attainment status', value: oldestAttainmentTask.status }
)
res.json(statuses)
})
app.listen(port, () => console.log(`listening on port ${port}!`))
module.exports = { app }
@@ -1,4 +0,0 @@
const stan = require('node-nats-streaming').connect('updaterNATS', process.env.NATS_CLIENTID || 'scheduler', {
url: process.env.NATS_URI
})
module.exports = { stan }
@@ -1,181 +0,0 @@
const { stan } = require('./nats_connection')
const Schedule = require('../models')
const { sleep, getStudentNumberChecksum } = require('./util')
stan.on('connect', async () => {
const taskspermin = 300
const opts = stan.subscriptionOptions()
opts.setManualAckMode(true)
opts.setAckWait(5 * 60 * 1000)
opts.setMaxInFlight(taskspermin)
const scheduleSub = stan.subscribe('schedule', opts)
scheduleSub.on('message', async msg => {
try {
const { task, priority } = JSON.parse(msg.getData())
stan.publish('status', JSON.stringify({ task, status: 'SCHEDULED', priority }), err => {
if (err) {
console.log('publish failed', err)
}
})
stan.publish(priority ? 'PriorityApi' : 'UpdateApi', JSON.stringify({ task }), err => {
if (err) {
console.log('publish failed', err)
}
})
await sleep(60 * 1000)
} catch (err) {
console.log(err)
}
msg.ack()
})
})
const publishSingle = async ({ priority, task }) => {
const promise = new Promise(resolve => {
stan.publish('schedule', JSON.stringify({ task: task.task, priority }), async err => {
if (err) {
console.log('publish failed, waiting 10 seconds and republishing', err)
await sleep(10 * 1000)
resolve(await publishSingle({ priority, task }))
} else {
resolve(true)
}
})
})
return await promise
}
const publishAll = async (tasks, priority = false) => {
for (const task of tasks) {
await publishSingle({ task, priority })
}
}
const scheduleActiveStudents = async () => {
const tasks = [...(await Schedule.find({ type: 'student', active: true }).sort({ updatedAt: 1 }))]
console.log(tasks.length, 'tasks to schedule')
publishAll(tasks)
return tasks.length
}
const scheduleAllStudents = async () => {
const tasks = [...(await Schedule.find({ type: 'student' }).sort({ updatedAt: 1 }))]
console.log(tasks.length, 'tasks to schedule')
publishAll(tasks)
return tasks.length
}
const scheduleMeta = async () => {
await publishAll([{ task: 'meta' }], true)
}
const scheduleAttainmentUpdate = async () => {
stan.publish('status', JSON.stringify({ task: 'attainment', status: 'SCHEDULED' }), err => {
if (err) {
console.log('publish failed')
}
})
stan.publish('UpdateAttainmentDates', null, err => {
if (err) {
console.log('publish failed', 'UpdateAttainmentDates')
}
})
}
const scheduleStudentCheck = async () => {
const tasks = [...(await Schedule.find({ type: 'student', status: 'NO_STUDENT' }).sort({ updatedAt: 1 }))]
console.log(tasks.length, 'tasks to schedule')
publishAll(tasks)
return tasks.length
}
const scheduleStudentsByArray = async studentNumbers => {
try {
const tasks = await Schedule.find({ type: 'student', task: { $in: studentNumbers } })
publishAll(tasks, true)
return tasks.length
} catch (e) {
console.log(e)
}
}
const scheduleOldestNStudents = async amount => {
try {
const tasks = [
...(await Schedule.find({ type: 'student' })
.sort({ updatedAt: 1 })
.limit(Number(amount)))
]
publishAll(tasks)
return tasks.length
} catch (e) {
console.log(e)
}
}
const rescheduleCreated = async () => {
const tasks = [...(await Schedule.find({ type: 'student', status: 'CREATED' }).sort({ updatedAt: 1 }))]
console.log(tasks.length, 'tasks to schedule')
publishAll(tasks)
return tasks.length
}
const rescheduleScheduled = async () => {
const tasks = [...(await Schedule.find({ type: 'student', status: 'SCHEDULED' }).sort({ updatedAt: 1 }))]
console.log(tasks.length, 'tasks to schedule')
publishAll(tasks)
return tasks.length
}
const rescheduleFetched = async () => {
const tasks = [...(await Schedule.find({ type: 'student', status: 'FETCHED' }).sort({ updatedAt: 1 }))]
console.log(tasks.length, 'tasks to schedule')
publishAll(tasks)
return tasks.length
}
const scheduleDaily = async () => {
const findStudentsRange = 10000
const highestStudentNumber = (await Schedule.find({ type: 'student', status: { $in: ['DONE', 'FETCHED'] } })
.sort({ task: -1 })
.limit(1))[0]
const regexResult = highestStudentNumber ? highestStudentNumber.task.match(/0(\d{7})\d/) : null
const minstudentnumber = 1000000
const studentNumberMid = (regexResult && Number(regexResult[1])) || minstudentnumber + findStudentsRange
const min = Math.max(studentNumberMid - findStudentsRange, minstudentnumber)
const max = Math.min(studentNumberMid + findStudentsRange, 9999999)
console.log({ findStudentsRange, minstudentnumber, studentNumberMid, min, max })
const activeStudents = [...(await Schedule.find({ type: 'student', active: true }).sort({ updatedAt: 1 }))]
const studentnumbers = activeStudents.map(t => t.task)
for (let i = min; i <= max; ++i) {
studentnumbers.push(`0${i}${getStudentNumberChecksum(i)}`)
}
const oldestStudents = [
...(await Schedule.find({ type: 'student' })
.sort({ updatedAt: 1 })
.limit(10000))
]
studentnumbers.push(...oldestStudents.map(t => t.task))
const tasks = [...new Set(studentnumbers)].map(sn => ({ task: sn }))
console.log(tasks.length, 'tasks to schedule')
publishAll(tasks)
return tasks.length
}
module.exports = {
scheduleActiveStudents,
scheduleStudentsByArray,
scheduleOldestNStudents,
scheduleMeta,
scheduleAllStudents,
scheduleAttainmentUpdate,
rescheduleScheduled,
rescheduleFetched,
rescheduleCreated,
scheduleDaily,
scheduleStudentCheck
}
@@ -1,62 +0,0 @@
const Schedule = require('../models')
const { sleep, getStudentNumberChecksum } = require('./util')
const createTasks = async (fromStudents = []) => {
console.log('Creating studentnumbers to DB...')
const writeStudents = async studentsToAdd => {
const tasks = studentsToAdd.map(student => ({ task: student, status: 'CREATED', type: 'student', active: true }))
const bulkOperation = Schedule.collection.initializeUnorderedBulkOp()
for (const task of tasks) {
bulkOperation.insert(task)
}
await bulkOperation.execute()
}
const writeToDB = async studentnumbers => {
try {
await writeStudents(studentnumbers)
} catch (err) {
const BulkWriteResult = err.result.result
const duplicateIndexError = 11000
const realErrors = BulkWriteResult.writeErrors.filter(e => e.err.code !== duplicateIndexError)
if (realErrors.length > 0 || BulkWriteResult.writeConcernErrors.length > 0) {
console.log(err)
return false
}
}
return true
}
const mongoWriteBatchSize = 1000
let studentnumbers = []
if (fromStudents.length) {
for (let i = 0; i < fromStudents.length; i++) {
studentnumbers.push(fromStudents[i])
if (studentnumbers.length % mongoWriteBatchSize === 0 || i === fromStudents.length - 1) {
while (!(await writeToDB(studentnumbers))) {
await sleep(60 * 1000)
}
console.log(`Created tasks from ${studentnumbers}`)
studentnumbers = []
}
}
} else {
const minStudentNumber = 1010000
const maxStudentNumber = 1530000
for (let i = minStudentNumber; i <= maxStudentNumber; ++i) {
const studentNumber = '0' + i + getStudentNumberChecksum(i)
studentnumbers.push(studentNumber)
if (studentnumbers.length % mongoWriteBatchSize === 0 || i === maxStudentNumber) {
console.log('current studentnumber', i)
while (!(await writeToDB(studentnumbers))) {
await sleep(60 * 1000)
}
studentnumbers = []
}
}
}
console.log('Studentnumbers created to DB')
}
module.exports = { createTasks }
@@ -1,43 +0,0 @@
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms))
// Shuffle in place, returns same array
const shuffleArray = a => {
for (let i = a.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1))
const x = a[i]
a[i] = a[j]
a[j] = x
}
return a
}
const isValidStudentId = id => {
if (/^0\d{8}$/.test(id)) {
// is a 9 digit number
const multipliers = [7, 1, 3, 7, 1, 3, 7]
const checksum = id
.substring(1, 8)
.split('')
.reduce((sum, curr, index) => {
return (sum + curr * multipliers[index]) % 10
}, 0)
return (10 - checksum) % 10 == id[8]
}
return false
}
const getStudentNumberChecksum = studentNumber => {
const studentNumberString = String(studentNumber)
let checksumNumbers = [7, 3, 1]
let checksum = 0
for (let i = 0; i < studentNumberString.length; i++) {
// go from end t start
let currentNumber = studentNumberString[studentNumberString.length - (i + 1)]
checksum += currentNumber * checksumNumbers[i % checksumNumbers.length]
}
return (10 - (checksum % 10)) % 10
}
module.exports = { sleep, isValidStudentId, getStudentNumberChecksum, shuffleArray }
@@ -1 +0,0 @@
node_modules
@@ -1,2 +0,0 @@
*.sql
updater/test_assets
@@ -1,13 +0,0 @@
{
"env": {
"es6": true,
"node": true,
"jest": true
},
"parser": "babel-eslint",
"extends": ["eslint:recommended", "plugin:prettier/recommended", "prettier/babel"],
"rules": {
"no-console": 0,
"prettier/prettier": "error"
}
}
@@ -1,4 +0,0 @@
node_modules
models
migrations
@@ -1,9 +0,0 @@
{
"semi": false,
"arrowParens": "avoid",
"singleQuote": true,
"printWidth": 120,
"tabWidth": 2,
"useTabs": false,
"jsxSingleQuote": false
}
@@ -1,18 +0,0 @@
FROM node:14-alpine
WORKDIR /usr/src/app
CMD npm start
RUN apk add postgresql-client
COPY ./updater_writer/package* ./
RUN npm ci
ARG NODE_ENV=production
ENV NODE_ENV=${NODE_ENV}
COPY ./updater_writer /usr/src/app
COPY ./shared/models /usr/src/app/models
COPY ./shared/migrations /usr/src/app/database/migrations
COPY ./shared/migrations_kone /usr/src/app/database/migrations_kone
@@ -1,77 +0,0 @@
require('dotenv').config()
const { NODE_ENV } = process.env
let DB_URL = process.env.DB_URL
let DB_SCHEMA = process.env.DB_SCHEMA || 'public'
const isTest = NODE_ENV === 'test'
if (isTest) {
DB_URL = process.env.TEST_DB
DB_SCHEMA = process.env.TEST_DB_SCHEMA
} else if (NODE_ENV === 'anon') {
DB_URL = process.env.ANON_DB
}
const frontend_addr = process.env.FRONT_URL
const redis = process.env.REDIS
const TOKEN_SECRET = process.env.TOKEN_SECRET
const CERT_PATH = process.env.CERT_PATH // production/staging only
const KEY_PATH = process.env.KEY_PATH // production/staging only
const USERSERVICE_URL = process.env.USERSERVICE_URL
const ANALYTICS_URL = process.env.ANALYTICS_URL
const PORT = isTest ? 8079 : 8080
const OODI_SECRET = process.env.OODI_SECRET
const FEATURES = {
ERROR_HANDLER: false
}
const formatURL = url => {
return !!url && !url.startsWith('http') ? `http://${url}` : url
}
if (process.env.NODE_ENV === 'dev' && process.env.FEATURES) {
const toggled = process.env.FEATURES.split(',')
toggled
.map(toggle => toggle.trim())
.forEach(feature => {
if (FEATURES[feature] !== undefined) {
FEATURES[feature] = true
}
})
}
const OODI = {
test: 'http://localhost',
anon: process.env.OODI_ADDR_ANON
}
const OODI_ADDR = OODI[process.env.NODE_ENV] || process.env.OODI_ADDR
const ACCESS_TOKEN_HEADER_KEY = 'x-access-token'
const OODI_SECRET_HEADER_KEY = 'x-oodi-secret'
let requiredGroup = ['grp-oodikone-users', 'grp-oodikone-basic-users']
if (process.env.NODE_ENV === 'staging') {
requiredGroup = ['grp-oodikone-staging-users', 'grp-oodikone-basic-staging-users']
}
if (process.env.NODE_ENV === 'dev' || isTest) {
requiredGroup = null
}
module.exports = {
frontend_addr,
DB_URL,
redis,
TOKEN_SECRET,
DB_SCHEMA,
OODI_ADDR,
CERT_PATH,
KEY_PATH,
FEATURES,
USERSERVICE_URL: formatURL(USERSERVICE_URL),
ACCESS_TOKEN_HEADER_KEY,
PORT,
ANALYTICS_URL: formatURL(ANALYTICS_URL),
requiredGroup,
OODI_SECRET,
OODI_SECRET_HEADER_KEY,
isTest
}
@@ -1,69 +0,0 @@
const Sequelize = require('sequelize')
const Umzug = require('umzug')
const conf = require('../conf-backend')
const sequelize = new Sequelize(conf.DB_URL, {
schema: conf.DB_SCHEMA,
searchPath: conf.DB_SCHEMA,
logging: false
})
sequelize.query(`SET SESSION search_path to ${conf.DB_SCHEMA}`)
const initializeDatabaseConnection = async () => {
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms))
const waitSeconds = 60
for (let i = 1; i <= waitSeconds; i++) {
try {
await sequelize.authenticate()
break
} catch (e) {
if (i === waitSeconds) {
console.log(`Could not connect to database in ${waitSeconds} seconds`)
throw e
}
console.log('.')
await sleep(1000)
}
}
if (!conf.isTest) {
try {
const migrator = new Umzug({
storage: 'sequelize',
storageOptions: {
sequelize: sequelize,
tableName: 'migrations'
},
logging: console.log,
migrations: {
params: [sequelize.getQueryInterface(), Sequelize],
path: `${process.cwd()}/database/migrations`,
pattern: /\.js$/,
schema: conf.DB_SCHEMA
}
})
const migrations = await migrator.up()
console.log('Migrations up to date', migrations)
} catch (e) {
console.log('Kone Migration error')
throw e
}
}
}
const forceSyncDatabase = async () => {
try {
await sequelize.getQueryInterface().createSchema(conf.DB_SCHEMA)
} catch (e) {
// console.log(e)
}
console.log('OODI SYNC')
await sequelize.sync({ force: true })
}
module.exports = {
sequelize,
initializeDatabaseConnection,
forceSyncDatabase
}
@@ -1,27 +0,0 @@
const spawn = require('child_process').spawn
const fs = require('fs')
const { DB_SCHEMA } = require('../conf-backend')
const dumpDatabase = async () => {
const { DB_URL } = process.env
const dumpOptions = ['-Fc', `-n${DB_SCHEMA}`, `${DB_URL}`]
console.log(dumpOptions)
const dumpProcess = spawn('pg_dump', dumpOptions, {
stdio: ['ignore', 'pipe', 'inherit']
})
dumpProcess.on('error', err => {
console.log(' dump process error:', err)
})
const writeStream = fs.createWriteStream(`./dumps/oodidb_prod-${new Date().toISOString()}`)
dumpProcess.stdout.pipe(writeStream)
dumpProcess.on('exit', async code => {
if (code !== 0) {
throw new Error('pg_dump: Bad exit code (' + code + ')')
} else {
console.log('database dump created')
}
})
}
module.exports = { dumpDatabase }
@@ -1,17 +0,0 @@
const { seedAllMigrations } = require('./seed_migrations')
const { forceSyncDatabase } = require('./connection')
const sync = async () => {
try {
await forceSyncDatabase()
await seedAllMigrations()
console.log('Force sync succeeded. ')
process.exit(0)
} catch (e) {
console.log(`Force sync failed: ${e}`)
process.exit(1)
}
}
//sync()
module.exports = { sync }
@@ -1,28 +0,0 @@
const fs = require('fs')
const { Migration, MigrationKone } = require('../models/index')
const DEFAULT_PATH = `${process.cwd()}/database/migrations`
const DEFAULT_PATH_KONE = `${process.cwd()}/database/migrations_kone`
const saveMigrationsToDatabase = Migration => async filenames =>
Promise.all(filenames.map(name => Migration.upsert({ name })))
const seedMigrations = async (migrationfilepath = DEFAULT_PATH) => {
const filenames = fs.readdirSync(migrationfilepath)
await saveMigrationsToDatabase(Migration)(filenames)
}
const seedMigrationsKone = async (migrationfilepath = DEFAULT_PATH_KONE) => {
const filenames = fs.readdirSync(migrationfilepath)
await saveMigrationsToDatabase(MigrationKone)(filenames)
}
const seedAllMigrations = async () => {
await seedMigrations()
await seedMigrationsKone()
}
module.exports = {
seedMigrations,
seedAllMigrations
}
@@ -1,69 +0,0 @@
const natsStreaming = require('node-nats-streaming')
const { updateStudent, updateMeta, updateAttainmentMeta } = require('./updater/database_updater')
const logger = require('./logger')
const { initializeDatabaseConnection } = require('./database/connection')
initializeDatabaseConnection()
.then(() => {
const stan = natsStreaming.connect('updaterNATS', process.env.HOSTNAME, process.env.NATS_URI)
console.log(`STARTING WITH ${process.env.HOSTNAME} as id`)
const opts = stan.subscriptionOptions()
opts.setManualAckMode(true)
opts.setAckWait(10 * 60 * 1000) // some students have taken over 5 min to write!
// opts.setDeliverAllAvailable()
// opts.setDurableName('durable')
opts.setMaxInFlight(1)
stan.on('connect', function() {
const sub = stan.subscribe('UpdateWrite', 'updater.workers', opts)
const attSub = stan.subscribe('UpdateAttainmentDates', 'updater.workers.attainments', opts)
const prioSub = stan.subscribe('PriorityWrite', 'updater.workers.prio', opts)
const writeStudent = priority => async msg => {
let data = null
try {
const start = new Date()
data = JSON.parse(msg.getData())
if (data.task === 'meta') {
await updateMeta(data.data)
} else {
await updateStudent(data.data)
}
stan.publish(
'status',
JSON.stringify({ task: data.task, status: 'DONE', timems: new Date() - start }, priority),
err => {
if (err) console.log(err)
}
)
} catch (err) {
console.log('update failed', data.task, err)
logger.info('failure', { service: 'WRITER' })
}
msg.ack()
}
sub.on('message', writeStudent(false))
prioSub.on('message', writeStudent(true))
attSub.on('message', async msg => {
try {
await updateAttainmentMeta()
stan.publish('status', JSON.stringify({ task: 'attainment', status: 'DONE' }), err => {
if (err) {
console.log('publish failed')
}
})
} catch (err) {
console.log('attainment meta update failed', err)
logger.info('failure', { service: 'WRITER' })
}
msg.ack()
})
})
})
.catch(e => {
process.exitCode = 1
console.log(e)
})
@@ -1,27 +0,0 @@
const winston = require('winston')
const Log2gelf = require('winston-log2gelf')
const transports = []
if (process.env.LOG_PORT && process.env.LOG_HOST) {
transports.push(
new Log2gelf({
hostname: process.env.LOG_HOSTNAME || 'UPDATER',
host: process.env.LOG_HOST,
port: process.env.LOG_PORT,
protocol: process.env.LOG_PROTOCOL || 'https',
environment: process.env.NODE_ENV,
protocolOptions: {
path: process.env.LOG_PATH || '/gelf'
}
})
)
}
if (process.env.NODE_ENV !== 'test') {
transports.push(new winston.transports.File({ filename: 'debug.log' }))
}
const logger = winston.createLogger({ transports })
module.exports = logger
File diff suppressed because it is too large Load Diff
@@ -1,39 +0,0 @@
{
"name": "updater_writer",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "npm run lint && npm run jest",
"jest": "NODE_ENV=test jest --runInBand --verbose",
"test_docker": "docker exec -t updater_writer /bin/bash -c 'npm test'",
"lint": "eslint .",
"lintfix": "prettier --write **/*.{js,json} && eslint --fix .",
"start": "node index.js",
"dev": "nodemon index.js",
"force_sync": "node -e \"require('./database/force_sync_database').sync()\"",
"dump": "node -e \"require('./database/dump_database').dumpDatabase()\""
},
"author": "",
"license": "ISC",
"dependencies": {
"dotenv": "^8.0.0",
"jest": "^24.8.0",
"lodash": "^4.17.19",
"node-nats-streaming": "^0.2.6",
"pg": "^8.6.0",
"sequelize": "^6.6.2",
"umzug": "^2.2.0",
"winston": "^3.2.1",
"winston-log2gelf": "^2.3.0"
},
"devDependencies": {
"babel-eslint": "^10.0.3",
"eslint": "^6.6.0",
"eslint-config-prettier": "^6.3.0",
"eslint-plugin-prettier": "^3.1.0",
"nodemon": "^1.19.4",
"prettier": "^1.18.2",
"prettier-eslint": "^9.0.0"
}
}
@@ -1,217 +0,0 @@
const { notIn } = require('sequelize').Op
const { sequelize } = require('../database/connection')
const { sortBy, flatMap, uniqBy, sortedUniqBy } = require('lodash')
const {
Student,
Credit,
Course,
CreditTeacher,
Teacher,
Organisation,
CourseRealisationType,
Semester,
CreditType,
CourseType,
Discipline,
CourseDisciplines,
SemesterEnrollment,
Provider,
CourseProvider,
Studyright,
StudyrightExtent,
ElementDetails,
StudyrightElement,
Transfers
} = require('../models/index')
const { updateAttainmentDates } = require('./update_attainment_dates')
const getColumnsToUpdate = arr => (arr[0] ? Object.keys(arr[0]) : [])
const updateAttainments = async (studyAttainments, transaction) => {
// Sort data to avoid deadlocks. If there are duplicate primary keys in the same array, then bulkCreate won't work.
const courses = sortedUniqBy(sortBy(studyAttainments.map(e => e.course), 'code'), 'code')
const disciplines = sortBy(flatMap(studyAttainments, e => e.course.disciplines || []), 'discipline_id', 'course_id')
const providers = sortedUniqBy(
sortBy(flatMap(studyAttainments, e => e.course.providers), 'providercode'),
'providercode'
)
const courseproviders = sortBy(flatMap(studyAttainments, e => e.course.courseproviders), 'coursecode', 'providercode')
const credits = sortedUniqBy(sortBy(studyAttainments.map(e => e.credit), 'id'), 'id')
const teachers = sortedUniqBy(sortBy(flatMap(studyAttainments, e => e.teachers || []), 'id'), 'id')
const creditTeachers = sortBy(flatMap(studyAttainments, e => e.creditTeachers), 'teacher_id', 'credit_id')
await Promise.all([
Course.bulkCreate(courses, {
transaction,
updateOnDuplicate: getColumnsToUpdate(courses)
}),
Provider.bulkCreate(providers, {
transaction,
updateOnDuplicate: getColumnsToUpdate(providers)
}),
Credit.bulkCreate(credits, {
transaction,
updateOnDuplicate: getColumnsToUpdate(credits)
}),
Teacher.bulkCreate(teachers, {
transaction,
updateOnDuplicate: getColumnsToUpdate(teachers)
})
])
await Promise.all([
// must be after courses
CourseDisciplines.bulkCreate(disciplines, {
transaction,
ignoreDuplicates: true
}),
// must be after providers
CourseProvider.bulkCreate(courseproviders, {
transaction,
ignoreDuplicates: true
}),
// must be after teachers
CreditTeacher.bulkCreate(creditTeachers, {
transaction,
ignoreDuplicates: true
})
])
}
const updateStudyRights = async (studyRights, studentnumber, transaction) => {
// Sort data to avoid deadlocks. If there are duplicate primary keys in the same array, then bulkCreate won't work.
const studyRightExtents = sortedUniqBy(sortBy(studyRights.map(e => e.studyRightExtent), 'extentcode'), 'extentcode')
const studyrights = sortedUniqBy(sortBy(studyRights.map(e => e.studyright), 'studyrightid'), 'studyrightid')
const elementDetails = sortedUniqBy(sortBy(flatMap(studyRights, e => e.elementDetails), 'code'), 'code')
const studyRightElements = uniqBy(
sortBy(flatMap(studyRights, e => e.studyRightElements), 'startdate', 'enddate', 'studyrightid', 'code'),
s => `${s.startdate}${s.enddate}${s.studyrightid}${s.code}`
)
const transfers = sortBy(
flatMap(studyRights, e => e.transfers),
'transferdate',
'studentnumber',
'studyrightid',
'sourcecode',
'targetcode'
)
const studyrightids = studyrights.map(s => s.studyrightid)
await Studyright.destroy(
{ where: { student_studentnumber: studentnumber, studyrightid: { [notIn]: studyrightids } } },
{ transaction }
)
await StudyrightElement.destroy({ where: { studentnumber } }, { transaction })
await Promise.all([
StudyrightExtent.bulkCreate(studyRightExtents, {
transaction,
updateOnDuplicate: getColumnsToUpdate(studyRightExtents)
}),
Studyright.bulkCreate(studyrights, {
transaction,
updateOnDuplicate: getColumnsToUpdate(studyrights)
}),
ElementDetails.bulkCreate(elementDetails, {
transaction,
updateOnDuplicate: getColumnsToUpdate(elementDetails)
}),
StudyrightElement.bulkCreate(studyRightElements, {
transaction,
ignoreDuplicates: true
}),
Transfers.bulkCreate(transfers, {
transaction,
ignoreDuplicates: true
})
])
}
const updateStudent = async student => {
let { studentInfo, studyAttainments, semesterEnrollments, studyRights } = student
// sort data to avoid deadlocks
semesterEnrollments = uniqBy(
sortBy(semesterEnrollments, 'studentnumber', 'semestercode'),
sE => `${sE.semestercode}${sE.studentnumber}`
)
const transaction = await sequelize.transaction()
try {
await Student.upsert(studentInfo, { transaction })
// Change this to bulkCreate after https://github.com/sequelize/sequelize/issues/11569
// is merged and released
for (let i = 0; i < semesterEnrollments.length; i++) {
await SemesterEnrollment.upsert(semesterEnrollments[i], { transaction })
}
if (studyAttainments) await updateAttainments(studyAttainments, transaction)
if (studyRights) await updateStudyRights(studyRights, studentInfo.studentnumber, transaction)
await transaction.commit()
} catch (err) {
await transaction.rollback()
throw err
}
}
const updateAttainmentMeta = async () => {
await updateAttainmentDates()
}
const updateMeta = async ({
faculties,
courseRealisationsTypes,
semesters,
creditTypeCodes,
courseTypeCodes,
disciplines
}) => {
const transaction = await sequelize.transaction()
try {
await Promise.all([
CourseType.bulkCreate(uniqBy(courseTypeCodes, 'coursetypecode'), {
transaction,
updateOnDuplicate: getColumnsToUpdate(courseTypeCodes)
}),
Organisation.bulkCreate(uniqBy(faculties, 'code'), {
transaction,
updateOnDuplicate: getColumnsToUpdate(faculties)
}),
CourseRealisationType.bulkCreate(uniqBy(courseRealisationsTypes, 'realisationtypecode'), {
transaction,
updateOnDuplicate: getColumnsToUpdate(courseRealisationsTypes)
}),
Semester.bulkCreate(uniqBy(semesters, 'semestercode'), {
transaction,
updateOnDuplicate: getColumnsToUpdate(semesters)
}),
CreditType.bulkCreate(uniqBy(creditTypeCodes, 'credittypecode'), {
transaction,
updateOnDuplicate: getColumnsToUpdate(creditTypeCodes)
}),
Discipline.bulkCreate(uniqBy(disciplines, 'discipline_id'), {
transaction,
updateOnDuplicate: getColumnsToUpdate(disciplines)
})
])
await transaction.commit()
} catch (err) {
await transaction.rollback()
throw err
}
}
module.exports = {
updateStudent,
updateMeta,
updateAttainmentMeta
}
@@ -1,518 +0,0 @@
const { updateStudent, updateMeta, updateAttainmentMeta } = require('./database_updater')
const {
Student,
Credit,
Course,
Organisation,
CourseRealisationType,
Semester,
CreditType,
CourseType,
Discipline,
CourseDisciplines,
Teacher,
CreditTeacher,
SemesterEnrollment,
Provider,
CourseProvider,
Studyright,
StudyrightExtent,
ElementDetails,
StudyrightElement,
Transfers
} = require('../models/index')
const { students } = require('./test_assets/test_students')
const meta = require('./test_assets/meta')
const { sequelize, forceSyncDatabase } = require('../database/connection')
const { seedMigrations } = require('../database/seed_migrations')
jest.setTimeout(10000)
describe('Updater works', () => {
beforeAll(async () => {
await forceSyncDatabase()
await seedMigrations()
await updateMeta(meta)
await updateStudent(students[0])
await updateStudent(students[1])
await updateStudent(students[2])
await updateAttainmentMeta()
})
afterAll(async () => {
await sequelize.close()
})
describe('All metadata is written', () => {
test('Organisations are written to database', async () => {
const organizations = await Organisation.findAll()
expect(organizations.length).toBe(17)
expect(organizations.find(_ => _.code === 'H50').name.find(n => n.langcode === 'fi').text).toBe(
'Matemaattis-luonnontieteellinen tiedekunta'
)
})
test('Course realisation types are written to database', async () => {
const courseRealisationTypes = await CourseRealisationType.findAll()
expect(courseRealisationTypes.length).toBe(18)
expect(courseRealisationTypes.map(_ => _.realisationtypecode)).not.toContain(null)
expect(courseRealisationTypes.map(_ => _.name)[0]).toMatchObject({
en: expect.any(String),
fi: expect.any(String),
sv: expect.any(String)
})
})
test('Semesters are written to database', async () => {
const semesters = await Semester.findAll()
expect(semesters.length).toBe(160)
expect(semesters.map(_ => _.semestercode)).toEqual(expect.arrayContaining([...Array(160).keys()].map(_ => ++_)))
expect(semesters.map(_ => _.name)[0]).toMatchObject({
en: expect.any(String),
fi: expect.any(String),
sv: expect.any(String)
})
expect(semesters.map(_ => _.startdate)).not.toContain(null)
expect(semesters.map(_ => _.enddate)).not.toContain(null)
expect(semesters.map(_ => _.createdAt)).not.toContain(null)
expect(semesters.map(_ => _.updatedAt)).not.toContain(null)
expect(new Set(semesters.map(_ => _.yearcode)).size).toBe(80)
expect(semesters.find(_ => _.yearcode === 1).yearname).toBe('1950-51')
})
test('Credit types are written to database', async () => {
const creditTypes = await CreditType.findAll()
expect(creditTypes.length).toBe(11)
expect(creditTypes.map(_ => _.credittypecode)).toEqual(expect.arrayContaining([expect.any(Number)]))
expect(creditTypes.map(_ => _.name)[0]).toMatchObject({
en: expect.any(String),
fi: expect.any(String),
sv: expect.any(String)
})
})
test('Course types are written to database', async () => {
const courseTypes = await CourseType.findAll()
expect(courseTypes.length).toBe(35)
expect(courseTypes.map(_ => _.coursetypecode)).toEqual(expect.arrayContaining([expect.any(Number)]))
expect(courseTypes.map(_ => _.name)[0]).toMatchObject({
en: expect.any(String),
fi: expect.any(String),
sv: expect.any(String)
})
})
test('Disciplines are written to database', async () => {
const disciplines = await Discipline.findAll()
expect(disciplines.length).toBe(430)
expect(disciplines.map(_ => _.discipline_id)).toEqual(expect.arrayContaining([expect.any(String)]))
expect(disciplines.map(_ => _.name)[0]).toMatchObject({
fi: expect.any(String)
})
})
})
describe('Updater writes students the right way', () => {
test('Student info is written to database', async () => {
const students = await Student.findAll({
order: [['studentnumber', 'DESC']]
})
expect(students.length).toBe(3)
const {
studentnumber,
lastname,
firstnames,
abbreviatedname,
birthdate,
communicationlanguage,
creditcount,
dateofuniversityenrollment,
city_fi,
city_sv,
gender_code,
gender_fi,
createdAt,
updatedAt
} = students[0]
expect(studentnumber).toBe('014441008')
expect(lastname).toBe('Orttenvuori')
expect(firstnames).toBe('Oona')
expect(abbreviatedname).toBe('Orttenvuori Oona')
expect(birthdate.toISOString()).toBe(new Date('1966-04-24 00:00:00+00').toISOString())
expect(communicationlanguage).toBe('Finnish')
expect(creditcount).toBe(34)
expect(dateofuniversityenrollment).toBe(null)
expect(city_fi).toBe('HELSINKI')
expect(city_sv).toBe('HELSINGFORS')
expect(gender_code).toBe(2)
expect(gender_fi).toBe('Nainen')
expect(createdAt).not.toBe(undefined)
expect(updatedAt).not.toBe(undefined)
const { creditcount: creditcount1, dateofuniversityenrollment: dofue } = students[1]
expect(creditcount1).toBe(380)
expect(dofue.toISOString()).toBe(new Date('2003-07-31 21:00:00+00').toISOString())
})
test('Courses are written to database', async () => {
const courses = await Course.findAll({ order: [['code', 'ASC']] })
const {
code,
name,
latest_instance_date,
is_study_module,
startdate,
enddate,
max_attainment_date,
min_attainment_date,
coursetypecode
} = courses[0]
expect(courses.length).toBe(147)
expect(code).toBe('00031')
expect(name.en).toBe('Master of Science (Agriculture and Forestry)')
expect(latest_instance_date.toISOString()).toBe(new Date('2010-03-16 00:00:00+00').toISOString())
expect(is_study_module).toBeTruthy()
expect(startdate.toISOString()).toBe(new Date('1899-12-31 00:00:00+00').toISOString())
expect(enddate.toISOString()).toBe(new Date('2112-12-20 00:00:00+00').toISOString())
expect(max_attainment_date.toISOString()).toBe(new Date('2010-03-16 00:00:00+00').toISOString())
expect(min_attainment_date.toISOString()).toBe(new Date('2010-03-16 00:00:00+00').toISOString())
expect(coursetypecode).toBe(8)
const { code: code1, is_study_module: ism, coursetypecode: ctc } = courses[6]
expect(code1).toBe('402512')
expect(ism).toBe(false)
expect(ctc).toBe(2)
})
test('Credits are written to database', async () => {
const allCredits = await Credit.findAll({ order: [['id', 'ASC']] })
const {
id,
grade,
student_studentnumber,
credits,
ordering,
attainment_date,
isStudyModule,
lastmodifieddate,
course_code,
credittypecode /*, semestercode*/
} = allCredits[0]
expect(allCredits.length).toBe(150)
expect(id).toBe('2340421')
expect(grade).toBe('4')
expect(allCredits.map(_ => _.grade)).not.toContain(null)
expect(student_studentnumber).toBe('014441008')
expect(credits).toBe(2)
expect(allCredits.map(_ => _.credits)).not.toContain(null)
expect(ordering).toBe('1997-10-21')
expect(attainment_date.toISOString()).toBe(new Date('1997-10-21 00:00:00+00').toISOString())
expect(isStudyModule).toBe(false)
expect(lastmodifieddate).not.toBe(null)
expect(course_code).toBe('65002')
expect(allCredits.map(_ => _.course_code)).not.toContain(null)
expect(credittypecode).toBe(4)
expect(allCredits.map(_ => _.semestercode)).toEqual(expect.arrayContaining([expect.any(Number)]))
})
test('Teachers are written to database', async () => {
const teachers = await Teacher.findAll({ order: [['name', 'ASC']] })
const { id, code, name } = teachers[0]
expect(teachers.length).toBe(51)
expect(id).toBe('9016947')
expect(code).toBe('hpyrhone')
expect(name).toBe('Ahvenniemi Emilia')
})
test('Credit Teachers are written to database', async () => {
const creditteachers = await CreditTeacher.findAll()
expect(creditteachers.length).toBe(119)
expect(creditteachers.map(_ => _.credit_id)).not.toContain(null)
expect(creditteachers.map(_ => _.teacher_id)).not.toContain(null)
})
test('Semester enrollments are written to database', async () => {
const semesterEnrollments = await SemesterEnrollment.findAll({
order: [['semestercode', 'ASC']]
})
const { id, enrollmenttype, enrollment_date, studentnumber, semestercode } = semesterEnrollments[0]
expect(semesterEnrollments.length).toBe(28)
expect(id).toEqual(expect.any(String))
expect(enrollmenttype).toBe(1)
expect(enrollment_date.toISOString()).toBe(new Date('2003-08-17 00:00:00+00').toISOString())
expect(studentnumber).toBe('014272112')
expect(semestercode).toBe(107)
})
test('Providers are written to database', async () => {
const providers = await Provider.findAll({
order: [['providercode', 'DESC']]
})
const { providercode, name } = providers[0]
expect(providers.length).toBe(20)
expect(providercode).toBe('H906')
expect(name).toMatchObject({
en: 'Language Centre',
fi: 'Kielikeskus',
sv: 'Språkcentrum'
})
expect(providers.map(_ => _.providercode)).not.toContain(null)
})
test('Course providers are written to database', async () => {
const courseproviders = await CourseProvider.findAll()
expect(courseproviders.length).toBe(123)
expect(courseproviders.map(_ => _.coursecode)).not.toContain(null)
expect(courseproviders.map(_ => _.providercode)).not.toContain(null)
})
test('Course disciplines are written to database', async () => {
const courseDisciplines = await CourseDisciplines.findAll()
expect(courseDisciplines.length).toBe(40)
expect(courseDisciplines.map(_ => _.discipline_id)).not.toContain(null)
expect(courseDisciplines.map(_ => _.course_id)).not.toContain(null)
})
test('Study right extent are written to database', async () => {
const studyrightextent = await StudyrightExtent.findAll()
const { extentcode, name, createdAt, updatedAt } = studyrightextent[0]
expect(studyrightextent.length).toBe(2)
expect(extentcode).toBe(1)
expect(name.en).toBe("Bachelor's Degree")
expect(createdAt).not.toBe(undefined)
expect(updatedAt).not.toBe(undefined)
})
test('Study rights are written to database', async () => {
const studyrights = await Studyright.findAll({
order: [['studyrightid', 'DESC']]
})
const {
studyrightid /*, canceldate*/,
cancelorganisation,
enddate,
givendate,
graduated,
prioritycode,
startdate,
studystartdate,
organization_code,
student_studentnumber,
extentcode
} = studyrights[0]
expect(studyrights.length).toBe(4)
expect(studyrightid).toBe('67628057')
expect(cancelorganisation).toBe('4325')
expect(enddate.toISOString()).toBe(new Date('2010-04-12 00:00:00+00').toISOString())
expect(givendate.toISOString()).toBe(new Date('2004-07-31 00:00:00+00').toISOString())
expect(graduated).toBe(1)
expect(prioritycode).toBe(30)
expect(startdate.toISOString()).toBe(new Date('2004-07-31 00:00:00+00').toISOString())
expect(studystartdate.toISOString()).toBe(new Date('2004-07-31 00:00:00+00').toISOString())
expect(organization_code).toBe('4325')
expect(student_studentnumber).toBe('011120775')
expect(extentcode).toBe(1)
})
test('element details are written to database', async () => {
const elementDetails = await ElementDetails.findAll({
order: [['code', 'DESC']]
})
const { code, name, type, createdAt, updatedAt } = elementDetails[0]
expect(elementDetails.length).toBe(12)
expect(code).toBe('A2004')
expect(name.en).toBe('Government Decree (794/2004) on University Degrees')
expect(type).toBe(15)
expect(createdAt).not.toBe(undefined)
expect(updatedAt).not.toBe(undefined)
})
test('study right elements are written to database', async () => {
const studyright_elements = await StudyrightElement.findAll({
order: ['startdate', 'enddate', 'studyrightid', 'code']
})
const { id, startdate, enddate, createdAt, updatedAt, studyrightid, code, studentnumber } = studyright_elements[0]
expect(studyright_elements.length).toBe(21)
expect(id).toBe(11)
expect(startdate.toISOString()).toBe(new Date('2003-07-31T21:00:00.000Z').toISOString())
expect(enddate.toISOString()).toBe(new Date('2007-08-24T21:00:00.000Z').toISOString())
expect(createdAt).not.toBe(undefined)
expect(updatedAt).not.toBe(undefined)
expect(studyrightid).toBe('45853818')
expect(code).toBe('A1995MAAT')
expect(studentnumber).toBe('014272112')
})
test('transfers are written to database', async () => {
const transfers = await Transfers.findAll()
const {
id,
transferdate,
createdAt,
updatedAt,
studentnumber,
studyrightid,
sourcecode,
targetcode
} = transfers[0]
expect(transfers.length).toBe(1)
expect(id).toBe('1')
expect(transferdate.toISOString()).toBe(new Date('2009-07-30 21:00:00+00').toISOString())
expect(createdAt).not.toBe(undefined)
expect(updatedAt).not.toBe(undefined)
expect(studentnumber).toBe('014272112')
expect(studyrightid).toBe('62346253')
expect(sourcecode).toBe('820016')
expect(targetcode).toBe('820042')
})
})
describe('Updater updates existing students correctly', () => {
beforeAll(async () => {
const { studentInfo, studyRights, studyAttainments } = students[0]
students[0].studentInfo = {
...studentInfo,
email: 'Calgary.Flames@NHLonJätettä.fi',
mobile: '0400521981',
address: 'Yliopistonkatu 4',
creditcount: 322,
age: 41,
lastname: 'Tauriainen',
gender_code: 1,
abbreviatedname: 'Tauriainen Maria Helena'
}
const newCredit = {
id: 'ayyyyylmao',
grade: '5',
credits: 5,
ordering: '2019.05.04',
credittypecode: 1,
student_studentnumber: '011120775',
attainment_date: new Date(),
course_code: '420_666',
semestercode: 138,
isStudyModule: false
}
const newCourse = {
code: '420_666',
name: {
fi: 'Kaljan juonti',
en: 'Beer drinking',
sv: 'dryckade öl'
},
latest_instance_date: new Date(),
is_study_module: false,
coursetypecode: 1,
disciplines: [],
startdate: new Date(),
enddate: new Date(),
providers: [],
courseproviders: []
}
const newCreditTeachers = [
{
credit_id: 'ayyyyylmao',
teacher_id: '9016417'
}
]
const newAttainment = {
credit: newCredit,
creditTeachers: newCreditTeachers,
course: newCourse
}
students[0].studyAttainments = [
...studyAttainments,
newAttainment,
{
...students[1].studyAttainments[2],
creditTeachers: [],
credit: {
...students[1].studyAttainments[2].credit,
id: 'Kvas',
student_studentnumber: '011120775'
}
}
]
students[0].studyAttainments[2] = {
...students[0].studyAttainments[2],
credit: {
...students[0].studyAttainments[2].credit,
grade: '0'
}
}
const newStudyRight = {
...studyRights[0],
studyright: {
...studyRights[0].studyright
}
}
students[0].studyRights = [newStudyRight]
await updateStudent(students[0])
})
test('StudentInfo for existing student is updated correclty', async () => {
const updatedStudent = await Student.findOne({
where: {
studentnumber: '011120775'
}
})
const {
studentnumber,
lastname,
abbreviatedname,
gender_code,
email,
age,
gender_fi,
address,
mobile
} = updatedStudent
expect(studentnumber).toBe('011120775')
expect(lastname).toBe('Tauriainen')
expect(abbreviatedname).toBe('Tauriainen Maria Helena')
expect(email).toBe('Calgary.Flames@NHLonJätettä.fi')
expect(gender_code).toBe(1)
expect(gender_fi).toBe('Nainen')
expect(age).toBe(41)
expect(address).toBe('Yliopistonkatu 4')
expect(mobile).toBe('0400521981')
})
test('Existing courses are updated (not duplicated) in database', async () => {
const courses = await Course.findAll({ order: [['code', 'ASC']] })
const newCourse = courses.find(_ => _.code === '420_666')
expect(courses.length).toBe(148)
expect(newCourse).not.toBe(undefined)
})
test('Updating credits works', async () => {
const allCredits = await Credit.findAll({ order: [['id', 'ASC']] })
const updatedCredit = allCredits.find(_ => _.id === students[0].studyAttainments[2].credit.id)
expect(allCredits.length).toBe(152)
expect(updatedCredit.grade).toBe('0')
})
test('Studyrights are rewritten', async () => {
const updatedStudyRights = await Studyright.findAll({
where: {
student_studentnumber: '011120775'
}
})
expect(updatedStudyRights.length).toBe(1)
// theres really not a lot to test here since everything related to studyrights are just rewritten
})
})
})
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -1,28 +0,0 @@
const { sequelize } = require('../database/connection')
const updateAttainmentDates = async () => {
const { schema } = sequelize.options
const transaction = await sequelize.transaction()
try {
await sequelize.query(
`
-- Block writing to table to avoid deadlocks:
LOCK TABLE ONLY ${schema}.course IN EXCLUSIVE MODE;
UPDATE ${schema}.course
SET max_attainment_date = cr.max, min_attainment_date = cr.min
FROM (select course_code, max(attainment_date), min(attainment_date) from ${schema}.credit group by course_code) cr
WHERE course.code=cr.course_code;
`,
{ transaction }
)
await transaction.commit()
} catch (e) {
await transaction.rollback()
throw e
}
}
module.exports = {
updateAttainmentDates
}
+128 -6686
View File
File diff suppressed because it is too large Load Diff
+15 -27
View File
@@ -2,10 +2,11 @@
"name": "oodikone",
"description": "An application for analyzing university data.",
"scripts": {
"concurrently": "concurrently",
"cypress": "cypress",
"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",
@@ -13,26 +14,9 @@
"updater:real": "./run.sh updater real up --detach",
"both": "./run.sh both anon up --detach",
"both:real": "./run.sh both real up --detach",
"docker:down": "./run.sh both down --remove-orphans",
"diff:students": "docker exec backend node /usr/src/app/src/sisoodi_diff/students/index.js",
"diff:populations": "docker exec backend node /usr/src/app/src/sisoodi_diff/populations.js",
"diff:courses": "docker exec backend node /usr/src/app/src/sisoodi_diff/courses/index.js",
"diff:transfers": "docker exec backend node /usr/src/app/src/sisoodi_diff/transfers.js",
"diff:trends": "docker exec backend node /usr/src/app/src/sisoodi_diff/trends.js",
"docker:down": "./run.sh both anon down --remove-orphans; ./run.sh both real down --remove-orphans",
"prepare": "husky install"
},
"lint-staged": {
"{services,updater}/**/*.{js,jsx}": "eslint --fix",
"*.{js,json,md}": "prettier --write",
"*.css": "stylelint --fix",
".github/workflows/*": "actionlint",
"Dockerfile": "hadolint",
"*.sh": "shellcheck -x",
"docker-compose.yml": "docker-compose config -q",
"docker-compose.real.yml": "docker-compose -f docker-compose.yml -f docker-compose.real.yml config -q",
"docker-compose.ci.yml": "docker-compose -f docker-compose.ci.yml config -q",
"docker-compose.test.yml": "docker-compose -f docker-compose.test.yml config -q"
},
"repository": {
"type": "git",
"url": "git+https://github.com/UniversityOfHelsinkiCS/oodikone.git"
@@ -43,27 +27,31 @@
},
"homepage": "https://github.com/UniversityOfHelsinkiCS/oodikone#readme",
"devDependencies": {
"@babel/eslint-parser": "^7.14.7",
"@babel/eslint-parser": "^7.15.0",
"@babel/plugin-syntax-jsx": "^7.14.5",
"concurrently": "^6.2.0",
"cypress": "^7.7.0",
"eslint": "^7.30.0",
"concurrently": "^6.2.1",
"cypress": "^8.3.1",
"eslint": "^7.32.0",
"eslint-config-airbnb": "^18.2.1",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-babel": "^5.3.1",
"eslint-plugin-cypress": "^2.11.3",
"eslint-plugin-import": "^2.23.4",
"eslint-plugin-import": "^2.24.2",
"eslint-plugin-jsx-a11y": "^6.4.1",
"eslint-plugin-prefer-object-spread": "^1.2.1",
"eslint-plugin-prettier": "^3.4.0",
"eslint-plugin-prettier": "^3.4.1",
"eslint-plugin-react": "^7.24.0",
"eslint-plugin-react-hooks": "^4.2.0",
"husky": "^7.0.1",
"lint-staged": "^11.0.1",
"husky": "^7.0.2",
"lint-staged": "^11.1.2",
"prettier": "^2.3.2",
"stylelint": "^13.13.1",
"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"
}
}

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