Compare commits

..
867 Commits
Author SHA1 Message Date
Sasu MäkinenandGitHub fbe21bed1b Merge pull request #385 from UniversityOfHelsinkiCS/trunk
changed numbers in credit statistics
2018-09-04 17:27:35 +03:00
Mäkinen Sasu S 12c585acde changed numbers in credit statistics 2018-09-04 17:20:14 +03:00
Sasu MäkinenandGitHub e829c35396 Merge pull request #384 from UniversityOfHelsinkiCS/trunk
priority filter
2018-09-04 16:41:15 +03:00
Mäkinen Sasu S d4371d2e09 priority filter 2018-09-04 16:28:03 +03:00
ikuismaandGitHub 090dfb7665 Merge pull request #383 from UniversityOfHelsinkiCS/trunk
Category selection to teacher leader board + info tooltips for filters
2018-09-03 13:07:36 +03:00
ikuisma 3cc06c5a7b Removed redundant getSemesters call from teacher leader board. 2018-09-03 13:00:26 +03:00
ikuisma 62bfbd884e Implemented category selection to teacher leader board. 2018-09-03 12:59:23 +03:00
ikuisma 2b243a0178 Add category arg to get top teachers action. 2018-09-03 12:58:32 +03:00
ikuisma 69bf9b60f4 Hook up top teacher categories reducer to store. 2018-09-03 12:57:59 +03:00
ikuisma 2392c36c98 New itemreducer for categories of top teachers. 2018-09-03 12:57:36 +03:00
ikuisma 601ea18df8 Fixed data formatting of leader board to match format returned by newer version of API. 2018-08-31 14:34:09 +03:00
Mäkinen Sasu S b51fea8ca9 clicking teacher id now jumps to the teachers page 2018-08-31 12:37:18 +03:00
Mäkinen Sasu S 929ed9d8e8 eslint 2018-08-30 15:17:48 +03:00
Mäkinen Sasu S 436ff94a06 put quarter stats behind a tab 2018-08-30 15:16:33 +03:00
Mäkinen Sasu S 4ce1f9a45e added english infotexts to all filters 2018-08-30 14:52:13 +03:00
Mäkinen Sasu S 5dd3022006 created infotooltip for creditsatleast 2018-08-29 13:46:24 +03:00
ikuismaandGitHub 7b5715fca8 Merge pull request #378 from UniversityOfHelsinkiCS/trunk
Teacher course statistics separation by semester/course
2018-08-29 12:08:08 +03:00
ikuisma 5f0034c4bf Broke function that exceeded lint max line length. 2018-08-29 11:56:46 +03:00
ikuisma 83ac1f6ff9 Removed unused component. 2018-08-29 11:55:14 +03:00
ikuisma 09d7ca36e8 Added new Credits tab for to teacher details. 2018-08-29 11:54:56 +03:00
ikuisma 09c47f2d1e Implemented new CoursesTab with semester/course -wise stats. 2018-08-29 11:53:33 +03:00
ikuisma 1ae263d5c6 Changed no stats message from plain text to Segment. 2018-08-29 11:52:33 +03:00
Matti LuukkainenandGitHub 43f34a417d Merge pull request #375 from UniversityOfHelsinkiCS/trunk
Close button for teacher stats
2018-08-28 16:12:41 +03:00
ikuisma 90975a6ae9 Merge branch 'trunk' of https://github.com/UniversityOfHelsinkiCS/oodikone2-frontend into trunk 2018-08-28 16:02:58 +03:00
ikuisma fefb37e510 Auto submit academic year in teacher leader board. 2018-08-28 16:02:53 +03:00
Matti LuukkainenandGitHub 6edccdd1a8 Merge branch 'master' into trunk 2018-08-28 16:01:58 +03:00
Matti Luukkainen ea21d8bfd9 Merge branch 'trunk' of github.com:UniversityOfHelsinkiCS/oodikone2-frontend into trunk 2018-08-28 15:59:42 +03:00
ikuismaandGitHub c048e5c1a7 Merge pull request #374 from UniversityOfHelsinkiCS/trunk
Academic year based search for teacher leader board
2018-08-28 15:21:23 +03:00
ikuisma 07b2a665bc Implemented searchable teacher leader board. 2018-08-28 15:13:46 +03:00
ikuisma b33181d8cc Add yearcode to top teachers API call function 2018-08-28 15:12:48 +03:00
Matti Luukkainen 0539306034 close button for teacher stats 2018-08-28 13:52:35 +03:00
ikuismaandGitHub 54300120fc Merge pull request #372 from UniversityOfHelsinkiCS/trunk
Add loader to individual teacher page.
2018-08-27 15:35:35 +03:00
ikuisma 0385a00bc7 Add loader to individual teacher page. 2018-08-27 13:43:38 +03:00
ikuismaandGitHub 7126943ccc Merge pull request #371 from UniversityOfHelsinkiCS/trunk
Teacher stats leaderboard + teacher stats visibility based on new roles.
2018-08-27 12:48:39 +03:00
ikuisma f4e82c7d1e Leader board to teacher statistics + icons to tabs. 2018-08-27 10:56:07 +03:00
ikuisma b6c0dbfa50 New teachers leader board component. 2018-08-27 10:55:35 +03:00
ikuisma a78a19d1ce Add top teachers reducer to store. 2018-08-27 10:55:08 +03:00
ikuisma 330c7a1290 Basic reducer for top teachers. 2018-08-27 10:54:45 +03:00
Mäkinen Sasu S 1c452d498d canceled studyright filter 2018-08-24 17:55:29 +03:00
Mäkinen Sasu S 3d92df6316 default sort in teacher table 2018-08-24 15:32:00 +03:00
Mäkinen Sasu S a822a4a33b merge 2018-08-24 14:18:27 +03:00
Mäkinen Sasu S 7cd202065c Teacher statistics is now shown for admins and czars 2018-08-24 14:17:06 +03:00
Matti LuukkainenandGitHub 18b5699cca Merge pull request #363 from UniversityOfHelsinkiCS/trunk
user request details
2018-08-23 18:29:13 +03:00
Matti LuukkainenandGitHub a44b02cc88 Merge branch 'master' into trunk 2018-08-23 18:22:36 +03:00
Matti Luukkainen a139e99c01 Merge branch 'trunk' of github.com:UniversityOfHelsinkiCS/oodikone2-frontend into trunk 2018-08-23 18:17:36 +03:00
Matti Luukkainen 40f68d2f43 users requests 2018-08-23 18:16:09 +03:00
ikuismaandGitHub 73c97b925f Merge pull request #360 from UniversityOfHelsinkiCS/trunk
Teacher statistics page to use semester codes instead of year codes for queries
2018-08-23 08:48:08 +03:00
ikuisma 3420d6fc09 Changed teacher stats search to use semester start + end date for queries. 2018-08-23 08:36:34 +03:00
ikuisma a200d5103b Changed order of tabs in teacher statistics. 2018-08-23 08:35:53 +03:00
ikuisma 2f2cef2560 Added instructional message to teacher search page. 2018-08-23 08:35:09 +03:00
ikuisma 7cbe19b041 Fix teacher statistics query params to match new API. 2018-08-23 08:34:15 +03:00
Sasu MäkinenandGitHub 648ec2e7fe Merge pull request #358 from UniversityOfHelsinkiCS/trunk
Czars, Teachers and Courses
2018-08-22 16:06:13 +03:00
Mäkinen Sasu S 7a6dcd2520 remove clog and merge 2018-08-22 15:59:35 +03:00
Mäkinen Sasu S d6bde89de8 Merge branch 'trunk' of https://github.com/UniversityOfHelsinkiCS/oodikone2-frontend into trunk 2018-08-22 15:59:15 +03:00
Mäkinen Sasu S 4ad257f227 coronate to czar at users page 2018-08-22 15:58:46 +03:00
ikuisma d3bdb75156 Merge branch 'trunk' of https://github.com/UniversityOfHelsinkiCS/oodikone2-frontend into trunk 2018-08-22 15:23:20 +03:00
ikuisma 1e0364c6ef Add sorting to teacher statistics table. 2018-08-22 15:23:12 +03:00
totutotu b5cd123fbb Merge branch 'trunk' of https://github.com/UniversityOfHelsinkiCS/oodikone2-frontend into trunk 2018-08-22 15:14:59 +03:00
totutotu a572d387dc Fixed course stats search to include query by code 2018-08-22 15:14:56 +03:00
ikuismaandGitHub 37b76cfd73 Merge pull request #355 from UniversityOfHelsinkiCS/trunk
Teacher yearly statistics page + statistics table refactoring.
2018-08-22 12:49:24 +03:00
ikuisma 2fc8ca6e78 Teacher yearly statistics components. 2018-08-22 12:19:20 +03:00
ikuisma 59983149e5 New reducers required for teacher statistics. 2018-08-22 12:18:53 +03:00
ikuisma 2f2a0728f8 Helper method for creating generic item reducers. 2018-08-22 12:18:14 +03:00
ikuisma d61ca334e5 Fixed query parameter issue. 2018-08-22 11:28:21 +03:00
Sasu MäkinenandGitHub 29283e06fc Merge pull request #350 from UniversityOfHelsinkiCS/trunk
course comparing table
2018-08-21 15:17:18 +03:00
Mäkinen Sasu S 0185d496ed and eslint 2018-08-21 15:06:32 +03:00
Mäkinen Sasu S 06435ab251 remove clog 2018-08-21 15:05:46 +03:00
Mäkinen Sasu S 1c4ad598c2 added comparing to table 2018-08-21 15:05:08 +03:00
Sasu MäkinenandGitHub 4cae824f1b Merge pull request #349 from UniversityOfHelsinkiCS/trunk
comparing courses and courseparticipationNtimes filter and some refactoring
2018-08-21 14:21:54 +03:00
Mäkinen Sasu S 975dcae051 put teachers behind admin users 2018-08-21 14:14:36 +03:00
Mäkinen Sasu S dc75fdf09e remove clog 2018-08-21 14:02:39 +03:00
Mäkinen Sasu S cc76f77831 some fixin 2018-08-21 14:01:43 +03:00
Mäkinen Sasu S ff0383881c fixed stuff totutotu broke 2018-08-21 13:11:19 +03:00
Mäkinen Sasu S 3470ff1892 Merge branch 'trunk' of https://github.com/UniversityOfHelsinkiCS/oodikone2-frontend into trunk 2018-08-21 12:51:02 +03:00
Mäkinen Sasu S 11e3cc6064 comparing courses is now possible 2018-08-21 12:50:55 +03:00
totutotu c3262ae30b Merge branch 'trunk' of https://github.com/UniversityOfHelsinkiCS/oodikone2-frontend into trunk 2018-08-21 12:35:53 +03:00
totutotu 89b503ce31 Course participation filter of Intern. Progress Test for medstudents 2018-08-21 12:35:47 +03:00
ikuisma d12176d2bf Refactored population statistics queries to use params argument instead of hardcoded param string. 2018-08-21 11:35:21 +03:00
ikuisma a3777e8666 Added optional query param passing to axios requests. 2018-08-21 11:33:49 +03:00
ikuisma 67ec3ae395 Add functions for generating _SUCCESS, etc. action types for given prefix. 2018-08-21 10:43:22 +03:00
Matti LuukkainenandGitHub 383dedec0c Merge pull request #347 from UniversityOfHelsinkiCS/trunk
refactoring usage stats
2018-08-21 00:20:18 +03:00
Matti Luukkainen 621f9ab851 refactoring usage stats 2018-08-21 00:08:45 +03:00
Matti LuukkainenandGitHub 925009b671 Merge pull request #346 from UniversityOfHelsinkiCS/trunk
debug
2018-08-20 23:40:59 +03:00
Matti Luukkainen ad8ff6b38d debug 2018-08-20 23:30:55 +03:00
Matti LuukkainenandGitHub c641df4a3e Merge pull request #345 from UniversityOfHelsinkiCS/trunk
replace direct axios usage with callApi
2018-08-20 23:16:18 +03:00
Matti LuukkainenandGitHub 54152e0128 Merge branch 'master' into trunk 2018-08-20 23:08:19 +03:00
Matti Luukkainen 445d25a830 Merge branch 'trunk' of github.com:UniversityOfHelsinkiCS/oodikone2-frontend into trunk 2018-08-20 18:43:47 +03:00
Matti Luukkainen 9b750ddba6 replace direct axios usage with callApi 2018-08-20 18:42:17 +03:00
Matti LuukkainenandGitHub 02c3b19128 Merge pull request #344 from UniversityOfHelsinkiCS/trunk
usage stats, quick'n'dirty
2018-08-20 18:26:01 +03:00
Matti LuukkainenandGitHub e4219ac039 Merge branch 'master' into trunk 2018-08-20 18:19:47 +03:00
Matti Luukkainen 449a7aa6d4 fixes 2018-08-20 18:16:31 +03:00
Matti Luukkainen 97b4e3bcb6 first edition of usage stats, quick and dirty 2018-08-20 18:09:51 +03:00
Matti Luukkainen c8ff5eb0ae first edition of usage stats, quick and dirty 2018-08-20 18:08:58 +03:00
ikuisma 8ce83243ee Removed unused import 2018-08-20 17:03:21 +03:00
ikuisma 24c9032293 Refactored name of TeacherStatistics to Teachers - reserving previous name for new component. 2018-08-20 17:02:45 +03:00
totutotu 51430fdb73 'Selected' table not visible if non selected 2018-08-20 15:50:34 +03:00
ikuismaandGitHub 3249cb0544 Merge pull request #342 from UniversityOfHelsinkiCS/trunk
Trunk
2018-08-20 13:57:28 +03:00
totutotu bd3a920a6d Merge branch 'trunk' of https://github.com/UniversityOfHelsinkiCS/oodikone2-frontend into trunk 2018-08-20 13:50:25 +03:00
ikuisma 029ac4ce4b Placeholder text to teacher stats table if no stats found. 2018-08-20 13:49:59 +03:00
totutotu 38d12d9724 Improvements to course stats search form 2018-08-20 13:49:37 +03:00
ikuismaandGitHub 3b12eceb70 Merge pull request #340 from UniversityOfHelsinkiCS/trunk
First version of teacher statistics page for individual teachers
2018-08-20 13:27:10 +03:00
ikuisma 07eda34bd6 Implemented first version of teacher statistics page for individual teachers. 2018-08-20 12:38:49 +03:00
Matti LuukkainenandGitHub f76ab7357f Merge pull request #338 from UniversityOfHelsinkiCS/trunk
user emails
2018-08-18 15:57:31 +03:00
Matti LuukkainenandGitHub 9fc5545aa9 Merge branch 'master' into trunk 2018-08-18 15:48:56 +03:00
Matti Luukkainen 8c539882ec Merge branch 'trunk' of github.com:UniversityOfHelsinkiCS/oodikone2-frontend into trunk 2018-08-18 15:47:19 +03:00
Matti Luukkainen 0646de00bc user emails 2018-08-18 15:47:15 +03:00
Sasu MäkinenandGitHub 814d20d915 Merge pull request #336 from UniversityOfHelsinkiCS/trunk
teacher statistics and hackfix
2018-08-17 15:33:54 +03:00
Mäkinen Sasu S bb21d7c789 Merge branch 'trunk' of https://github.com/UniversityOfHelsinkiCS/oodikone2-frontend into trunk 2018-08-17 15:26:53 +03:00
Mäkinen Sasu S b25c10095b hackfixed legacy hackfix in webpack 2018-08-17 15:26:36 +03:00
Mäkinen Sasu S a6ddb2ee57 Revert "moved hot-loader to dev dependencies"
This reverts commit 816ae576d7.
2018-08-17 13:31:02 +03:00
Mäkinen Sasu S 816ae576d7 moved hot-loader to dev dependencies 2018-08-17 13:30:11 +03:00
ikuisma b4d38ad3a1 Route to TeacherStatistics 2018-08-17 12:59:37 +03:00
ikuisma e56426f7d6 Frontend route for teachers and teachers/:id 2018-08-17 12:59:05 +03:00
Sasu MäkinenandGitHub 8eaaf78cd9 Merge pull request #335 from UniversityOfHelsinkiCS/trunk
Extent graduated rewrite and added as default filter
2018-08-17 12:58:38 +03:00
ikuisma c1a7eae149 Teacher statistics components. 2018-08-17 12:58:28 +03:00
ikuisma b1da93e508 Reducer for querying teachers endpoint. 2018-08-17 12:57:54 +03:00
Sasu MäkinenandGitHub e8f56e266f Merge branch 'master' into trunk 2018-08-17 12:51:17 +03:00
Mäkinen Sasu S d7ebc4ebbe proptype fix 2018-08-17 12:50:26 +03:00
Mäkinen Sasu S 39cd9b6ab3 Merge branch 'trunk' of https://github.com/UniversityOfHelsinkiCS/oodikone2-frontend into trunk 2018-08-17 12:33:08 +03:00
Mäkinen Sasu S a315c0f37c rewritten ExtentGraduated Filter and added some default filters for all populations. Removed all kludges and legacy bullshit from Highcharts credit accumulation graph. Updated react-hot-loader 2018-08-17 12:32:29 +03:00
Tuomo TorppaandGitHub 96a99481f7 Merge pull request #332 from UniversityOfHelsinkiCS/trunk
New course stats form
2018-08-17 10:42:20 +03:00
Tuomo TorppaandGitHub 68f3145716 Merge pull request #331 from UniversityOfHelsinkiCS/new_coursestats
New coursestats
2018-08-17 10:34:36 +03:00
totutotu 2d80613c0d New course stats search form MVP completed 2018-08-17 10:31:14 +03:00
totutotu f40c27d8db New coures statistics 2018-08-16 16:18:24 +03:00
totutotu 15fc168855 v2 added to new enpoint 2018-08-16 10:20:32 +03:00
totutotu 175732fff8 css fix 2018-08-16 09:58:04 +03:00
totutotu 8a08ec16e5 new coursestats, not staging ready yet 2018-08-16 09:53:42 +03:00
Sasu MäkinenandGitHub e0f78b8b83 Merge pull request #328 from UniversityOfHelsinkiCS/trunk
transferfilterfix
2018-08-14 15:07:06 +03:00
Mäkinen Sasu S 534f828679 transferfilterfix 2018-08-14 14:51:29 +03:00
Sasu MäkinenandGitHub 343ba5d629 Merge pull request #319 from UniversityOfHelsinkiCS/trunk
table sorting
2018-08-13 13:36:07 +03:00
Sasu MäkinenandGitHub 8cd4d9fd1f Merge branch 'master' into trunk 2018-08-13 13:28:25 +03:00
Mäkinen Sasu S a3767cca58 Merge branch 'trunk' of https://github.com/UniversityOfHelsinkiCS/oodikone2-frontend into trunk 2018-08-13 13:24:16 +03:00
Mäkinen Sasu S 5ab92540c0 table sorting greater again 2018-08-13 13:23:29 +03:00
Sasu MäkinenandGitHub 3a0399f3cc Merge pull request #314 from UniversityOfHelsinkiCS/trunk
@sasumaki (drunk) link from population statistics course statistics table course to course statistics with fitting query
2018-08-13 10:03:57 +03:00
Sasu MäkinenandGitHub 774d21c4cf Merge branch 'master' into trunk 2018-08-10 16:41:50 +03:00
Mäkinen Sasu S 4e36236ffd eslint fix 2018-08-10 16:39:02 +03:00
Mäkinen Sasu S 8d39dbb067 link from population statistics course statistics table course cell to course statistics with fitting query 2018-08-10 16:38:30 +03:00
Mäkinen Sasu S c0d8f9bf22 transfer filter back online 2018-08-10 11:55:35 +03:00
Sasu MäkinenandGitHub 2c3514f992 Merge pull request #309 from UniversityOfHelsinkiCS/trunk
Filters and grade distributions redone
2018-08-09 15:49:38 +03:00
Mäkinen Sasu S be05cb834d lint fix 2018-08-09 15:43:26 +03:00
Mäkinen Sasu S 98c13d35a0 redone population statistics course grade distribution table 2018-08-09 15:42:40 +03:00
Mäkinen Sasu S 1ab1d61818 Saving filters redone with description 2018-08-09 12:11:28 +03:00
Sasu MäkinenandGitHub 0f53b6337e Merge pull request #305 from UniversityOfHelsinkiCS/trunk
Advanced options for filters and remove gender filter
2018-08-08 17:55:19 +03:00
Mäkinen Sasu S e00762d595 made superbara use advanced filters 2018-08-08 17:32:13 +03:00
Mäkinen Sasu S 97ead4877a Merge branch 'trunk' of https://github.com/UniversityOfHelsinkiCS/oodikone2-frontend into trunk 2018-08-08 17:28:50 +03:00
Mäkinen Sasu S a0cc9f9030 Hide some filter features behind advanced filters option and removed gender filter from use 2018-08-08 17:28:22 +03:00
Tuomo TorppaandGitHub 4676c0a8cb Merge pull request #294 from UniversityOfHelsinkiCS/trunk
Order course search by latest instance held
2018-08-08 10:37:09 +03:00
totutotu 4b085ee560 Order course search by latest instance held 2018-08-08 10:26:04 +03:00
Tuomo TorppaandGitHub 2bf74aa5ef Merge pull request #292 from UniversityOfHelsinkiCS/trunk
Course stats programs update
2018-08-07 16:08:24 +03:00
totutotu e5c8b1dec8 failure % to pass rate 2018-08-07 16:01:48 +03:00
totutotu e2be82aa8f Headers to course stats, stats table NaN bug fixed 2018-08-07 15:55:57 +03:00
totutotu 6e597525f5 Show amount od students in each study programme and order dropdown 2018-08-07 15:31:31 +03:00
totutotu dc049bfea6 Merge branch 'trunk' of github.com:UniversityOfHelsinkiCS/oodikone2-frontend into trunk 2018-08-07 14:12:20 +03:00
totutotu 13ee8e00e2 Programmes to new data format 2018-08-07 14:12:14 +03:00
Mäkinen Sasu S 1519806e07 superbara now works with new dataset 2018-08-07 13:47:42 +03:00
Sasu MäkinenandGitHub 16f71a30e6 Merge pull request #290 from UniversityOfHelsinkiCS/trunk
bugfixes
2018-08-07 12:55:05 +03:00
Mäkinen Sasu S d92bdd64d8 fixed default course student limit amount 2018-08-07 10:30:52 +03:00
Mäkinen Sasu S a2204d1723 fixed rounding error at semseter enrollment 2018-08-07 09:45:51 +03:00
Mäkinen Sasu S 5e0666db64 mapped language to course participation ffilters 2018-08-07 09:38:34 +03:00
ikuismaandGitHub c49145ba42 Merge pull request #288 from UniversityOfHelsinkiCS/trunk
Users page improvements Part 1.
2018-08-06 14:12:45 +03:00
ikuisma 9bf058d256 Implemented single-component version of new users page with /users/:id endpoint for single users. 2018-08-06 14:03:56 +03:00
ikuisma d7194d3873 Added missing pending=false to successful user GET in reducer. 2018-08-06 14:03:08 +03:00
Tuomo TorppaandGitHub 8b026fd215 Merge pull request #286 from UniversityOfHelsinkiCS/trunk
Language chooser to student page 60792a8
2018-08-03 15:27:13 +03:00
totutotu e1de28cb56 Merge branch 'trunk' of github.com:UniversityOfHelsinkiCS/oodikone2-frontend into trunk 2018-08-03 15:20:24 +03:00
totutotu 60792a80d8 Language chooser to student page 2018-08-03 15:20:19 +03:00
Tuomo TorppaandGitHub 38d0b92bea Merge pull request #284 from UniversityOfHelsinkiCS/trunk
Languages based on user preference, anonymizer and studyrights to course statistics
2018-08-03 14:58:47 +03:00
Mäkinen Sasu S 682193c742 surprise eslint 2018-08-03 14:50:39 +03:00
Mäkinen Sasu S 2a14bedb0e merge 2018-08-03 14:50:07 +03:00
Mäkinen Sasu S e7aa6fb439 studyright selection to coursestatistics 2018-08-03 14:46:51 +03:00
totutotu 425b86d5a0 Languages from users preferences from DB 2018-08-03 14:37:29 +03:00
totutotu d2e850ebf5 Languages to course table and everywhere 2018-08-03 12:34:29 +03:00
totutotu 917cc29286 Course statistics languages 2018-08-03 10:57:40 +03:00
ikuismaandGitHub a4fb790f13 Merge pull request #283 from UniversityOfHelsinkiCS/trunk
Separate reducers for studyrightelements that are used by users and pop. statistics page
2018-08-03 10:54:01 +03:00
ikuisma 6c29f8abc1 Merge branch 'trunk' of https://github.com/UniversityOfHelsinkiCS/oodikone2-frontend into trunk 2018-08-03 10:36:21 +03:00
ikuisma bf19a78202 Replace PopulationSearchFrom unit request to new endpoint 2018-08-03 10:36:11 +03:00
ikuisma f4e6d93d7c New reducer for population statistics degrees and programmes. 2018-08-03 10:35:21 +03:00
Tuomo TorppaandGitHub 12b9e62ddc Merge pull request #282 from UniversityOfHelsinkiCS/trunk
Languages to course search
2018-08-03 10:33:14 +03:00
totutotu 9fd4c8d610 Languages to course search 2018-08-03 10:21:47 +03:00
Tuomo TorppaandGitHub 436e28464d Merge pull request #280 from UniversityOfHelsinkiCS/trunk
Languages to users page, populations page bug fixed
2018-08-03 09:50:48 +03:00
totutotu cf19ddbb4e Languages for user management page 2018-08-03 09:43:53 +03:00
totutotu a94b3693e9 course code mapping ui improvement 2018-08-03 09:06:14 +03:00
totutotu 2f6ae7e88d population page filter bug fixed 2018-08-03 08:52:59 +03:00
totutotu b59a41a766 Commented not-complete language chooser 2018-08-02 15:38:06 +03:00
totutotu 652c31d97c Languages 2018-08-02 15:32:24 +03:00
totutotu dacb82b610 Languages to course code mapper 2018-08-02 15:32:12 +03:00
Tuomo TorppaandGitHub 822ce8af6c Merge pull request #278 from UniversityOfHelsinkiCS/trunk
Trunk
2018-08-02 14:27:23 +03:00
totutotu e7bea0302c Removed console.lof 2018-08-02 14:18:00 +03:00
totutotu 58b1886857 Merge branch 'trunk' of github.com:UniversityOfHelsinkiCS/oodikone2-frontend into trunk 2018-08-02 14:16:14 +03:00
totutotu 27fcff3075 Languages for courses 2018-08-02 14:16:07 +03:00
ikuismaandGitHub a6da50cf19 Merge pull request #277 from UniversityOfHelsinkiCS/trunk
Language selection to Population page and replacement text for invalid student start dates.
2018-08-02 13:20:32 +03:00
ikuisma 50d0586db4 Changed invalid date to be 'Unavailable' instead. ' 2018-08-02 13:11:10 +03:00
totutotu 147864affc Fixed merge 2018-08-02 12:48:32 +03:00
totutotu 82aea61f12 Language selection to Population page 2018-08-02 12:45:52 +03:00
Sasu MäkinenandGitHub 4516e3d328 Merge pull request #274 from UniversityOfHelsinkiCS/trunk
bugfix
2018-08-01 12:10:43 +03:00
Mäkinen Sasu S 2913e01cff Merge branch 'trunk' of https://github.com/UniversityOfHelsinkiCS/oodikone2-frontend into trunk 2018-08-01 12:02:12 +03:00
Mäkinen Sasu S 5d1c9feb87 rightfix 2018-08-01 12:01:55 +03:00
Tuomo TorppaandGitHub f64349e695 Merge pull request #273 from UniversityOfHelsinkiCS/trunk
Credit gain table filter to population page, bugfixes
2018-08-01 10:59:55 +03:00
totutotu dad9ebcada population credit gain table division by zero avoided 2018-07-31 13:29:43 +03:00
totutotu f38cfac57f Population credit gain table size fix 2018-07-31 13:04:45 +03:00
totutotu a30e374246 Filter population by clicking credit amount in credit gain table 2018-07-31 12:24:20 +03:00
Mäkinen Sasu S 8036fdd9ac typo fix 2018-07-30 14:30:03 +03:00
Mäkinen Sasu S 25af5d47b7 bugfix2 2018-07-30 13:41:45 +03:00
Sasu MäkinenandGitHub a2d3cf083f Merge pull request #270 from UniversityOfHelsinkiCS/trunk
bugfix
2018-07-30 13:37:32 +03:00
Mäkinen Sasu S 42de728ce5 Merge branch 'trunk' of https://github.com/UniversityOfHelsinkiCS/oodikone2-frontend into trunk 2018-07-30 13:31:21 +03:00
Mäkinen Sasu S 253fe616ac removed highlevelname 2018-07-30 13:31:07 +03:00
Tuomo TorppaandGitHub f1995e84a3 Merge pull request #268 from UniversityOfHelsinkiCS/trunk
Population coursetable new sorting, extent graduation filters rework, updated at fields for students and population
2018-07-30 12:07:27 +03:00
totutotu 422c16bfa5 Sort courses by % of population passed and attempted 2018-07-30 10:21:43 +03:00
Mäkinen Sasu S dc9c10df32 added updated at field for population 2018-07-27 16:27:33 +03:00
Mäkinen Sasu S d7d7295647 added an error message if user has no rights to any student data 2018-07-27 15:38:43 +03:00
Mäkinen Sasu S 6e63ea3363 updated at to student 2018-07-27 14:06:28 +03:00
Mäkinen Sasu S 78a04b6a46 Reworked extent graduation filters and added functionality to create persistant filters 2018-07-26 16:27:54 +03:00
Sasu MäkinenandGitHub 3f2c41c185 Merge pull request #266 from UniversityOfHelsinkiCS/trunk
Million new features
2018-07-26 13:54:14 +03:00
Mäkinen Sasu S c0e7c03f19 Filter by students transferring study programme and some unifying of syntax between filters 2018-07-26 13:42:45 +03:00
Mäkinen Sasu S c799b0edbc cleanup crew 2018-07-25 15:23:49 +03:00
Mäkinen Sasu S 5c478c1d63 Enrollment filter done 2018-07-25 13:52:08 +03:00
Mäkinen Sasu S e3bf53bce3 fixed saving course filters 2018-07-24 15:47:58 +03:00
Mäkinen Sasu S f0cbd6611e fixed coursesearch 2018-07-24 15:16:03 +03:00
Mäkinen Sasu S e304c21523 filtering for discipline and type now possible 2018-07-24 13:59:33 +03:00
Mäkinen Sasu S 4c284a4483 reworked student statistics studyrights table 2018-07-23 15:22:19 +03:00
Mäkinen Sasu S cd30cc7a3a some updates to tests 2018-07-20 10:47:12 +03:00
Mäkinen Sasu S f56e3aa45a randomly filtering out filters for extent codes less than 10 2018-07-19 15:56:31 +03:00
Mäkinen Sasu S 467238e87a Course grade distribution graph 2018-07-19 15:36:59 +03:00
Sasu MäkinenandGitHub 08156b5b90 Merge pull request #259 from UniversityOfHelsinkiCS/trunk
extentcode graduated filters
2018-07-19 11:31:05 +03:00
Mäkinen Sasu S 76b7e97dd3 little tweaks 2018-07-19 11:23:12 +03:00
Mäkinen Sasu S 3b0a6d2235 you can now filter based on if student has a graduated extentcode, need to update to check only for selected studyright 2018-07-18 17:03:21 +03:00
Sasu MäkinenandGitHub 069ebc3ce9 Merge pull request #257 from UniversityOfHelsinkiCS/trunk
degree and program mapping
2018-07-18 13:24:49 +03:00
Mäkinen Sasu S 8157010959 loader for getting studyrights 2018-07-18 13:07:59 +03:00
Mäkinen Sasu S 6e66336841 eslint 2018-07-18 11:57:58 +03:00
Mäkinen Sasu S 1ff98c5efd program, degree associations and some minor tweaks as extra 2018-07-18 11:57:09 +03:00
Sasu MäkinenandGitHub ebffacafdc Merge pull request #256 from UniversityOfHelsinkiCS/trunk
grade distribution in population statistics
2018-07-18 10:29:45 +03:00
Mäkinen Sasu S 5d791dc7f1 grade distribution is now from backend instead of hacking it in frontend. Fixed all the problems 2018-07-18 10:06:00 +03:00
Mäkinen Sasu S 17172faa40 clog removed 2018-07-17 16:33:48 +03:00
Mäkinen Sasu S dc43625df5 bugfix 2018-07-17 16:33:24 +03:00
Mäkinen Sasu S 93edc01e8f eslint fix 2018-07-17 16:10:01 +03:00
Mäkinen Sasu S 2443b611f2 #229 2018-07-17 16:06:07 +03:00
Mäkinen Sasu S b8dcf7f1c4 fixed course participation filters name being in english 2018-07-17 13:49:29 +03:00
Mäkinen Sasu S a0c03be52d added grade distribution to population statistics 2018-07-17 13:33:51 +03:00
Sasu MäkinenandGitHub 748c9d30f8 Merge pull request #254 from UniversityOfHelsinkiCS/trunk
Deleting preset filters
2018-07-16 15:50:56 +03:00
Mäkinen Sasu S 52424a4069 added tests for filters in population statistics 2018-07-16 15:36:54 +03:00
Mäkinen Sasu S 15d6938883 bugfix 2 2018-07-16 13:59:14 +03:00
Mäkinen Sasu S e7ff8952ad bug fix 2018-07-16 13:32:31 +03:00
Mäkinen Sasu S a2e0c00087 remove clog 2018-07-16 12:34:23 +03:00
Mäkinen Sasu S 77e853c1a1 Preset filters can now be deleted from database 2018-07-16 12:33:45 +03:00
Sasu MäkinenandGitHub 0c039d877a Merge pull request #251 from UniversityOfHelsinkiCS/trunk
Preset filter course filters are now re-calculated on load and some p…
2018-07-13 14:16:05 +03:00
Mäkinen Sasu S df68e87d29 Preset filter course filters are now re-calculated on load and some preset filter redesign 2018-07-13 14:02:37 +03:00
Sasu MäkinenandGitHub 8a7aaca325 Merge pull request #249 from UniversityOfHelsinkiCS/trunk
Update students on demand
2018-07-12 15:41:25 +03:00
Mäkinen Sasu S 259fa10e9c removed clog 2018-07-12 15:34:07 +03:00
Mäkinen Sasu S cb437c7559 Update population or student on demand 2018-07-12 15:33:26 +03:00
Mäkinen Sasu S 82d92aa0b5 testing image now updated everytime pushing to trunk and superbara tests open oodikone.cs.helsinki.fi/testing/ instead of localhost 2018-07-11 15:02:16 +03:00
Sasu MäkinenandGitHub aed9029c7c Merge pull request #248 from UniversityOfHelsinkiCS/trunk
Testing environment at oodikone.cs.helsinki.fi/testing/
2018-07-11 14:10:15 +03:00
Mäkinen Sasu S 724a749e2c removed clog 2018-07-11 14:01:40 +03:00
Mäkinen Sasu S 092bc11265 tester user authentication 2018-07-11 14:01:03 +03:00
Mäkinen Sasu S 371f8ba5ab added another needle in the haystack again 2018-07-10 14:58:49 +03:00
Sasu MäkinenandGitHub a916711106 Merge pull request #247 from UniversityOfHelsinkiCS/trunk
trying to fix my bad decisions
2018-07-10 12:07:13 +03:00
Mäkinen Sasu S dc1502de8d trying to fix my bad decisions 2018-07-10 12:00:39 +03:00
Sasu MäkinenandGitHub c570986161 Merge pull request #246 from UniversityOfHelsinkiCS/trunk
reverting login options
2018-07-10 11:41:36 +03:00
Mäkinen Sasu S 8ff11df102 reverting login options 2018-07-10 11:35:07 +03:00
Sasu MäkinenandGitHub 5cd20a8b63 Merge pull request #245 from UniversityOfHelsinkiCS/trunk
Im not sure what happened here
2018-07-10 10:38:29 +03:00
Sasu MäkinenandGitHub 29c39c68ea Merge branch 'master' into trunk 2018-07-10 10:30:06 +03:00
Mäkinen Sasu S 1ff6b7b31f reset 2018-07-10 10:10:59 +03:00
Sasu MäkinenandGitHub 20ab0bbfc9 Merge pull request #244 from UniversityOfHelsinkiCS/travis_master
testing devops mayhem
2018-07-09 14:29:05 +03:00
Sasu MäkinenandGitHub fc096f5cb9 Merge branch 'master' into trunk 2018-07-09 14:24:23 +03:00
Sasu MäkinenandGitHub 04ff187b5b Merge branch 'master' into travis_master 2018-07-09 14:22:08 +03:00
Mäkinen Sasu S b4c3e45323 testing updating production for routing 2018-07-09 14:19:59 +03:00
Mäkinen Sasu S f1c8c339b4 same stuff as always 2018-07-09 12:07:30 +03:00
Mäkinen Sasu S 4f5f98329b trying out new strat 2018-07-09 11:37:23 +03:00
Mäkinen Sasu S 4df1853525 testing stuff 2018-07-06 15:49:17 +03:00
Mäkinen Sasu S 678a4320de filters visible to all 2018-07-05 10:16:23 +03:00
Mäkinen Sasu S aa493e9f31 no detach on docker compose up 2018-07-05 10:13:29 +03:00
Mäkinen Sasu S c19f99953b räääh tsup 2018-07-04 15:36:04 +03:00
Mäkinen Sasu S 510fd8e5a2 lets seeif this wokrs ==) 2018-07-04 14:53:37 +03:00
Mäkinen Sasu S c3eeea14d9 create database in travis 2018-07-04 14:40:44 +03:00
Mäkinen Sasu S e0521e89d2 psql install in travis 2018-07-04 14:32:01 +03:00
Mäkinen Sasu S 1c6268307e travis mayhem 2018-07-04 14:22:46 +03:00
Mäkinen Sasu S e734dbf6e2 docker versions... 2018-07-04 14:06:04 +03:00
Mäkinen Sasu S 32dd714b98 changed version.... 2018-07-04 14:00:41 +03:00
Mäkinen Sasu S 97555865d9 fix yaml 2018-07-04 13:56:13 +03:00
Mäkinen Sasu S 23aabaf959 update docker and docker-compose in travis 2018-07-04 13:50:12 +03:00
Mäkinen Sasu S c3ae5bf15a trying again with docker-compose file 2018-07-04 13:34:30 +03:00
Mäkinen Sasu S 6d79dddd4d travis test 2018-07-04 13:19:10 +03:00
Mäkinen Sasu S 2a00ce59ad testing travis stuff 2018-07-04 13:06:12 +03:00
Sasu MäkinenandGitHub 1a3b924ac9 Merge pull request #237 from UniversityOfHelsinkiCS/revert-236-presetfilter
Revert "Presetfilter"
2018-07-04 11:42:30 +03:00
Sasu MäkinenandGitHub 9ed155b42b Merge pull request #238 from UniversityOfHelsinkiCS/presetfilter
Presetfilter
2018-07-04 11:41:34 +03:00
Sasu MäkinenandGitHub 09189e34be Revert "Presetfilter" 2018-07-04 11:40:11 +03:00
Sasu MäkinenandGitHub 2a8103443d Merge pull request #236 from UniversityOfHelsinkiCS/presetfilter
Preset filter
2018-07-04 11:39:16 +03:00
Mäkinen Sasu S 3f3f295376 tests removed from travis aswell for merge to trunk 2018-07-04 11:36:34 +03:00
Mäkinen Sasu S d38f3774ca removed tests for trunk merge 2018-07-04 10:57:50 +03:00
Mäkinen Sasu S 5abcac8dc2 cleaned up a little 2018-07-04 10:43:42 +03:00
Mäkinen Sasu S cbeca7fbf5 preset filters can now be nested 2018-07-04 10:40:16 +03:00
Mäkinen Sasu S c0fae224f0 trying if xvfb fixes travis superbara 2018-07-03 16:53:37 +03:00
Mäkinen Sasu S 04f46c1828 Saving preset filters now visible only to 'tktl' and 'sasumaki' 2018-07-03 16:47:54 +03:00
Mäkinen Sasu S a021a92ecc preset button basic functionality now working 2018-07-03 16:25:27 +03:00
ikuisma 20d7fc2607 Removed npm test from prepush due to superbara issues. 2018-07-03 15:13:44 +03:00
ikuisma e5b3f00cf5 Added course codes to degree and studyprogram dropdown lists. 2018-07-03 14:30:02 +03:00
Mäkinen Sasu S 1faf4c8c83 Merge branch 'trunk' into presetfilter 2018-07-03 11:38:30 +03:00
Mäkinen Sasu S 2ea11e4724 added google chrome in travis to maybe get superbara working 2018-07-02 16:22:03 +03:00
Mäkinen Sasu S c72cfc0699 trying superbara in travis again 2018-07-02 16:16:36 +03:00
Mäkinen Sasu S 8b436c7442 Merge branch 'trunk' of https://github.com/UniversityOfHelsinkiCS/oodikone2-frontend into trunk 2018-07-02 16:07:16 +03:00
Mäkinen Sasu S 563d3c9459 travis superbara maybe 2018-07-02 16:04:58 +03:00
Sasu MäkinenandGitHub 85fbb02338 Merge branch 'master' into trunk 2018-07-02 15:54:12 +03:00
Mäkinen Sasu S ff6252b889 fixed credit graph tooltip showing course title as an object 2018-07-02 15:47:17 +03:00
Mäkinen Sasu S d741b4741e wait in test 2018-07-02 15:18:22 +03:00
Mäkinen Sasu S 60d1bcbf8d added tests as compulsory pre-push hook. From now on skipping tests will be settled in the court of Toska 2018-07-02 15:14:50 +03:00
Mäkinen Sasu S c0f4abc5f5 progression in filters 2018-06-29 15:31:26 +03:00
ikuismaandGitHub 29a0c40e01 Merge pull request #215 from UniversityOfHelsinkiCS/trunk
Update frontend to work with data returned by the latest version of the backend
2018-06-28 15:04:59 +03:00
ikuisma c7c0241a78 Fixed enable users page to work with new elementdetails array. 2018-06-28 13:06:27 +03:00
Mäkinen Sasu S b059a2f276 Merge branch 'trunk' into presetfilter 2018-06-28 10:43:58 +03:00
Mäkinen Sasu S 52b819424b course names now visible in student page (again) 2018-06-28 10:36:12 +03:00
Mäkinen Sasu S b74cbd8789 updated tests to correspond to new population query 2018-06-28 10:22:06 +03:00
ikuisma 5702f0e49c Removed unused import 2018-06-28 08:58:29 +03:00
ikuisma cf88efde90 Added all elementdetail names be visible in the query search card. 2018-06-28 08:57:58 +03:00
ikuisma b63b845b02 Updated frontend to handle new language objects that were previously strings – should probably be handled by a selector later on. 2018-06-28 07:53:24 +03:00
Mäkinen Sasu S 4858084f24 reworking population query 2018-06-27 20:08:49 +03:00
Mäkinen Sasu S e21b956483 started preset filters 2018-06-27 11:53:43 +03:00
Mäkinen Sasu S 74c0b2486d email now shows in student page 2018-06-27 10:21:23 +03:00
Mäkinen Sasu S cbd84c5ffd fixed an issue with copying email list in population statistics 2018-06-26 15:05:59 +03:00
Mäkinen Sasu S ce8e7def89 population page now shows emails of students. Emails can also be copied to clipboard by clicking copy icon 2018-06-26 14:46:30 +03:00
Mäkinen Sasu S 1c2f36b158 done population statistics: sort population names alphabetically in dropdown #199 2018-06-26 13:14:29 +03:00
Mäkinen Sasu S e1d54e4c63 made graph calculations a bit more robust. Still not correct because of no way to perfectly differentiate study modules from real courses. Also modules are now visually shown on graph tooltips. 2018-06-26 12:55:33 +03:00
Mäkinen Sasu S a95f96b96d rm clog 2018-06-26 12:12:25 +03:00
Mäkinen Sasu S 1c6d45bfdc centrified all credit calculations in common functions and added study module credits are now filtered based on backend information instead of calculated in frontend. Also added a written marker '[Study Module]' on courses that are not calculated in the full credit amount 2018-06-26 12:11:50 +03:00
Sasu MäkinenandGitHub 7cbff2c1b6 Merge pull request #209 from UniversityOfHelsinkiCS/trunk
Million new superbara tests, some population stats updates and student search bugfixes.
2018-06-26 09:27:08 +03:00
Mäkinen Sasu S 88bc3667fc pushing without tests before I get my database :( 2018-06-26 09:24:47 +03:00
Mäkinen Sasu S 0682c89aac prehooks 2018-06-26 09:23:43 +03:00
Mäkinen Sasu S 96ff6c190a prehook push test 2018-06-26 09:23:05 +03:00
Mäkinen Sasu S 9f82cda4bf superbara tests can be now run by writing 'npm test' 2018-06-26 09:14:46 +03:00
Mäkinen Sasu S 24d9ea947e student search: the old search resuts are not zeroed if user moves to different tab #200 2018-06-25 17:35:39 +03:00
Mäkinen Sasu S d4e020669b fixed student statistics superbara test 2018-06-25 16:43:46 +03:00
Mäkinen Sasu S 29e6264660 course statistics end-to-end superbara test done 2018-06-25 16:31:46 +03:00
Mäkinen Sasu S 30cc569236 made population statistics superbara test bigger and better 2018-06-25 14:40:51 +03:00
Mäkinen Sasu S b17688a06e rewritten superbara (yet again) in a PCOM way 2018-06-25 13:58:45 +03:00
Mäkinen Sasu S c5dff7ac63 added new population statistics test 2018-06-21 15:57:55 +03:00
totutotu af6771cbd6 Merge branch 'trunk' of github.com:UniversityOfHelsinkiCS/oodikone2-frontend into trunk 2018-06-21 15:32:35 +03:00
totutotu 78205fd1b2 Credit gain for population stats updated 2018-06-21 15:32:31 +03:00
Mäkinen Sasu S 5094fb3bee navbar_test rewritten 2018-06-21 13:41:30 +03:00
Tuomo TorppaandGitHub 03d28b0e6f Merge pull request #197 from UniversityOfHelsinkiCS/trunk
Credit gain year fix
2018-06-21 11:27:40 +03:00
totutotu 48f8b38ccf Credit gain year fix 2018-06-21 11:24:52 +03:00
Tuomo TorppaandGitHub 0031c5f7f5 First year credit gain table, small bug fixes
First year credit gain table, small bug fixes
2018-06-21 11:07:25 +03:00
totutotu e150bede1c Merge branch 'trunk' of github.com:UniversityOfHelsinkiCS/oodikone2-frontend into trunk 2018-06-21 10:41:34 +03:00
totutotu e48a9f2ff0 First year credit gain to population statistics 2018-06-21 10:41:29 +03:00
Mäkinen Sasu S 95e98746e4 removed clog 2018-06-21 10:41:09 +03:00
Mäkinen Sasu S 8e6b4b1d11 heisenbug of creditlines not always rendering (maybe) fixed 2018-06-21 10:33:57 +03:00
Mäkinen Sasu S ee58112c97 removed clog 2018-06-20 17:31:03 +03:00
Mäkinen Sasu S 75e7b65a93 fixed study rights not showing because of finnish language 2018-06-20 17:29:18 +03:00
ikuismaandGitHub fe261c288c Merge pull request #191 from UniversityOfHelsinkiCS/trunk
Frontend fixes to work with latest backend
2018-06-20 17:28:02 +03:00
ikuisma 02a3a8531d Removed unused import. 2018-06-20 17:16:33 +03:00
ikuisma 62d0a10a03 Fixed credit accumulation graph date format handling. 2018-06-20 17:15:15 +03:00
totutotu 4213ada22d Remove console.log 2018-06-20 14:51:15 +03:00
totutotu 722bcfb6ee Filter code mappings based on code or name 2018-06-20 14:50:37 +03:00
totutotu 22af3a19eb Started date to students 2018-06-20 14:23:29 +03:00
totutotu 9ea3ebd9e4 removed unnecessary proptype 2018-06-20 11:46:20 +03:00
totutotu 57a0794651 Remove univenrolmentdate which does not exist in new oodi api 2018-06-20 10:29:37 +03:00
ikuisma 436c2cc0d7 Removed unused import. 2018-06-20 09:27:16 +03:00
ikuisma d7dc2d8ab6 Fixed broken student page header for new-style highlevelnames. 2018-06-20 09:26:00 +03:00
Tuomo TorppaandGitHub d8e3215cc8 Code mapping to redis, course stats table updated
Trunk
2018-06-19 15:44:08 +03:00
Tuomo TorppaandGitHub 1b038abbf5 Merge branch 'master' into trunk 2018-06-19 15:40:45 +03:00
Tuomo TorppaandGitHub 0fcfb727c0 Course code mapping to redis, new data format
Coursecode map update
2018-06-19 15:40:23 +03:00
totutotu 51e413a134 Loader to population stats course segment 2018-06-19 15:22:14 +03:00
totutotu b20e8a6e10 Settings page to new redis data format 2018-06-18 15:54:31 +03:00
Mäkinen Sasu S 08f42b50e2 course statistics statistics table now shows percentages in a better format 2018-06-18 13:38:50 +03:00
Sasu MäkinenandGitHub 0434243491 Merge pull request #176 from UniversityOfHelsinkiCS/trunk
course statistics statistics table fix'd
2018-06-18 13:25:01 +03:00
Mäkinen Sasu S ece9fdaff1 removed console.log and fixed eslint 2018-06-18 13:21:44 +03:00
Mäkinen Sasu S 6cdab87f0f course statistics table now dynamically changes with course/student level view 2018-06-18 13:15:30 +03:00
Sasu MäkinenandGitHub e8bd4b9d2c Merge pull request #175 from UniversityOfHelsinkiCS/trunk
Trunk
2018-06-18 12:47:49 +03:00
Mäkinen Sasu S 2ec18fa14c Course statistics: Switch for showing course level statistics and student level course statistics 2018-06-18 12:44:38 +03:00
Mäkinen Sasu S 2f39975efd rewritten bar chart to match new backend 2018-06-15 13:12:22 +03:00
Tuomo TorppaandGitHub f4bcfa0539 Merge pull request #174 from UniversityOfHelsinkiCS/trunk
Simple table for course statistics
2018-06-15 10:36:08 +03:00
totutotu e61abe4525 Simple table for course statistics 2018-06-15 10:29:47 +03:00
Tuomo TorppaandGitHub 1cd7bb149f Merge pull request #166 from UniversityOfHelsinkiCS/trunk
Course stats new querys to top, UI improvements
2018-06-14 11:59:56 +03:00
totutotu 873e9609dd Loading spinner moved from on top of the search box 2018-06-14 11:50:58 +03:00
totutotu eec2000eaa Added newest query course on top and transition animation 2018-06-14 10:24:51 +03:00
totutotu 87607165ce student search when 6 numbers or 4 letters 2018-06-14 09:37:29 +03:00
Sasu MäkinenandGitHub 22ec9e4c79 Merge pull request #159 from UniversityOfHelsinkiCS/trunk
Modified navbar, student statistics xAxis and did some filter refactoring.
2018-06-13 16:16:44 +03:00
Mäkinen Sasu S deeec81efd refactored filters to use params as object instead of array 2018-06-13 16:11:20 +03:00
Mäkinen Sasu S 22129188a9 Student statistics graph xAxis is now formatted as real dates instead of months from 0 to X 2018-06-13 15:53:47 +03:00
Mäkinen Sasu S c261b148c5 hid information about settings since the feature is not in production 2018-06-13 14:49:41 +03:00
Mäkinen Sasu S b9690808d8 eslint =) 2018-06-13 14:39:37 +03:00
Mäkinen Sasu S f982fcfbf9 Removed starting page route and improved starting page 2018-06-13 14:38:52 +03:00
ikuismaandGitHub 8adf0109f1 Merge pull request #156 from UniversityOfHelsinkiCS/trunk
New starting page and tabindexes
2018-06-13 13:09:47 +03:00
Mäkinen Sasu S 64e7414c10 Merge branch 'trunk' of https://github.com/UniversityOfHelsinkiCS/oodikone2-frontend into trunk 2018-06-13 13:04:21 +03:00
Mäkinen Sasu S cd8cd4e7f0 eslint fix 2018-06-13 13:04:09 +03:00
Mäkinen Sasu S 187af16f85 new starting page 2018-06-13 13:02:47 +03:00
ikuisma a4d57e51ea Added -1 tabindex to plus-minus calendar buttons in search filter form. 2018-06-13 12:49:48 +03:00
ikuisma 1531d77d5c Added -1 tabindex to navbar menu items so that the tab focus jumps straight to page content instead of navbar. 2018-06-13 12:49:07 +03:00
Sasu MäkinenandGitHub ae40e59fa9 Merge pull request #155 from UniversityOfHelsinkiCS/trunk
Two new course filters
2018-06-13 11:07:18 +03:00
Mäkinen Sasu S d532788e6a refactored code of courseparticipation filters to be a little bit cleaner and easier to implement new ones by removing copy-paste code 2018-06-13 11:02:50 +03:00
Mäkinen Sasu S c4ac1a04c3 Added two new filters: not participated and not participated or failed 2018-06-13 10:31:09 +03:00
Sasu MäkinenandGitHub 34e76883de Merge pull request #148 from UniversityOfHelsinkiCS/trunk
Refactored student statistics to be able to use browser history.
2018-06-12 16:10:44 +03:00
Mäkinen Sasu S 427f729675 Refactored student statistics to be able to use browser history. 2018-06-12 16:05:48 +03:00
Sasu MäkinenandGitHub 0efdc9dd42 Merge pull request #143 from UniversityOfHelsinkiCS/trunk
Navigation panel for population statistics, popup names for course filters, and formatted yAxis in student graph.
2018-06-11 15:08:05 +03:00
Mäkinen Sasu S d086d8fb09 Placed navigation panel to right bottom corner 2018-06-11 14:50:20 +03:00
Mäkinen Sasu S 7d03b58c3f made population statistics page navigation panel collapsable 2018-06-11 14:42:07 +03:00
Mäkinen Sasu S 5629f3af38 Added navigation buttons to population statistics page 2018-06-11 14:03:04 +03:00
Mäkinen Sasu S fc1b9f110e added a popup on course filters to show its full name in case its truncated for being too long. 2018-06-11 11:39:19 +03:00
Mäkinen Sasu S d1c117da2b Added tickformatter to student statistics graphs Y axis to fix bug of too many decimal numbers 2018-06-11 10:45:29 +03:00
Matti LuukkainenandGitHub 780d9f7e69 Merge pull request #140 from UniversityOfHelsinkiCS/trunk
wording
2018-06-10 00:20:21 +03:00
Matti Luukkainen 34c00de187 wording 2018-06-10 00:17:39 +03:00
Sasu MäkinenandGitHub 4f8b700a12 Merge pull request #137 from UniversityOfHelsinkiCS/trunk
Population Statistics page: structure redone, complementation filter done and some improvementes and refactoring here and there.
2018-06-08 16:06:37 +03:00
Mäkinen Sasu S 7ed20990b9 course filters CSS 2018-06-08 16:04:05 +03:00
Mäkinen Sasu S 8ca3cb6ca0 Fixed removing course filter by clicking course name crashing the app 2018-06-08 15:01:52 +03:00
Mäkinen Sasu S 514ac093be Added filter complementation and refactoring populationstatistics. Updated superbara tests accordingly 2018-06-08 14:51:46 +03:00
Sasu MäkinenandGitHub eae5efdb62 Merge pull request #130 from UniversityOfHelsinkiCS/trunk
Improvements to population graph, course-code mapper and superbara tests.
2018-06-07 15:50:42 +03:00
Mäkinen Sasu S 4716fc0dd6 removed console logging 2018-06-07 15:46:10 +03:00
Mäkinen Sasu S 28c0519ca3 fixed default course limit filter 2018-06-07 15:45:35 +03:00
Mäkinen Sasu S 283303386a Fixed population graph xAxis to react properly when filtering students 2018-06-07 15:16:12 +03:00
totutotu eb6c9d087b Merge branch 'trunk' of github.com:UniversityOfHelsinkiCS/oodikone2-frontend into trunk 2018-06-07 13:33:10 +03:00
totutotu 27fdcadd3f loading spinner for course code mapper 2018-06-07 13:33:03 +03:00
Mäkinen Sasu S 64f1398f1a fixed superbara tests to work stupidly until push history is done properly 2018-06-07 10:28:16 +03:00
Mäkinen Sasu S 9968ef1685 Merge branch 'trunk' of https://github.com/UniversityOfHelsinkiCS/oodikone2-frontend into trunk 2018-06-07 10:05:40 +03:00
Mäkinen Sasu S 5577f3ad0c Populationgraph is no longer ordinal so it actually makes sense 2018-06-07 10:05:14 +03:00
Matti LuukkainenandGitHub 7b377d90d8 Merge pull request #121 from UniversityOfHelsinkiCS/trunk
performance improvements on filtering
2018-06-06 22:02:50 +03:00
Matti Luukkainen 3e8046efaf Merge branch 'trunk' of github.com:UniversityOfHelsinkiCS/oodikone2-frontend into trunk 2018-06-06 21:52:54 +03:00
Matti Luukkainen bdda5fb048 performance improvements on filtering 2018-06-06 21:48:22 +03:00
Mäkinen Sasu S fab8a653dc minor changes to filepath configuration in superbara tests 2018-06-06 16:18:50 +03:00
Matti LuukkainenandGitHub 1bb1824331 Merge pull request #117 from UniversityOfHelsinkiCS/trunk
refacoring to use filter architecture in course based filters
2018-06-06 15:04:42 +03:00
Matti Luukkainen a91ca60fad fixes 2018-06-06 14:57:53 +03:00
Matti LuukkainenandGitHub 4e625c6ba8 Merge pull request #116 from UniversityOfHelsinkiCS/trunk
Trunk
2018-06-06 14:28:57 +03:00
Matti Luukkainen d440ef18ff Merge branch 'trunk' of github.com:UniversityOfHelsinkiCS/oodikone2-frontend into trunk 2018-06-06 14:27:56 +03:00
Matti Luukkainen 80275f793a refacoring to use filter architecture in course based filters 2018-06-06 14:27:50 +03:00
Mäkinen Sasu S ae1846e65b Merge branch 'trunk' of https://github.com/UniversityOfHelsinkiCS/oodikone2-frontend into trunk 2018-06-06 14:05:35 +03:00
Mäkinen Sasu S dcba353b2d new superbara tests 2018-06-06 14:04:55 +03:00
totutotu 0e5e470bcb CourseSearch nad yearlystats small UI improvements 2018-06-06 11:35:34 +03:00
totutotu 52ea016995 lint newline 2018-06-06 10:24:33 +03:00
totutotu 3329225b50 Course code mapping filter, add and delete added 2018-06-06 10:20:22 +03:00
ikuisma 204657bd7c Merge branch 'trunk' of https://github.com/UniversityOfHelsinkiCS/oodikone2-frontend into trunk 2018-06-05 17:51:52 +03:00
ikuisma 82d651d8b2 Made main.rb work with latest version of superbara, added script for running all tests. 2018-06-05 17:51:48 +03:00
totutotu f05d46cbbc Merge branch 'trunk' of github.com:UniversityOfHelsinkiCS/oodikone2-frontend into trunk 2018-06-05 17:26:52 +03:00
totutotu 6a478b1d8c course code map course names added 2018-06-05 17:26:46 +03:00
ikuisma 10f068666c Implemented new test for viewing student credit accumulation graph. 2018-06-05 17:18:52 +03:00
ikuisma 49cfd61c16 Refactored populations filter actions to separate helper file. 2018-06-05 16:48:16 +03:00
ikuisma e9f47563b0 Added population statistics test back. 2018-06-05 15:45:31 +03:00
ikuisma 7b38aad1d8 Removed redundant _test from the end of test files. 2018-06-05 15:45:00 +03:00
ikuisma 2a4f5fad27 Refactored tests to use globvar basepath 2018-06-05 15:40:50 +03:00
ikuisma 03e97ceb9a Two first tests made with superbara. 2018-06-05 15:29:18 +03:00
sasumakiandGitHub 054fad76d7 Merge pull request #113 from UniversityOfHelsinkiCS/trunk
Trunk
2018-06-04 17:20:45 +03:00
sasumakiandGitHub acdcf8ce1a Merge branch 'master' into trunk 2018-06-04 17:16:45 +03:00
Mäkinen Sasu S 71593d26af StudyRight highlevelname filtering now matches for a more sophisticated regular expression 2018-06-04 17:15:52 +03:00
Mäkinen Sasu S 72aa373bb6 disabled matr filter properly 2018-06-04 16:25:54 +03:00
Mäkinen Sasu S f5b525a382 disabled matriculation examination filter until further notice 2018-06-04 16:11:48 +03:00
Mäkinen Sasu S 5ea94908ca getStudentTotalCredits in common no longer counts courses that the student have not passed 2018-06-04 16:05:24 +03:00
Mäkinen Sasu S 508c365d42 fixed bug when removing matriculation and sex filter removing every other filter as well 2018-06-04 15:55:41 +03:00
ikuisma d41f97f43a Removed console.log 2018-06-04 15:54:04 +03:00
ikuisma 765de6f9d8 Refactored EnableUsers component to match the style of the other pages. 2018-06-04 15:53:14 +03:00
totutotu 8b494b79a9 Merge branch 'trunk' of github.com:UniversityOfHelsinkiCS/oodikone2-frontend into trunk 2018-06-04 15:45:34 +03:00
totutotu 1b8bfcaac9 alternative codes header changed, created unique keys for passaratechart 2018-06-04 15:45:29 +03:00
Mäkinen Sasu S 86f4c46ace student search now doesn't search with less than 3 letters in query 2018-06-04 15:40:35 +03:00
Mäkinen Sasu S faa528d01f eslint 2018-06-04 15:23:38 +03:00
Mäkinen Sasu S e8ec56db00 fixing to expect matriculation examination data to be in true/false values instead of 0/1 2018-06-04 15:22:57 +03:00
totutotu 8cc4802ca1 Merge branch 'trunk' of github.com:UniversityOfHelsinkiCS/oodikone2-frontend into trunk 2018-06-04 14:51:42 +03:00
totutotu 2dc47d3f37 Show alternative codes on yearlystats 2018-06-04 14:51:38 +03:00
sasumakiandGitHub e08776fc07 Merge pull request #112 from UniversityOfHelsinkiCS/trunk
Trunk
2018-06-04 13:45:07 +03:00
sasumakiandGitHub 67d7438e09 Merge branch 'master' into trunk 2018-06-04 13:42:20 +03:00
Mäkinen Sasu S f2a7846c26 changed date format in Population search form 2018-06-04 13:06:05 +03:00
Mäkinen Sasu S 05ddbde60b Added sex and matriculation filters 2018-06-04 12:58:07 +03:00
ikuisma 2df28be735 Added translation string for empty query result in PopulationStatistics page. 2018-06-04 10:28:13 +03:00
ikuisma ec97274ad1 Removed trailing whitespace to fix lint error. 2018-06-04 10:19:41 +03:00
ikuisma 0dffe56060 Implemented error message for empty query result in PopulationDetails component. 2018-06-04 10:19:04 +03:00
ikuisma bd2128626a Made conditional rendering of population search form more readable. 2018-06-04 09:35:29 +03:00
Matti LuukkainenandGitHub 59b46cbcc3 Merge pull request #109 from UniversityOfHelsinkiCS/mluukkai-patch-1
Update .travis.yml
2018-06-02 23:47:34 +03:00
Matti LuukkainenandGitHub 3bece7392c Update .travis.yml 2018-06-02 23:44:09 +03:00
Matti LuukkainenandGitHub 109fbb0b40 Merge pull request #107 from UniversityOfHelsinkiCS/trunk
filter by start time
2018-06-02 13:58:13 +03:00
Matti Luukkainen 4d986484a0 filter by start time 2018-06-02 13:55:28 +03:00
Matti LuukkainenandGitHub d19cfc1378 Merge pull request #105 from UniversityOfHelsinkiCS/trunk
support for setting different filters, disabling broken student detail page
2018-06-02 12:14:45 +03:00
Matti Luukkainen 752f589b04 disable broken student search 2018-06-02 12:12:22 +03:00
Matti Luukkainen f74a9900f4 Merge branch 'trunk' of github.com:UniversityOfHelsinkiCS/oodikone2-frontend into trunk 2018-06-02 11:25:54 +03:00
Matti Luukkainen a5047170b6 support for setting different filters 2018-06-02 11:25:49 +03:00
ikuisma f3d97ab3c4 Fixed broken student tables after populations refactor. 2018-06-01 16:26:52 +03:00
ikuisma 281658fc1b Merge 2018-06-01 15:58:09 +03:00
ikuisma 6a7f279844 Fixed filters in populations page after refactor. 2018-06-01 15:55:25 +03:00
ikuisma e1b5c50367 Updated PopulationSearchForm to use queries object instead of array of queries objects. 2018-06-01 15:50:45 +03:00
ikuisma ebc450a97b Refactored PopulationSearchHistory to use the stored populations object instead of an array of the single populations object. 2018-06-01 15:31:20 +03:00
ikuisma 657438d695 Changed population reducer to save a single population object instead of an array, refactored components to work with single object instead of array. 2018-06-01 15:20:18 +03:00
Mäkinen Sasu S bb567047d1 Studyrights filtering no longer cares about prioritycodes 2018-06-01 15:18:43 +03:00
sasumakiandGitHub 71adbcf36a Merge pull request #101 from UniversityOfHelsinkiCS/trunk
added degree filter to details aswell
2018-06-01 14:47:11 +03:00
Mäkinen Sasu S ab363a59e8 added degree filter to details aswell 2018-06-01 14:45:08 +03:00
sasumakiandGitHub 3985d906db Merge pull request #100 from UniversityOfHelsinkiCS/trunk
Trunk
2018-06-01 14:16:02 +03:00
Mäkinen Sasu S e609d04e14 Student statistics now shows detailed view of study rights 2018-06-01 14:13:48 +03:00
Mäkinen Sasu S 50b2270f26 Student infocard now shows latest proper degree studyright 2018-06-01 13:37:23 +03:00
sasumakiandGitHub ec9a001a3b Merge pull request #99 from UniversityOfHelsinkiCS/trunk
Trunk
2018-06-01 12:45:11 +03:00
Mäkinen Sasu S 85b18406c2 Student page now shows high level name of study rights 2018-06-01 12:33:08 +03:00
Mäkinen Sasu S cc4bb03560 Population query card now shows how many months is in the query. 2018-05-31 16:11:11 +03:00
Mäkinen Sasu S 8415aa6904 population graph now calculates credits correctly and population search form time format changed from YYYY-MM -> YYYY-MMM 2018-05-31 15:46:09 +03:00
sasumakiandGitHub 5e3039fee7 Merge pull request #98 from UniversityOfHelsinkiCS/trunk
Population search month selection upgraded
2018-05-31 13:43:56 +03:00
Mäkinen Sasu S df2e038b70 Population search month selection upgraded 2018-05-31 13:33:59 +03:00
Matti LuukkainenandGitHub fc75025ebc Merge pull request #96 from UniversityOfHelsinkiCS/trunk
travis->dockerhub testing...
2018-05-30 20:00:09 +03:00
Matti Luukkainen 4b700a5c32 eslint test 2018-05-30 19:56:16 +03:00
Matti LuukkainenandGitHub e2bdc7695f Merge pull request #95 from UniversityOfHelsinkiCS/trunk
check if travis->docker works
2018-05-30 19:25:16 +03:00
Matti Luukkainen bc8e7de98a check if travis->dockex works 2018-05-30 19:15:26 +03:00
Matti LuukkainenandGitHub c15a003fa8 Merge pull request #94 from UniversityOfHelsinkiCS/trunk
build docker image with travis
2018-05-30 18:58:55 +03:00
Matti Luukkainen 505043f0bb better travis 2018-05-30 18:55:22 +03:00
Matti Luukkainen 5bd3a9e6fc build docker image with travis 2018-05-30 18:45:06 +03:00
Matti LuukkainenandGitHub 5a306be4b4 Merge pull request #93 from UniversityOfHelsinkiCS/trunk
fix
2018-05-30 17:50:52 +03:00
Matti Luukkainen 830845395a fix 2018-05-30 17:48:04 +03:00
Matti LuukkainenandGitHub 163b0396b6 Merge pull request #91 from UniversityOfHelsinkiCS/trunk
chart size options
2018-05-30 17:25:02 +03:00
Matti Luukkainen 889e193b01 cludge for initial graph render problem 2018-05-30 17:13:39 +03:00
totutotu dc84cae141 Merge branch 'trunk' of github.com:UniversityOfHelsinkiCS/oodikone2-frontend into trunk 2018-05-30 15:56:52 +03:00
Mäkinen Sasu S 23fe77c27c resizing remembered through redux settings 2018-05-30 15:56:10 +03:00
totutotu a22d04bd25 Merge branch 'trunk' of github.com:UniversityOfHelsinkiCS/oodikone2-frontend into trunk 2018-05-30 15:53:21 +03:00
totutotu 34484d9e3d Course duplicate code managing UI started 2018-05-30 15:53:16 +03:00
Mäkinen Sasu S c5253ef5fd Merge branch 'trunk' of https://github.com/UniversityOfHelsinkiCS/oodikone2-frontend into trunk 2018-05-30 14:28:57 +03:00
Mäkinen Sasu S 1fe0c2142a eslint fix 2018-05-30 14:28:34 +03:00
Mäkinen Sasu S 2d8249f6e8 Chart size options added 2018-05-30 14:27:08 +03:00
Matti LuukkainenandGitHub 1739e27a3b Merge pull request #90 from UniversityOfHelsinkiCS/trunk
highchart for grade cumulation graphs
2018-05-30 13:58:08 +03:00
Matti Luukkainen 908f2bdecc Merge branch 'trunk' of github.com:UniversityOfHelsinkiCS/oodikone2-frontend into trunk 2018-05-30 13:51:15 +03:00
Matti Luukkainen d6b85acf35 do not rely on default port of dist 2018-05-30 13:34:00 +03:00
Mäkinen Sasu S 8b8317ccb0 Merge branch 'trunk' of https://github.com/UniversityOfHelsinkiCS/oodikone2-frontend into trunk 2018-05-30 13:13:08 +03:00
Mäkinen Sasu S 2423797246 removed console log 2018-05-30 13:12:45 +03:00
Matti Luukkainen 90e47c21bb Merge branch 'trunk' of github.com:UniversityOfHelsinkiCS/oodikone2-frontend into trunk 2018-05-30 13:12:24 +03:00
Matti Luukkainen 1b1827c3b3 removed segment dimmer from filter 2018-05-30 13:12:04 +03:00
Mäkinen Sasu S 8f4809dffd Removed automatic yAxis fitting on HighCharts 2018-05-30 13:11:19 +03:00
Mäkinen Sasu S 61dc90c5e0 eslint repairing 2018-05-30 12:44:08 +03:00
Mäkinen Sasu S 426ebfe46a highcharts refactor 2018-05-30 12:40:07 +03:00
Mäkinen Sasu S 77e5e3f731 New Graph plotting library 2018-05-30 11:09:30 +03:00
Matti LuukkainenandGitHub bfafc42266 Merge pull request #89 from UniversityOfHelsinkiCS/trunk
fix serve to 6.5.5
2018-05-30 00:28:55 +03:00
Matti Luukkainen 805e436c92 fix serve to 6.5.5 2018-05-30 00:22:25 +03:00
Matti Luukkainen 0c26a26491 docker 2018-05-30 00:20:58 +03:00
Matti LuukkainenandGitHub 26049d72d2 Merge pull request #86 from UniversityOfHelsinkiCS/trunk
default population statistic months up to current date
2018-05-29 22:37:30 +03:00
Matti Luukkainen 439ecb4e7f Merge branch 'trunk' of github.com:UniversityOfHelsinkiCS/oodikone2-frontend into trunk 2018-05-29 22:33:54 +03:00
Matti Luukkainen 12290823b9 default population statistic months up to current date 2018-05-29 22:33:48 +03:00
Mäkinen Sasu S 354cd5e68b Started changing recharts to highcharts 2018-05-28 18:18:35 +03:00
Matti LuukkainenandGitHub 0032045f5b Merge pull request #82 from UniversityOfHelsinkiCS/trunk
student list visibility improvements
2018-05-27 23:15:30 +03:00
Matti LuukkainenandGitHub e57e16ba68 Merge branch 'master' into trunk 2018-05-27 23:12:33 +03:00
Matti Luukkainen fbf7624359 fixes 2018-05-27 23:08:25 +03:00
Matti Luukkainen 1a3f1b7cf4 student list visibility improvements 2018-05-27 23:01:08 +03:00
Matti LuukkainenandGitHub 24e68e2a4e Merge pull request #80 from UniversityOfHelsinkiCS/trunk
remove spinner setting from filter
2018-05-27 22:13:33 +03:00
Matti Luukkainen 5ab46da608 remove spinner setting from filter 2018-05-27 22:10:00 +03:00
Matti LuukkainenandGitHub 45f5d092e8 Merge pull request #79 from UniversityOfHelsinkiCS/trunk
fixes in spinner
2018-05-27 22:02:17 +03:00
Matti Luukkainen 54678dbe39 added prop 2018-05-27 21:57:56 +03:00
Matti Luukkainen 7e46dcf296 fixes in spinner 2018-05-27 21:57:17 +03:00
Matti LuukkainenandGitHub 52d2add5a1 Merge pull request #75 from UniversityOfHelsinkiCS/trunk
improvements in student search
2018-05-27 19:49:49 +03:00
Matti Luukkainen 8942bac990 more fixes 2018-05-27 19:46:38 +03:00
Matti Luukkainen 1d82154811 eslint fix 2018-05-27 19:45:12 +03:00
Matti Luukkainen e47362be43 improvements in student search 2018-05-27 19:44:19 +03:00
Matti LuukkainenandGitHub 0ecbfe275d Merge pull request #73 from UniversityOfHelsinkiCS/trunk
bugfixes on graphs, population student list clickableTrunk
2018-05-27 15:19:15 +03:00
Matti Luukkainen e3239b5f95 fix eslint 2018-05-27 15:17:06 +03:00
Matti Luukkainen c824453ef8 bubfixes on graphs, population student list clickable 2018-05-27 15:15:11 +03:00
Matti LuukkainenandGitHub ea026081bd Merge pull request #63 from UniversityOfHelsinkiCS/trunk
population limitable by students less than n credits
2018-05-26 16:46:38 +03:00
Matti Luukkainen c4e71122e6 population limitable by students less than n credits 2018-05-26 16:44:11 +03:00
Matti LuukkainenandGitHub 0a737d2cc7 Merge pull request #61 from UniversityOfHelsinkiCS/trunk
remove links from student list
2018-05-24 07:40:02 +03:00
Matti Luukkainen 099ee64639 Merge branch 'trunk' of github.com:UniversityOfHelsinkiCS/oodikone2-frontend into trunk 2018-05-24 07:37:29 +03:00
Matti Luukkainen 73c491aaae remove links 2018-05-24 07:36:39 +03:00
Matti LuukkainenandGitHub dca980e6af Merge pull request #60 from UniversityOfHelsinkiCS/trunk
population statistics: student names togglable, links to student view
2018-05-24 00:00:44 +03:00
Matti LuukkainenandGitHub 3187c4bcee Merge branch 'master' into trunk 2018-05-23 23:57:55 +03:00
Matti Luukkainen 601107796a more lint fixes 2018-05-23 23:56:04 +03:00
Matti Luukkainen 6df8ab6fa4 lint fixes 2018-05-23 23:54:57 +03:00
Matti Luukkainen 17387514c5 population statistics: student names togglable, links to student view 2018-05-23 23:53:40 +03:00
Matti LuukkainenandGitHub b36fcfc730 Merge pull request #59 from UniversityOfHelsinkiCS/trunk
show students of selected population
2018-05-23 22:48:16 +03:00
Matti Luukkainen d1745003b4 show students of selected population 2018-05-23 22:45:12 +03:00
Matti LuukkainenandGitHub e17fa0886f Merge pull request #58 from UniversityOfHelsinkiCS/trunk
student name visibility togglable
2018-05-23 20:23:51 +03:00
Matti Luukkainen 6633a2f2b4 Merge branch 'trunk' of github.com:UniversityOfHelsinkiCS/oodikone2-frontend into trunk 2018-05-23 20:21:36 +03:00
Matti Luukkainen c1ccb22470 remove unused import 2018-05-23 20:20:28 +03:00
Matti Luukkainen cef98cca22 button to toggle student name visibility 2018-05-23 20:17:42 +03:00
totutotu 17a1095f00 Lint newline added 2018-05-23 14:56:48 +03:00
totutotu 94bb1d6b39 Separated pass/fail barcharts and improved Course statistics layout 2018-05-23 14:54:08 +03:00
Matti LuukkainenandGitHub 4cefe627fb Merge pull request #53 from UniversityOfHelsinkiCS/trunk
credit graph spinner betterment
2018-05-22 01:07:14 +03:00
Matti Luukkainen 28594af9a2 graphSpinner reducer 2018-05-22 01:04:31 +03:00
Matti Luukkainen 85905d98fa Merge branch 'master' of github.com:UniversityOfHelsinkiCS/oodikone2-frontend into trunk 2018-05-22 01:00:42 +03:00
Matti Luukkainen 3b1faff253 fixes 2018-05-22 01:00:12 +03:00
Matti Luukkainen c8e1db5f19 credit graph spinner 2018-05-22 00:54:29 +03:00
Matti LuukkainenandGitHub 6fb102808d Merge pull request #49 from UniversityOfHelsinkiCS/trunk
limit population credit grapg by course performace
2018-05-21 15:10:33 +03:00
Matti Luukkainen e910f8660b fix lint 2018-05-21 14:57:22 +03:00
Matti Luukkainen a0ba838f3b Merge branch 'trunk' of github.com:UniversityOfHelsinkiCS/oodikone2-frontend into trunk 2018-05-21 14:45:15 +03:00
Matti Luukkainen faac4aa816 limit population stufy graph by course 2018-05-21 14:43:59 +03:00
totutotu 48b8177133 Changed 'Courses' page to 'Course Instances' 2018-05-18 09:12:46 +03:00
totutotu f87800cbfc Enabled Course Stats page for all users, Refactored remove button 2018-05-18 09:01:27 +03:00
Matti Luukkainen f2afd9e91a limit population by course performace 2018-05-18 01:57:53 +03:00
totutotu f94b3e18af Student name to student page 2018-05-17 14:40:03 +03:00
totutotu 5384074527 remove selected course from yearlystats 2018-05-16 13:45:06 +03:00
totutotu 366abfc0d9 Course year stats name 2018-05-16 12:05:27 +03:00
totutotu 08cfb3de80 lint 2018-05-16 10:40:35 +03:00
totutotu 8c6326a668 Course yearly stats styles, error message, started "remove course stats" 2018-05-15 10:39:26 +03:00
totutotu 626d3cf462 Course Statistics page separate spring/fall and year select 2018-05-14 17:20:22 +03:00
totutotu 5c7931ac5c Resolved merge 2018-05-11 14:17:40 +03:00
totutotu 6bf613fb03 Merge 2018-05-11 14:14:27 +03:00
totutotu fec01a2c02 Refactored isValidYear from popstats to common, contnd yearly course 2018-05-11 14:12:49 +03:00
Matti LuukkainenandGitHub 24e5be335f Merge pull request #46 from UniversityOfHelsinkiCS/trunk
reorder components in population stats
2018-05-11 08:37:20 +03:00
Matti Luukkainen c19017b2bd reorder components in population stats 2018-05-11 08:33:51 +03:00
Matti LuukkainenandGitHub eea6fb0918 Merge pull request #45 from UniversityOfHelsinkiCS/trunk
month selection for population statistics, intial population course view
2018-05-11 00:50:04 +03:00
Matti LuukkainenandGitHub 674ea56f99 Merge branch 'master' into trunk 2018-05-11 00:47:04 +03:00
Matti Luukkainen ce49e64e45 month selection for population statistics, intial population course view 2018-05-11 00:39:56 +03:00
totutotu 8b483cf4e8 Course Yearly Stats MVP, for admin users only for now 2018-05-09 11:01:13 +03:00
totutotu bc8febe25e Renamed CourseStatistics to CourseInstanceStatistics 2018-05-07 12:12:44 +03:00
totutotu 00ee23750c Refactor: Separated CourseSearch component from Courses page 2018-05-07 11:38:27 +03:00
Tuomo TorppaandGitHub 7634179d78 Merge pull request #43 from UniversityOfHelsinkiCS/totutotu-patch-1
Update README.md
2018-05-04 16:33:24 +03:00
Tuomo TorppaandGitHub 8c58ffd9e7 Update README.md 2018-05-04 16:31:25 +03:00
Tuomo TorppaandGitHub 6208691147 Merge pull request #42 from UniversityOfHelsinkiCS/trunk
login/logout http methods, v2 api for population statistics
2018-05-04 15:24:17 +03:00
Matti Luukkainen 19d7a53f1d use api v2 for population stats 2018-05-04 15:20:51 +03:00
totutotu 11d039da25 Added selector to populationsearchform 2018-05-02 11:02:05 +03:00
totutotu 527206981d Fixed mouse hover bug in student graph 2018-05-02 10:28:01 +03:00
Jami Kousa 57546e4455 send data in headers 2018-04-26 15:46:12 +03:00
Jami Kousa 39dbd37469 token refresh if unexpected failure 2018-04-26 13:46:42 +03:00
Jami Kousa 5c50088152 Merge branch 'trunk' of github.com:UniversityOfHelsinkiCS/oodikone2-frontend into trunk 2018-04-25 16:09:49 +03:00
Jami Kousa 621f2e45e7 swap users & refactor login 2018-04-25 16:09:46 +03:00
totutotu 6808f0a5ae Student list date format corrected 2018-04-25 14:42:00 +03:00
totutotu 7f8df7acee Merge branch 'trunk' of https://github.com/UniversityOfHelsinkiCS/oodikone2-frontend into trunk 2018-04-25 14:33:38 +03:00
totutotu be12a44bbf Selector for Student page 2018-04-25 14:33:34 +03:00
Jami Kousa 904058aa02 Merge branch 'trunk' of github.com:UniversityOfHelsinkiCS/oodikone2-frontend into trunk 2018-04-25 11:23:52 +03:00
Jami Kousa 8331c8ef77 logout button to access denied page 2018-04-25 11:23:49 +03:00
totutotu 75686c3768 Course search: non-numerical codes first, improved UI 2018-04-25 11:04:50 +03:00
Jami Kousa 3509644f05 logout button, without functionality 2018-04-24 13:58:51 +03:00
Jami Kousa 96095fa218 Merge branch 'trunk' of github.com:UniversityOfHelsinkiCS/oodikone2-frontend into trunk 2018-04-24 13:53:58 +03:00
Jami Kousa 8aa2dc70d8 loader if app is not yet loaded 2018-04-24 13:51:56 +03:00
totutotu 321a2da3ef Sentry conf 2018-04-24 10:10:20 +03:00
totutotu 8627ba85c6 Lint refactor for selector 2018-04-24 09:33:27 +03:00
totutotu 6948e7c2df Fixed issue 25 and proptype error from popquerycard 2018-04-24 09:27:38 +03:00
totutotu 462af37d37 Merge branch 'trunk' of https://github.com/UniversityOfHelsinkiCS/oodikone2-frontend into trunk 2018-04-23 12:54:35 +03:00
Jami Kousa 3b93d970de logger 2018-04-23 12:53:59 +03:00
totutotu 26fad4a031 Reselect to courses 2018-04-13 12:11:26 +03:00
totutotu ed28394566 different token for staging 2018-04-13 11:08:03 +03:00
totutotu f9467a8edb Login fix and course instance date reformatted 2018-04-13 10:23:36 +03:00
totutotu 22fbfd6f29 Login/logout from get to post/delete 2018-04-13 10:04:29 +03:00
Matti LuukkainenandGitHub a0ce5d5b0f Merge pull request #41 from UniversityOfHelsinkiCS/trunk
improved no result message
2018-04-11 17:12:22 +03:00
Jami Kousa 5f252d5470 Merge branch 'trunk' of github.com:UniversityOfHelsinkiCS/oodikone2-frontend into trunk 2018-04-11 17:05:45 +03:00
Jami Kousa e232dc684e improved no result message 2018-04-11 17:05:43 +03:00
Jami KousaandGitHub 35b7c4e38e Merge pull request #40 from UniversityOfHelsinkiCS/trunk
repair CreditAccumulationGraph
2018-04-11 13:47:51 +03:00
Jami KousaandGitHub 00db629958 Merge branch 'master' into trunk 2018-04-11 13:46:03 +03:00
Jami Kousa 307d87c3e6 repair CreditAccumulationGraph 2018-04-11 13:45:24 +03:00
Tuomo TorppaandGitHub 265c24421e Merge pull request #39 from UniversityOfHelsinkiCS/trunk
Date formatting and sort courses
2018-04-11 11:14:30 +03:00
totutotu 8269e4bce8 Merge branch 'trunk' of https://github.com/UniversityOfHelsinkiCS/oodikone2-frontend into trunk 2018-04-11 11:11:36 +03:00
totutotu 24c9e7b50f fixed lint error 2018-04-11 11:10:59 +03:00
Tuomo TorppaandGitHub 06a2b0655e Merge branch 'master' into trunk 2018-04-11 11:06:44 +03:00
totutotu fa55bcfa79 Sorting and date formatting 2018-04-11 10:57:12 +03:00
totutotu 6b48e86848 date from - - to . . and descending courselist for student page 2018-04-11 09:44:45 +03:00
totutotu 8d131131f7 Merge branch 'trunk' of https://github.com/UniversityOfHelsinkiCS/oodikone2-frontend into trunk 2018-04-11 09:10:38 +03:00
totutotu 478079ac74 Sort courseinstancelist by date 2018-04-11 09:10:01 +03:00
Jami Kousa 38f40c1dc5 Merge branch 'trunk' of github.com:UniversityOfHelsinkiCS/oodikone2-frontend into trunk 2018-04-10 12:44:33 +03:00
Jami Kousa 983f110ffa fix readme badges 2018-04-10 12:44:31 +03:00
Jami KousaandGitHub a1be78fc18 Merge pull request #38 from UniversityOfHelsinkiCS/trunk
logout
2018-04-10 12:43:50 +03:00
Jami KousaandGitHub 5a8b36d028 Merge branch 'master' into trunk 2018-04-10 12:42:05 +03:00
Jami Kousa bef5fe1881 logout case for staging 2018-04-10 12:40:01 +03:00
Jami Kousa 1135d9ed99 fix merge conflict 2018-04-10 12:32:56 +03:00
Jami Kousa 747d2729e5 logout 2018-04-10 12:29:51 +03:00
totutotu 01dc6f183b reselect sort for user enabling 2018-04-10 11:46:00 +03:00
totutotu 0f68978909 Merge branch 'trunk' of https://github.com/UniversityOfHelsinkiCS/oodikone2-frontend into trunk 2018-04-10 08:44:49 +03:00
Jami KousaandGitHub b8235a88e2 Merge pull request #37 from UniversityOfHelsinkiCS/trunk
graceful crash
2018-04-09 17:10:46 +03:00
Jami Kousa a64b961a4b graceful crash 2018-04-09 17:07:51 +03:00
totutotu b058f19e47 totutotu 2018-04-06 08:59:36 +03:00
Tuomo TorppaandGitHub 7346e666c5 Merge pull request #36 from UniversityOfHelsinkiCS/trunk
Webpack 4 and remove deprecated features
2018-04-05 13:00:58 +03:00
Jami Kousa ad7de5c63d reduce graph steps to 1 2018-04-05 12:54:24 +03:00
Jami Kousa b54eef59af remove tags 2018-04-05 12:47:14 +03:00
Jami Kousa 862a75dd7a remove teachers 2018-04-05 12:41:56 +03:00
Jami Kousa a89daf5f4f creditlines okayish 2018-04-05 12:39:35 +03:00
Jami Kousa 51ed24bfdc struggle with population statistics 2018-04-04 17:18:35 +03:00
Jami Kousa 87ff19e595 webpack 4 2018-04-04 14:53:23 +03:00
Jami Kousa 29e2654c33 webpack stats 2018-04-04 11:47:51 +03:00
Jami Kousa 2fca45c394 correct status image 2018-04-04 11:16:35 +03:00
totutotu c915e305f0 Travis badges 2018-04-04 09:44:02 +03:00
Jami Kousa e1585a77b1 upgrade dependencies 2018-04-03 16:41:12 +03:00
Jami KousaandGitHub 1dabb7a26b configure travis
configure travis
2018-04-03 16:38:14 +03:00
Jami KousaandGitHub ed6f0f1337 Merge branch 'master' into trunk 2018-04-03 16:24:00 +03:00
Jami Kousa 9cfdc42a8d resend login if not enabled 2018-04-03 15:02:21 +03:00
Jami Kousa 200401b7be fix test script 2018-04-03 14:39:26 +03:00
Jami Kousa 95d85cd24d travis 2018-04-03 14:34:04 +03:00
Tuomo TorppaandGitHub 52ef63d9e3 Merge pull request #34 from UniversityOfHelsinkiCS/trunk
Enagle units users
2018-04-03 14:12:56 +03:00
Jami Kousa 3a4c0df383 Merge branch 'trunk' of github.com:UniversityOfHelsinkiCS/oodikone2-frontend into trunk 2018-04-03 14:12:26 +03:00
Jami Kousa 61d35cbd26 default to no access , fix missing end of line 2018-04-03 14:12:23 +03:00
totutotu 0737722e5a Merge branch 'trunk' of https://github.com/UniversityOfHelsinkiCS/oodikone2-frontend into trunk 2018-04-03 14:07:36 +03:00
totutotu 3b582ba5d3 user enabling and user units 2018-04-03 14:07:31 +03:00
Jami KousaandGitHub 6c31202923 Merge pull request #33 from UniversityOfHelsinkiCS/trunk
page for access denied
2018-04-03 13:35:40 +03:00
Jami Kousa 0fa7670e94 page for access denied 2018-04-03 13:32:19 +03:00
Jami Kousa 586a257098 some refactors, add styles to enableUsers 2018-03-29 17:05:21 +03:00
Jami KousaandGitHub b010963b56 Merge pull request #32 from UniversityOfHelsinkiCS/trunk
refactors
2018-03-29 14:18:18 +03:00
Jami Kousa 3591297247 user access controls 2018-03-29 14:16:53 +03:00
Jami Kousa 7edc2ac5a8 refactor routes and navbar 2018-03-29 12:39:40 +03:00
Jami Kousa 25d5eb9561 fix token expire 2018-03-29 10:51:39 +03:00
Jami Kousa 53416146bd fix warnings 2018-03-28 16:13:18 +03:00
Jami Kousa 11c217a9ad removing courseinstance from list 2018-03-28 15:57:43 +03:00
Jami Kousa fc63a895a5 some color into oodikone 2018-03-28 15:43:00 +03:00
Jami Kousa 9bf0ae3771 some color into oodikone 2018-03-28 15:42:39 +03:00
Jami Kousa 5a458120ff fix course page when one student in instance 2018-03-28 14:43:00 +03:00
Jami Kousa d8f2aa5bef add missing keys 2018-03-28 13:34:41 +03:00
Jami Kousa 31402e5b71 fix warnings on dropdown misuse 2018-03-28 13:07:51 +03:00
Jami Kousa ac99125c2b add timeout to courses page 2018-03-28 12:26:33 +03:00
Jami Kousa b835c0938c fix flickering and add keys to course list 2018-03-28 12:12:32 +03:00
Jami Kousa 61e252dabb fix warnings, flickering 2018-03-28 11:29:41 +03:00
Jami Kousa f2a6955666 loading icon delayed display 2018-03-28 11:29:17 +03:00
Jami Kousa b52517891c refactored login & logout, new package: jwt-decode for expired token 2018-03-27 18:28:44 +03:00
totutotu 082d1466a9 Fixed merge 2018-03-27 12:39:38 +03:00
totutotu 19d01bf2c9 Dev new auth fix 2018-03-27 12:26:15 +03:00
Jami KousaandGitHub d9a099f3b9 Merge pull request #31 from UniversityOfHelsinkiCS/trunk
fix dev env login
2018-03-27 12:13:39 +03:00
Jami Kousa 448cf2d14e fix dev env login 2018-03-27 12:12:33 +03:00
Tuomo TorppaandGitHub a8b570e4bb Merge pull request #30 from UniversityOfHelsinkiCS/trunk
refactoring, login
2018-03-27 11:13:33 +03:00
Jami Kousa fbdf23125b Merge branch 'trunk' of github.com:UniversityOfHelsinkiCS/oodikone2-frontend into trunk 2018-03-27 11:12:12 +03:00
Jami Kousa a0b2fae993 refactors and timeout 2018-03-27 11:12:09 +03:00
Jami Kousa c11e588e13 send login before other 2018-03-27 11:11:58 +03:00
totutotu 65f7808c36 switched from personprincipalname to uid 2018-03-27 10:37:57 +03:00
Tuomo TorppaandGitHub 060297676d Merge pull request #29 from UniversityOfHelsinkiCS/trunk
Refactoring, admin UI for users added
2018-03-26 13:05:04 +03:00
Jami Kousa a3055e518d remove semicolons 2018-03-23 19:00:54 +02:00
Jami Kousa d2b5231c31 enable new reducers 2018-03-23 18:46:34 +02:00
Jami Kousa 1d2c54fe4b course page reducer 2018-03-23 18:42:44 +02:00
Jami Kousa 433d57ff24 small refactor on course statistics 2018-03-23 15:47:45 +02:00
Jami Kousa fb84c9f1e5 removed dangling comma 2018-03-23 15:29:40 +02:00
Jami Kousa d1f87d0952 remove old reducers 2018-03-23 15:28:25 +02:00
Jami Kousa 7e54071f07 fix husky errors 2018-03-23 13:38:34 +02:00
Jami Kousa df091f8645 population reducer 2018-03-23 13:36:18 +02:00
Jami Kousa 776ac42cc6 add timeout to departmentsuccess 2018-03-23 09:42:11 +02:00
Jami Kousa 6d36a1a37a department success new reducer 2018-03-22 16:58:04 +02:00
Jami Kousa 939235051e student reducer implemented 2018-03-22 15:47:40 +02:00
Jami Kousa 320f31e224 add semicolon 2018-03-22 14:03:05 +02:00
Jami Kousa 1148fe7a0d student reducer 2018-03-22 14:00:19 +02:00
Jami Kousa 4ce9f1c26f missing semicolon 2018-03-22 13:11:43 +02:00
Jami Kousa b51c8ef2ee tag & unit reducers 2018-03-22 13:09:55 +02:00
totutotu a34aa5967e merge 2018-03-22 09:47:07 +02:00
totutotu 0db528f3a3 Admin UI for enabling users added in path /users 2018-03-22 09:44:16 +02:00
Jami Kousa 84d43be5d6 array of populations 2018-03-20 13:18:20 +02:00
Jami Kousa 2cc928f24b population reducer 2018-03-20 13:08:39 +02:00
Jami Kousa e903e81cdb query as parameter 2018-03-20 13:08:28 +02:00
Tuomo TorppaandGitHub 20a5a74bc0 Merge pull request #28 from UniversityOfHelsinkiCS/trunk
Initial population stats query improved
2018-03-16 12:50:00 +02:00
totutotu 6965b830f1 Changed intial year of postatsquery to 2017 2018-03-16 12:42:56 +02:00
totutotu 3f778e65ac Switched initial pop stats query from Spring to Fall 2018-03-16 11:22:43 +02:00
Tuomo TorppaandGitHub 8f3a144f30 Merge pull request #27 from UniversityOfHelsinkiCS/trunk
removed adding non-passed course credits to creditAccGrpah
2018-03-09 13:50:14 +02:00
Jami Kousa bcf31de217 Merge branch 'trunk' of github.com:UniversityOfHelsinkiCS/oodikone2-frontend into trunk 2018-03-08 12:24:59 +02:00
Jami Kousa 59a60dbc56 new get "departmentSuccess" 2018-03-08 12:24:56 +02:00
totutotu 497c14fbf4 Merge branch 'trunk' of https://github.com/UniversityOfHelsinkiCS/oodikone2-frontend into trunk 2018-03-08 08:36:26 +02:00
totutotu cb89c2fcc7 removed adding non-passed course credits to creditAccGrpah 2018-03-08 08:36:21 +02:00
Jami Kousa 9544156a57 lint 2018-03-06 16:53:54 +02:00
Jami Kousa d8752e7f65 new reducers started 2018-03-06 16:52:05 +02:00
totutotu 228e50a763 Merge branch 'trunk' of https://github.com/UniversityOfHelsinkiCS/oodikone2-frontend into trunk 2018-03-06 12:01:47 +02:00
totutotu c28182101f Removed showing student numbers from populations page 2018-03-06 12:01:43 +02:00
Jami KousaandGitHub b4742d3862 Merge pull request #22 from UniversityOfHelsinkiCS/trunk
welcome same-origin back
2018-03-05 13:57:14 +02:00
Jami Kousa 63a4a20103 welcome same-origin back 2018-03-05 13:56:40 +02:00
Jami KousaandGitHub bc5ca17fe5 Merge pull request #21 from UniversityOfHelsinkiCS/trunk
Style fixes and text handling
2018-03-02 15:56:48 +02:00
eero3 a0f56461fd - Moved dev env things on auth to frontend and changed front end auth to use only single path 2018-03-02 15:38:03 +02:00
eero3 24c8c9eaec - fixed query card bug
- fixed noResultText => noResultsText in PopulationSearch
2018-03-02 14:12:33 +02:00
eero3 85df38552f - removed unused import 2018-03-02 11:07:33 +02:00
eero3 9e970d7a9d - fixed string literal position 2018-03-02 11:01:13 +02:00
eero3 e4f118b234 - fixed PopulationQueryCard for new PopulationQuery 2018-03-02 11:00:30 +02:00
eero3 d0cdf4908c Merge remote-tracking branch 'remotes/origin/master' into trunk
# Conflicts:
#	src/actions/index.js
2018-03-02 10:42:44 +02:00
eero3andGitHub 5276906203 Merge pull request #20 from UniversityOfHelsinkiCS/frontend-token
- added token handling with following flows:
2018-03-02 10:28:44 +02:00
eero3 9dedc05d6b - added token handling with following flows:
prod: checks if token exists in localStorage before querying api,
if not => logs in and adds it to localStorage
 if yes => adds token to headers. Logout removes token
- Added logout to Navbar
- 403 error removes also removes token from localStroage, to get rid of old tokens etc.

- Dev:
Same flow as in prod, except logs in as tktl as default
Also added UGLY dev controls, where user can select from list of users who he/she wants to be
2018-03-02 10:18:23 +02:00
totutotu 3ce4eaca61 Merge branch 'master' of https://github.com/UniversityOfHelsinkiCS/oodikone2-frontend 2018-03-01 10:20:09 +02:00
totutotu d084a41ffe StudyrightSearch added 2018-03-01 10:12:33 +02:00
eero3 3123ef88dd - added / to population statistics query 2018-02-28 16:32:56 +02:00
eero3 865eacc6d4 - changed population statistics query to to GET with new params 2018-02-28 16:30:37 +02:00
eero3 0d0107c9c3 - fixed date validation on population statistics not to accept dates before 1901 2018-02-28 16:10:07 +02:00
eero3 3c91747283 - reworked population statistics query controls to look like they are supposed to 2018-02-28 15:56:43 +02:00
eero3 6c18abb88b - fixed active route in navbar problem after introducing optional route 2018-02-28 10:28:32 +02:00
eero3 887d0a6ca3 - added hover style to multiple students accumulation graph 2018-02-27 18:20:07 +02:00
eero3 0568756662 - fixed API_BASE_PATH from ...//api to .../api 2018-02-27 18:06:37 +02:00
Jami Kousa ad2c4104f8 remove console.logs 2018-02-27 17:50:22 +02:00
Jami KousaandGitHub fe0eab8a26 Merge pull request #18 from UniversityOfHelsinkiCS/trunk
fix basepath
2018-02-27 17:47:53 +02:00
Jami Kousa 6251eeb297 fix basepath 2018-02-27 17:46:49 +02:00
Jami KousaandGitHub 4317383c9e Merge pull request #17 from UniversityOfHelsinkiCS/trunk
try to fix basepath
2018-02-27 17:29:57 +02:00
Jami Kousa 65b73234e7 try to fix basepath 2018-02-27 17:29:29 +02:00
Jami KousaandGitHub 2932e0b631 Merge pull request #16 from UniversityOfHelsinkiCS/trunk
click course opens student page
2018-02-27 16:55:07 +02:00
Jami Kousa 745342d985 Merge branch 'trunk' of github.com:UniversityOfHelsinkiCS/oodikone2-frontend into trunk 2018-02-27 16:54:11 +02:00
Jami Kousa 4245599321 trying to fix api basename 2018-02-27 16:54:09 +02:00
eero3 a9e7cb714d Merge branch 'master' of https://github.com/UniversityOfHelsinkiCS/oodikone2-frontend into trunk 2018-02-27 16:38:18 +02:00
eero3 37db94f2ad - added feature: click course on PopulationStatistics graph -> opens student page 2018-02-27 16:38:05 +02:00
Jami KousaandGitHub 731aa5dfc3 Merge pull request #15 from UniversityOfHelsinkiCS/trunk
api base path
2018-02-27 16:33:00 +02:00
Jami Kousa f65cb9f8ed api base path 2018-02-27 16:31:11 +02:00
Jami KousaandGitHub 8b8fd233d5 Merge pull request #14 from UniversityOfHelsinkiCS/trunk
magical basename
2018-02-27 15:59:11 +02:00
Jami Kousa da7587b871 magical basename 2018-02-27 15:57:25 +02:00
Jami KousaandGitHub 8e21c47487 Merge pull request #13 from UniversityOfHelsinkiCS/trunk
Update README.md
2018-02-27 15:34:40 +02:00
Jami KousaandGitHub 63f1f76e58 Update README.md 2018-02-27 12:44:41 +02:00
eero3 dea5126a5d - removed glob from webpack prod build 2018-02-27 12:23:19 +02:00
eero3 bf38483d32 Merge branch 'master' of https://github.com/UniversityOfHelsinkiCS/oodikone2-frontend 2018-02-27 12:22:18 +02:00
eero3 b8a87b7bd4 - removed purify css to fix prod build 2018-02-27 12:21:49 +02:00
Jami Kousa 6e2718897f update node version 2018-02-23 12:05:44 +02:00
Tiainen db41071f1b Merge branch 'master' of https://github.com/UniversityOfHelsinkiCS/oodikone2-frontend 2018-02-23 11:38:00 +02:00
Tiainen 09b90bb6db fixing/refactoring course statistics 2018-02-23 11:37:06 +02:00
Tiainen 972e26aa89 created Dockerfile 2018-02-23 11:33:40 +02:00
eero3 40d68a34a3 -fixed student loading bug 2018-02-23 09:03:55 +02:00
Tiainen a3005fd325 Merge branch 'master' of https://github.com/UniversityOfHelsinkiCS/oodikone2-frontend 2018-02-20 16:07:06 +02:00
Tiainen e745fbe0f5 start of selecting multiple courses 2018-02-20 16:06:51 +02:00
eero3 3bc8fd2ce8 - fixed gateway timeout error etc. error handling in error reducer 2018-02-20 08:51:35 +02:00
eero3andGitHub 6365104f97 Merge pull request #12 from UniversityOfHelsinkiCS/add-error-container
- added error handling to ui
2018-02-19 17:07:35 +02:00
eero3 69ce1554a7 - added error handling to ui
- fixed problems with fetch api TypeErrors
- fixed student and population statistics error handling
2018-02-19 17:06:39 +02:00
eero3 2cec3f68be - simplified population sample rendering check 2018-02-09 16:35:52 +02:00
eero3andGitHub 8fd8a962f4 Merge pull request #11 from UniversityOfHelsinkiCS/population-statistics-base
Population statistics base
2018-02-09 16:23:53 +02:00
eero3 c19c251c51 - cleaned students & population statistics
- added date selector to population statistics query
- added error reducer
2018-02-09 16:20:36 +02:00
eero3 fe3c993143 added error reduver 2018-02-09 11:45:49 +02:00
eero3 88ac7c5c18 refactored class components to use static properties 2018-02-09 10:53:34 +02:00
eero3 b81fb676a7 - cleaned studentStatistics 2018-02-09 10:10:33 +02:00
eero3 293d9d097b Merge remote-tracking branch 'remotes/origin/course_stats' into population-statistics-base
# Conflicts:
#	src/actions/index.js
#	src/api/index.js
#	src/common/index.js
2018-02-08 17:02:45 +02:00
eero3 26826f626a - removed console.log 2018-02-08 16:57:11 +02:00
eero3 4be9149bcd - cleaned things
- fixed texts
2018-02-08 16:56:17 +02:00
eero3 ee91d57e3a - finished population query card
- simplified population reducers
- simplified population actions
2018-02-08 16:38:01 +02:00
eero3 cf01e648e1 - started population query card 2018-02-07 18:30:40 +02:00
eero3 5a83811cb6 - removed constructor binds and moved on to object initalizing 2018-02-07 17:38:57 +02:00
eero3 311a69a414 - chopped render code 2018-02-07 14:34:30 +02:00
eero3 83fa615ff9 - exported SegmentDimmer component to remove repetition 2018-02-07 10:02:02 +02:00
eero3 12508202d2 - added global layout for segments
- cleaned styles
2018-02-07 09:44:45 +02:00
eero3 c4c02c43dd - renamed CourseStatistics related to populationst to CourseQuarters 2018-02-06 18:06:29 +02:00
eero3 207d00579b Ugly megacommit:
- Changed CreditAccumulationGrap to support multiple people
- Added custom tool tip to creditGraph array
- Added a lot of other Course statistics functionality
- Started CourseStatistics for population analysis
2018-02-06 18:04:08 +02:00
Tiainen 9a2ae922f7 pleased lint again 2018-02-06 15:21:58 +02:00
Tiainen af896688d1 pleased lint 2018-02-06 15:20:52 +02:00
Tiainen 2d66cf80ba added table for stats and sorting 2018-02-06 14:59:59 +02:00
Tuomo TorppaandGitHub 28559533f2 Tietosuojaseloste pdf added 2018-02-02 13:33:51 +02:00
Tiainen d6d5008d3f trying to plot course stats 2018-02-02 12:56:05 +02:00
eero3andGitHub 7b4e3d8dfa Merge pull request #10 from UniversityOfHelsinkiCS/reducer-use-object-spread
- added elinter for object spread
2018-02-02 12:07:01 +02:00
eero3 3a22617d8d - added elinter for object spread
- refactored object assings to object spread
2018-02-02 12:04:47 +02:00
eero3andGitHub 359dbbe1c5 Merge pull request #9 from UniversityOfHelsinkiCS/credit-accumulation-graph
Credit accumulation graph
2018-02-02 11:38:53 +02:00
eero3 c6e5a33584 - removed useless max width from studentSearch 2018-02-02 11:29:27 +02:00
eero3 a7cd4d08b1 - cleaned linting errors from generic moment sort func 2018-02-02 11:26:35 +02:00
eero3 618590cb0c - added min widht to credit accumulation graph
- added sort to credi acc graph data
2018-02-02 11:24:28 +02:00
eero3 43087d7e61 - added object spread to babelrc
- cleaned studentStatistics
2018-02-02 09:56:04 +02:00
eero3 ccd61eb27c added selectable attribute to searhResultTable component that defaults to false 2018-02-01 13:22:00 +02:00
eero3 f794a45ab6 lint fixes 2018-02-01 13:11:54 +02:00
eero3 df116a47d5 optimized student credit accumulation graph a bit
added title to chart
editet student styles
2018-02-01 13:11:13 +02:00
eero3 6aebc404be Implemented super slow version of CreditAccumulationGraph 2018-01-31 17:16:06 +02:00
eero3 16c764df4e added coursesList to student statistics
moved some common translations under common
2018-01-31 13:19:44 +02:00
eero3andGitHub 94d9a43dab Merge pull request #8 from UniversityOfHelsinkiCS/student-component
Student component
2018-01-31 09:02:53 +02:00
eero3 44d6685af8 started credit accumulation graph 2018-01-31 08:58:55 +02:00
totutotu abc8189616 Navigator MenuItem error fixed 2018-01-31 08:54:08 +02:00
Tuomo TorppaandGitHub 61b4b684d0 Merge pull request #7 from UniversityOfHelsinkiCS/student-component
Student component
2018-01-31 08:50:14 +02:00
Tiainen 6b95d6a7b0 added course statistics fetching 2018-01-30 14:29:58 +02:00
eero3 cce375a3fe lint fix 2018-01-30 12:08:41 +02:00
eero3 8c6cc3c89a cleaned TagListSelector 2018-01-30 12:08:30 +02:00
eero3 12cf733ae1 added first version of student tags reducer and connected it to studenDetails 2018-01-30 11:05:07 +02:00
Tiainen eb49fcde1a added fetching course instances 2018-01-29 17:10:13 +02:00
eero3 05682e973d created Components for student info, cretated search/add/remove tags base for students 2018-01-29 16:40:16 +02:00
eero3 9061a6a3a5 - Modularised StudentStatistics component more 2018-01-26 15:08:43 +02:00
eero3 785cfd0780 - created dynamic search result component 2018-01-26 14:03:08 +02:00
eero3andGitHub ea9daef50b Merge pull request #6 from UniversityOfHelsinkiCS/course-statistics-component
Course statistics component
2018-01-26 09:22:12 +02:00
eero3 8ade9a003b - styled courses 2018-01-25 16:43:01 +02:00
eero3 78cec1dd89 - fixed active route on reload bug 2018-01-25 16:42:33 +02:00
eero3 50023d4ab5 - added search for courses 2018-01-25 15:56:44 +02:00
eero3andGitHub fe7a0caa1f Merge pull request #5 from UniversityOfHelsinkiCS/add-i18n-lib
Add i18n lib
2018-01-25 14:04:01 +02:00
eero3 b04fa49df7 -finished react-localize-redux intgration to keep texts in check 2018-01-25 14:02:59 +02:00
eero3 ba87890445 fixed lints 2018-01-24 16:30:03 +02:00
eero3 76e2980bb9 added react-localize redux library
started adding i18n
2018-01-24 16:27:42 +02:00
eero3andGitHub 02b5fdc6f3 Merge pull request #4 from UniversityOfHelsinkiCS/department-success-graph
Department success graph
2018-01-24 15:37:30 +02:00
eero3 8ac3ff1786 fixed linting 2018-01-24 15:36:49 +02:00
eero3 991c92fdaf cleaned conditional rendering 2018-01-24 15:35:04 +02:00
eero3 31d01d58de added conditional rendering to department success 2018-01-24 15:22:22 +02:00
eero3 33118fc244 cleaned code 2018-01-24 12:08:31 +02:00
eero3 14496b7934 added ignore accessibility rules 2018-01-24 12:05:39 +02:00
eero3 eff7e0a5fb modulariset department success 2018-01-24 11:05:23 +02:00
eero3 dac69b38fd fixed stylelint config to accept :global & -- starting custom properties 2018-01-24 11:04:58 +02:00
eero3 7424530d49 added date selector next info proto 2018-01-23 17:49:16 +02:00
eero3 2f24cb8116 fixed css lint 2018-01-23 16:48:07 +02:00
eero3 a1831d70e0 added onKeyPress to dateselectors 2018-01-23 16:47:16 +02:00
eero3 d1b30e415c added recharts and react-date-time
added responsive department success graph with controls
2018-01-23 16:42:11 +02:00
eero3 ceaeaf03e8 removed default focus outline from global.css 2018-01-22 14:22:53 +02:00
eero3andGitHub 83e95ec8d3 Merge pull request #3 from UniversityOfHelsinkiCS/create-navigation-proto
Create navigation proto
2018-01-22 14:13:11 +02:00
eero3 1c36563d1b optimised navbar item creation 2018-01-22 14:12:18 +02:00
eero3 3e6cd896cc lint fixes 2018-01-22 13:24:55 +02:00
eero3 86b2f559cc css lint fix 2018-01-22 13:10:08 +02:00
eero3 091127ee63 removed empty style 2018-01-22 13:09:06 +02:00
eero3 a57012d422 added placeholder components for teacher and courses
fixed navbar styles
2018-01-22 13:08:24 +02:00
eero3 64a3c84d1b added placeholder components for teacher and courses
fixed navbar styles
2018-01-22 13:08:12 +02:00
eero3 75713d4228 added react-semantic-ui
made first draft of navigation bar
2018-01-22 10:57:36 +02:00
eero3 799e703750 added navigation button 2018-01-16 15:32:13 +02:00
eero3andGitHub e1b0db4a86 Merge pull request #2 from UniversityOfHelsinkiCS/remove-eject-debris
Remove eject debris
2018-01-16 14:03:53 +02:00
eero3 13e17cf084 fixed lint:css problems 2018-01-16 14:00:29 +02:00
eero3 dde753562d eslint style fixes 2018-01-16 12:29:49 +02:00
eero3 3f39e59826 added .eslintignore 2018-01-16 11:52:18 +02:00
eero3 4aaba75099 husky test 2018-01-16 11:42:09 +02:00
eero3 f9ed86fec7 ran lintix 2018-01-16 11:36:03 +02:00
eero3 1aff38613c added npm install to readme 2018-01-16 11:31:54 +02:00
eero3 a94f7f73b6 started readme.md 2018-01-16 11:29:46 +02:00
eero3 b7ac291a48 removed old config files 2018-01-16 11:23:12 +02:00
eero3 dd6ad7b84b checked postcss / cssnext config 2018-01-16 11:22:58 +02:00
eero3 e9d049eef9 added production build webpack config 2018-01-16 10:44:05 +02:00
eero3 0819c0599d added eslint config 2018-01-16 10:36:09 +02:00
eero3 4ce9379522 removed predefined scripts 2018-01-16 10:35:48 +02:00
eero3andGitHub b593ee7d2b Merge pull request #1 from UniversityOfHelsinkiCS/eject-create-react-app
Eject create react app
2018-01-16 09:48:04 +02:00
eero3 31623bea66 - added StudentStatistics with api usage test 2018-01-12 18:53:28 +02:00
eero3 05ed218ec6 - added api connection & departmentSuccess component
- right now using react-promise-mw, this might be changed to redux-thunk
2018-01-12 16:27:56 +02:00
eero3 6e55ecac39 added route constants and created populations component 2018-01-12 14:23:16 +02:00
eero3 a91b44c47c added header 2018-01-12 13:58:46 +02:00
eero3 13817e741f reconfigured dependencies & webpack for dev, started building structure 2018-01-12 13:36:19 +02:00
eero3 d2df706162 ejected create-react-app 2018-01-12 08:21:07 +02:00
eero3 c9e3439087 added IDE metadata block to .gitignore 2018-01-12 07:22:37 +02:00
totutotu 8717e3cfce started fronend with create-react-app 2018-01-10 11:34:45 +02:00
Tuomo TorppaandGitHub 6a7e721722 Initial commit 2018-01-10 11:14:07 +02:00
1279 changed files with 29002 additions and 293219 deletions
+25
View File
@@ -0,0 +1,25 @@
{
"presets": [
["latest", {
"es2015": {
"modules": false
}
}],
"react"
],
"env": {
"development": {
"plugins": [
"transform-object-rest-spread",
"transform-class-properties",
"react-hot-loader/babel"
]
},
"test": {
"plugins": [
"transform-async-to-generator",
"inline-json-import",
]
}
}
}
-12
View File
@@ -1,12 +0,0 @@
{
"permissions": {
"deny": [
"Read(**/.env*)",
"Read(./.databasedumps/**)",
"Read(**/*.sqz)",
"Read(**/*.sql)",
"Bash(docker exec *-db *)",
"Bash(*psql *)"
]
}
}
-10
View File
@@ -1,10 +0,0 @@
# Populate and change this to .env
# To get the crypt key, run `openssl rand -hex 32` or
# `node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"`
IMPORTER_DB_API_TOKEN=
PATE_TOKEN=
CRYPT_KEY=
# Changes the user and group docker runs as. Use "id -u" and "id -g" to run containers as current user+group.
# Fixes errors with permissions when mounting.
DOCKER_UID=
DOCKER_GID=
+2
View File
@@ -0,0 +1,2 @@
dist/
config/
+44 -11
View File
@@ -1,13 +1,46 @@
{
"ignorePatterns": [
"eslint.config.mjs",
"**/build/**",
"**/dist/**",
"**/node_modules/**",
"**/instrumented/**",
"**/coverage/**",
"nyc-config.cjs",
".lintstagedrc.mjs",
"playwright/**/*.js"
"root": true,
"parser": "babel-eslint",
"extends": "airbnb",
"env": {
"browser": true
},
"rules": {
"comma-dangle": [
2,
"never"
],
"no-param-reassign": [
2,
{
"props": false
}
],
"import/no-unresolved": 2,
"react/jsx-no-bind": 2,
"react/no-did-mount-set-state": 0,
"react/prefer-stateless-function": 2,
"react/no-typos": 0,
"jsx-a11y/no-static-element-interactions": 0,
"jsx-a11y/click-events-have-key-events": 0,
"jsx-a11y/label-has-for": 0,
"object-curly-newline": "off",
"prefer-object-spread/prefer-object-spread": 2,
"prefer-arrow-callback": 2,
"babel/no-invalid-this": 2,
"babel/semi": [
2,
"never"
],
"semi": [
2,
"never"
]
},
"plugins": [
"react",
"import",
"prefer-object-spread",
"babel"
]
}
}
-61
View File
@@ -1,61 +0,0 @@
name: 'Build backend'
description: 'Build backend in staging or production version'
inputs:
environment:
required: true
description: 'staging or production'
default: 'production'
release_version:
required: false
description: 'Release version added to production builds'
default: ${{ github.sha }}
push:
required: false
description: Also push built image to toska registry
default: 'false'
registry_password:
required: false
description: Password to quay.io registry
default: ''
runs:
using: composite
steps:
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
- name: Docker meta
id: meta
uses: docker/metadata-action@v6
with:
images: |
quay.io/toska/oodikone-backend
tags: |
${{ inputs.environment }}
${{ github.sha }}
- name: Login to quay.io
if: ${{ inputs.push == 'true' }}
uses: docker/login-action@v4
with:
registry: quay.io/toska
username: toska+github
password: ${{ inputs.registry_password }}
- name: 'Build image and export it'
id: build-image-action
uses: docker/build-push-action@v7
with:
context: services
file: services/backend/Dockerfile
build-args: |
GIT_SHA=${{ github.sha }}
STAGING=${{ case(inputs.environment == 'staging', 'true', 'false') }}
BASE_PATH=${{ case(inputs.environment == 'staging', '/oodikone', '') }}
SENTRY_ENVIRONMENT=${{ inputs.environment }}
SENTRY_RELEASE=${{ inputs.release_version }}
push: ${{ inputs.push }}
tags: ${{ steps.meta.outputs.tags }}
env:
DOCKER_BUILD_SUMMARY: false
@@ -1,72 +0,0 @@
name: 'Build frontend'
description: 'Build frontend in staging or production versions'
inputs:
environment:
required: true
description: 'staging or production'
default: 'production'
release_version:
required: false
description: 'Release version added to production builds'
default: ${{ github.sha }}
sentry_dns:
required: false
description: 'Sentry url to send error logs to (only production, enforced)'
default: ''
push:
required: false
description: Also push built image to toska registry
default: 'false'
registry_password:
required: false
description: Password to quay.io registry
default: ''
runs:
using: composite
steps:
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
- name: Login to quay.io
if: ${{ inputs.push == 'true' }}
uses: docker/login-action@v4
with:
registry: quay.io/toska
username: toska+github
password: ${{ inputs.registry_password }}
- name: Docker meta
id: meta
uses: docker/metadata-action@v6
with:
images: |
quay.io/toska/oodikone-frontend
tags: |
${{ inputs.environment }}
${{ github.sha }}
- name: Get current time
id: get_current_time
shell: bash
run: echo "NOW=$(date --iso-8601=seconds)" >> $GITHUB_OUTPUT
- name: 'Build image and export it'
id: build-image-action
uses: docker/build-push-action@v7
with:
context: services
file: services/frontend/Dockerfile
build-args: |
GIT_SHA=${{ github.sha }}
STAGING=${{ case(inputs.environment == 'staging', 'true', 'false') }}
BASE_PATH=${{ case(inputs.environment == 'staging', '/oodikone', '') }}
SENTRY_ENVIRONMENT=${{ inputs.environment }}
SENTRY_RELEASE=${{ inputs.release_version }}
BUILT_AT=${{ steps.get_current_time.outputs.NOW }}
SENTRY_DSN=${{ case(inputs.environment == 'production', inputs.sentry_dns, '') }}
push: ${{ inputs.push }}
tags: ${{ steps.meta.outputs.tags }}
env:
DOCKER_BUILD_SUMMARY: false
@@ -1,60 +0,0 @@
name: 'Build updater scheduler'
description: 'Build updater scheduler in staging or production version'
inputs:
environment:
required: true
description: 'staging or production'
default: 'production'
release_version:
required: false
description: 'Release version added to production builds'
default: ${{ github.sha }}
push:
required: false
description: Also push built image to toska registry
default: 'false'
registry_password:
required: false
description: Password to quay.io registry
default: ''
runs:
using: composite
steps:
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
- name: Login to quay.io
if: ${{ inputs.push == 'true' }}
uses: docker/login-action@v4
with:
registry: quay.io/toska
username: toska+github
password: ${{ inputs.registry_password }}
- name: Docker meta
id: meta
uses: docker/metadata-action@v6
with:
images: |
quay.io/toska/updater-scheduler
tags: |
${{ inputs.environment }}
${{ github.sha }}
- name: 'Build image and export it'
id: build-image-action
uses: docker/build-push-action@v7
with:
context: updater/sis-updater-scheduler
file: updater/sis-updater-scheduler/Dockerfile
build-args: |
GIT_SHA=${{ github.sha }}
STAGING=${{ case(inputs.environment == 'staging', 'true', 'false') }}
SENTRY_ENVIRONMENT=${{ inputs.environment }}
SENTRY_RELEASE=${{ inputs.release_version }}
push: ${{ inputs.push }}
tags: ${{ steps.meta.outputs.tags }}
env:
DOCKER_BUILD_SUMMARY: false
-60
View File
@@ -1,60 +0,0 @@
name: 'Build updater worker'
description: 'Build updater worker in staging or production version'
inputs:
environment:
required: true
description: 'staging or production'
default: 'production'
release_version:
required: false
description: 'Release version added to production builds'
default: ${{ github.sha }}
push:
required: false
description: Also push built image to toska registry
default: 'false'
registry_password:
required: false
description: Password to quay.io registry
default: ''
runs:
using: composite
steps:
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
- name: Login to quay.io
if: ${{ inputs.push == 'true' }}
uses: docker/login-action@v4
with:
registry: quay.io/toska
username: toska+github
password: ${{ inputs.registry_password }}
- name: Docker meta
id: meta
uses: docker/metadata-action@v6
with:
images: |
quay.io/toska/updater-worker
tags: |
${{ inputs.environment }}
${{ github.sha }}
- name: 'Build image and export it'
id: build-image-action
uses: docker/build-push-action@v7
with:
context: updater/sis-updater-worker
file: updater/sis-updater-worker/Dockerfile
build-args: |
GIT_SHA=${{ github.sha }}
STAGING=${{ case(inputs.environment == 'staging', 'true', 'false') }}
SENTRY_ENVIRONMENT=${{ inputs.environment }}
SENTRY_RELEASE=${{ inputs.release_version }}
push: ${{ inputs.push }}
tags: ${{ steps.meta.outputs.tags }}
env:
DOCKER_BUILD_SUMMARY: false
-18
View File
@@ -1,18 +0,0 @@
name: 'Lint code'
description: 'Runs Oxlint and Stylelint for frontend and backend'
runs:
using: composite
steps:
- uses: actions/setup-node@v7
with:
node-version: 24
- name: 'Install dependencies'
shell: bash
run: npm ci && npm ci --prefix services/shared && npm ci --prefix services/frontend && npm ci --prefix services/backend
- name: 'Run Stylelint for CSS files'
shell: bash
run: npm run stylelint "services/frontend/src/**/*.css"
- name: 'Run Oxlint for the whole repository'
shell: bash
run: npm run lint
-9
View File
@@ -1,9 +0,0 @@
name: Teardown docker compose (docker-compose.ci.yml)
description: 'Teardown all containers started by docker (docker-compose.ci.yml)'
runs:
using: composite
steps:
- name: 'Teardown docker compose'
shell: bash
run: docker compose --file docker-compose.ci.yml --profile="*" down --remove-orphans
-41
View File
@@ -1,41 +0,0 @@
name: 'Run api tests'
description: 'Runs api tests in backend'
inputs:
username:
required: true
description: 'Toska registry username'
password:
required: true
description: 'Toska registry password'
runs:
using: composite
steps:
- name: Install node-24
uses: actions/setup-node@v6
with:
node-version: 24
- name: Login to toska docker registry
uses: docker/login-action@v4
with:
registry: registry-toska.ext.ocp-prod-0.k8s.it.helsinki.fi
username: ${{ inputs.username }}
password: ${{ inputs.password }}
- name: Start oodikone (without frontend and backend)
shell: bash
run: docker compose --file docker-compose.ci.yml up --quiet-pull --detach --wait
- name: Install dependencies
shell: bash
run: npm ci --prefix ./services/backend
- name: Run api-tests
shell: bash
run: npm run test:api --prefix ./services/backend
- name: Teardown containers started by compose
if: always()
uses: ./.github/actions/teardown
@@ -1,31 +0,0 @@
name: 'Run component tests'
description: 'Runs frontend component tests'
runs:
using: composite
steps:
- name: Install node-24
uses: actions/setup-node@v6
with:
node-version: 24
- name: Install dependencies
shell: bash
run: npm ci --prefix ./services/shared && npm ci --prefix ./services/frontend
- name: Install Playwright Browsers
shell: bash
run: npm exec -- playwright install --with-deps
working-directory: ./services/frontend
- name: Run Playwright tests
shell: bash
run: npm run test:component --prefix ./services/frontend
- uses: actions/upload-artifact@v4
if: |
!cancelled()
with:
name: playwright-report
path: playwright-report/
retention-days: 7
@@ -1,44 +0,0 @@
name: Run e2e tests with playwright
description: Runs playwright tests for individual spec
inputs:
username:
required: true
description: 'Toska registry username'
password:
required: true
description: 'Toska registry password'
spec:
required: true
description: 'Path to test file'
test_name:
required: true
description: 'Name of the test'
runs:
using: composite
steps:
- name: 'Login to toska Docker registry'
uses: docker/login-action@v4
with:
registry: registry-toska.ext.ocp-prod-0.k8s.it.helsinki.fi
username: ${{ inputs.username }}
password: ${{ inputs.password }}
- name: 'Run services, pull rest of the needed images'
shell: bash
run: docker compose --file docker-compose.ci.yml --profile oodikone up --detach --quiet-pull
- name: Run playwright tests (${{ inputs.test_name }}})
shell: bash
run: |
CI='true' npm run playwright ${{ inputs.spec }}
- name: 'Print logs on failure'
shell: bash
if: failure()
run: npm run logs:ci && sleep 15
- name: Teardown containers started by compose
if: always()
uses: ./.github/actions/teardown
-41
View File
@@ -1,41 +0,0 @@
name: 'Run service tests'
description: 'Runs service tests in backend'
inputs:
username:
required: true
description: 'Toska registry username'
password:
required: true
description: 'Toska registry password'
runs:
using: composite
steps:
- name: Install node-24
uses: actions/setup-node@v6
with:
node-version: 24
- name: Login to toska docker registry
uses: docker/login-action@v4
with:
registry: registry-toska.ext.ocp-prod-0.k8s.it.helsinki.fi
username: ${{ inputs.username }}
password: ${{ inputs.password }}
- name: Start oodikone (without frontend and backend)
shell: bash
run: docker compose --file docker-compose.ci.yml up --quiet-pull --detach --wait
- name: Install dependencies
shell: bash
run: npm ci --prefix ./services/backend
- name: Run service-tests
shell: bash
run: npm run test:services --prefix ./services/backend
- name: Teardown containers started by compose
if: always()
uses: ./.github/actions/teardown
-26
View File
@@ -1,26 +0,0 @@
name: 'Run unit tests'
description: 'Runs unit tests in backend and frontend'
runs:
using: composite
steps:
- name: Install node-24
uses: actions/setup-node@v6
with:
node-version: 24
- name: Install dependencies
shell: bash
run: npm ci --prefix ./services/shared && npm ci --prefix ./services/backend && npm ci --prefix ./services/frontend
- name: Run shared unit tests
shell: bash
run: npm run test:unit --prefix ./services/shared
- name: Run frontend unit tests
shell: bash
run: npm run test:unit --prefix ./services/frontend
- name: Run backend unit tests
shell: bash
run: npm run test:unit --prefix ./services/backend
@@ -1,18 +0,0 @@
name: 'Run updater unit tests'
description: 'Runs unit tests in sis-updater-worker'
runs:
using: composite
steps:
- name: Install node-24
uses: actions/setup-node@v6
with:
node-version: 24
- name: Install dependencies
shell: bash
run: npm ci --prefix ./updater/sis-updater-worker
- name: Run updater unit tests
shell: bash
run: npm run test:unit --prefix ./updater/sis-updater-worker
-162
View File
@@ -1,162 +0,0 @@
name: 'Release to production'
on:
release:
types: [published]
jobs:
release_backend:
name: 'Build and release backend'
runs-on: ${{ vars.RUNS_ON || 'ubuntu-latest' }}
timeout-minutes: 15
steps:
- uses: actions/checkout@v7
- name: Extract release version
id: extract_version
run: echo "VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT
- name: 'Build backend'
id: build-image
uses: ./.github/actions/build/backend
with:
environment: 'production'
release_version: ${{ steps.extract_version.outputs.VERSION }}
push: true
registry_password: ${{ secrets.QUAY_IO_TOKEN }}
release_frontend:
name: 'Build and release frontend'
runs-on: ${{ vars.RUNS_ON || 'ubuntu-latest' }}
timeout-minutes: 15
steps:
- uses: actions/checkout@v7
- name: Extract release version
id: extract_version
run: echo "VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT
- name: 'Build frontend'
id: build-image
uses: ./.github/actions/build/frontend
with:
environment: 'production'
release_version: ${{ steps.extract_version.outputs.VERSION }}
sentry_dns: ${{ secrets.OODIKONE_FRONTEND_SENTRY_DSN }} # Used only in production
push: true
registry_password: ${{ secrets.QUAY_IO_TOKEN }}
sentry_release_frontend:
name: 'Create Sentry release for frontend'
runs-on: ${{ vars.RUNS_ON || 'ubuntu-latest' }}
timeout-minutes: 15
needs:
- release_frontend
- release_backend
steps:
- uses: actions/checkout@v7
- name: Extract release version
id: extract_version
run: echo "VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT
- name: Pull frontend image and create a container
run: docker create --name frontend quay.io/toska/oodikone-frontend:production
- name: Copy assets from the frontend container
run: docker cp frontend:/opt/app-root/frontend/build ./build
- name: Create Sentry release
uses: getsentry/action-release@v3
env:
SENTRY_URL: https://toska.it.helsinki.fi/
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_TOKEN }}
SENTRY_ORG: sentry
SENTRY_PROJECT: oodikone-frontend
with:
environment: production
set_commits: 'skip'
sourcemaps: './build/assets'
url_prefix: '~/assets'
version: ${{ steps.extract_version.outputs.VERSION }}
sentry_release_backend:
name: 'Create Sentry release for backend'
runs-on: ${{ vars.RUNS_ON || 'ubuntu-latest' }}
timeout-minutes: 15
needs:
- release_frontend
- release_backend
steps:
- uses: actions/checkout@v7
- name: Extract release version
id: extract_version
run: echo "VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT
- name: Pull backend image and create a container
run: docker create --name backend quay.io/toska/oodikone-backend:production
- name: Copy assets from the backend container
run: docker cp backend:/opt/app-root/backend/dist ./dist
- name: Create Sentry release
uses: getsentry/action-release@v3
env:
SENTRY_URL: https://toska.it.helsinki.fi/
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_TOKEN }}
SENTRY_ORG: sentry
SENTRY_PROJECT: oodikone-backend
with:
environment: production
set_commits: 'skip'
sourcemaps: './dist'
url_prefix: '~/opt/app-root/backend/dist/'
version: ${{ steps.extract_version.outputs.VERSION }}
report_success:
name: 'Report deployment success to the "oodikone_notifications" channel'
runs-on: ${{ vars.RUNS_ON || 'ubuntu-latest' }}
timeout-minutes: 15
needs:
- release_frontend
- release_backend
- sentry_release_frontend
- sentry_release_backend
if: success()
steps:
- uses: actions/checkout@v7
- name: 'Send notification to Slack'
uses: UniversityOfHelsinkiCS/slackbot-action@v1.6.1
with:
webhook-url: ${{ secrets.WEBHOOK_URL }}
message-type: deployment
softa-url: https://oodikone.helsinki.fi/
deployment-target: production
report_failure:
name: 'Report deployment failure to the "oodikone_notifications" channel'
runs-on: ${{ vars.RUNS_ON || 'ubuntu-latest' }}
timeout-minutes: 15
needs:
- release_frontend
- release_backend
- sentry_release_frontend
- sentry_release_backend
if: failure()
steps:
- uses: actions/checkout@v7
- name: 'Send notification to Slack'
uses: UniversityOfHelsinkiCS/slackbot-action@v1.6.1
with:
webhook-url: ${{ secrets.WEBHOOK_URL }}
message-type: deployment-failure
deployment-target: production
-30
View File
@@ -1,30 +0,0 @@
name: 'Run tests'
on:
pull_request:
branches:
- master
paths:
- '.github/actions/**'
- '.github/workflows/pull_request.yaml'
- '.github/workflows/test.yaml'
- 'services/**'
- 'updater/**'
- 'e2e/**'
- 'docker-compose.ci.yml'
- 'docker-compose.test.yml'
- '.oxlintrc.json'
- '.oxlint-backend-rules.cjs'
- 'package*.json'
jobs:
test:
if: ${{ github.event.pull_request.user.login != 'renovate[bot]' }}
uses: ./.github/workflows/test.yaml
with:
skip_db_update: true
secrets:
username: ${{ secrets.TOSKAREGISTRY_USERNAME }}
password: ${{ secrets.TOSKAREGISTRY_PASSWORD }}
s3_access_key: ${{ secrets.S3_ACCESS_KEY }}
s3_secret_key: ${{ secrets.S3_SECRET_KEY }}
-140
View File
@@ -1,140 +0,0 @@
name: 'Test and release to staging'
on:
push:
branches:
- master
paths:
- '.github/actions/**'
- '.github/workflows/staging.yaml'
- '.github/workflows/test.yaml'
- 'services/**'
- 'updater/**'
- 'e2e/**'
- 'docker-compose.ci.yml'
- 'docker-compose.test.yml'
- '.oxlintrc.json'
- '.oxlint-backend-rules.cjs'
- 'package*.json'
jobs:
test:
uses: UniversityOfHelsinkiCS/oodikone/.github/workflows/test.yaml@master
secrets:
username: ${{ secrets.TOSKAREGISTRY_USERNAME }}
password: ${{ secrets.TOSKAREGISTRY_PASSWORD }}
s3_access_key: ${{ secrets.S3_ACCESS_KEY }}
s3_secret_key: ${{ secrets.S3_SECRET_KEY }}
release_backend:
name: 'Build and release backend'
runs-on: ${{ vars.RUNS_ON || 'ubuntu-latest' }}
timeout-minutes: 15
needs: test
steps:
- uses: actions/checkout@v7
- name: 'Build backend'
id: build-image
uses: ./.github/actions/build/backend
with:
environment: 'staging'
release_version: ${{ github.sha }}
push: true
registry_password: ${{ secrets.QUAY_IO_TOKEN }}
release_frontend:
name: 'Build and release frontend'
runs-on: ${{ vars.RUNS_ON || 'ubuntu-latest' }}
timeout-minutes: 15
needs: test
steps:
- uses: actions/checkout@v7
- name: 'Build frontend'
id: build-image
uses: ./.github/actions/build/frontend
with:
environment: 'staging'
release_version: ${{ github.sha }}
push: true
registry_password: ${{ secrets.QUAY_IO_TOKEN }}
release_updater_worker:
name: 'Build and release updater worker'
runs-on: ${{ vars.RUNS_ON || 'ubuntu-latest' }}
timeout-minutes: 15
needs: test
steps:
- uses: actions/checkout@v7
- name: 'Build worker'
id: build-image
uses: ./.github/actions/build/worker
with:
environment: 'staging'
release_version: ${{ github.sha }}
push: true
registry_password: ${{ secrets.QUAY_IO_TOKEN }}
release_updater_scheduler:
name: 'Build and release updater scheduler'
runs-on: ${{ vars.RUNS_ON || 'ubuntu-latest' }}
timeout-minutes: 15
needs: test
steps:
- uses: actions/checkout@v7
- name: 'Build scheduler'
id: build-image
uses: ./.github/actions/build/scheduler
with:
environment: 'staging'
release_version: ${{ github.sha }}
push: true
registry_password: ${{ secrets.QUAY_IO_TOKEN }}
report_success:
name: 'Report deployment success to the "oodikone_notifications" channel'
runs-on: ${{ vars.RUNS_ON || 'ubuntu-latest' }}
timeout-minutes: 15
if: success()
needs:
- release_frontend
- release_backend
- release_updater_worker
- release_updater_scheduler
steps:
- uses: actions/checkout@v7
- name: 'Send notification to Slack'
uses: UniversityOfHelsinkiCS/slackbot-action@v1.6.1
with:
webhook-url: ${{ secrets.WEBHOOK_URL }}
message-type: deployment
softa-url: https://toska-staging.cs.helsinki.fi/oodikone
deployment-target: staging
report_failure:
name: 'Report deployment failure to the "oodikone_notifications" channel'
runs-on: ${{ vars.RUNS_ON || 'ubuntu-latest' }}
timeout-minutes: 15
if: failure()
needs:
- release_frontend
- release_backend
- release_updater_worker
- release_updater_scheduler
steps:
- uses: actions/checkout@v7
- name: 'Send notification to Slack'
uses: UniversityOfHelsinkiCS/slackbot-action@v1.6.1
with:
webhook-url: ${{ secrets.WEBHOOK_URL }}
message-type: deployment-failure
deployment-target: staging
-474
View File
@@ -1,474 +0,0 @@
name: 'Lint and test'
on:
workflow_call:
inputs:
skip_db_update:
description: 'When true, skip committing/pushing anon DB images (used for PR runs)'
required: false
type: string
default: 'false'
secrets:
username:
required: true
description: 'Toska registry username'
password:
required: true
description: 'Toska registry password'
s3_access_key:
required: true
description: 'S3 access key for the test database dumps'
s3_secret_key:
required: true
description: 'S3 secret key for the test database dumps'
jobs:
# === Static tests / linting ===
lint_packages:
name: 'Lint packages'
runs-on: ${{ vars.RUNS_ON || 'ubuntu-latest' }}
timeout-minutes: 15
steps:
- uses: actions/checkout@v7
- name: Lint backend, frontend and shared
uses: ./.github/actions/lint
# === Build database images ===
build_backend:
name: 'Build backend'
runs-on: ${{ vars.RUNS_ON || 'ubuntu-latest' }}
timeout-minutes: 15
steps:
- uses: actions/checkout@v7
- name: Build backend
uses: ./.github/actions/build/backend
with:
environment: 'staging'
release_version: ${{ github.sha }}
push: false
build_frontend:
name: 'Build frontend'
runs-on: ${{ vars.RUNS_ON || 'ubuntu-latest' }}
timeout-minutes: 15
steps:
- uses: actions/checkout@v7
- name: Build frontend
uses: ./.github/actions/build/frontend
with:
environment: 'staging'
release_version: ${{ github.sha }}
push: false
# === Update databases ===
check_migration_files:
name: Check if new migrations have been added
runs-on: ${{ vars.RUNS_ON || 'ubuntu-latest' }}
timeout-minutes: 15
outputs:
kone_db_migrations: ${{ steps.check_migrations.outputs.kone_db_migrations }}
user_db_migrations: ${{ steps.check_migrations.outputs.user_db_migrations }}
sis_db_migrations: ${{ steps.check_migrations.outputs.sis_db_migrations }}
steps:
- uses: actions/checkout@v7
- name: Check if new migrations have been added
id: check_migrations
uses: dorny/paths-filter@v4
with:
filters: |
kone_db_migrations:
- 'services/backend/src/database/migrations_kone/**'
- '.github/workflows/test.yaml'
user_db_migrations:
- 'services/backend/src/database/migrations_user/**'
- '.github/workflows/test.yaml'
sis_db_migrations:
- 'updater/sis-updater-worker/src/db/migrations/**'
- '.github/workflows/test.yaml'
build_and_push_anon_sis_db:
name: 'Build anon sis-db and push to toska hub if new migrations have been added'
runs-on: ${{ vars.RUNS_ON || 'ubuntu-latest' }}
timeout-minutes: 15
if: |
!cancelled() && !failure() &&
needs.check_migration_files.outputs.sis_db_migrations == 'true' &&
inputs.skip_db_update != 'true'
needs:
- check_migration_files
- build_backend
- build_frontend
- lint_packages
- test_unit
- test_updater_unit
steps:
- uses: actions/checkout@v7
- name: 'Login to toska docker registry'
uses: docker/login-action@v4
with:
registry: registry-toska.ext.ocp-prod-0.k8s.it.helsinki.fi
username: ${{ secrets.username }}
password: ${{ secrets.password }}
- name: 'Let sis-updater-worker run migrations, then push new version of image to toska hub'
env:
ACCESS_KEY: ${{ secrets.s3_access_key }}
SECRET_KEY: ${{ secrets.s3_secret_key }}
run: |
docker compose --file docker-compose.ci.yml up --detach --quiet-pull sis-updater-worker
sleep 30 # wait for sis-updater-worker to start and run migrations
sudo apt update -y && sudo apt install s3cmd -y
./anonyymioodi/manage_test_dbs.sh release sis-db
- name: Teardown containers started by compose
if: always()
uses: ./.github/actions/teardown
build_and_push_anon_kone_db:
name: 'Build anon kone-db and push to toska hub if new migrations have been added'
runs-on: ${{ vars.RUNS_ON || 'ubuntu-latest' }}
timeout-minutes: 15
if: |
!cancelled() && !failure() &&
needs.check_migration_files.outputs.kone_db_migrations == 'true' &&
inputs.skip_db_update != 'true'
needs:
- check_migration_files
- build_backend
- build_frontend
- lint_packages
- test_unit
- test_updater_unit
steps:
- uses: actions/checkout@v7
- name: 'Login to toska docker registry'
uses: docker/login-action@v4
with:
registry: registry-toska.ext.ocp-prod-0.k8s.it.helsinki.fi
username: ${{ secrets.username }}
password: ${{ secrets.password }}
- name: 'Let backend run migrations, then push new version of image to toska hub'
env:
ACCESS_KEY: ${{ secrets.s3_access_key }}
SECRET_KEY: ${{ secrets.s3_secret_key }}
run: |
docker compose --file docker-compose.ci.yml up --detach --quiet-pull backend --wait
sleep 30 # wait for backend to start and run migrations
sudo apt update -y && sudo apt install s3cmd -y
./anonyymioodi/manage_test_dbs.sh release kone-db
- name: Teardown containers started by compose
if: always()
uses: ./.github/actions/teardown
build_and_push_anon_user_db:
name: 'Build anon user-db and push to toska hub if new migrations have been added'
runs-on: ${{ vars.RUNS_ON || 'ubuntu-latest' }}
timeout-minutes: 15
if: |
!cancelled() && !failure() &&
needs.check_migration_files.outputs.user_db_migrations == 'true' &&
inputs.skip_db_update != 'true'
needs:
- check_migration_files
- build_backend
- build_frontend
- lint_packages
- test_unit
- test_updater_unit
steps:
- uses: actions/checkout@v7
- name: 'Login to toska docker registry'
uses: docker/login-action@v4
with:
registry: registry-toska.ext.ocp-prod-0.k8s.it.helsinki.fi
username: ${{ secrets.username }}
password: ${{ secrets.password }}
- name: 'Let backend run migrations, then push new version of image to toska hub'
env:
ACCESS_KEY: ${{ secrets.s3_access_key }}
SECRET_KEY: ${{ secrets.s3_secret_key }}
run: |
docker compose --file docker-compose.ci.yml up --detach --quiet-pull backend --wait
sleep 30 # wait for backend to start and run migrations
sudo apt update -y && sudo apt install s3cmd -y
./anonyymioodi/manage_test_dbs.sh release user-db
- name: Teardown containers started by compose
if: always()
uses: ./.github/actions/teardown
# === Update sis-db with updater ===
check_updater_changed:
name: 'Check if updater has been updated'
runs-on: ${{ vars.RUNS_ON || 'ubuntu-latest' }}
timeout-minutes: 15
if: |
!cancelled() && !failure()
outputs:
has_updater_changed: ${{ steps.check_updater_changed.outputs.has_updater_changed }}
steps:
- uses: actions/checkout@v7
- uses: dorny/paths-filter@v4
id: check_updater_changed
with:
filters: |
has_updater_changed:
- 'updater/**'
- '.github/workflows/test.yaml'
- '.github/workflows/staging.yaml'
- 'docker-compose.ci.yml'
run_updater_updates:
name: 'Run updater tests and updates'
runs-on: ${{ vars.RUNS_ON || 'ubuntu-latest' }}
timeout-minutes: 15
if: |
!cancelled() && !failure() &&
(needs.check_updater_changed.outputs.has_updater_changed == 'true' ||
needs.check_migration_files.outputs.sis_db_migrations == 'true')
needs:
- check_migration_files
- check_updater_changed
- build_and_push_anon_sis_db
steps:
- uses: actions/checkout@v7
- name: 'Login to toska docker registry'
uses: docker/login-action@v4
with:
registry: registry-toska.ext.ocp-prod-0.k8s.it.helsinki.fi
username: ${{ secrets.username }}
password: ${{ secrets.password }}
- name: Start worker containers
run: docker compose -f docker-compose.ci.yml --profile updater up sis-updater-worker --scale sis-updater-worker=3 --quiet-pull --detach --wait
- name: Run hourly jobs
run: docker compose -f docker-compose.ci.yml --profile updater up sis-updater-scheduler --exit-code-from sis-updater-scheduler
env:
SCHEDULE_IMMEDIATE: hourly
EXIT_AFTER_IMMEDIATES: yes
- name: Run weekly jobs
run: docker compose -f docker-compose.ci.yml --profile updater up sis-updater-scheduler --exit-code-from sis-updater-scheduler
env:
SCHEDULE_IMMEDIATE: weekly
EXIT_AFTER_IMMEDIATES: yes
# TODO: Push the updated sis-db to toska-hub when updater is deterministic with test-data
- name: Push newly build db to toska hub
if: |
success() && inputs.skip_db_update != 'true'
env:
ACCESS_KEY: ${{ secrets.s3_access_key }}
SECRET_KEY: ${{ secrets.s3_secret_key }}
run: |
sudo apt update -y && sudo apt install s3cmd -y
./anonyymioodi/manage_test_dbs.sh release sis-db
- name: Dump logs on failure
if: failure()
run: docker compose --file docker-compose.ci.yml --profile updater logs
- name: Teardown containers started by compose
if: always()
uses: ./.github/actions/teardown
# === Backend api-tests ===
test_api:
name: 'Run backend api tests'
runs-on: ${{ vars.RUNS_ON || 'ubuntu-latest' }}
timeout-minutes: 15
if: |
!cancelled() && !failure()
needs:
- run_updater_updates
- build_and_push_anon_user_db
- build_and_push_anon_kone_db
steps:
- uses: actions/checkout@v7
- name: Run api tests
uses: ./.github/actions/test/api
with:
username: ${{ secrets.username }}
password: ${{ secrets.password }}
#
# === Backend service-tests ===
test_services:
name: 'Run backend service-tests'
runs-on: ${{ vars.RUNS_ON || 'ubuntu-latest' }}
timeout-minutes: 15
if: |
!cancelled() && !failure()
needs:
- run_updater_updates
- build_and_push_anon_user_db
- build_and_push_anon_kone_db
steps:
- uses: actions/checkout@v7
- name: Run service-tests
uses: ./.github/actions/test/services
with:
username: ${{ secrets.username }}
password: ${{ secrets.password }}
# === Unit testing ===
test_unit:
name: 'Run unit tests'
runs-on: ${{ vars.RUNS_ON || 'ubuntu-latest' }}
timeout-minutes: 15
if: |
!cancelled() && !failure()
steps:
- uses: actions/checkout@v7
- name: Run unit tests (frontend and backend)
uses: ./.github/actions/test/unit
# === Updater unit testing ===
test_updater_unit:
name: 'Run updater unit tests'
runs-on: ${{ vars.RUNS_ON || 'ubuntu-latest' }}
timeout-minutes: 15
if: |
!cancelled() && !failure()
steps:
- uses: actions/checkout@v7
- name: Run updater unit tests
uses: ./.github/actions/test/updater-unit
# === Frontend component testing ===
test_components:
name: 'Test frontend components'
runs-on: ${{ vars.RUNS_ON || 'ubuntu-latest' }}
timeout-minutes: 15
if: |
!cancelled() && !failure()
needs:
- run_updater_updates
- build_and_push_anon_user_db
- build_and_push_anon_kone_db
steps:
- uses: actions/checkout@v7
- name: Run frontend component tests
uses: ./.github/actions/test/component
# === End-to-end tests ===
test_end_to_end_playwright:
runs-on: ${{ vars.RUNS_ON || 'ubuntu-latest' }}
timeout-minutes: 15
if: |
!cancelled() && !failure()
needs:
- run_updater_updates
- build_and_push_anon_user_db
- build_and_push_anon_kone_db
strategy:
fail-fast: false
matrix:
include:
- name: 'Population statistics'
spec: e2e/Population_statistics.spec.ts
- name: 'Course statistics'
spec: e2e/Course_statistics.spec.ts
- name: 'Degree programme overview'
spec: e2e/Degree_programme_overview.spec.ts
- name: 'faculty statistics'
spec: e2e/Faculty_statistics.spec.ts
- name: 'Teachers'
spec: e2e/Teachers.spec.ts
- name: 'Users'
spec: e2e/Users.spec.ts
- name: 'Access rights'
spec: e2e/Routes.spec.ts
- name: 'University'
spec: e2e/University.spec.ts
- name: 'Language center view'
spec: e2e/Language_center.spec.ts
- name: 'Study guidance groups'
spec: e2e/Study_guidance_groups.spec.ts
- name: 'Custom population'
spec: e2e/Custom_population.spec.ts
- name: 'Filters'
spec: e2e/Filters.spec.ts
- name: 'Students'
spec: e2e/Students.spec.ts
- name: 'Completed courses'
spec: e2e/Completed_courses.spec.ts
- name: 'Numbers match'
spec: e2e/Numbers_match.spec.ts
name: ${{ matrix.name }}
steps:
- name: Checkout
uses: actions/checkout@v7
- name: Install node-24 for playwright
uses: actions/setup-node@v7
with:
node-version: 24
- name: Install dependencies
run: |
npm ci
- name: Install Playwright Browsers
run: npx playwright install firefox --with-deps
- name: Run tests
uses: ./.github/actions/test/e2e-playwright
with:
username: ${{ secrets.username }}
password: ${{ secrets.password }}
spec: ${{ matrix.spec }}
test_name: ${{ matrix.name }}
# === Other tests ===
test_service_exit_statuses:
name: 'Check services run correctly'
runs-on: ${{ vars.RUNS_ON || 'ubuntu-latest' }}
timeout-minutes: 15
if: |
!cancelled() && !failure()
needs:
- run_updater_updates
- build_and_push_anon_user_db
- build_and_push_anon_kone_db
steps:
- uses: actions/checkout@v7
- name: 'Login to toska docker registry'
uses: docker/login-action@v4
with:
registry: registry-toska.ext.ocp-prod-0.k8s.it.helsinki.fi
username: ${{ secrets.username }}
password: ${{ secrets.password }}
- name: 'Run services, build rest of the needed images'
run: docker compose --file docker-compose.ci.yml --profile oodikone up --detach --quiet-pull --wait
- 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: npm run logs:ci && sleep 15
-174
View File
@@ -1,174 +0,0 @@
name: 'Test (and release) Updater'
on:
release:
types: [published]
paths:
- '.github/workflows/updater.yaml'
- 'updater/**'
jobs:
lint_scheduler_and_worker:
name: 'Lint scheduler and worker'
runs-on: ${{ vars.RUNS_ON || 'ubuntu-latest' }}
timeout-minutes: 15
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: 24
- name: 'Install dependencies'
run: npm ci && npm ci --prefix updater/sis-updater-scheduler && npm ci --prefix updater/sis-updater-worker
- name: 'Run linter'
run: npm run lint -- updater
test_updater_unit:
name: 'Run updater unit tests'
runs-on: ${{ vars.RUNS_ON || 'ubuntu-latest' }}
timeout-minutes: 15
steps:
- uses: actions/checkout@v7
- name: Run updater unit tests
uses: ./.github/actions/test/updater-unit
build_worker:
name: 'Build Updater worker'
runs-on: ${{ vars.RUNS_ON || 'ubuntu-latest' }}
timeout-minutes: 15
steps:
- uses: actions/checkout@v7
- uses: docker/setup-buildx-action@v4
- name: 'Build the image'
uses: docker/build-push-action@v7
with:
context: updater/sis-updater-worker/
push: false
outputs: type=docker,dest=/tmp/worker.tar
tags: toska/sis-updater-worker:latest
env:
DOCKER_BUILD_SUMMARY: false
- name: 'Upload image for other jobs'
uses: actions/upload-artifact@v7
with:
name: updater-worker-image
path: /tmp/worker.tar
retention-days: 7
build_scheduler:
name: 'Build Updater scheduler'
runs-on: ${{ vars.RUNS_ON || 'ubuntu-latest' }}
timeout-minutes: 15
steps:
- uses: actions/checkout@v7
- uses: docker/setup-buildx-action@v4
- name: 'Build the image'
uses: docker/build-push-action@v7
with:
context: updater/sis-updater-scheduler/
push: false
outputs: type=docker,dest=/tmp/scheduler.tar
tags: toska/sis-updater-scheduler:latest
env:
DOCKER_BUILD_SUMMARY: false
- name: 'Upload image for other jobs'
uses: actions/upload-artifact@v7
with:
name: updater-scheduler-image
path: /tmp/scheduler.tar
retention-days: 7
test_updater:
name: 'Test Updater'
runs-on: ${{ vars.RUNS_ON || 'ubuntu-latest' }}
timeout-minutes: 15
needs:
- lint_scheduler_and_worker
- test_updater_unit
- build_worker
- build_scheduler
steps:
- uses: actions/checkout@v7
- name: 'Login to toska docker registry'
uses: docker/login-action@v4
with:
registry: registry-toska.ext.ocp-prod-0.k8s.it.helsinki.fi
username: ${{ secrets.TOSKAREGISTRY_USERNAME }}
password: ${{ secrets.TOSKAREGISTRY_PASSWORD }}
- name: 'Download built images'
uses: actions/download-artifact@v8
with:
pattern: 'updater-{worker,scheduler}-image'
merge-multiple: true
- name: 'Load worker image'
run: docker load --input worker.tar
- name: 'Load scheduler image'
run: docker load --input scheduler.tar
- name: 'Start worker'
run: docker compose --file docker-compose.ci.yml up --detach --quiet-pull --no-recreate --wait sis-updater-worker
- name: 'Run hourly jobs'
run: docker compose -f docker-compose.ci.yml --profile updater up sis-updater-scheduler --exit-code-from sis-updater-scheduler
env:
SCHEDULE_IMMEDIATE: hourly
EXIT_AFTER_IMMEDIATES: yes
- name: 'Run weekly jobs'
run: docker compose -f docker-compose.ci.yml --profile updater up sis-updater-scheduler --exit-code-from sis-updater-scheduler
env:
SCHEDULE_IMMEDIATE: weekly
EXIT_AFTER_IMMEDIATES: yes
- name: 'Run prepurge'
run: docker compose -f docker-compose.ci.yml --profile updater up sis-updater-scheduler --exit-code-from sis-updater-scheduler
env:
SCHEDULE_IMMEDIATE: prepurge
EXIT_AFTER_IMMEDIATES: yes
- name: 'Run purge'
run: docker compose -f docker-compose.ci.yml --profile updater up sis-updater-scheduler --exit-code-from sis-updater-scheduler
env:
SCHEDULE_IMMEDIATE: purge
EXIT_AFTER_IMMEDIATES: yes
release_worker:
name: 'Build and release Updater worker'
runs-on: ${{ vars.RUNS_ON || 'ubuntu-latest' }}
timeout-minutes: 15
needs: test_updater
steps:
- uses: actions/checkout@v7
- name: 'Build updater worker'
id: build-image
uses: ./.github/actions/build/worker
with:
environment: 'production'
release_version: ${{ github.sha }}
push: true
registry_password: ${{ secrets.QUAY_IO_TOKEN }}
release_scheduler:
name: 'Build and release Updater scheduler'
runs-on: ${{ vars.RUNS_ON || 'ubuntu-latest' }}
timeout-minutes: 15
needs: test_updater
steps:
- uses: actions/checkout@v7
- name: 'Build udpater scheduler'
id: build-image
uses: ./.github/actions/build/scheduler
with:
environment: 'production'
release_version: ${{ github.sha }}
push: true
registry_password: ${{ secrets.QUAY_IO_TOKEN }}
+26 -70
View File
@@ -1,74 +1,30 @@
# Logs
logs
*.log
# See https://help.github.com/ignore-files/ for more about ignoring files.
# dependencies
/node_modules
# testing
/coverage
# production
/build
dist
# IDE metadata
.vscode
.idea
*.iml
# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*
test-results
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Production
dist/
# Dependency directories
jspm_packages/
node_modules/
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Misc
*.sqz
# Environment variable files
.env
.env.test
.env.production
# Oodikone related stuff
.databasedumps/
# Cypress
cypress/downloads/
cypress/screenshots/
cypress/videos/
# Code coverage
.nyc_output/
coverage/
instrumented/
# Pycharm
.idea
# VS Code
.vscode/settings.json
.vscode/tasks.json
.vscode/launch.json
.vscode/terminals.json
# Target of shared symlink
services/**/src/shared
!services/backend/.env.test
#Stats
stats.json
-9
View File
@@ -1,9 +0,0 @@
npx lint-staged
if git grep -qE --cached "(it|describe)\.only" $(git diff --cached --name-only --diff-filter=ACMR | xargs); then
echo "\e[0;31mDo not commit .only-tests."
exit 1
else
echo "No .only-tests found"
exit 0
fi
-32
View File
@@ -1,32 +0,0 @@
import { relative } from 'path'
const cwd = process.cwd()
const dockerCmdBase = `docker run --rm --volume ${cwd}:/oodikone --workdir /oodikone`
const relativeFilePaths = files => [...files.map(file => relative(cwd, file))].join(' ')
export default {
// INFO: oxlint --type-check is equivalent to running tsc --noEmit
'{services,updater}/**/*.{js,jsx,ts,tsx}': files =>
`oxlint --fix --type-aware --type-check --no-error-on-unmatched-pattern --quiet ${files.join(' ')}`,
'*.{js,jsx,ts,tsx,json,md,yml,yaml,html,css}': files => `oxfmt --no-error-on-unmatched-pattern ${files.join(' ')} `,
'*.css': files => `stylelint --fix ${files.join(' ')}`,
Dockerfile: files =>
`${dockerCmdBase} hadolint/hadolint:v2.14.0 hadolint --ignore DL3006 ${relativeFilePaths(files)}`,
'*.sh': files => `${dockerCmdBase} koalaman/shellcheck:v0.11.0 ${relativeFilePaths(files)} -x`,
'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`)
},
}
-3
View File
@@ -1,3 +0,0 @@
ignore-scripts=true
min-release-age=7
save-prefix=
-14
View File
@@ -1,14 +0,0 @@
{
"$schema": "./node_modules/oxfmt/configuration_schema.json",
"semi": false,
"arrowParens": "avoid",
"singleQuote": true,
"printWidth": 120,
"tabWidth": 2,
"useTabs": false,
"jsxSingleQuote": false,
"trailingComma": "es5",
"endOfLine": "lf",
"sortPackageJson": false,
"ignorePatterns": []
}
-56
View File
@@ -1,56 +0,0 @@
module.exports = {
meta: {
name: 'oodikone',
},
rules: {
'no-date-now': {
meta: {
type: 'suggestion',
docs: {
description: 'Require the backend clock helper instead of Date.now()',
},
schema: [],
messages: {
useNow: 'Use now().getTime() instead of Date.now() so the current time can be controlled in tests.',
},
},
create(context) {
return {
CallExpression(node) {
if (
node.callee.type === 'MemberExpression' &&
node.callee.object.type === 'Identifier' &&
node.callee.object.name === 'Date' &&
node.callee.property.type === 'Identifier' &&
node.callee.property.name === 'now' &&
node.arguments.length === 0
) {
context.report({ node, messageId: 'useNow' })
}
},
}
},
},
'no-new-date': {
meta: {
type: 'suggestion',
docs: {
description: 'Require the backend clock helper for the current date',
},
schema: [],
messages: {
useNow: 'Use now() instead of new Date() so the current time can be controlled in tests.',
},
},
create(context) {
return {
NewExpression(node) {
if (node.callee.type === 'Identifier' && node.callee.name === 'Date' && node.arguments.length === 0) {
context.report({ node, messageId: 'useNow' })
}
},
}
},
},
},
}
-401
View File
@@ -1,401 +0,0 @@
{
"$schema": "./node_modules/oxlint/configuration_schema.json",
"plugins": ["import", "typescript"],
"jsPlugins": ["./.oxlint-backend-rules.cjs"],
"categories": {
"correctness": "off"
},
"options": {
"typeAware": true,
"typeCheck": true
},
"env": {
"builtin": true,
"es2021": true,
"browser": true,
"node": true
},
"settings": {
"import-x/extensions": [".ts", ".tsx", ".cts", ".mts", ".js", ".jsx", ".cjs", ".mjs"],
"import-x/external-module-folders": ["node_modules", "node_modules/@types"],
"import-x/parsers": {
"@typescript-eslint/parser": [".ts", ".tsx", ".cts", ".mts"]
},
"import-x/resolver": {
"typescript": true
},
"import-x/resolver-next": [
{
"interfaceVersion": 3,
"name": "eslint-import-resolver-typescript"
},
{
"interfaceVersion": 3,
"name": "eslint-plugin-import-x:node"
}
]
},
"ignorePatterns": [
"eslint.config.mjs",
".oxlint-backend-rules.cjs",
"**/build/**",
"**/dist/**",
"**/node_modules/**",
"**/instrumented/**",
"**/coverage/**",
"nyc-config.cjs",
".lintstagedrc.mjs",
"playwright/**/*.js",
"playwright.config.ts"
],
"rules": {
"constructor-super": "error",
"for-direction": "error",
"getter-return": "error",
"no-async-promise-executor": "error",
"no-case-declarations": "error",
"no-class-assign": "error",
"no-compare-neg-zero": "error",
"no-cond-assign": "error",
"no-const-assign": "error",
"no-constant-binary-expression": "error",
"no-constant-condition": "error",
"no-control-regex": "error",
"no-debugger": "error",
"no-delete-var": "error",
"no-dupe-class-members": "error",
"no-dupe-else-if": "error",
"no-dupe-keys": "error",
"no-duplicate-case": "error",
"no-empty": "error",
"no-empty-character-class": "error",
"no-empty-pattern": "error",
"no-empty-static-block": "error",
"no-ex-assign": "error",
"no-extra-boolean-cast": "error",
"no-fallthrough": "error",
"no-func-assign": "error",
"no-global-assign": "error",
"no-import-assign": "error",
"no-invalid-regexp": "error",
"no-irregular-whitespace": "error",
"no-loss-of-precision": "error",
"no-misleading-character-class": "error",
"no-new-native-nonconstructor": "error",
"no-nonoctal-decimal-escape": "error",
"no-obj-calls": "error",
"no-prototype-builtins": "error",
"no-redeclare": "error",
"no-regex-spaces": "error",
"no-self-assign": "error",
"no-setter-return": "error",
"no-shadow-restricted-names": "error",
"no-sparse-arrays": "error",
"no-this-before-super": "error",
"no-undef": "error",
"no-unexpected-multiline": "error",
"no-unreachable": "error",
"no-unsafe-finally": "error",
"no-unsafe-negation": "error",
"no-unsafe-optional-chaining": "error",
"no-unused-labels": "error",
"no-unused-private-class-members": "error",
"no-unused-vars": [
"error",
{
"argsIgnorePattern": "^_",
"caughtErrors": "none",
"ignoreRestSiblings": true,
"varsIgnorePattern": "^_"
}
],
"no-useless-backreference": "error",
"no-useless-catch": "error",
"no-useless-escape": "error",
"no-with": "error",
"require-yield": "error",
"use-isnan": "error",
"valid-typeof": "error",
"no-array-constructor": "error",
"no-unused-expressions": "error",
"no-empty-function": "error",
"class-methods-use-this": "error",
"no-alert": "error",
"no-await-in-loop": "error",
"no-console": "error",
"no-param-reassign": "error",
"no-promise-executor-return": "error",
"no-return-assign": "error",
"object-shorthand": ["error", "always"],
"prefer-const": "error",
"no-restricted-imports": [
"error",
{
"patterns": [
{
"regex": "^@mui/[^/]+$",
"message": "Import only the needed module. This has no effect in prod due to tree-shake, but speeds up development builds."
},
{
"regex": "^src/.*",
"message": "Root imports will not resolve in production build (instead silently crash)."
}
]
}
],
"import/namespace": "error",
"import/default": "error",
"import/export": "error",
"import/no-named-as-default": "warn",
"import/no-named-as-default-member": "warn",
"import/no-duplicates": "warn",
"import/no-commonjs": "error",
"import/no-default-export": "error",
"typescript/await-thenable": "error",
"typescript/ban-ts-comment": "error",
"typescript/no-array-delete": "error",
"typescript/no-base-to-string": "error",
"typescript/no-duplicate-enum-values": "error",
"typescript/no-duplicate-type-constituents": "error",
"typescript/no-empty-object-type": [
"error",
{
"allowInterfaces": "with-single-extends"
}
],
"typescript/no-extra-non-null-assertion": "error",
"typescript/no-floating-promises": "error",
"typescript/no-for-in-array": "error",
"typescript/no-implied-eval": "error",
"typescript/no-misused-new": "error",
"typescript/no-misused-promises": "error",
"typescript/no-namespace": "error",
"typescript/no-non-null-asserted-optional-chain": "error",
"typescript/no-redundant-type-constituents": "error",
"typescript/no-require-imports": "error",
"typescript/no-this-alias": "error",
"typescript/no-unnecessary-type-assertion": "error",
"typescript/no-unnecessary-type-constraint": "error",
"typescript/no-unsafe-declaration-merging": "error",
"typescript/no-unsafe-enum-comparison": "error",
"typescript/no-unsafe-function-type": "error",
"typescript/no-unsafe-unary-minus": "error",
"typescript/no-wrapper-object-types": "error",
"typescript/only-throw-error": "error",
"typescript/prefer-as-const": "error",
"typescript/prefer-namespace-keyword": "error",
"typescript/prefer-promise-reject-errors": "error",
"typescript/require-await": "error",
"typescript/restrict-plus-operands": "error",
"typescript/triple-slash-reference": "error",
"typescript/unbound-method": "error",
"typescript/adjacent-overload-signatures": "error",
"typescript/ban-tslint-comment": "error",
"typescript/class-literal-property-style": "error",
"typescript/consistent-generic-constructors": "error",
"typescript/consistent-indexed-object-style": "error",
"typescript/consistent-type-assertions": "error",
"typescript/dot-notation": "error",
"typescript/no-confusing-non-null-assertion": "error",
"typescript/no-inferrable-types": "error",
"typescript/non-nullable-type-assertion-style": "error",
"typescript/prefer-find": "error",
"typescript/prefer-for-of": "error",
"typescript/prefer-function-type": "error",
"typescript/prefer-includes": "error",
"typescript/prefer-nullish-coalescing": "warn",
"typescript/prefer-optional-chain": "error",
"typescript/prefer-regexp-exec": "error",
"typescript/prefer-string-starts-ends-with": "error",
"typescript/consistent-return": "error"
},
"overrides": [
{
"files": ["**/*.ts", "**/*.tsx", "**/*.mts", "**/*.cts"],
"rules": {
"constructor-super": "off",
"getter-return": "off",
"no-class-assign": "off",
"no-const-assign": "off",
"no-dupe-class-members": "off",
"no-dupe-keys": "off",
"no-func-assign": "off",
"no-import-assign": "off",
"no-new-native-nonconstructor": "off",
"no-obj-calls": "off",
"no-redeclare": "off",
"no-setter-return": "off",
"no-this-before-super": "off",
"no-undef": "off",
"no-unreachable": "off",
"no-unsafe-negation": "off",
"no-var": "error",
"no-with": "off",
"prefer-rest-params": "error",
"prefer-spread": "error"
}
},
{
"files": ["services/backend/src/**/*.{js,ts,tsx}"],
"excludeFiles": ["services/backend/src/util/clock.ts"],
"rules": {
"oodikone/no-new-date": "error",
"oodikone/no-date-now": "error"
}
},
{
"files": ["services/backend/src/routes/*.{ts,js}"],
"rules": {
"typescript/no-misused-promises": "off",
"typescript/consistent-return": "off"
}
},
{
"files": ["{updater,services}/**/*.{js,cjs}"],
"rules": {
"typescript/await-thenable": "off",
"typescript/consistent-return": "off",
"typescript/consistent-type-exports": "off",
"typescript/dot-notation": "off",
"typescript/no-array-delete": "off",
"typescript/no-base-to-string": "off",
"typescript/no-confusing-void-expression": "off",
"typescript/no-deprecated": "off",
"typescript/no-duplicate-type-constituents": "off",
"typescript/no-floating-promises": "off",
"typescript/no-for-in-array": "off",
"typescript/no-implied-eval": "off",
"typescript/no-meaningless-void-operator": "off",
"typescript/no-misused-promises": "off",
"typescript/no-misused-spread": "off",
"typescript/no-mixed-enums": "off",
"typescript/no-redundant-type-constituents": "off",
"typescript/no-unnecessary-boolean-literal-compare": "off",
"typescript/no-unnecessary-condition": "off",
"typescript/no-unnecessary-qualifier": "off",
"typescript/no-unnecessary-template-expression": "off",
"typescript/no-unnecessary-type-arguments": "off",
"typescript/no-unnecessary-type-assertion": "off",
"typescript/no-unnecessary-type-conversion": "off",
"typescript/no-unnecessary-type-parameters": "off",
"typescript/no-unsafe-enum-comparison": "off",
"typescript/no-unsafe-type-assertion": "off",
"typescript/no-unsafe-unary-minus": "off",
"typescript/non-nullable-type-assertion-style": "off",
"typescript/only-throw-error": "off",
"typescript/prefer-find": "off",
"typescript/prefer-includes": "off",
"typescript/prefer-nullish-coalescing": "off",
"typescript/prefer-optional-chain": "off",
"typescript/prefer-promise-reject-errors": "off",
"typescript/prefer-readonly": "off",
"typescript/prefer-readonly-parameter-types": "off",
"typescript/prefer-reduce-type-parameter": "off",
"typescript/prefer-regexp-exec": "off",
"typescript/prefer-return-this-type": "off",
"typescript/prefer-string-starts-ends-with": "off",
"typescript/promise-function-async": "off",
"typescript/related-getter-setter-pairs": "off",
"typescript/require-array-sort-compare": "off",
"typescript/require-await": "off",
"typescript/restrict-plus-operands": "off",
"typescript/return-await": "off",
"typescript/strict-boolean-expressions": "off",
"typescript/switch-exhaustiveness-check": "off",
"typescript/unbound-method": "off",
"typescript/use-unknown-in-catch-callback-variable": "off"
}
},
{
"files": ["services/frontend/**/*.{js,jsx,mjs,cjs,ts,tsx}"],
"rules": {
"react/react-compiler": "warn",
"react/display-name": "error",
"react/jsx-key": "error",
"react/jsx-no-comment-textnodes": "error",
"react/jsx-no-duplicate-props": "error",
"react/jsx-no-target-blank": "error",
"react/jsx-no-undef": "error",
"react/no-children-prop": "error",
"react/no-danger-with-children": "error",
"react/no-direct-mutation-state": "error",
"react/no-find-dom-node": "error",
"react/no-is-mounted": "error",
"react/no-render-return-value": "error",
"react/no-string-refs": "error",
"react/no-unescaped-entities": "off",
"react/no-unknown-property": "error",
"react/no-unsafe": "off",
"react/react-in-jsx-scope": "off",
"react/require-render-return": "error",
"react/rules-of-hooks": "error",
"react/exhaustive-deps": "warn",
"react/no-array-index-key": "error",
"react/no-unstable-nested-components": "warn",
"react/no-this-in-sfc": "error",
"react/jsx-boolean-value": ["error", "never"],
"react/jsx-filename-extension": [
"error",
{
"allow": "as-needed",
"extensions": [".jsx", ".tsx"]
}
],
"react/jsx-no-useless-fragment": "error"
},
"plugins": ["react"]
},
{
"files": ["services/backend/tests/**/*.ts"],
"rules": {
"no-console": "off"
}
},
{
"files": [
"services/backend/**/*.js",
"updater/sis-updater-worker/src/db/migrations/*.cjs",
"updater/sis-updater-scheduler/**/*.js"
],
"rules": {
"import/no-commonjs": "off",
"typescript/no-require-imports": "off"
}
},
{
"files": ["updater/sis-updater-worker/src/db/migrations/*.cjs", "services/backend/src/database/**/*.js"],
"rules": {
"no-empty-function": "off",
"typescript/require-await": "off"
}
},
{
"files": ["services/backend/**/*.{js,ts}", "updater/**/*.js"],
"rules": {
"no-await-in-loop": "off",
"no-promise-executor-return": "off",
"import/no-default-export": "off"
}
},
{
"files": ["e2e/**/*.ts"],
"rules": {
"no-await-in-loop": "off"
}
},
{
"files": [
"services/frontend/tests/**/*.{ts,tsx}",
"services/frontend/playwright/**/*.{ts,tsx}",
"services/frontend/playwright-ct.config.ts"
],
"rules": {
"react/jsx-filename-extension": "off",
"import/no-default-export": "off"
},
"plugins": ["react"]
}
]
}
+12 -2
View File
@@ -1,6 +1,16 @@
{
"extends": ["stylelint-config-standard"],
"extends": "stylelint-config-standard",
"rules": {
"selector-class-pattern": "^(?:[a-z][a-zA-Z-]*|Mui[A-Za-z-]+)$"
"selector-type-no-unknown": [true, {
"ignoreTypes": ["/^--/", "--"]
}],
"selector-pseudo-class-no-unknown": [
true,
{
"ignorePseudoClasses": [
"global"
]
}
]
}
}
+34
View File
@@ -0,0 +1,34 @@
language: node_js
node_js:
- '8'
cache:
directories:
- node_modules
addons:
chrome: stable
sudo: required
before_install:
- "/sbin/start-stop-daemon --start --quiet --pidfile /tmp/custom_xvfb_99.pid --make-pidfile --background --exec /usr/bin/Xvfb -- :99 -ac -screen 0 1280x1024x16"
- gem install superbara
before_script:
- "export DISPLAY=:99.0"
- "sh -e /etc/init.d/xvfb start"
- sleep 3 # give xvfb some time to start
services:
- docker
- redis-server
after_success:
- "[[ $TRAVIS_BRANCH =~ (trunk) ]] && docker build -t toska/oodikone2-frontend:testing ."
- "[[ $TRAVIS_BRANCH =~ (trunk) ]] && docker login -u $DOCKER_USER -p $DOCKER_PASS"
- "[[ $TRAVIS_BRANCH =~ (trunk) ]] && docker push toska/oodikone2-frontend:testing"
- "[[ $TRAVIS_BRANCH =~ (^master) ]] && docker build -t toska/oodikone2-frontend:staging ."
- "[[ $TRAVIS_BRANCH =~ (^master) ]] && docker login -u $DOCKER_USER -p $DOCKER_PASS"
- "[[ $TRAVIS_BRANCH =~ (^master) ]] && docker push toska/oodikone2-frontend:staging"
- "[[ $TRAVIS_TAG =~ ([0-1].+) ]] && docker build -t toska/oodikone2-frontend:latest ."
- "[[ $TRAVIS_TAG =~ ([0-1].+) ]] && docker login -u $DOCKER_USER -p $DOCKER_PASS"
- "[[ $TRAVIS_TAG =~ ([0-1].+) ]] && docker push toska/oodikone2-frontend:latest"
-63
View File
@@ -1,63 +0,0 @@
# AGENTS.md
Oodikone is a Docker-based Node.js/TypeScript application for analysing University of Helsinki study data. Main areas are:
- `services/frontend`: React, Vite, Redux Toolkit, and Material UI.
- `services/backend`: Express, Sequelize, PostgreSQL, Redis, and BullMQ.
- `services/shared`: shared types and utilities.
- `updater/`: Sisu data workers and scheduler.
- `e2e/`: Playwright e2e tests
Use Node.js 24 or newer. Packages have separate dependencies and lockfiles. Install locally with
`npm run install:local`, or use `npm run ci:local` for lockfile-exact installs. Docker uses host-mounted locks:
update dependencies locally, then run `npm run ci:docker`; do not install in containers.
## Development Rules
- Read nearby code and tests before changing behavior; follow local conventions.
- Keep changes focused and avoid unrelated formatting.
- Reuse `services/shared` for cross-service types and utilities.
- Never commit `.env` files, credentials, database dumps, or real student data.
- Use anonymized data. Do not reset real databases, use destructive commands, or use real-data Docker profiles unless
explicitly required.
- Treat migrations as production-impacting. They are in `services/backend/src/database/migrations_kone`,
`services/backend/src/database/migrations_user`, and `updater/sis-updater-worker/src/db/migrations`; test migration
changes against the relevant database service.
- Do not access the database directly (e.g. `docker exec` into a database container, `psql`, Adminer, or similar).
Go through the application's models or services instead or ask if there is questions about the data.
- Add tests, especially unit tests, when adding or changing behavior.
## Local Commands
```text
npm run oodikone # start the anonymized environment
npm run oodikone:ci # start the test mode
npm run both # start Oodikone and the updater
npm run docker:down # stop containers
docker compose ps # show service status
```
The app runs at `http://localhost:3000` and Adminer at `http://localhost:5050`. Follow logs with
`docker compose logs --follow frontend` or `docker compose logs --follow backend`.
## Style And Verification
- TypeScript/JavaScript: 2 spaces, single quotes, no semicolons, trailing commas where valid, 120-character lines.
- Prefer explicit types at package boundaries, API payloads, Redux state, and database interfaces. Avoid `any` except
where needed to contain an untyped external API.
- Run `npm run fmt` on changed source, `npm run lint`, and `npm run stylelint` when CSS changes. Do not bypass the
type-aware Oxlint/Oxfmt pre-commit hooks.
- Run the narrowest relevant check, then broader checks as needed:
- Frontend: `npm run build --prefix services/frontend`, `npm run test:unit --prefix services/frontend`, or
`npm run test:component --prefix services/frontend`.
- Backend: `npm run build --prefix services/backend`, or `npm run test:unit --prefix services/backend`,
`npm run test:services --prefix services/backend`, or `npm run test:api --prefix services/backend`.
- Shared: `npm run test --prefix services/shared`.
- E2E: `npm run playwright -- e2e/<spec>.spec.ts`.
- If running `docker-compose.ci.yml` add env variable `CI=true` before the e2e npm command.
Otherwise some tests fail.
- API, service, component, and E2E tests require Docker; start the relevant environment with `npm run oodikone:test`
or its documented profile. Report any checks that could not run and why.
CI also validates Docker Compose files, Dockerfiles, shell scripts, workflows, and migrations. Keep such changes
minimal and validate them locally where practical.
+14
View File
@@ -0,0 +1,14 @@
FROM node:8.9.4
RUN mkdir -p /usr/src/app
COPY . /usr/src/app
WORKDIR /usr/src/app
RUN npm i
RUN npm run dist
RUN npm install -g serve
EXPOSE 5000
CMD ["serve", "-s", "-l", "5000", "dist"]
-19
View File
@@ -1,19 +0,0 @@
Copyright (c) 2024 University of Helsinki
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+18 -257
View File
@@ -1,270 +1,31 @@
# Oodikone
# OodiKone2-Frontend
Master:
[![Build Status](https://travis-ci.org/UniversityOfHelsinkiCS/oodikone2-frontend.svg?branch=master)](https://travis-ci.org/UniversityOfHelsinkiCS/oodikone2-frontend)
[![Maintainability](https://qlty.sh/gh/UniversityOfHelsinkiCS/projects/oodikone/maintainability.svg)](https://qlty.sh/gh/UniversityOfHelsinkiCS/projects/oodikone)
Trunk:
[![Build Status](https://travis-ci.org/UniversityOfHelsinkiCS/oodikone2-frontend.svg?branch=trunk)](https://travis-ci.org/UniversityOfHelsinkiCS/oodikone2-frontend)
An application for analyzing university data, running at [https://oodikone.helsinki.fi/](https://oodikone.helsinki.fi/).
## ✔️ Requirements
### Development
fetch dependencies: `npm install`
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 `1.29+`)
- [npm](https://docs.npmjs.com/cli/v7) (version `7+`)
2. Access to Toska Docker Hub
3. For real data setup, access to Toska's production servers
4. For some features to work, access to Toska's service tokens
## 🚀 Installation
Pre-installation steps:
- Clone this repo
- `git@github.com:UniversityOfHelsinkiCS/oodikone.git`
- Login to Toska's Docker Hub (see Toska's GitLab for more information)
- Copy the `.env.template` file to `.env` and populate with correct tokens
- `cp .env.template .env`
Then launch the CLI and follow the instructions:
```bash
npm run cli
```
_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
- Setup happens with anonymized data, which developers should use by default
- **Note:** Running this option cleans up all real data too, so please don' 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 a single database
- Like option 2, but only for single database
- Leaves other databases untouched
4. Restore data from dumps
- Like option 2, but skips downloading, and simply restores databases from the dumps downloaded earlier
5. Docker system prune
- Runs `docker system prune -a && docker volume prune -a`
- This will remove all your Docker data, also related to any other applications than Oodikone!
- Sometimes necessary after strange errors due to caching
6. Clear node_nodules
- Deletes "nodemod" docker volumes and all local node_modules from each service
- Useful when resolving permission/installation/library issues without the need to reset all data
- Runs `npm ci` for every service locally
## ⌨️ Development
### Architecture
```mermaid
graph TB
subgraph Oodikone & Updater
subgraph Oodikone
client[React app] --> server[NodeJS server]
server --> ok_redis(Redis)
konedb[(kone-db)]
userdb[(user-db)]
end
server -->|Read| sisdb[(sis-db)]
subgraph Updater
updaterscheduler[Scheduler] --> |Push BullMQ jobs| redis(Redis)
updaterworker[Worker] -->|Write| sisdb
updaterworker --> |Fetch BullMQ jobs| redis
end
end
subgraph Common Toska services
jami[JAMI]
pate[Pate]
importer[Importer]
end
updaterworker -->|Read db| importer
updaterscheduler -->|Read db| importer
importer -->|Fetch data| sisu[Sisu export APIs]
server -->|Get IAM access| jami
server -->|Send mail| pate
subgraph Analytics
direction TB
sentry[Sentry]
graylog[Graylog]
grafana[Grafana]
end
```
In development, backend and updater use the same Redis, but in production they are separate.
See also the [older graph](documentation/img/oodikone.png) for more information.
To read a bit more details about what updater-scheduler and updater-worker do, see their own READMEs:
- [updater-scheduler's README](./updater/sis-updater-scheduler/README.md)
- [updater-worker's README](./updater/sis-updater-worker/README.md)
### Documentation
The `documentation` directory contains some notes on the sis-db schema and is meant as a place for storing useful information on Oodikone to make life easier for future developers. Feel free to add anything relevant!
For internal meeting notes and sensitive information, see Toska GitLab.
### Basics
The development environment is entirely configured in the `docker-compose.yml` file located in this repository. The file defines the services for Oodikone's two main components: **oodikone** and **updater**.
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 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
- `npm run oodikone:test`: starts Oodikone with non-persistent database (wipes after every reset)
- `npm run oodikone:ci`: starts Oodikone in CI mode
- `npm run updater`: starts updater with anonymized data
- `npm run updater:real`: starts updater with real data
- `npm run both`: starts Oodikone and updater with anonymized data
- `npm run both:real`: starts Oodikone and updater with real data
- `npm run docker:down`: stops the whole environment
- `npm run testupdater`: runs tests for updater
- `npm run flushredis`: clears Redis, which forces big calculations to be redone in e.g. studyprogramme overview and faculty views
- `npm run install:local`: installs `node_modules` for each package (backend, frontend, sis-updater-scheduler and sis-updater-worker) locally
- `npm run ci:local`: same as above, but sticks to the package versions pinned in package-lock
- `npm run ci:docker`: installs `node_modules` for each package inside containers
- `npm run ci:both`: runs both `npm run ci:local` and `npm run ci:docker`
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
As said, the development environment runs entirely inside Docker containers. To keep `package.json` clean and not filled with predefined scripts, we have created a simple helper script called `./run.sh`. The script allows you to use docker-compose commands without the need to write long list of parameters. Try to run `./run.sh` in the root of the project and see what happens!
`./run.sh` is simply a wrapper script to run Oodikone, updater or both services in either anon or real mode. If you take a look at `package.json`, you can see that most of the predefined scripts above use `./run.sh` under the hood.
It is recommended to spend some time to become familiar with `docker` and `docker compose` cli commands. You can then use them directly or with `./run.sh` wrapper. Here is some examples for day-to-day development situations:
- `./run.sh oodikone anon pull`: pull all images related to Oodikone development
- `./run.sh updater real up --build --force-recreate --detach`: start updater detached (in the background) in real data mode, but build new images before starting
- `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 oodikone-backend sh`: open bash terminal inside backend container
- `docker exec -it oodikone-sis-db psql -U postgres sis-db-real`: open psql client to investigate sis real data database
### User types for development
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 the "Users" page, click "edit" on the user you want to use and then click the 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.
Anonymous 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).
local development: `npm run dev`
### Linting
Install eslint: `npm install -g eslint`
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`.
lint js & css: `npm run lint`
lint only js: `npm run lint:js`
lint only css: `npm run lint:css`
lint fix for js: `npm run lintfix`
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.
Pre-commit hooks will fix auto-fixable problems. To set up quick formatting: In VSCode you can go to command palette, select "ESLint: Fix all auto-fixable problems" and assign a hotkey for it, like `SHIFT + CTRL + '.'`.
### Building
`npm run dist`
## 🔨 Testing & CI
Builds optimized production package to /dist
We use Playwright for end-to-end testing.
### Deployment
Playwright
Read part deployment from [backend README](https://github.com/UniversityOfHelsinkiCS/oodikone2-backend/)
- E2E specs are in the `e2e/` folder.
- Run all e2e specs with `npm run playwright`.
- Run in headed mode with `npm run playwright:headed`.
- Open in UI mode with `npm run playwright:ui`.
- If using a command that runs `docker-compose.ci.yml`, remember to add CI=true before the playwright command, eg. `CI=true npm run playwright`.
Continuous integration (CI) works with Github actions and is defined in workflow files in `.github/workflows` folder:
- Oodikone setup for e2e 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 a successful test run, Oodikone is deployed to staging
- After creating a release, Oodikone is deployed to production
- Updater is deployed to production when pushed if tests pass
- Test databases can be updated, instructions in [anonyymioodi README](anonyymioodi/README.md)
## ❓FAQ
### How to install a new package
Install the package locally to the relevant service with `npm install` followed by the script `npm run ci:docker`. The application running inside container uses your host-mounted package-lock; therefore when making any changes to packages do them first locally. Avoid running `npm install` inside docker containers. Our docker images run as non-root, and cannot write on the host mounted package-lock files (without sudo'ing and changing back the permissions later). The CLI has an option to clear all node_modules should the need arise.
### Modules are missing after an update to package.json
Module might be missing for example when someone else installs a new library and you pull the changes to the package files, without installing the acual packages. To sync the node_modules with the package files run `npm run ci:both`.
### Study guidance groups don't work on my machine
Make sure you have your VPN on.
### Everything is broken, can't get oodikone running, data is not there etc.
First: Try `npm run cli` option 1: _Set up oodikone from scratch_ and then option 2 to download and restore databases (or option 4 to skip downloading, if you've done it recently).
If that does not help, try option 5: _Docker system prune_. Notice that this clears up **all Docker data**, including data related to other projects. If this is not ok, you'll need to identify the Oodikone-specific Docker volumes yourself and caches and clear those invididually.
### How to run tests locally faster
Vite simplifies development but can be **very slow** when running tests.
To speed up testing, you can use Vite's preview mode. This will make the tests run faster, but keep in mind that you must rebuild the frontend code every time you make changes to it. To use the preview mode, follow these steps:
1. Run `npm run build` in the `services/frontend` directory.
2. Run `npm run preview` in the `services/frontend` directory and keep it running.
3. Set `PLAYWRIGHT_BASE_URL=http://localhost:4173` for Playwright.
4. Start Oodikone with `npm run oodikone`.
After these steps, you can run your tests:
- Use `npm run playwright:ui` to open Playwright UI and run tests interactively.
Tests can also be run in headless mode, similar to how they are run in the CI/CD pipeline. To do this, use:
```bash
npm run playwright -- <file path>
```
For example:
```bash
npm run playwright -- e2e/Language_center.js
```
Running tests in headless mode may also help if your browser keeps crashing when running tests interactively. If you want to run all tests, use:
```bash
npm run playwright
```
## How to read clients minds
Search for client queries in grafana using a variation of this query:
```
{app="oodikone"} | regexp `\s(?P<payload>\{.*\}$)` | line_format `{{.payload}}` | json userId, mockedBy | userId = `client` | mockedBy != `admin`
```
For quick access to _any and all params_ under certain path:
```
{app="oodikone"} |= `path` | regexp `\s(?P<payload>\{.*\}$)` | line_format `{{.payload}}` | json userId, mockedBy | userId = `client` | mockedBy != `admin`
```
## ✌🏼 Maintainers and contribution
[Toska](https://toska.dev) - University of Helsinki
## 🛡️ Tietosuoja / data protection
See [tietosuoja.md](./documentation/tietosuoja.md).
Binary file not shown.
-9
View File
@@ -1,9 +0,0 @@
<?php
require_once('plugins/login-password-less.php');
/** Set allowed password
* @param string result of password_hash
*/
return new AdminerLoginPasswordLess(
$password_hash = password_hash("postgres", PASSWORD_DEFAULT)
);
-15
View File
@@ -1,15 +0,0 @@
<?php
require_once('plugins/login-servers.php');
$driver = "pgsql";
/** Set supported servers
* @param array array($description => array("server" => , "driver" => "server|pgsql|sqlite|..."))
*/
return new AdminerLoginServers(
$servers = array(
"kone-db" => array("server" => "kone-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)
)
);
-96
View File
@@ -1,96 +0,0 @@
# Anonyymioodi - test database setup
This folder contains a guide and scripts to modify or create anonymous database images for Oodikone, mainly used for testing.
The quick guide should be enough for most purposes: updating schema, data, or the PostgreSQL version. If you need to generate completely new `sis-importer-db` and `sis-db` images (which shouldn't happen too often), follow the instructions in the section [Generate new sis-importer-db and sis-db completely from scratch](#generate-new-sis-importer-db-and-sis-db-completely-from-scratch).
## Scripts
`manage_test_dbs.sh` is the front end for the test database tooling. Run it without arguments for an interactive menu, or give it an action and one or more databases (or `all`) to run it directly:
```bash
./manage_test_dbs.sh dump sis-db # dump sis-db from its running container
./manage_test_dbs.sh upload sis-db # upload the current dump to s3
./manage_test_dbs.sh release all # dump, upload and push images for every database
```
Actions are `download`, `dump`, `upload` (upload + prune), `push`, `prune`, `release` (dump + upload + prune + push) and `rebuild` (download + push). Each one is a thin call to a script in this folder, which can also be used on its own:
| Script | What it does |
| -------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
| `dump_local_db.sh` | `pg_dump`s the databases from the running containers into `.databasedumps/` |
| `download_dump_from_s3.sh` | Downloads the newest dump of each database from s3 (`-b <branch>` to pick dumps from a single branch) |
| `upload_dump_to_s3.sh` | Uploads the dumps in `.databasedumps/test` to s3 as `<database>-<git_branch>-<datetime>.sql` |
| `prune_s3_dumps.sh` | Deletes dumps older than 30 days from s3, always keeping the newest `master` dump of each database (`-d <days>`, `-n`) |
| `build_and_push_image.sh` | Builds database images from the current dumps and pushes them to the Toska registry |
| `create_s3_bucket.sh` | Creates the s3 bucket if it doesn't exist yet, run automatically before an upload |
| `lib/common.sh` | Shared configuration (dump directories, s3 bucket, registry, database names) |
Dumps end up in `.databasedumps/test/<database>.sql`, which is where `db.Dockerfile` reads them from when an image is built. The s3 commands read their credentials from the `ACCESS_KEY` and `SECRET_KEY` environment variables (e.g. in CI). If neither is set, they fall back to `~/.s3cfg`, which can be found on the toska/dokumentaatio repo.
## Quick guide to updating a test database
1. Ensure you have the latest test-db. Just pulling images might not replace your existing volume, so you may need to remove it manually. This command might work: `docker volume rm oodikone-sis-db-data` (replace `oodikone-sis-db-data` with the correct volume name). The new image, including data, should be pulled the next time you launch Oodikone normally.
2. Do your dev work as usual with the test database, for example, using `npm run both`. Edit data as needed, (for example, run the updater via the frontend), and then run tests to see if they pass (follow the instructions in the [README](../README.md) to run the tests more quickly).
3. If you're updating `sis-db`:
- Modify the scheduler limit in [config.js](../updater/sis-updater-scheduler/src/config.js) by setting `module.exports.DEV_SCHEDULE_COUNT = null`.
- Follow the `sis-updater-worker` logs to ensure everything is running smoothly (`docker logs --follow oodikone-sis-updater-worker`).
4. When you're satisfied with the database, create a dump:
- Use the command: `docker exec -i <dbname> pg_dump -Fc -U postgres <dbname> > anonyymioodi/<dbname>.sqz`.
5. Shut down the environment with `npm run docker:down`.
6. Navigate to this folder: `cd anonyymioodi`.
7. If you need to update the PostgreSQL version, do it now by changing the version in this directory's `docker-compose.yml`. The version should match the one used on the server to avoid compatibility issues.
8. Create a new container from the dump: `./create_container_from_dump.sh <dbname>`.
9. Commit and push it to the Toska registry: `./commit_and_push_to_toska_hub.sh <dbname>`.
10. Bring down the environment with `docker compose down`.
## Generate new sis-importer-db and sis-db completely from scratch
> [!CAUTION]
> The more the data in `sis-importer-db` and `sis-db` changes, the more tests might break. Because of this, it's highly recommended to make as few changes as possible (for example, avoid changing the degree programs whose students are used in the test data).
>
> If you only need to make small changes, follow the quick guide above.
### Sis-importer-db
1. To generate a sample dump from test Sisu data, follow the instructions at [importer-db-staging-sampletaker](https://github.com/UniversityOfHelsinkiCS/sis-importer/tree/master/importer-db-staging-sampletaker) inside the `sis-importer` repo.
2. Copy `sis-importer-db.sqz` into this folder.
3. Run the following commands (in this folder) to create a new image from the dump and push it to the Toska registry:
```bash
./create_container_from_dump.sh sis-importer-db
./commit_and_push_to_toska_hub.sh sis-importer-db
docker compose down
```
### Sis-db
1. To ensure the newest `sis-importer-db` image is used:
- Delete all `sis-importer-db` images you have locally (list images with `docker images`, delete with `docker rmi <image ID>`).
- Also, delete the old `sis-importer-db` volume (`docker volume rm sis-importer-db-data`).
2. Start Oodikone and Updater with `npm run both` (this will pull the newest `sis-importer-db` image you just pushed in the previous step).
3. Truncate all tables in `sis-db` **_except for `migrations` and `semesters`_** (the easiest way is to use Adminer).
4. Modify the scheduler limit in [config.js](../updater/sis-updater-scheduler/src/config.js) by setting `module.exports.DEV_SCHEDULE_COUNT = null`.
5. Navigate to <http://localhost:3000/updater> and click the three buttons (Update meta, Update students, Update curriculums) one by one. This will populate `sis-db` with data from the new `sis-importer-db`. You might want to do each update a couple of times, as the database tables affect each other, and not all necessary data might be generated on the first try.
6. Follow the `sis-updater-worker` logs to ensure everything is running smoothly (`docker logs --follow oodikone-sis-updater-worker`). If you see any errors, try clicking the buttons again.
7. Run all the tests to ensure everything is working as expected (follow the instructions in the [README](../README.md) to run the tests more quickly). If any tests fail, update the test files accordingly.
-54
View File
@@ -1,54 +0,0 @@
#!/usr/bin/env bash
# Build database images from the dumps in .databasedumps/test and push them to
# the Toska registry.
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source-path=SCRIPTDIR
# shellcheck source=lib/common.sh
source "$SCRIPT_DIR"/lib/common.sh
usage() {
cat >&2 <<EOF
Usage: $(basename "$0") <database>... | all
Builds an image of each given database and pushes it to $REGISTRY.
The dump is read from .databasedumps/test/<database>.sql, so run
dump_local_db.sh or download_dump_from_s3.sh first.
Databases: ${DATABASES[*]}
EOF
}
if [[ "${1-}" == "-h" || "${1-}" == "--help" ]]; then
usage
exit 0
fi
if [[ $# -eq 0 ]]; then
usage
die "No database given"
fi
build_and_push_image() {
local database="$1"
local image="$REGISTRY/$database:latest"
local dump_file="$TEST_DUMP_DIR/$database.sql"
[[ -f "$dump_file" ]] || die "No dump found at $dump_file, run dump_local_db.sh or download_dump_from_s3.sh first"
infomsg "Building $image"
docker build "$PROJECT_ROOT" -f "$PROJECT_ROOT/db.Dockerfile" -t "$image" --build-arg DB_NAME="$database"
infomsg "Pushing $image"
docker push "$image"
successmsg "Pushed $image"
}
resolve_databases "$@"
for database in "${selected_databases[@]}"; do
build_and_push_image "$database"
done
@@ -1,48 +0,0 @@
#!/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 "oodikone-$database" "registry-toska.ext.ocp-prod-0.k8s.it.helsinki.fi/$database"
docker push "registry-toska.ext.ocp-prod-0.k8s.it.helsinki.fi/$database"
successmsg "Succesfully pushed"
@@ -1,64 +0,0 @@
#!/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 "oodikone-$database" /bin/bash -c "pg_restore --username=postgres \
--format=custom --dbname=$database --no-owner" < "$database_dump"
successmsg "Database $database succesfully created"
-36
View File
@@ -1,36 +0,0 @@
#!/usr/bin/env bash
# Create the s3 bucket used for the test database dumps, if it doesn't exist yet.
# Doing nothing when the bucket is already there, so this is safe to run every time.
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source-path=SCRIPTDIR
# shellcheck source=lib/common.sh
source "$SCRIPT_DIR"/lib/common.sh
usage() {
cat >&2 <<EOF
Usage: $(basename "$0")
Creates the bucket $S3_BUCKET if it doesn't exist yet, and does nothing if it does.
EOF
}
if [[ "${1-}" == "-h" || "${1-}" == "--help" ]]; then
usage
exit 0
fi
require_s3_config
# s3cmd info exits with an error when the bucket doesn't exist
if s3 info "$S3_BUCKET" > /dev/null 2>&1; then
infomsg "Bucket $S3_BUCKET already exists"
exit 0
fi
warningmsg "Bucket $S3_BUCKET doesn't exist, creating it"
s3 mb "$S3_BUCKET"
successmsg "Created bucket $S3_BUCKET"
-59
View File
@@ -1,59 +0,0 @@
x-defaults: &defaults
restart: unless-stopped
x-healthcheck-defaults: &healthcheck-defaults
interval: 30s # Normal healthcehck interval
timeout: 5s
retries: 10 # Retries until SIGKILl
start_interval: 3s # How often healthcheck runs while starting
start_period: 15s # How long startup period is
services:
kone-db:
<<: *defaults
container_name: oodikone-kone-db
environment:
PGDATA: /data
POSTGRES_DB: kone-db
POSTGRES_HOST_AUTH_METHOD: trust
image: postgres:15.10
healthcheck:
test: ['CMD', 'pg_isready', '-U', 'postgres']
<<: *healthcheck-defaults
user-db:
<<: *defaults
container_name: oodikone-user-db
environment:
PGDATA: /data
POSTGRES_DB: user-db
POSTGRES_HOST_AUTH_METHOD: trust
image: postgres:15.10
healthcheck:
test: ['CMD', 'pg_isready', '-U', 'postgres']
<<: *healthcheck-defaults
sis-db:
<<: *defaults
container_name: oodikone-sis-db
environment:
PGDATA: /data
POSTGRES_DB: sis-db
POSTGRES_HOST_AUTH_METHOD: trust
image: postgres:15.10
shm_size: 2gb
healthcheck:
test: ['CMD', 'pg_isready', '-U', 'postgres']
<<: *healthcheck-defaults
sis-importer-db:
<<: *defaults
container_name: oodikone-sis-importer-db
environment:
PGDATA: /data
POSTGRES_DB: sis-importer-db
POSTGRES_HOST_AUTH_METHOD: trust
image: postgres:15.10
healthcheck:
test: ['CMD', 'pg_isready', '-U', 'postgres']
<<: *healthcheck-defaults
-93
View File
@@ -1,93 +0,0 @@
#!/usr/bin/env bash
# Download database dumps from s3 into .databasedumps/s3, and copy them to
# .databasedumps/test where db.Dockerfile reads them from.
#
# Dumps are uploaded as <db_name>-<git_branch>-<datetime>.sql (see
# upload_dump_to_s3.sh), so the newest matching dump is the one downloaded.
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source-path=SCRIPTDIR
# shellcheck source=lib/common.sh
source "$SCRIPT_DIR"/lib/common.sh
usage() {
cat >&2 <<EOF
Usage: $(basename "$0") [-b|--branch <branch>] <database>... | all
Downloads the newest dump of each given database from $S3_TARGET.
Databases: ${DATABASES[*]}
Options:
-b, --branch <branch> Only consider dumps uploaded from this git branch
-h, --help Show this help
EOF
}
branch=""
arguments=()
while [[ $# -gt 0 ]]; do
case "$1" in
-b | --branch)
branch="${2-}"
[[ -n "$branch" ]] || {
usage
die "Missing value for $1"
}
shift 2
;;
-h | --help)
usage
exit 0
;;
-*)
usage
die "Unknown option: $1"
;;
*)
arguments+=("$1")
shift
;;
esac
done
# Newest object in s3 for the given database, or empty if there are none
newest_dump_in_s3() {
local database="$1"
local prefix="$S3_TARGET/$database-"
if [[ -n "$branch" ]]; then prefix="$prefix$(sanitize_branch_name "$branch")-"; fi
# s3cmd ls prints "<date> <time> <size> <object>", so sorting by the first two
# columns in reverse puts the newest dump first
s3 ls "$prefix" | sort -rk1,2 | awk 'NR == 1 { print $4 }'
}
download_database() {
local database="$1"
local dump_file="$S3_DUMP_DIR/$database.sql"
local newest_dump
newest_dump="$(newest_dump_in_s3 "$database")"
[[ -n "$newest_dump" ]] || die "No dumps found for $database in $S3_TARGET${branch:+ (branch $branch)}"
infomsg "Downloading $newest_dump"
s3 get --force "$newest_dump" "$dump_file"
cp --remove-destination "$dump_file" "$TEST_DUMP_DIR/$database.sql"
successmsg "Downloaded $newest_dump to $dump_file"
}
if [[ ${#arguments[@]} -eq 0 ]]; then
usage
die "No database given"
fi
resolve_databases "${arguments[@]}"
require_s3_config
make_dump_dirs
for database in "${selected_databases[@]}"; do
download_database "$database"
done
-53
View File
@@ -1,53 +0,0 @@
#!/usr/bin/env bash
# Dump one or more databases from the running docker containers into
# .databasedumps/local_containers, and copy them to .databasedumps/test where
# db.Dockerfile reads them from.
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source-path=SCRIPTDIR
# shellcheck source=lib/common.sh
source "$SCRIPT_DIR"/lib/common.sh
usage() {
cat >&2 <<EOF
Usage: $(basename "$0") <database>... | all
Dumps the given databases from their running containers.
Databases: ${DATABASES[*]}
EOF
}
if [[ "${1-}" == "-h" || "${1-}" == "--help" ]]; then
usage
exit 0
fi
if [[ $# -eq 0 ]]; then
usage
die "No database given"
fi
dump_database() {
local database="$1"
local container_name="oodikone-$database"
local database_name
database_name="$(postgres_database_name "$database")"
local dump_file="$LOCAL_DUMP_DIR/$database.sql"
infomsg "Dumping $database from container $container_name"
# Dump to a temporary file first, so that a failed dump doesn't overwrite the previous one
docker exec -i "$container_name" pg_dump -Fp -U postgres -d "$database_name" > "$dump_file.tmp"
mv "$dump_file.tmp" "$dump_file"
cp --remove-destination "$dump_file" "$TEST_DUMP_DIR/$database.sql"
successmsg "Dumped $database to $dump_file"
}
resolve_databases "$@"
make_dump_dirs
for database in "${selected_databases[@]}"; do
dump_database "$database"
done
-133
View File
@@ -1,133 +0,0 @@
#!/usr/bin/env bash
# Shared configuration and helpers for the anonyymioodi database scripts.
# Meant to be sourced by the other scripts in this folder, not run on its own.
# The configuration below is used by the sourcing scripts, not by this file
# shellcheck disable=SC2034
PROJECT_ROOT="$(git rev-parse --show-toplevel)"
source "$PROJECT_ROOT"/scripts/utils.sh
# Dumps taken from running docker containers
LOCAL_DUMP_DIR="$PROJECT_ROOT/.databasedumps/local_containers"
# Dumps downloaded from s3
S3_DUMP_DIR="$PROJECT_ROOT/.databasedumps/s3"
# Where db.Dockerfile copies the .sql files from when an image is built
TEST_DUMP_DIR="$PROJECT_ROOT/.databasedumps/test"
# Used as a fallback when ACCESS_KEY and SECRET_KEY aren't set in the environment
S3_CONFIG_FILE=~/.s3cfg
S3_HOST="s3.datacloud.helsinki.fi"
S3_BUCKET="s3://oodikone"
# Dumps live under a prefix of their own inside the bucket
S3_TARGET="$S3_BUCKET/test-dbs"
# Dumps older than this are pruned from s3, except for the newest dump of each database
S3_DUMP_RETENTION_DAYS=30
REGISTRY="registry-toska.ext.ocp-prod-0.k8s.it.helsinki.fi"
# Databases, named after their docker compose services
DATABASES=("user-db" "kone-db" "sis-db" "sis-importer-db" "jami-db")
# Set by resolve_databases, read by the scripts sourcing this file
selected_databases=()
# The name of the postgres database inside the container. It matches the service
# name for every database except jami-db.
postgres_database_name() {
local database="$1"
if [[ "$database" == "jami-db" ]]; then
echo "postgres"
else
echo "$database"
fi
}
# Validates the given database names and expands "all" to every database.
# The result is set to the global array $selected_databases, because dying
# inside a subshell wouldn't stop the calling script.
resolve_databases() {
selected_databases=()
if [[ $# -eq 0 ]]; then
die "No database given, expected one or more of: ${DATABASES[*]} all"
fi
for argument in "$@"; do
if [[ "$argument" == "all" ]]; then
selected_databases=("${DATABASES[@]}")
return
fi
# Check if given databases are correct ones
local known=false
for database in "${DATABASES[@]}"; do
if [[ "$argument" == "$database" ]]; then known=true; fi
done
$known || die "Unknown database: $argument. Expected one or more of: ${DATABASES[*]} all"
selected_databases+=("$argument")
done
}
# Dumps in s3 are named after the branch they were uploaded from, so the branch
# name has to be usable as a part of a file name. E.g. feature/foo -> feature-foo.
# feature/foo would result in a new bucket "feature" with a sub-bucket "foo"
sanitize_branch_name() {
echo "${1//[^A-Za-z0-9._-]/-}"
}
current_branch_name() {
local branch
branch="$(git rev-parse --abbrev-ref HEAD)"
if [[ "$branch" == "HEAD" ]]; then # Detached head, use the commit instead
branch="$(git rev-parse --short HEAD)"
fi
sanitize_branch_name "$branch"
}
has_s3_env_credentials() {
[[ -n "${ACCESS_KEY-}" && -n "${SECRET_KEY-}" ]]
}
# Credentials come from ACCESS_KEY and SECRET_KEY when both are set, and from
# $S3_CONFIG_FILE when neither is
require_s3_config() {
command -v s3cmd > /dev/null || die "s3cmd not found, please install it first"
if [[ -n "${ACCESS_KEY-}" || -n "${SECRET_KEY-}" ]]; then
has_s3_env_credentials || die "Only one of ACCESS_KEY and SECRET_KEY is set, set both or neither"
return
fi
[[ -f "$S3_CONFIG_FILE" ]] || die "Set ACCESS_KEY and SECRET_KEY, or add $S3_CONFIG_FILE. The \".s3cfg\" config file can be found on toska/dokumentaatio repo"
}
s3_env_config() {
cat <<EOF
[default]
access_key = $ACCESS_KEY
secret_key = $SECRET_KEY
host_base = $S3_HOST
host_bucket = $S3_HOST
use_https = True
check_ssl_certificate = True
EOF
}
# Runs s3cmd with the credentials chosen by require_s3_config. The environment
# credentials are passed through process substitution, so that they aren't
# written to disk or visible in the process list.
s3() {
if has_s3_env_credentials; then
s3cmd -c <(s3_env_config) "$@"
else
s3cmd -c "$S3_CONFIG_FILE" "$@"
fi
}
# The dump directories are created lazily by the scripts that write into them
make_dump_dirs() {
mkdir -p "$LOCAL_DUMP_DIR" "$S3_DUMP_DIR" "$TEST_DUMP_DIR"
}
-155
View File
@@ -1,155 +0,0 @@
#!/usr/bin/env bash
# Front end for the test database scripts in this folder. Run it without
# arguments for an interactive menu, or give it an action and a database to run
# it directly, e.g. in CI.
#
# Each action is just a call to one of the scripts next to this one, so they can
# also be used on their own.
set -euo pipefail
# Here BASH_SOURCE[0] is used instead of $0 because it works
# consistently when ran normally and when sourced
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source-path=SCRIPTDIR
# shellcheck source=lib/common.sh
source "$SCRIPT_DIR"/lib/common.sh
ACTIONS=("download" "dump" "upload" "push" "prune" "release" "rebuild")
usage() {
cat >&2 <<EOF
Usage: $(basename "$0") [<action> <database>... | all]
Runs without arguments for an interactive menu.
Actions:
download Download the newest dumps from s3
dump Dump the databases from the running docker containers
upload Upload the current dumps to s3, then prune old dumps
push Build images from the current dumps and push them to the registry
prune Delete dumps older than $S3_DUMP_RETENTION_DAYS days from s3, keeping the newest one
release Dump from the running containers, then upload, prune and push (shorthand)
rebuild Download the dumps from s3, then push (shorthand)
Databases: ${DATABASES[*]} all
EOF
}
# === Actions ===
download_dump() {
"$SCRIPT_DIR"/download_dump_from_s3.sh "$1"
}
dump_database() {
"$SCRIPT_DIR"/dump_local_db.sh "$1"
}
upload_dump() {
"$SCRIPT_DIR"/upload_dump_to_s3.sh "$1"
}
prune_dumps() {
"$SCRIPT_DIR"/prune_s3_dumps.sh "$1"
}
push_image() {
"$SCRIPT_DIR"/build_and_push_image.sh "$1"
}
run_action() {
local action="$1"
local database="$2"
case "$action" in
download) download_dump "$database" ;;
dump) dump_database "$database" ;;
upload)
upload_dump "$database"
prune_dumps "$database"
;;
push) push_image "$database" ;;
prune) prune_dumps "$database" ;;
release)
dump_database "$database"
upload_dump "$database"
prune_dumps "$database"
push_image "$database"
;;
rebuild)
download_dump "$database"
push_image "$database"
;;
*)
usage
die "Unknown action: $action"
;;
esac
}
run_action_for_all() {
local action="$1"
shift
# Check if command was a proper command
local known_action=false
for known in "${ACTIONS[@]}"; do
if [[ "$action" == "$known" ]]; then known_action=true; fi
done
if ! $known_action; then
usage
die "Unknown action: $action"
fi
resolve_databases "$@"
for database in "${selected_databases[@]}"; do
run_action "$action" "$database"
done
successmsg "Action $action done for: ${selected_databases[*]}"
}
# === Interactive menu ===
ask_and_run() {
local action database
PS3="Select action: "
select action in "${ACTIONS[@]}" "quit"; do
[[ -n "${action-}" ]] || continue
if [[ "$action" == "quit" ]]; then exit 0; fi
break
done
PS3="Select database: "
select database in "${DATABASES[@]}" "all"; do
[[ -n "${database-}" ]] || continue
break
done
run_action_for_all "$action" "$database"
}
# === Run ===
if [[ "${1-}" == "-h" || "${1-}" == "--help" ]]; then
usage
exit 0
fi
if [[ $# -eq 0 ]]; then
while true; do
ask_and_run
echo
done
fi
if [[ $# -lt 2 ]]; then
usage
die "Both an action and a database are required"
fi
run_action_for_all "$@"
-113
View File
@@ -1,113 +0,0 @@
#!/usr/bin/env bash
# Delete old database dumps from s3. Dumps older than the retention period are
# removed, but the newest master dump of each database is always kept, so that a
# database which hasn't changed in a while can still be downloaded. Dumps from
# other branches aren't kept, so that old branches don't accumulate dumps.
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source-path=SCRIPTDIR
# shellcheck source=lib/common.sh
source "$SCRIPT_DIR"/lib/common.sh
# The newest dump of each database from this branch is never pruned
MASTER_BRANCH="master"
usage() {
cat >&2 <<EOF
Usage: $(basename "$0") [-n|--dry-run] [-d|--days <days>] <database>... | all
Deletes the dumps in $S3_TARGET older than the given number of days
(default $S3_DUMP_RETENTION_DAYS). The newest $MASTER_BRANCH dump of each database is
always kept, or the newest dump of any branch if the database has no $MASTER_BRANCH dumps.
Databases: ${DATABASES[*]}
Options:
-n, --dry-run Print the dumps that would be deleted instead of deleting them
-d, --days <days> Delete dumps older than this many days
-h, --help Show this help
EOF
}
dry_run=false
days="$S3_DUMP_RETENTION_DAYS"
arguments=()
while [[ $# -gt 0 ]]; do
case "$1" in
-n | --dry-run)
dry_run=true
shift
;;
-d | --days)
days="${2-}"
[[ "$days" =~ ^[0-9]+$ ]] || {
usage
die "Expected a number of days for $1, got: $days"
}
shift 2
;;
-h | --help)
usage
exit 0
;;
-*)
usage
die "Unknown option: $1"
;;
*)
arguments+=("$1")
shift
;;
esac
done
if [[ ${#arguments[@]} -eq 0 ]]; then
usage
die "No database given"
fi
prune_database() {
local database="$1"
local deleted=0
# s3cmd ls prints "<date> <time> <size> <object>", so sorting by the first two
# columns in reverse puts the newest dump first
local dumps
dumps="$(s3 ls "$S3_TARGET/$database-" | sort -rk1,2)"
# Find the newest master dump or in the case of it missing, the newest dump in general
local kept
kept="$(awk -v pattern="/$database-$MASTER_BRANCH-[0-9]+-[0-9]+[.]sql$" '$4 ~ pattern { print $4; exit }' <<< "$dumps")"
[[ -n "$kept" ]] || kept="$(awk 'NR == 1 { print $4 }' <<< "$dumps")"
# Remove any dumps older than the dump retention date (or -d)
# except for master for which at least one dump is saved
while read -r date time _ dump; do
[[ -n "$dump" && "$dump" != "$kept" && "$date $time" < "$cutoff" ]] || continue
if [[ "$dry_run" == true ]]; then
infomsg "Dry run, would delete $dump"
else
s3 del "$dump"
fi
deleted=$((deleted + 1))
done <<< "$dumps"
if [[ "$dry_run" == true ]]; then
successmsg "Would have pruned $deleted dump(s) of $database older than $days days"
else
successmsg "Pruned $deleted dump(s) of $database older than $days days"
fi
}
resolve_databases "${arguments[@]}"
require_s3_config
cutoff="$(date -u -d "-$days days" '+%Y-%m-%d %H:%M')"
for database in "${selected_databases[@]}"; do
prune_database "$database"
done
-103
View File
@@ -1,103 +0,0 @@
#!/usr/bin/env bash
# This script will first make db dumps of the test environment and upload the dumps to staging
. ../scripts/utils.sh
set -euo pipefail
main() {
declare -r db_names="sis-db kone-db user-db"
declare -A db_url_names
db_url_names["sis-db"]="SIS_DB_URL"
db_url_names["kone-db"]="DB_URL_KONE"
db_url_names["user-db"]="DB_URL_USER"
declare -r dump_directory="dumps"
# Takes the first db-tools pod
check_oc_logged_staging
db_tools_name=$(oc get pods -l app=db-tools --output jsonpath='{.items[0].metadata.name}')
readonly db_tools_name
create_dumps
upload_and_migrate_dumps
}
function create_dumps() {
echo "Starting test/anon containers..."
(npm run docker:down && npm run both) || die "Failed to start containers"
echo
echo "Waiting for containers to start..."
sleep 10 # wait for containers to start
# Create dumps for all dbs
echo
echo "Creating dumps for $db_names"
echo
# Create dir if not exists
if [ ! -d $dump_directory ]; then mkdir -p $dump_directory; fi
for db in $db_names
do
create_dump_from_db "$db"
if [ ! -f "$dump_directory/$db.sqz" ]; then die "Db dump not created"; fi
done
echo "Dumps can be found in $PWD/\*.sqz"
}
# $1 db-name
function create_dump_from_db() {
echo "Dumping db $1..."
docker exec "oodikone-$1" pg_dump -Fc -U postgres "$1" > "$dump_directory/$1.sqz" || die "Failed to dump db $1"
}
function check_oc_logged_staging() {
echo -n "Checking if oc is logged into toska staging..."
[[ $(oc config current-context) == "toska/api-ocp-test-0-k8s-it-helsinki-fi"* ]] || die "\nNot logged in to toska staging environment: $(oc config current-context)"
echo " OK"
}
function upload_and_migrate_dumps() {
local target_dump_directory="$db_tools_name:/tmp/oodikone-staging-dumps"
echo -n "Dumping $dump_directory/ to $target_dump_directory..."
oc rsync "$dump_directory/" "$target_dump_directory" 1> /dev/null # show errors
echo " OK"
echo
for db in $db_names
do
migrate_dumps "$db"
done
}
function migrate_dumps() {
local db_url_b64
local db_url_raw
local db_url
db_url_b64=$(oc get secrets -l app.kubernetes.io/instance=oodikone --output jsonpath="{.items[0].data.${db_url_names[$1]}}")
db_url_raw=$(echo "$db_url_b64" | base64 --decode)
db_url="${db_url_raw%\?*}"
[[ $db_url != *"oodikone_*_test"* ]] || die "Trying to use non-test database: $db_url"
echo "Migrating $1 to url: $db_url"
oc exec "$db_tools_name" -- pg_restore --clean --no-owner -d "$db_url" "/tmp/oodikone-staging-dumps/$1.sqz"
}
# Run script
main
-90
View File
@@ -1,90 +0,0 @@
#!/usr/bin/env bash
# Upload database dumps from .databasedumps/test to s3, naming the objects
# <database>-<git_branch>-<datetime>.sql so that dumps don't overwrite each other.
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source-path=SCRIPTDIR
# shellcheck source=lib/common.sh
source "$SCRIPT_DIR"/lib/common.sh
usage() {
cat >&2 <<EOF
Usage: $(basename "$0") [-n|--dry-run] <database>... | all
Uploads the dumps in .databasedumps/test to $S3_TARGET
as <database>-<git_branch>-<datetime>.sql. Run dump_local_db.sh first to
create the dumps.
Databases: ${DATABASES[*]}
Options:
-n, --dry-run Print the s3cmd command instead of running it
-h, --help Show this help
EOF
}
dry_run=false
arguments=()
while [[ $# -gt 0 ]]; do
case "$1" in
-n | --dry-run)
dry_run=true
shift
;;
-h | --help)
usage
exit 0
;;
-*)
usage
die "Unknown option: $1"
;;
*)
arguments+=("$1")
shift
;;
esac
done
if [[ ${#arguments[@]} -eq 0 ]]; then
usage
die "No database given"
fi
upload_database() {
local database="$1"
local dump_file="$TEST_DUMP_DIR/$database.sql"
[[ -f "$dump_file" ]] || die "No dump found at $dump_file, run dump_local_db.sh first"
[[ -s "$dump_file" ]] || die "Dump is empty: $dump_file"
local datetime
datetime="$(date +%Y%m%d-%H%M%S)"
local target="$S3_TARGET/$database-$branch-$datetime.sql"
if [[ "$dry_run" == true ]]; then
infomsg "Dry run, would run: s3cmd put $dump_file $target"
return
fi
infomsg "Pushing $dump_file to $target"
s3 put "$dump_file" "$target"
successmsg "Uploaded $dump_file to $target"
}
resolve_databases "${arguments[@]}"
require_s3_config
if [[ "$dry_run" == false ]]; then
"$SCRIPT_DIR"/create_s3_bucket.sh
fi
branch="$(current_branch_name)"
for database in "${selected_databases[@]}"; do
upload_database "$database"
done
-91
View File
@@ -1,91 +0,0 @@
#!/usr/bin/env bash
# This script is used to setup oodikone with interactive cli. Base for script:
# https://betterdev.blog/minimal-safe-bash-script-template/
# === Config ===
# Fail immediately if script fails, unbound variables are referenced
# or command inside pipe fails. -E ensures cleanup trap fires in rare ERR cases.
set -euoE pipefail
# Set up constants
PROJECT_ROOT="$(git rev-parse --show-toplevel)"
# Source functions for setup
source "$PROJECT_ROOT"/scripts/functions_for_setup.sh
# Try to run cleanup function if things fail.
trap cleanup SIGINT SIGTERM ERR EXIT
# Run docker compose down on cleanup
cleanup() {
trap - SIGINT SIGTERM ERR EXIT
warningmsg "Trying to run docker compose down and remove orphans"
docker compose --profile oodikone --profile updater down --remove-orphans
}
# === CLI ===
show_welcome() {
local cashmoneyyellow
cashmoneyyellow=$(tput setaf 221)
local normal
normal=$(tput sgr0)
if [ "$(tput cols)" -gt "76" ]; then
while IFS="" read -r p || [ -n "$p" ]; do
printf '%40s\n' "${cashmoneyyellow}$p${normal}"
done < "$PROJECT_ROOT"/scripts/assets/logo.txt
fi
infomsg "Welcome to Oodikone CLI!"
msg "This tool helps you in managing the project configuration. If you are new to
Oodikone development, you should probably run \"Set up oodikone from scratch.\" which
will take care of setting up Oodikone for you. See README for more details."
msg ""
}
set_custom_select_prompt() {
PS3="Please enter your choice: "
options=(
"Set up Oodikone from scratch"
"Reset all data"
"Reset single database"
"Restore data from dumps"
"Docker system prune"
"Nuke local node_modules"
"Quit"
)
}
# Run scripts and set the prompt
show_welcome
init_dirs
get_s3_config
set_custom_select_prompt
while true; do
select opt in "${options[@]}"; do
case $opt in
"Set up Oodikone from scratch")
set_up_oodikone_from_scratch;;
"Reset all data")
reset_all_data;;
"Reset single database")
reset_single_database
set_custom_select_prompt # Set prompt back to initial values
;;
"Restore data from dumps")
restore_data_from_dumps;;
"Docker system prune")
docker_prune;;
"Nuke local node_modules")
nuke_node_modules;;
"Quit")
break 2;;
*) msg "${RED}Invalid option:${NOFORMAT} $REPLY
";;
esac
break
done
done
-19
View File
@@ -1,19 +0,0 @@
FROM postgres:15.10
# Remember to define this variable
ARG DB_NAME
ENV DB_NAME=$DB_NAME
ENV PGDATA=/data
ENV POSTGRES_USER=postgres
ENV POSTGRES_PASSWORD=postgres
ENV POSTGRES_HOST_AUTH_METHOD=trust
ENV POSTGRES_DB=$DB_NAME
# RUN apt-get update && apt-get upgrade -y && apt-get install -y --no-install-recommends pgcli
COPY ./.databasedumps/test/$DB_NAME.sql /docker-entrypoint-initdb.d/
EXPOSE 5432
HEALTHCHECK --interval=30s --timeout=5s --retries=10 --start-interval=3s --start-period=15s \
CMD pg_isready -U postgres -h 127.0.0.1
-243
View File
@@ -1,243 +0,0 @@
name: oodikone
x-defaults: &defaults
restart: unless-stopped
x-healthcheck-defaults: &healthcheck-defaults
interval: 30s # Normal healthcehck interval
timeout: 5s
retries: 10 # Retries until SIGKILl
start_interval: 3s # How often healthcheck runs while starting
start_period: 30s # How long startup period is
services:
adminer:
<<: *defaults
container_name: oodikone-adminer
environment:
ADMINER_DESIGN: pepa-linha
image: adminer:5.5.1
ports:
- 5050:8080
profiles:
- adminer
volumes:
- ./adminer-plugins:/var/www/html/plugins-enabled
backend:
<<: *defaults
container_name: oodikone-backend
image: oodikone-backend:staging
build:
context: services
dockerfile: backend/Dockerfile
depends_on:
kone-db:
condition: service_healthy
redis:
condition: service_healthy
sis-db:
condition: service_healthy
user-db:
condition: service_healthy
environment:
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
IMPORTER_DB_API_URL: 'https://api-toska.apps.ocp-prod-0.k8s.it.helsinki.fi/importer'
JAMI_URL: 'http://jami:3003/'
CRYPT_KEY: 'e16730fce263d5270ba8dcf84d068a32556542455e71331880d6c1c5e4db9b4e'
ports:
- 8080:8080
profiles:
- oodikone
healthcheck:
test: ['CMD', 'curl', '-f', 'http://localhost:8080/health'] # In CI/prod the routing happens at a different level, hence no /api/ needed
<<: *healthcheck-defaults
frontend:
<<: *defaults
container_name: oodikone-frontend
image: oodikone-frontend:staging
build:
context: services
dockerfile: frontend/Dockerfile
depends_on:
backend:
condition: service_healthy
ports:
- 5000:5000
profiles:
- oodikone
kone-db:
<<: *defaults
container_name: oodikone-kone-db
image: registry-toska.ext.ocp-prod-0.k8s.it.helsinki.fi/kone-db
environment:
PGDATA: /data
POSTGRES_DB: kone-db
POSTGRES_HOST_AUTH_METHOD: trust
ports:
- 4321:5432
healthcheck:
test: ['CMD', 'pg_isready', '-U', 'postgres', '-h', '127.0.0.1']
<<: *healthcheck-defaults
user-db:
<<: *defaults
container_name: oodikone-user-db
image: registry-toska.ext.ocp-prod-0.k8s.it.helsinki.fi/user-db
environment:
PGDATA: /data
POSTGRES_DB: user-db
POSTGRES_HOST_AUTH_METHOD: trust
ports:
- 4320:5432
healthcheck:
test: ['CMD', 'pg_isready', '-U', 'postgres', '-h', '127.0.0.1']
<<: *healthcheck-defaults
redis:
<<: *defaults
container_name: oodikone-redis
image: redis:7.2.6
command: >
sh -c 'redis-server --appendonly yes && redis-cli flushall'
ports:
- 6379:6379
healthcheck:
test: ['CMD', 'redis-cli', 'ping', '|', 'grep', 'PONG']
<<: *healthcheck-defaults
sis-db:
<<: *defaults
container_name: oodikone-sis-db
image: registry-toska.ext.ocp-prod-0.k8s.it.helsinki.fi/sis-db
environment:
PGDATA: /data
POSTGRES_DB: sis-db
POSTGRES_HOST_AUTH_METHOD: trust
shm_size: 2gb
ports:
- 12345:5432
healthcheck:
test: ['CMD', 'pg_isready', '-U', 'postgres', '-h', '127.0.0.1']
<<: *healthcheck-defaults
sis-importer-db:
<<: *defaults
container_name: oodikone-sis-importer-db
image: registry-toska.ext.ocp-prod-0.k8s.it.helsinki.fi/sis-importer-db
profiles:
- updater
ports:
- 12346:5432
healthcheck:
test: ['CMD', 'pg_isready', '-U', 'postgres', '-h', '127.0.0.1']
<<: *healthcheck-defaults
jami:
<<: *defaults
container_name: oodikone-jami
image: quay.io/toska/jami:production
depends_on:
jami-db:
condition: service_healthy
environment:
- npm_config_cache=/tmp
- PORT=3003
- NODE_ENV=test # previously was development which borks CI
- DATABASE_URL=postgres://postgres:postgres@jami-db:5432/postgres
ports:
- 3003:3003
jami-db:
<<: *defaults
container_name: oodikone-jami-db
image: registry-toska.ext.ocp-prod-0.k8s.it.helsinki.fi/jami-db
environment:
- PGDATA=/data
- POSTGRES_PASSWORD=postgres
ports:
- 5433:5432
healthcheck:
test: ['CMD', 'pg_isready', '-U', 'postgres', '-h', '127.0.0.1']
<<: *healthcheck-defaults
# This is still needed to make backend calls correctly route from localhost:5000/api to localhost:8080
nginx:
container_name: oodikone-nginx
image: nginx:latest
depends_on:
frontend:
condition: service_started
backend:
condition: service_healthy
ports:
- 1337:80
profiles:
- oodikone
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf
# Following services + sis-importer-db 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-updater-scheduler:
restart: no
container_name: oodikone-updater-scheduler
build:
context: updater/sis-updater-scheduler
dockerfile: ./Dockerfile
depends_on:
redis:
condition: service_healthy
sis-importer-db:
condition: service_healthy
environment:
CI: 'true'
REDIS_HOST: redis
SECRET_TOKEN: dev
SIS_IMPORTER_DATABASE: sis-importer-db
SIS_IMPORTER_HOST: sis-importer-db
SIS_IMPORTER_PASSWORD: postgres
SIS_IMPORTER_USER: postgres
SCHEDULE_IMMEDIATE: '${SCHEDULE_IMMEDIATE}'
EXIT_AFTER_IMMEDIATES: '${EXIT_AFTER_IMMEDIATES}'
ports:
- 8082:8082
profiles:
- updater
healthcheck:
test: ['CMD', 'curl', '-f', 'http://localhost:8082/health']
<<: *healthcheck-defaults
sis-updater-worker:
<<: *defaults
# Disabled so that container can be scaled
# container_name: oodikone-updater-worker
build:
context: updater/sis-updater-worker
dockerfile: ./Dockerfile
depends_on:
redis:
condition: service_healthy
sis-db:
condition: service_healthy
sis-importer-db:
condition: service_healthy
environment:
CI: 'true'
DB_URL: postgres://postgres@sis-db:5432/sis-db
REDIS_HOST: redis
SIS_IMPORTER_DATABASE: sis-importer-db
SIS_IMPORTER_HOST: sis-importer-db
SIS_IMPORTER_PASSWORD: postgres
SIS_IMPORTER_USER: postgres
SIS_PASSWORD: postgres
profiles:
- updater
-75
View File
@@ -1,75 +0,0 @@
# This compose file is used update test dbs
x-defaults: &defaults
restart: unless-stopped
volumes: !reset [] # Remove previous volume definitions
x-healthcheck-defaults: &healthcheck-defaults
test: ['CMD', 'pg_isready', '-U', 'postgres']
interval: 30s # Normal healthcehck interval
timeout: 5s
retries: 10 # Retries until SIGKILl
start_interval: 3s # How often healthcheck runs while starting
start_period: 15s # How long startup period is
services:
kone-db:
<<: *defaults
container_name: oodikone-kone-db
build:
context: .
dockerfile: db.Dockerfile
args:
DB_NAME: kone-db
environment:
PGDATA: /data
POSTGRES_DB: kone-db
POSTGRES_HOST_AUTH_METHOD: trust
healthcheck:
<<: *healthcheck-defaults
user-db:
<<: *defaults
container_name: oodikone-user-db
build:
context: .
dockerfile: db.Dockerfile
args:
DB_NAME: user-db
environment:
PGDATA: /data
POSTGRES_DB: user-db
POSTGRES_HOST_AUTH_METHOD: trust
healthcheck:
<<: *healthcheck-defaults
sis-db:
<<: *defaults
container_name: oodikone-sis-db
build:
context: .
dockerfile: db.Dockerfile
args:
DB_NAME: sis-db
environment:
PGDATA: /data
POSTGRES_DB: sis-db
POSTGRES_HOST_AUTH_METHOD: trust
shm_size: 2gb
healthcheck:
<<: *healthcheck-defaults
sis-importer-db:
<<: *defaults
container_name: oodikone-sis-importer-db
build:
context: .
dockerfile: db.Dockerfile
args:
DB_NAME: sis-importer-db
environment:
PGDATA: /data
POSTGRES_DB: sis-importer-db
POSTGRES_HOST_AUTH_METHOD: trust
healthcheck:
<<: *healthcheck-defaults
-25
View File
@@ -1,25 +0,0 @@
name: oodikone
services:
backend:
environment:
DB_URL_USER: postgres://postgres@user-db:5432/user-db-real
DB_URL_KONE: postgres://postgres@kone-db:5432/kone-db-real
SIS_DB_URL: postgres://postgres@sis-db:5432/sis-db-real
redis:
volumes:
- redis-data-real:/data
sis-updater-scheduler:
environment:
SIS_IMPORTER_DATABASE: sis-importer-db-real
sis-updater-worker:
environment:
DB_URL: postgres://postgres@sis-db:5432/sis-db-real
SIS_IMPORTER_DATABASE: sis-importer-db-real
volumes:
redis-data-real:
name: oodikone-redis-data-real
-29
View File
@@ -1,29 +0,0 @@
name: oodikone
x-defaults: &defaults
restart: unless-stopped
x-healthcheck-defaults: &healthcheck-defaults
interval: 30s # Normal healthcehck interval
timeout: 5s
retries: 10 # Retries until SIGKILl
start_interval: 3s # How often healthcheck runs while starting
start_period: 30s # How long startup period is
services:
sis-db:
<<: *defaults
container_name: oodikone-sis-db
image: registry-toska.ext.ocp-prod-0.k8s.it.helsinki.fi/sis-db
pull_policy: always
volumes: !reset [] # Remove previous volume definitions
environment:
PGDATA: /data
POSTGRES_DB: sis-db
POSTGRES_HOST_AUTH_METHOD: trust
shm_size: 2gb
ports:
- 12345:5432
healthcheck:
test: ['CMD', 'pg_isready', '-U', 'postgres', '-h', '127.0.0.1']
<<: *healthcheck-defaults
-274
View File
@@ -1,274 +0,0 @@
name: oodikone
x-defaults: &defaults
restart: unless-stopped
x-build_defaults: &build_defaults
dockerfile: dev.Dockerfile
x-user_defaults: &user_defaults
# Run containers as user:group defined in .env
# If run as $(id -u):$(id -g) will reduce permission errors when mounting
# Defaults to registry.access.redhat.com/ubi10/nodejs-24-minimal's default user:group
user: '${DOCKER_UID:-1001}:${DOCKER_GID:-0}'
x-healthcheck-defaults: &healthcheck-defaults
interval: 30s # Normal healthcehck interval
timeout: 5s
retries: 10 # Retries until SIGKILl
start_interval: 3s # How often healthcheck runs while starting
start_period: 30s # How long startup period is
services:
adminer:
<<: *defaults
container_name: oodikone-adminer
environment:
ADMINER_DESIGN: pepa-linha
image: adminer:5.5.1
ports:
- 5050:8080
volumes:
- ./adminer-plugins:/var/www/html/plugins-enabled
backend:
<<: [*defaults, *user_defaults]
container_name: oodikone-backend
build:
dockerfile: backend/dev.Dockerfile
context: services
depends_on:
kone-db:
condition: service_healthy
redis:
condition: service_healthy
sis-db:
condition: service_healthy
user-db:
condition: service_healthy
env_file:
- .env
tty: true
environment:
DB_URL_KONE: postgres://postgres@kone-db:5432/kone-db
DB_URL_USER: postgres://postgres@user-db:5432/user-db
FRONT_URL: http://localhost:3000
REDIS: redis
SECRET_TOKEN: dev
SIS_DB_URL: postgres://postgres@sis-db:5432/sis-db
SIS_UPDATER_URL: http://sis-updater-scheduler:8082
IMPORTER_DB_API_URL: 'https://api-toska.apps.ocp-prod-0.k8s.it.helsinki.fi/importer'
JAMI_URL: 'http://jami:3003/'
ports:
- 8080:8080
profiles:
- oodikone
volumes:
- ./services/backend/:/opt/app-root/backend
- ./services/shared/:/opt/app-root/shared
- shared-nodemod:/opt/app-root/shared/node_modules
- backend-nodemod:/opt/app-root/backend/node_modules
healthcheck:
test: ['CMD', 'curl', '-f', 'http://localhost:8080/api/health']
<<: *healthcheck-defaults
frontend:
<<: [*defaults, *user_defaults]
container_name: oodikone-frontend
build:
dockerfile: frontend/dev.Dockerfile
context: services
depends_on:
backend:
condition: service_healthy
ports:
- 3000:3000
profiles:
- oodikone
volumes:
- ./services/frontend/:/opt/app-root/frontend
- ./services/shared/:/opt/app-root/shared
- shared-nodemod:/opt/app-root/shared/node_modules
- frontend-nodemod:/opt/app-root/frontend/node_modules
kone-db:
<<: *defaults
container_name: oodikone-kone-db
image: registry-toska.ext.ocp-prod-0.k8s.it.helsinki.fi/kone-db
profiles:
- oodikone
ports:
- 4321:5432
volumes:
- kone-db-data:/data
healthcheck:
test: ['CMD', 'pg_isready', '-U', 'postgres', '-h', '127.0.0.1']
<<: *healthcheck-defaults
user-db:
<<: *defaults
container_name: oodikone-user-db
image: registry-toska.ext.ocp-prod-0.k8s.it.helsinki.fi/user-db
profiles:
- oodikone
ports:
- 4320:5432
volumes:
- user-db-data:/data
healthcheck:
test: ['CMD', 'pg_isready', '-U', 'postgres', '-h', '127.0.0.1']
<<: *healthcheck-defaults
redis:
<<: *defaults
container_name: oodikone-redis
command: 'redis-server --appendonly yes'
image: redis:7.2.6
ports:
- 6379:6379
volumes:
- redis-data:/data
healthcheck:
test: ['CMD', 'redis-cli', 'ping', '|', 'grep', 'PONG']
<<: *healthcheck-defaults
sis-db:
<<: *defaults
container_name: oodikone-sis-db
image: registry-toska.ext.ocp-prod-0.k8s.it.helsinki.fi/sis-db
volumes:
- sis-db-data:/data
shm_size: 2gb
ports:
- 12345:5432
healthcheck:
test: ['CMD', 'pg_isready', '-U', 'postgres', '-h', '127.0.0.1']
<<: *healthcheck-defaults
sis-importer-db:
<<: *defaults
container_name: oodikone-sis-importer-db
image: registry-toska.ext.ocp-prod-0.k8s.it.helsinki.fi/sis-importer-db
profiles:
- updater
volumes:
- sis-importer-db-data:/data
ports:
- 12346:5432
healthcheck:
test: ['CMD', 'pg_isready', '-U', 'postgres', '-h', '127.0.0.1']
<<: *healthcheck-defaults
jami:
<<: *defaults
container_name: oodikone-jami
image: quay.io/toska/jami:production
depends_on:
jami-db:
condition: service_healthy
env_file:
- .env
environment:
- npm_config_cache=/tmp
- PORT=3003
- NODE_ENV=development
- DATABASE_URL=postgres://postgres:postgres@jami-db:5432/jami-db-real
ports:
- 3003:3003
jami-db:
<<: *defaults
container_name: oodikone-jami-db
image: registry-toska.ext.ocp-prod-0.k8s.it.helsinki.fi/jami-db
environment:
- PGDATA=/data
- POSTGRES_PASSWORD=postgres
ports:
- 5433:5432
volumes:
- jami-data:/data
healthcheck:
test: ['CMD', 'pg_isready', '-U', 'postgres', '-h', '127.0.0.1']
<<: *healthcheck-defaults
sis-updater-worker:
<<: [*defaults, *user_defaults]
container_name: oodikone-updater-worker
build:
<<: *build_defaults
context: updater/sis-updater-worker
depends_on:
redis:
condition: service_healthy
sis-db:
condition: service_healthy
sis-importer-db:
condition: service_healthy
environment:
DB_URL: postgres://postgres@sis-db:5432/sis-db
REDIS_HOST: redis
SIS_IMPORTER_DATABASE: sis-importer-db
SIS_IMPORTER_HOST: sis-importer-db
SIS_IMPORTER_PASSWORD: postgres
SIS_IMPORTER_USER: postgres
SIS_PASSWORD: postgres
profiles:
- updater
volumes:
- ./updater/sis-updater-worker/:/opt/app-root/src
- sis-updater-worker-nodemod:/opt/app-root/src/node_modules
sis-updater-scheduler:
<<: [*defaults, *user_defaults]
container_name: oodikone-updater-scheduler
build:
<<: *build_defaults
context: updater/sis-updater-scheduler
depends_on:
redis:
condition: service_healthy
sis-importer-db:
condition: service_healthy
environment:
REDIS_HOST: redis
SECRET_TOKEN: dev
SIS_IMPORTER_DATABASE: sis-importer-db
SIS_IMPORTER_HOST: sis-importer-db
SIS_IMPORTER_PASSWORD: postgres
SIS_IMPORTER_USER: postgres
SCHEDULE_IMMEDIATE: '${SCHEDULE_IMMEDIATE}'
EXIT_AFTER_IMMEDIATES: '${EXIT_AFTER_IMMEDIATES}'
ports:
- 8082:8082
profiles:
- updater
volumes:
- ./updater/sis-updater-scheduler/:/opt/app-root/src
- sis-updater-scheduler-nodemod:/opt/app-root/src/node_modules
healthcheck:
test: ['CMD', 'curl', '-f', 'http://localhost:8082/health']
<<: *healthcheck-defaults
volumes:
backend-nodemod:
name: oodikone-backend-nodemod
frontend-nodemod:
name: oodikone-frontend-nodemod
shared-nodemod:
name: oodikone-shared-nodemod
kone-db-data:
name: oodikone-kone-db-data
redis-data:
name: oodikone-redis-data
sis-db-data:
name: oodikone-sis-db-data
sis-importer-db-data:
name: oodikone-sis-importer-db-data
sis-updater-scheduler-nodemod:
name: oodikone-updater-scheduler-nodemod
sis-updater-worker-nodemod:
name: oodikone-updater-worker-nodemod
user-db-data:
name: oodikone-user-db-data
jami-data:
name: oodikone-jami-data
-3
View File
@@ -1,3 +0,0 @@
# Documentation
Oodikone public documentation. For internal documentation, see Toska GitLab.
-69
View File
@@ -1,69 +0,0 @@
# Access rights in Oodikone
## Roles
Roles are defined [here](../services/backend/src/config/roles.ts). Criteria for each role can be found [here](../services/backend/src/services/userService.ts). Currently these roles exist:
- admin
- courseStatistics
- facultyStatistics
- fullSisuAccess
- studyGuidanceGroups
- teachers
The roles `facultyStatistics` and `studyGuidanceGroups` simply enable certain views (_Faculties_ and _Guidance groups_ respectively).
### courseStatistics
- Can see statistics of every course but year statistics of ≤ 5 students are hidden
- If the user has [full rights](#programmes) to **any** programme, they can also see exact statistics even if there are ≤ 5 students (the same behavior as with `admin` or `fullSisuAccess` roles)
### teachers
- By default, the role only allows seeing the statistics of course providers (degree programmes) they have full rights to (see [Programmes](#programmes))
- This role always needs to be given and removed manually
- If the user is a member of the `hy-dekaanit` or `hy-varadekaanit-opetus` IAM group (or is an admin), they can see leaderboards, information about individual teachers, and statistics of all course providers. They don't need to be given the `teachers` role manually.
### Other roles
| | Everybody\* | Admin | fullSisuAccess |
| -------------------- | :---------: | :---: | :------------: |
| Front page | ✅ | ✅ | ✅ |
| Feedback | ✅ | ✅ | ✅ |
| University | ✅ | ✅ | ✅ |
| Language center view | | ✅ | |
| Faculties | | ✅ | ✅ |
| Updater | | ✅ | |
| Users | | ✅ | |
| Programmes | | ✅ | ✅ |
| Students | | ✅ | ✅ |
| Teachers | | ✅ | |
| Guidance groups | | | |
| Custom population | | ✅ | ✅ |
| Completed courses | ✅ | ✅ | ✅ |
| Close to graduation | | ✅ | ✅ |
| Courses | | ✅ | ✅ |
\* = anyone who can log in to Oodikone, check the [IAM groups](#iam-groups) section for more information
## Programmes
There are two types of programme rights: limited and full. Limited rights mean that the user cannot see individual students of those programmes, while a user with full rights can.
Manually given programme rights are **always** full rights. Some of the programme rights are based on user's IAM groups: some only qualify for limited rights, some for full rights.
## IAM groups
A user must be a member of either the `grp-oodikone-basic-users` or `grp-oodikone-users` IAM group to be able to log in to Oodikone. The following IAM groups are used to give roles:
| IAM group | Role given based on the group |
| ------------------------ | ----------------------------- |
| grp-oodikone-basic-users | courseStatistics |
| grp-oodikone-users | facultyStatistics |
| grp-toska | admin |
`grp-oodikone-basic-users`: basically the whole staff (hy-\*-allstaff)
`grp-oodikone-users`: more limited (e.g. _koulutussuunnittelijat_, _ospa_, _opintoasiainpäälliköt_, _(vara)dekaanit_, _johtoryhmä_)
`grp-kielikeskus-esihenkilot`: gives access to the language center view
Binary file not shown.

Before

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 KiB

-96
View File
@@ -1,96 +0,0 @@
# Combined programmes
At the moment only programmes KH90_001 and MH90_001 have a combined view.
The made changes do not have any effects on faculty or trends views. **Needs manual work** is added into those parts that needs visit, if this functionality is added to other programmes.
Basic rule of thumb is that for the master/licentiate programme I used names `combinedProgramme` and in some rare cases `secondProgramme` as whole or part of the variable names.
## Degree programme
**Needs manual work** in the function `getCombinedProgrammeId` in `services/frontend/src/common/index.js` if new programmes is added. For showing the both programme data correctly, the filtering mechanism is changed to add both programmes and it is not caring for which programme user has acual rights or IAMrights. However, the links for the class stats level are hidden in studytrack tables, if the user has no rights for the programme.
- Search by class
- **Needs manual work** Combine programmes here
- Overview
- **Needs manual work**
**Redis nightly degree programme update**
- Updates automatically combined programme stats. **Needs manual work** here `services/backend/src/services/studyprogrammeHelpers.js` in the constant `combinedStudyprogrammes`. This might needs refactoring if bachelor programme can have many separate master programmes. Then remember to change function `refreshNewOverview` in `services/backend/src/events.js` accordingly to handle lists.
**NOTE:** `combinedStudyprogrammes` is also used to fetch course modules (mandatory courses). If the logic of this constant is changed in `studyProgrammeHelpers.js` -> the needed changes must be done also here `services/backend/src/services/programmeModules.js`. While writing this, the cleverer idea of doing this might have been to send `code` in form `KHxx_xxx+MHxx_xxx` and do the splitting into two codes in the backend as done in Study guidance groups logic. Changes were done here 'services/backend/src/routes/population.js', acual place is near `getFilteredAssociations` function.
**Basic information**
All things listed below should work automatically, if new combinations are done.
- Credits produced by the studyprogramme
- credits are computed for both programmes separately
- Average graduation times
- For bachelor + master/licentiate studyright the time is the whole study time starting from bachelor
- _Students of the programme_ and _Graduated and thesis writers of the programme_
- separate fields for started, graduated and thesis writers
**Studytracks and class statistics**
- _Students of the studyprogramme by starting year_
- Graduated field separated for both programmes
- All = currently enrolled + absent + passive + graduated licentiate
- _Progress of students of the studyprogramme by starting year_
- computed for whole bachelor + master/licentiate studyright period
- _Average graduation times_
- bachelor + master/licentiate graph: start is bachelor studyright start
**Programme courses**
- Fetch all courses for both programmes and show them in one table.
**Degree courses**
- Possibility to add criteria for whole bachelor-master study time
- For eläinlääkis this criteria is saved by bachelor programme code. Other cases may need other approaches.
In eläinlääkis this done by adding the criteria for 6 years -> the whole study time criteria is saved with the bachelor programme code. That is, same criteria is found, if bachelor or combined programme view is shown. This approach might need extra thinking for different programmes.
- Show own tables for both programmes
- To fetch data **needs manual work** here `services/backend/src/services/studyprogrammeHelpers.js` in the constant `combinedStudyprogrammes`.
**Tags**
- Possibility to add tags for combined programme.
## Class statistics view
**Credit accumulation**
- Show graduations for both programmes
**Credit statistics**
- Bachelor and bachelor+master combo have own graphs.
**Courses of class**
- Show courses for both programmes
**Students**
- **General tab** - Additional fields for graduation and hops
- **Courses tab** - show all courses for both programmes. The courses are fetched with same logic as for Degree programmes.
- **Tags tab** - show tabs related to this combination.
- **Progress tab** - possibility to have progress for whole bachelor-master study time. **See Degree courses approach** to add criteria. **May need future manual work**, depending on the programme id with which the data is saved on the database.
## Filters
- **Hops (study plan)** - possibility filter out credits separate for both programmes or for both at the same time
- **Graduated from programme** - possibility to filter out students based on graduation either in bachelor's or master's.
- **Studyright status** - active or passive based on the chosen degree
- **Studytrack filter** (needs programme code) is not touched at the moment, thus **may need future manual work**.
## Study guidance groups
- Backend: student data is fetched by bachelor degree programme code in `/populationstatisticsbystudentnumbers`. Separation from code 'KHxx_xxx+MHxx_xxx' is done in backend.
- Frontend: things are done in the same way as with class stats.
- **Need manual work**: `./oodikone/services/frontend/src/redux/elementdetails.js` add combined programme codes (both bachelor and master) to `combinedProgrammeCodes` and modify `combinedOptions` list accordingly in `useFilterAndFormattedElementDetails()` function.
-78
View File
@@ -1,78 +0,0 @@
# Databases
Oodikone uses three main PostgreSQL databases, each for a different purpose. This document contains a simplified overview of each database and the relations between models.
Use Adminer or refer to the actual database for a detailed view of each model and their attributes.
## Storage
### sis-db
The `sis-db` database contains information originating from Sisu.
```mermaid
---
title: sis-db
---
erDiagram
enrollment }o--|| course : ""
enrollment }o--|| semesters : ""
enrollment }o--|| student : ""
credit_teachers }o--|| teacher : ""
course_types
curriculum_periods
credit_teachers }o--|| credit : ""
credit }o--|| course : ""
credit }o--|| semesters : ""
credit }o--|| credit_types : ""
credit }o--|| student : ""
studyplan }o--|| student : ""
studyplan }o--|| sis_study_rights : ""
sis_study_rights }o--|| studyright_extents : ""
sis_study_rights }o--|| organization : ""
sis_study_rights }o--|| student : ""
sis_study_right_elements }o--|| sis_study_rights : ""
course_providers }o--|| organization : ""
course_providers }o--|| course : ""
programme_module_children }o--|| programme_modules : "references parent"
programme_module_children }o--|| programme_modules : "references child"
programme_modules }o--o| organization : "belong to"
```
### kone-db
The `kone-db` database contains information used in various features of Oodikone. This functionality is native to Oodikone and not necessarily directly related to Sisu.
```mermaid
---
title: kone-db
---
erDiagram
custom_population_searches
excluded_courses
open_uni_population_searches
progress_criteria
study_guidance_group_tags
degree_programme_pins
tag ||--o{ tag_student : "belongs to"
tag
tag_student
```
### user-db
The `user-db` database contains information about users of Oodikone.
```mermaid
---
title: user-db
---
erDiagram
users
```
## Caching
### Redis
Redis is used in the backend to cache calculation results.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 106 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 72 KiB

-127
View File
@@ -1,127 +0,0 @@
# How to update the database schema
These instructions explain how to update the schemas for the following databases used by Oodikone:
- `sis-db`
- `user-db`
- `kone-db`
The steps to **add, remove, or modify columns** are mostly the same for each database, but the file paths and container names differ. Always **double-check** that you have the correct function name, column name, data type, and other relevant details in your migration file.
> [!NOTE]
> If you need to change the `sis-importer-db` schema, see the instructions in the [sis-importer repo](https://github.com/UniversityOfHelsinkiCS/sis-importer/blob/master/README.md#tricks--tips).
## General guidelines
### File names
- Name new migration files using the format `YYYYMMDD_XX_description.js`.
- The `XX` part is a migration number (starting from `00` each day).
- Example: `20250122_00_add_column_to_table.js`.
### Model vs. column names
- Use **camelCase** in your model definitions.
- Use **snake_case** in the actual database columns.
- If a model has `underscored: true`, Sequelize automatically converts camelCase to snake_case in all queries. Otherwise, specify the column name explicitly.
> [!IMPORTANT]
> The column name **in the database** (snake_case) must be used in the migration file, not the camelCase name from your model.
### Verifying the migration locally
- Migrations are run automatically whenever the server restarts.
- After creating a migration, ensure the relevant service is running:
- For `sis-db`: `npm run both:real` or `npm run updater:real`
- For `user-db`/`kone-db`: `npm run both:real` or `npm run oodikone:real`
- **Connect to the database container** and check:
```bash
docker exec -it <container-name> psql -U postgres <database-name>-real
```
Replace `<container-name>` and `<database-name>` with the actual values (e.g., `sis-db`, `user-db`, or `kone-db`).
- In the psql prompt, verify your migration:
```sql
SELECT * FROM migrations;
```
Also confirm that the new (or updated) column exists in the appropriate table and has the correct type.
### Reverting a migration locally
If you need to **revert a migration locally**, follow these steps:
1. Locate the `migrator.up()` function in the code:
- For `user-db` and `kone-db`: [`services/backend/src/database/connection.ts`](../services/backend/src/database/connection.ts)
- For `sis-db`: [`updater/sis-updater-worker/src/db/connection.js`](../updater/sis-updater-worker/src/db/connection.js)
1. Change `migrator.up()` to `migrator.down()` and save the file. The latest migration will be reverted automatically when the server reloads.
1. **Important:** After the migration is reverted, change `migrator.down()` back to `migrator.up()` to ensure future migrations run correctly.
1. For more instructions, see the [umzug documentation](https://github.com/sequelize/umzug?tab=readme-ov-file#usage).
### Deploying changes to production
- **Push changes** to the `master` branch and wait for the workflow to finish.
- For **`sis-db`** migrations (in the updater):
- You **do not** need to create a new release. Once tests pass, changes to the updater go directly to production.
- For **`user-db`** and **`kone-db`** migrations (in the backend):
- A new release must be made before the changes go to production.
- After deployment, check logs (for example, in OpenShift) to confirm the migration ran successfully.
- Optionally, use the `db-cli` tool to inspect the production database. See `how_to_db_cli.md` (in GitLab) for instructions.
## `sis-db`
> [!NOTE]
> These instructions assume that needed data is already in the `sis-importer-db`. If not, you must add the data there first. See the [sis-importer repo](https://github.com/UniversityOfHelsinkiCS/sis-importer/blob/master/README.md#tricks--tips) for more details.
1. **Update the updater model:**
- Go to `updater/sis-updater-worker/src/db/models/` and choose the model you want to change.
- Add or modify the column (with its **type** and any other necessary fields).
- Check the correct data type in the importer.
- Use camelCase in the model, snake_case in the database.
1. **Update the backend model:**
- Go to `services/backend/src/models/`.
- The models are in different formats (JavaScript in the updater, TypeScript in the backend), but follow existing examples.
1. **Create a migration file:**
- Go to `updater/sis-updater-worker/src/db/migrations/`.
- Name your file using the format described above (e.g. `20250122_00_add_column_to_table.js`).
- **Remember**: The column name **in the database** (snake_case) must be used in the migration file, not the camelCase name from your model.
1. **Run and verify the migration:**
- Ensure that the updater is running (`npm run both:real` or `npm run updater:real`).
- Use the steps in [General guidelines](#general-guidelines) to verify.
1. **Push to master and deploy:**
- Your migration will go to production automatically after tests pass.
- Check logs in production (OpenShift) or use `db-cli` to confirm success.
## `user-db` and `kone-db`
1. **Update the model:**
- Go to `services/backend/src/models/kone` (for `kone-db`) or `services/backend/src/models/user.ts` (the only model for user-db)
- Add or modify the column, type, and any other needed fields. Follow existing models for structure.
1. **Create a migration file:**
- Go to `services/backend/src/database/`.
- Use `migrations_kone` (for `kone-db`) or `migrations_user` (for `user-db`).
- Name the file using the same `YYYYMMDD_XX_description.js` format.
- **Remember**: The column name **in the database** (snake_case) must be used in the migration file, not the camelCase name from your model.
1. **Run and verify the migration:**
- Make sure Oodikone is running (`npm run both:real` or `npm run oodikone:real`).
- Follow the same verification steps as in [General Guidelines](#general-guidelines).
1. **Push to master and release:**
- Wait for the CI workflow to pass.
- **Create a new release**; then check the production logs (in OpenShift) to confirm the migrationor use `db-cli`.
-84
View File
@@ -1,84 +0,0 @@
# Numeric types
Data used in Oodikone contains some numeric types (or _magic numbers_) that are not self-explanatory. The values of the numeric types are gathered here for convenience and quick reference.
## Extent code
Appears in model SISStudyRight (db: `sis_study_rights`) as _extentCode_ (db: `extent_code`). Values are defined in the database table `studyright_extents`.
| value | meaning |
| ----: | :-------------------------------------------------- |
| 1 | Bachelor's degree |
| 2 | Master's degree |
| 3 | Licentiate |
| 4 | Doctor |
| 5 | Bachelor's and Master's degree |
| 6 | Continuing education |
| 7 | Exchange studies |
| 9 | Open University studies |
| 13 | Non-degree pedagogical studies for teachers |
| 14 | Contract training |
| 16 | Studies for secondary school students |
| 18 | Specialisation studies |
| 22 | Non-degree programme for special education teachers |
| 23 | Specialist training in medicine and dentistry |
| 31 | Summer and winter school |
| 34 | Exchange studies (postgraduate studies) |
| 99 | Non-Degree studies |
## Phase
Appears in model SISStudyRightElement (db: `sis_study_right_elements`) as _phase_ (db: `phase`).
In Finland it is common for new students to be awarded the study right to both a bachelor's and a master's programme when they are admitted to the university. In Oodikone these two-phase study rights are marked with the [extent code](#extent-code) 5. In the study right elements of these study rights, phase 2 marks the second phase of the study right.
For example: Student is awarded the study right in the bachelor's programme in computer science with the extent code 5. The study right element for study right in the bachelor's programme is marked with phase 1. After graduating from the bachelor's programme, the student continues their studies in the master's programme in computer science. The new study right element for the study right in the master's programme is marked with phase 2.
All study right elements for other study rights (extent code other than 5) are marked with phase 1.
The level of the programme (bachelor's, master's etc.) of the study right can be directly checked from degreeProgrammeType (db: `degree_programme_type`) in model SISStudyRightElement (db: `sis_study_right_elements`).
| value | meaning |
| ----: | :------- |
| 1 | any |
| 2 | master's |
## Enrollment type
This information is used in the model SISStudyRight (db: `sis_study_rights`), in the field _semesterEnrollments_ (db: `semester_enrollments`).
The `semester_enrollments` column is an array of JSON objects. Each object contains:
- `type` (the type of enrollment),
- `semester` (the code for a semester),
- and sometimes `statutoryAbsence` (this field is included only when the enrollment type is 2).
The `type` field represents one of the following enrollment types. The `semester` field refers to a semester code, defined as `semestercode` in the model Semester.
| value | meaning | notes |
| ----: | :------ | :------------------------------- |
| 1 | Present | |
| 2 | Absent | |
| 3 | Passive | neither 1 or 2 has been declared |
## Credit type code
Appears in model Credit (db: `credit`) as _credittypecode_ (db: `credittypecode`). Values are defined in the database table `credit_types`.
| value | meaning |
| ----: | :--------------- |
| 4 | Completed |
| 7 | Improved (grade) |
| 9 | Transferred |
| 10 | Failed |
## Gender code
Appears in model Student (db: `student`) as _gender_code_ (db: `gender_code`).
| value | meaning |
| ----: | :------ |
| 0 | Unknown |
| 1 | Male |
| 2 | Female |
| 3 | Other |
-50
View File
@@ -1,50 +0,0 @@
# Resources
A collection of useful tools, documentation and other resources.
## Code style and maintainability
- [ESLint documentation](https://eslint.org/docs/v8.x)
- [qlty.sh](https://qlty.sh/gh/UniversityOfHelsinkiCS/projects/oodikone)
- Qlty.sh replaced Code climate which was introduced by Riku
- The usefulness of qlty.sh is sometimes questionable
## Database
- [pgcli](https://www.pgcli.com)
- A useful commandline tool to interact with the database
- Can be installed from some package managers or optionally via `$ pip install pgcli`
- [Postgres documentation](https://www.postgresql.org/docs/15/index.html)
- [Sequelize documentation](https://sequelize.org/docs/v6/)
## GitHub
- [GitHub documentation](https://docs.github.com/en)
- [GitHub Actions](https://docs.github.com/en/actions)
- [Mermaid.js documentation](https://mermaid.js.org/intro/syntax-reference.html)
## TypeScript
- [TypeScript documentation](https://www.typescriptlang.org/docs)
- [`tsconfig` reference](https://www.typescriptlang.org/tsconfig)
- [TypeScript instructions for Sequelize](https://sequelize.org/docs/v6/other-topics/typescript)
- [`sequelize-typescript` instructions](https://github.com/sequelize/sequelize-typescript?tab=readme-ov-file#readme)
## User interface
- [Material UI documentation](https://mui.com/material-ui/getting-started/)
- [Material Icons](https://mui.com/material-ui/material-icons/)
- [MUI X Date pickers](https://mui.com/x/react-date-pickers/)
- [Material React Table documentation](https://www.material-react-table.com/docs)
<!--
## Visual Studio Code
_Todo: add relevant plugins_
-->
## Web technologies
- [MDN Web Docs](https://developer.mozilla.org/en-US/)
-3
View File
@@ -1,3 +0,0 @@
# Tietosuoja
Oodikoneessa noudatetaan Helsingin yliopiston [yleistä linjausta](https://www.helsinki.fi/fi/tutustu-meihin/tietosuoja#opiskelijoiden-henkil-tietojen-k-sittely--title) opiskelijoiden henkilötietojen käsittelystä.
-175
View File
@@ -1,175 +0,0 @@
import { expect, test, type Page } from '@playwright/test'
import { init } from './support/commands'
const studentSet1 = ['433237', '457144', '458090', '474270', '479440']
const coursesSet1 = ['TKT10001', 'TKT10002', 'TKT10003', 'TKT10004']
const hasLanded = async (page: Page) => {
await expect(page.getByRole('button', { name: 'Search completed courses of students' })).toBeVisible()
await expect(
page.getByText(
'Here you can search by a list of student and course numbers to see whether students have completed certain courses yet'
)
).toBeVisible()
}
const openSearch = async (page: Page) => {
await init(page, '/completedcoursessearch')
await hasLanded(page)
}
const openCompletedCoursesModal = async (page: Page) => {
await page.getByRole('button', { name: 'Search completed courses of students' }).click()
}
const selectSavedCourselist = async (page: Page, name: string) => {
const searchInput = page.getByTestId('history-search').getByRole('combobox')
await searchInput.fill(name)
await page.getByRole('option', { name }).click()
}
const deleteAllPreviousSearches = async (page: Page) => {
const historySearch = page.getByTestId('history-search').getByRole('combobox')
while (await historySearch.isVisible()) {
await historySearch.click()
const options = page.getByRole('option')
if ((await options.count()) === 0) break
await options.first().click()
await page.getByRole('button', { name: 'Delete' }).click()
await expect(page.getByRole('button', { name: 'Delete' })).toBeDisabled()
}
await expect(page.getByText('You have no previous searches.')).toBeVisible()
}
const createCourseList = async (page: Page, courseCodes: string[], courseListName: string) => {
await openSearch(page)
await openCompletedCoursesModal(page)
await page.getByTestId('course-list-input').getByRole('textbox').fill(courseCodes.join('\n'))
await page.getByTestId('search-name').getByRole('textbox').fill(courseListName)
await page.getByRole('button', { name: 'Save' }).click()
}
const generateCourseListName = () => `TEST-course-list-${Date.now()}`
test.describe('When search modal is opened', () => {
test.afterAll(async ({ browser }) => {
const context = await browser.newContext()
const page = await context.newPage()
await openSearch(page)
await openCompletedCoursesModal(page)
if (await page.getByTestId('history-search').getByRole('combobox').isVisible()) {
await deleteAllPreviousSearches(page)
}
await context.close()
})
test('Modal opens correctly', async ({ page }) => {
await openSearch(page)
await openCompletedCoursesModal(page)
await expect(page.getByRole('dialog')).toContainText('Search completed courses of students')
await expect(
page.getByText('Insert one or more student numbers, separated by a space, a newline, a comma, or a semicolon.')
).toBeVisible()
await expect(
page.getByText('Insert one or more courses, separated by a space, a newline, a comma, or a semicolon.')
).toBeVisible()
await expect(page.getByText('Insert name for this course list if you wish to save it')).toBeVisible()
})
test('Modal gets the correct course codes and student numbers from the URL', async ({ page }) => {
await init(
page,
'/completedcoursessearch?courseList=TKT10001&courseList=TKT10002&studentList=433237&studentList=457144'
)
await hasLanded(page)
await openCompletedCoursesModal(page)
await expect(page.getByTestId('student-no-input').getByRole('textbox')).toHaveValue('433237, 457144')
await expect(page.getByTestId('course-list-input').getByRole('textbox')).toHaveValue('TKT10001, TKT10002')
})
test('Returns empty table and notification for invalid input', async ({ page }) => {
await openSearch(page)
await openCompletedCoursesModal(page)
await page.getByTestId('student-no-input').getByRole('textbox').fill('1')
await page.getByTestId('course-list-input').getByRole('textbox').fill('1')
await page.getByRole('button', { name: 'Search' }).click()
await expect(page.getByTestId('rights-notification')).toContainText(
'The information for the following students could not be displayed'
)
await expect(page.getByTestId('rights-notification')).toContainText('1')
const table = page.getByTestId('completed-courses-table-div')
await expect(table).toContainText('Student number')
await expect(table.getByText('1', { exact: true })).toHaveCount(0)
})
test.describe('When a search with correct data is executed', () => {
test.beforeEach(async ({ page }) => {
await openSearch(page)
await openCompletedCoursesModal(page)
await page.getByTestId('student-no-input').getByRole('textbox').fill(studentSet1.join('\n'))
await page.getByTestId('course-list-input').getByRole('textbox').fill(coursesSet1.join('\n'))
await page.getByRole('button', { name: 'Search' }).click()
})
test('Pushes the query to url', async ({ page }) => {
await expect(page).toHaveURL(
new RegExp(
`/completedcoursessearch\\?courseList=${coursesSet1.join('&courseList=')}&studentList=${studentSet1.join('&studentList=')}`
)
)
})
test('Finds correct students and courses', async ({ page }) => {
const table = page.getByTestId('completed-courses-table-div')
for (const studentNumber of studentSet1) await expect(table).toContainText(studentNumber)
for (const courseCode of coursesSet1) await expect(table).toContainText(courseCode)
})
})
})
test.describe('Courselist saving-related functions work', () => {
test.beforeEach(async ({ page }) => {
await openSearch(page)
await openCompletedCoursesModal(page)
if (await page.getByTestId('history-search').getByRole('combobox').isVisible()) {
await deleteAllPreviousSearches(page)
} else {
await expect(page.getByText('You have no previous searches.')).toBeVisible()
}
})
test('Course list can be saved', async ({ page }) => {
const courseList = generateCourseListName()
const courses = ['CSM14204', 'TKT10004']
await createCourseList(page, courses, courseList)
await openSearch(page)
await openCompletedCoursesModal(page)
await selectSavedCourselist(page, courseList)
await expect(page.getByTestId('course-list-input').getByRole('textbox')).toHaveValue(courses.join(', '))
})
test('Course list can be deleted', async ({ page }) => {
const courseList = generateCourseListName()
await createCourseList(page, ['CSM14204', 'TKT10004'], courseList)
await openSearch(page)
await openCompletedCoursesModal(page)
await selectSavedCourselist(page, courseList)
await page.getByRole('button', { name: 'Delete' }).click()
await expect(page.getByText('You have no previous searches.')).toBeVisible()
})
test('Course list can be updated', async ({ page }) => {
const courseList = generateCourseListName()
await createCourseList(page, ['CSM14204', 'TKT10004'], courseList)
await openSearch(page)
await openCompletedCoursesModal(page)
await selectSavedCourselist(page, courseList)
await page.getByTestId('course-list-input').getByRole('textbox').pressSequentially(',TKT10001')
await page.getByRole('button', { name: 'Save' }).click()
await openSearch(page)
await openCompletedCoursesModal(page)
await selectSavedCourselist(page, courseList)
await expect(page.getByTestId('course-list-input').getByRole('textbox')).toHaveValue('CSM14204, TKT10004, TKT10001')
})
})
File diff suppressed because it is too large Load Diff
-197
View File
@@ -1,197 +0,0 @@
import { expect, test, type Page } from '@playwright/test'
import { init } from './support/commands'
const students1 = ['433237', '457144', '458090', '465136']
const students2 = ['474270', '479440', '470391', '474789']
const nonExistentStudentNumbers = ['123', 'X', '-']
const fillForm = async (page: Page, studentNumbers: string[], separator: string) => {
// NOTE: This helper uses pressSequentially instead of the normal fill so that it can append to a textfield
await page.getByTestId('student-number-input').getByRole('textbox').pressSequentially(studentNumbers.join(separator))
}
const search = async (page: Page) => {
await page.getByTestId('search-button').click()
}
const searchFor = async (page: Page, studentNumbers: string[], separator: string) => {
await fillForm(page, studentNumbers, separator)
await search(page)
}
const hasLanded = async (page: Page) => {
await expect(page.getByText('Credit accumulation')).toBeVisible()
await expect(page.getByText('Programme distribution')).toBeVisible()
await expect(page.getByText('Courses of population')).toBeVisible()
await expect(page.getByText('Students (4)')).toBeVisible()
}
const containsAmountOfStudents = async (page: Page, amount = 0) => {
await expect(page.getByText(`Credit accumulation (for ${amount} students)`)).toBeVisible()
await expect(page.getByText(`Students (${amount})`)).toBeVisible()
}
const containsSpecificStudents = async (page: Page, studentNumbers: string[]) => {
const studentsTab = page.getByText(`Students (${studentNumbers.length})`)
await studentsTab.click()
await Promise.all(studentNumbers.map(async number => await expect(page.getByText(number)).toBeVisible()))
}
const checkRightsNotification = async (page: Page, studentNumbers: string[]) => {
const notification = page.getByTestId('rights-notification')
await expect(notification).toContainText(
'The following students information could not be displayed. This could be either because they do not exist, or you do not have the right to view their information.'
)
await Promise.all(studentNumbers.map(number => expect(notification.locator('li').getByText(number)).toBeVisible()))
}
const fillName = async (page: Page) => {
const name = `TEST-${Date.now()}`
await expect(page.getByText('Insert a name for this custom population if you wish to save it')).toBeVisible()
await page.getByTestId('custom-population-name-input').getByRole('textbox').fill(name)
return name
}
const selectSavedPopulation = async (page: Page, name: string) => {
const searchInput = page.getByTestId('history-search').getByRole('combobox')
await searchInput.fill(name)
await searchInput.press('Enter')
}
const save = async (page: Page, name: string) => {
await page.getByRole('button', { name: 'Save' }).click()
// Wait for saved custom pop to be visible
await page.getByTestId('history-search').click()
await expect(page.getByText(name)).toBeVisible()
}
const deleteAllSearches = async (page: Page) => {
if (await page.getByTestId('history-search').isVisible()) {
const searchInput = page.getByTestId('history-search')
await searchInput.click()
const options = page.getByRole('cell')
while (await options.count()) {
await options.first().click()
await page.getByRole('button', { name: 'Delete' }).click()
await searchInput.click()
}
}
}
test.describe('Custom population tests', () => {
test.beforeEach(async ({ page }) => {
await init(page, '/custompopulation')
await expect(page).toHaveURL(/\/custompopulation/)
await expect(page.getByRole('heading', { name: 'Custom population', exact: true })).toBeVisible()
})
test.afterAll(async ({ browser }) => {
const context = await browser.newContext()
const page = await context.newPage()
await init(page, '/custompopulation')
await deleteAllSearches(page)
await context.close()
})
test.describe('Custom population searching', () => {
test('Finds a proper population', async ({ page }) => {
await searchFor(page, students1, '\n')
await hasLanded(page)
await containsAmountOfStudents(page, students1.length)
await containsSpecificStudents(page, students1)
})
test("Doesn't return non-existing students", async ({ page }) => {
await searchFor(page, [...students1, ...nonExistentStudentNumbers], ' ')
await hasLanded(page)
await containsAmountOfStudents(page, students1.length)
await containsSpecificStudents(page, students1)
await checkRightsNotification(page, nonExistentStudentNumbers)
})
test("Doesn't find empty custom population", async ({ page }) => {
await searchFor(page, nonExistentStudentNumbers, ',')
await checkRightsNotification(page, nonExistentStudentNumbers)
await expect(page.getByText('Credit accumulation')).not.toBeVisible()
await expect(page.getByText('Programme distribution')).not.toBeVisible()
await expect(page.getByText('Courses of population')).not.toBeVisible()
await expect(page.getByRole('button', { name: 'Back to search form' })).toBeVisible()
})
test("Doesn't return students user has no right to", async ({ page }) => {
const studentsForEduBachStudents = ['014990067', '013069465', '014853890']
await searchFor(page, studentsForEduBachStudents, ';;')
await checkRightsNotification(page, studentsForEduBachStudents)
await expect(page.getByText('Credit accumulation')).not.toBeVisible()
await expect(page.getByText('Programme distribution')).not.toBeVisible()
await expect(page.getByText('Courses of population')).not.toBeVisible()
await expect(page.getByRole('button', { name: 'Back to search form' })).toBeVisible()
})
})
test.describe('Custom population by programme', () => {
test('Finds students of programmes matching a prefix', async ({ page }) => {
await page.getByTestId('search-mode').click()
await expect(page.getByTestId('search-button')).toBeDisabled()
await page.getByTestId('custom-population-programme-prefix-input').getByRole('textbox').fill('KH50')
await page.getByTestId('custom-population-add-by-prefix-button').click()
await expect(page.getByText('KH50_001')).toBeVisible()
await page.getByTestId('population-year-selector').click()
await page.getByText('2020 - 2021').click()
await page.getByTestId('search-button').click()
await expect(page.getByText('Credit accumulation')).toBeVisible()
await expect(page.getByText('Courses of population')).toBeVisible()
await expect(page.getByRole('button', { name: 'Back to search form' })).toBeVisible()
})
})
test.describe('Custom population search saving', () => {
test('Saves a custom population search', async ({ page }) => {
const name = await fillName(page)
await fillForm(page, students1, ',\n')
await save(page, name)
await selectSavedPopulation(page, name)
await search(page)
await expect(page.getByText(`Custom population: ${name}`)).toBeVisible()
await containsAmountOfStudents(page, students1.length)
await containsSpecificStudents(page, students1)
await page.goto('/custompopulation')
await expect(page.getByRole('heading', { name: 'Custom population', exact: true })).toBeVisible()
await selectSavedPopulation(page, name)
await search(page)
await expect(page.getByText(`Custom population: ${name}`)).toBeVisible()
await containsAmountOfStudents(page, students1.length)
await containsSpecificStudents(page, students1)
})
test('Updates a custom population search', async ({ page }) => {
const name = await fillName(page)
await fillForm(page, students1, ' ')
await save(page, name)
await selectSavedPopulation(page, name)
await search(page)
await expect(page.getByText(`Custom population: ${name}`)).toBeVisible()
await containsAmountOfStudents(page, students1.length)
await containsSpecificStudents(page, students1)
await page.getByRole('button', { name: 'Back to search form' }).click()
await selectSavedPopulation(page, name)
await fillForm(page, ['\n', ...students2], ', ')
await save(page, name)
await page.goto('/custompopulation')
await expect(page.getByRole('heading', { name: 'Custom population', exact: true })).toBeVisible()
await selectSavedPopulation(page, name)
await search(page)
await expect(page.getByText(`Custom population: ${name}`)).toBeVisible()
await containsAmountOfStudents(page, students1.length + students2.length)
await containsSpecificStudents(page, [...students1, ...students2])
})
})
})
-872
View File
@@ -1,872 +0,0 @@
import { test, expect, Page } from '@playwright/test'
import { checkTableStats, init, testMockTime } from './support/commands'
const getEmptyYears = (isAcademicYear = false) => {
const today = new Date(testMockTime)
const latestYear = isAcademicYear && today.getMonth() < 7 ? today.getFullYear() - 1 : today.getFullYear()
const years = []
for (let year = latestYear; year >= 2024; year--) {
if (isAcademicYear) {
years.push(`${year} - ${year + 1}`)
} else {
years.push(year)
}
}
return years
}
const tagName = `tag-${new Date().getTime()}`
const selectYear = async (page: Page, year: number) => {
await page.getByLabel('Associated start year (optional)').fill(year.toString())
}
const deleteTag = async (page: Page, tagName: string) => {
await page.getByTestId(`delete-tag-${tagName}-button`).click()
await expect(page.getByText('Delete tag', { exact: true })).toBeVisible()
await expect(page.getByText('Are you sure you want to delete tag')).toBeVisible()
await page.getByTestId('confirm-delete-tag-button').click()
}
test.describe('Degree programme overview', () => {
test.describe('Degree programme selector', () => {
test('Degree programme search filter', async ({ page }) => {
await init(page, '/study-programme', 'admin')
await expect(page.getByText('Tietojenkäsittelytieteen kandiohjelma')).toBeVisible()
await expect(page.getByText('Matemaattisten tieteiden kandiohjelma')).toBeVisible()
await page.getByTestId('study-programme-filter').click()
await page.getByPlaceholder('Type here to filter degree programmes').fill('Tietojenkäsittelytieteen')
await expect(page.getByText('Tietojenkäsittelytieteen kandiohjelma')).toBeVisible()
await expect(page.getByText('Matemaattisten tieteiden kandiohjelma')).not.toBeVisible()
})
})
test.describe('Basic information tab works for basic user', () => {
test.beforeEach(async ({ page }) => {
await init(page, '/study-programme')
await page.getByRole('link', { name: 'Matemaattisten tieteiden kandiohjelma' }).click()
await page.getByTestId('year-toggle').click() // NOTE: Tests are written for calendar years
})
// If the backend breaks for one of the sections, the section header is not rendered and this will fail
test('Basic information tab loads', async ({ page }) => {
await expect(page.getByTestId('students-of-the-study-programme-section')).toBeVisible()
await expect(page.getByTestId('credits-produced-by-the-study-programme-section')).toBeVisible()
await expect(page.getByTestId('graduated-and-thesis-writers-of-the-programme-section')).toBeVisible()
await expect(page.getByTestId('programmes-before-or-after-section')).toBeVisible()
await expect(page.getByTestId('average-graduation-times-section')).toBeVisible()
})
test('Basic information contains correct students', async ({ page }) => {
const years = getEmptyYears()
const tableContents = [
// [Year, Started studying, Accepted, Graduated, Cancelled, Transferred Away, Transferred to]
...years.map(year => [year, 0, 0, 0, 0, 0, 0]),
[2023, 8, 8, 26, 2, 0, 0],
[2022, 25, 26, 47, 0, 1, 3],
[2021, 29, 32, 48, 1, 0, 2],
[2020, 26, 27, 12, 0, 1, 3],
[2019, 28, 34, 1, 0, 0, 1],
[2018, 40, 45, 0, 0, 0, 1],
[2017, 41, 47, 0, 0, 0, 0],
]
await checkTableStats(page, tableContents, 'students-of-the-study-programme')
})
test('Basic information contains correct credits', async ({ page }) => {
const years = getEmptyYears()
const tableContents = [
...years.map(year => [year, 0, 0, 0, 0, 0, 0, 0, 0, 0]),
[2023, 1519, 1519, 0, 0, 0, 0, 0, 222, 0],
[2022, 3235, 3205, 0, 0, 30, 0, 0, 209, 0],
[2021, 5133, 5108, 0, 0, 25, 0, 0, 428, 25],
[2020, 5801, 5796, 0, 0, 5, 0, 0, 94, 10],
[2019, 5305, 5305, 0, 0, 0, 0, 0, 162, 0],
[2018, 3442, 3432, 0, 0, 10, 0, 0, 21, 0],
[2017, 1211, 1211, 0, 0, 0, 0, 0, 189, 0],
]
await checkTableStats(page, tableContents, 'credits-produced-by-the-study-programme')
})
test('Basic information contains correct thesis writers and graduates', async ({ page }) => {
const years = getEmptyYears()
const tableContents = [
// [Year, Graduated, Wrote thesis]
...years.map(year => [year, 0, 0]),
[2023, 26, 5],
[2022, 47, 13],
[2021, 48, 76],
[2020, 12, 23],
[2019, 1, 19],
[2018, 0, 0],
[2017, 0, 1],
]
await checkTableStats(page, tableContents, 'graduated-and-graduations-of-the-programme')
})
test('Special study rights can be excluded and basic data changes accordingly', async ({ page }) => {
await page.getByTestId('study-right-toggle').click()
const years = getEmptyYears()
const studentTableContents = [
// [Year, Started studying, Accepted, Graduated, Cancelled]
...years.map(year => [year, 0, 0, 0, 0]),
[2023, 8, 8, 24, 2],
[2022, 25, 26, 43, 0],
[2021, 29, 32, 47, 1],
[2020, 26, 27, 11, 0],
[2019, 28, 34, 1, 0],
[2018, 40, 45, 0, 0],
[2017, 41, 47, 0, 0],
]
await checkTableStats(page, studentTableContents, 'students-of-the-study-programme')
const graduatedTableContents = [
// [Year, Graduated, Wrote thesis]
...years.map(year => [year, 0, 0]),
[2023, 24, 5],
[2022, 43, 12],
[2021, 47, 69],
[2020, 11, 22],
[2019, 1, 18],
[2018, 0, 0],
[2017, 0, 1],
]
await checkTableStats(page, graduatedTableContents, 'graduated-and-graduations-of-the-programme')
})
test('Year can be changed to academic year, and data changes accordingly', async ({ page }) => {
await page.getByTestId('year-toggle').click()
const isAcademicYear = true
const years = getEmptyYears(isAcademicYear)
const studentTableContents = [
// [Year, Started studying, Accepted, Graduated, Cancelled, Transferred away, Transferred to]
...years.map(year => [year, 0, 0, 0, 0, 0, 0]),
['2023 - 2024', 8, 8, 4, 1, 0, 0],
['2022 - 2023', 25, 26, 35, 1, 0, 0],
['2021 - 2022', 29, 32, 58, 1, 1, 5],
['2020 - 2021', 26, 27, 30, 0, 0, 3],
['2019 - 2020', 28, 34, 7, 0, 1, 1],
['2018 - 2019', 40, 45, 0, 0, 0, 1],
['2017 - 2018', 41, 47, 0, 0, 0, 0],
]
await checkTableStats(page, studentTableContents, 'students-of-the-study-programme')
const creditTableContents = [
...years.map(year => [year, 0, 0, 0, 0, 0, 0, 0, 0, 0]),
['2023 - 2024', 160, 160, 0, 0, 0, 0, 0, 67, 0],
['2022 - 2023', 2725, 2720, 0, 0, 5, 0, 0, 337, 0],
['2021 - 2022', 4092, 4042, 0, 0, 50, 0, 0, 198, 0],
['2020 - 2021', 5420, 5415, 0, 0, 5, 0, 0, 321, 25],
['2019 - 2020', 6043, 6043, 0, 0, 0, 0, 0, 101, 10],
['2018 - 2019', 4856, 4851, 0, 0, 5, 0, 0, 107, 0],
['2017 - 2018', 2350, 2345, 0, 0, 5, 0, 0, 26, 0],
]
await checkTableStats(page, creditTableContents, 'credits-produced-by-the-study-programme')
// await page.getByTestId('year-toggle').click()
})
test('Basic information graphs render', async ({ page }) => {
const graph = page.getByTestId('students-of-the-study-programme-line-graph-section')
await expect(graph.getByText('Started studying')).toBeVisible()
await expect(graph.getByText('Accepted')).toBeVisible()
await expect(graph.getByText('Graduated')).toBeVisible()
await expect(graph.getByText('Cancelled')).toBeVisible()
await expect(graph.getByText('Transferred away')).toBeVisible()
await expect(graph.getByText('Transferred to')).toBeVisible()
const creditsBarChart = page.getByTestId('credits-produced-by-the-study-programme-stacked-bar-chart-section')
await expect(creditsBarChart.getByText('Degree students')).toBeVisible()
await expect(creditsBarChart.getByText('Transferred')).toBeVisible()
await expect(creditsBarChart.getByText('5796')).toBeVisible()
await expect(creditsBarChart.getByText('428')).toBeVisible()
const thesisBarChart = page.getByTestId('graduated-and-thesis-writers-of-the-programme-bar-chart-section')
await expect(thesisBarChart.getByText('Graduated students')).toBeVisible()
await expect(thesisBarChart.getByText('Wrote thesis')).toBeVisible()
await expect(thesisBarChart.getByText('47')).toBeVisible()
await expect(thesisBarChart.getByText('76')).toBeVisible()
await page.getByTestId('graduation-mode-selector').getByTestId('select-median').click()
await expect(page.getByTestId('unset-median-bar-chart').getByText('47 graduated')).toBeVisible()
await page.getByTestId('unset-median-bar-chart').getByText('47 graduated').hover()
await expect(page.locator('.grad-vals').getByText('47 students graduated in year 2022')).toBeVisible()
await expect(page.getByText('median study time: 6 semesters')).toBeVisible()
await expect(page.getByText('24 graduated on time')).toBeVisible()
await expect(page.getByText('12 graduated max year overtime')).toBeVisible()
await expect(page.getByText('11 graduated over year late')).toBeVisible()
const programmesBarChart = page.getByTestId('programmes-before-or-after-stacked-bar-chart-section')
await expect(programmesBarChart.getByText('Tietojenkäsittelytieteen maisteriohjelma')).toBeVisible()
await expect(programmesBarChart.getByText('Datatieteen maisteriohjelma')).toBeVisible()
await expect(programmesBarChart.getByText('Matematiikan ja tilastotieteen maisteriohjelma')).toBeVisible()
const row = page
.getByTestId('programmes-before-or-after-data-table')
.locator('tr')
.filter({ hasText: 'Matematiikan ja tilastotieteen maisteriohjelma' })
await expect(row.getByText('33')).toBeVisible()
await expect(row.getByText('20')).toBeVisible()
await expect(row.getByText('17')).toBeVisible()
})
})
test.describe('Graduation times of master programmes', () => {
test('are split into three graphs', async ({ page }) => {
await init(page, '/study-programme')
await page.getByRole('link', { name: 'Matematiikan ja tilastotieteen maisteriohjelma' }).click()
await page.getByTestId('year-toggle').click() // Tests are written for calendar year
await page.getByTestId('graduation-mode-selector').getByTestId('select-average').click()
const masterChart = page
.getByTestId('unset-graduation-times-section')
.filter({ hasText: 'Master study right', hasNotText: 'Bachelor +' })
await expect(masterChart).toBeVisible()
await expect(masterChart.getByText('2 graduated')).toBeVisible()
await masterChart.getByText('2 graduated').hover()
await expect(page.locator('.grad-vals').getByText('2 students graduated in year 2021')).toBeVisible()
await expect(page.getByText('average study time: 4.5 semesters')).toBeVisible()
await expect(page.getByText('1 graduated on time')).toBeVisible()
await expect(page.getByText('1 graduated max year overtime')).toBeVisible()
await expect(page.getByText('0 graduated over year late')).toBeVisible()
const bachelorMasterChart = page
.getByTestId('unset-graduation-times-section')
.filter({ hasText: 'Bachelor + master study right' })
await expect(bachelorMasterChart).toBeVisible()
await expect(bachelorMasterChart.getByText('11 graduated')).toBeVisible()
await bachelorMasterChart.getByText('11 graduated').hover()
await expect(page.locator('.grad-vals').getByText('11 students graduated in year 2023')).toBeVisible()
await expect(page.getByText('average study time: 11.18 semesters')).toBeVisible()
await expect(page.getByText('4 graduated on time')).toBeVisible()
await expect(page.getByText('6 graduated max year overtime')).toBeVisible()
await expect(page.getByText('1 graduated over year late')).toBeVisible()
})
})
test.describe('Study tracks and class statistics tab works for basic user', () => {
test.beforeEach(async ({ page }) => {
await init(page, '/study-programme')
await page.getByRole('link', { name: 'Matemaattisten tieteiden kandiohjelma' }).click()
await page.getByTestId('StudyTracksAndClassStatisticsTab').click()
})
test.describe('Info boxes', () => {
test('Study track overview section', async ({ page }) => {
await page.getByTestId('study-track-overview-info-box-button').hover()
await expect(
page.getByTestId('study-track-overview-info-box-content').getByText('Opiskelijat, joiden')
).toBeVisible()
})
test('Progress of students section', async ({ page }) => {
await page.getByTestId('progress-of-students-info-box-button').hover()
await expect(
page.getByTestId('progress-of-students-info-box-content').getByText('Kuvaa koulutusohjelmassa')
).toBeVisible()
})
test('Average graduation times section', async ({ page }) => {
await page.getByTestId('average-graduation-times-info-box-button').click()
await expect(
page.getByTestId('average-graduation-times-info-box-content').getByText('Yksittäinen palkki')
).toBeVisible()
})
})
test('All sections are visible', async ({ page }) => {
await expect(page.getByTestId('study-track-selector-section')).toBeVisible()
await expect(page.getByTestId('study-track-overview-section')).toBeVisible()
await expect(page.getByTestId('progress-of-students-section')).toBeVisible()
await expect(page.getByTestId('average-graduation-times-section')).toBeVisible()
})
test('Students of the degree programme are shown correctly', async ({ page }) => {
const isAcademicYear = true
const years = getEmptyYears(isAcademicYear)
const tableContents = [
// [Year, All, Started studying, Present, Absent, Passive, Graduated, Cancelled, Has recent attainments, Men, Women, Other/Unknown, Finland, Other]
...years.map(year => [year, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]),
['2023 - 2024', 8, 8, 0, 0, 8, 0, 0, 0, 5, 3, 0, 8, 1],
['2022 - 2023', 26, 25, 0, 0, 24, 2, 1, 0, 19, 7, 0, 25, 1],
['2021 - 2022', 37, 29, 0, 0, 33, 4, 0, 0, 29, 8, 0, 35, 4],
['2020 - 2021', 30, 26, 0, 0, 11, 19, 0, 0, 15, 15, 0, 29, 3],
['2019 - 2020', 35, 28, 0, 0, 8, 27, 0, 0, 22, 13, 0, 35, 2],
['2018 - 2019', 46, 40, 0, 0, 6, 40, 1, 0, 26, 20, 0, 45, 1],
['2017 - 2018', 47, 41, 0, 0, 5, 42, 1, 0, 31, 16, 0, 47, 0],
['Total', 229, 197, 0, 0, 95, 134, 3, 0, 147, 82, 0, 224, 12],
]
await checkTableStats(page, tableContents, 'study-tracks-and-class-statistics')
})
test('Years in the students table can be expanded and study track data will be shown', async ({ page }) => {
const totalStats: [string, ...Array<number>] = ['2020 - 2021', 30, 26, 0, 0, 11, 19, 0, 0, 15, 15, 0, 29, 3]
const table = page.getByTestId('study-tracks-and-class-statistics-data-table').locator('tbody')
const row = table.locator('tr').filter({ hasText: '2020 - 2021' })
await expect(row.locator('td')).toHaveCount(14)
await expect(row.locator('td')).toHaveText(totalStats.map(n => n.toString()))
const studyTrackStats: [string, ...Array<number>][] = [
['Ekonometria (MAT-EKO)', 2, 2, 0, 0, 0, 2, 0, 0, 1, 1, 0, 2, 0],
['Matematiikka (MAT-MAT)', 13, 10, 0, 0, 1, 12, 0, 0, 7, 6, 0, 12, 3],
['Tietojenkäsittelyteoria (MAT-TIE)', 2, 1, 0, 0, 1, 1, 0, 0, 2, 0, 0, 2, 0],
['Tilastotiede (MAT-TIL)', 4, 4, 0, 0, 0, 4, 0, 0, 1, 3, 0, 4, 0],
]
await row.getByTestId('show-study-tracks-button').click()
await Promise.all(
studyTrackStats.map(async ([studyTrack, ...stats]) => {
await expect(page.getByText(studyTrack)).toBeVisible()
const studyTrackRow = table.locator('tr').filter({ hasText: studyTrack })
await expect(studyTrackRow.locator('td')).toHaveCount(14)
await expect(studyTrackRow.locator('td')).toHaveText([studyTrack, ...stats].map(n => n.toString()))
})
)
await row.getByTestId('show-study-tracks-button').click()
await Promise.all(
studyTrackStats.map(async ([studyTrack]) => {
await expect(page.getByText(studyTrack)).not.toBeVisible()
})
)
})
test.describe('Population link button works for', () => {
test('a single year', async ({ page }) => {
await page.getByTestId('2023-population-link-button').click()
await expect(page.getByText('Matemaattisten tieteiden kandiohjelma')).toBeVisible()
await expect(page.getByText('Class of 2023 - 2024, 8 students')).toBeVisible()
})
test('total', async ({ page }) => {
await page.getByTestId('total-population-link-button').click()
await expect(page.getByText('Matemaattisten tieteiden kandiohjelma')).toBeVisible()
await expect(page.getByText('Class of 2017 - 2026, 227 students')).toBeVisible()
})
test('Links to class statistics page with study track info included work', async ({ page }) => {
const table = page.getByTestId('study-tracks-and-class-statistics-data-table').locator('tbody')
const row = table.locator('tr').filter({ hasText: '2022 - 2023' })
await row.getByTestId('show-study-tracks-button').click()
await table.locator('tr').filter({ hasText: 'Matematiikka (MAT-MAT)' }).getByRole('link').click()
await expect(page.getByText('Matemaattisten tieteiden kandiohjelma')).toBeVisible()
await expect(page.getByText('Studytrack MAT-MAT')).toBeVisible()
await expect(page.getByText('Class of 2022 - 2023, 26 students')).toBeVisible()
await expect(page.getByText('Showing 3 out of 26 students')).toBeVisible()
})
})
test('Student progress data is shown correctly', async ({ page }) => {
const years = getEmptyYears(true)
const tableContents = [
// [Year, All, < 30 credits, 30–60 credits, 60–90 credits, 90–120 credits, 120–150 credits, 150–180 credits, ≥ 180 credits, Graduated]
...years.map(year => [year, 0, 0, 0, 0, 0, 0, 0, 0, 0]),
['2023 - 2024', 8, 8, 0, 0, 0, 0, 0, 0, 0],
['2022 - 2023', 26, 9, 8, 4, 3, 0, 0, 0, 2],
['2021 - 2022', 37, 8, 4, 9, 8, 4, 0, 0, 4],
['2020 - 2021', 30, 2, 1, 2, 5, 0, 0, 1, 19],
['2019 - 2020', 35, 1, 0, 2, 1, 0, 0, 4, 27],
['2018 - 2019', 46, 0, 1, 1, 2, 0, 0, 2, 40],
['2017 - 2018', 47, 0, 1, 3, 0, 0, 1, 0, 42],
['Total', 229, 28, 15, 21, 19, 4, 1, 7, 134],
]
await checkTableStats(page, tableContents, 'study-programme-progress')
})
test('Progress section', async ({ page }) => {
const barChart = page.getByTestId('programme-progress-bar-chart-section')
await expect(barChart.getByText('Less than 30 credits')).toBeVisible()
await expect(barChart.getByText('30–60 credits')).toBeVisible()
await expect(barChart.getByText('At least 180 credits')).toBeVisible()
await expect(barChart.getByText('58.5%')).toBeVisible() // The percentage for total graduated, to check that the graph renders
await page.getByTestId('programme-progress-bar-chart-section').getByText('58.5%').hover()
await expect(page.getByText('Graduated: 134')).toBeVisible()
})
test.describe('Average graduation times section', () => {
test('Shows correct data for all years and one year', async ({ page }) => {
// NOTE: This test could be flaky. Many nth and first/last used
await expect(page.getByTestId('unset-graduation-times-section').getByText('2019 - 2020')).toBeVisible()
await page.getByTestId('unset-graduation-times-section').getByText('21', { exact: true }).hover()
await expect(page.getByText('On time: 21')).toBeVisible()
await expect(
page
.getByTestId('unset-graduation-times-section')
.getByText('Click any bar on the chart to open a study track level breakdown for that year')
).toBeVisible()
await page.getByTestId('unset-graduation-times-section').getByText('15', { exact: true }).nth(1).click() // Skip legend
await expect(
page
.getByTestId('unset-graduation-times-section')
.getByText('Click any bar on the chart to open a study track level breakdown for that year')
).not.toBeVisible()
await expect(page.getByText('Year 2020 - 2021 by start year')).toBeVisible()
await expect(page.getByText('MAT-MAT')).toBeVisible()
await page.getByTestId('unsetBreakdownBarChartFaculty').getByText('9', { exact: true }).hover()
await expect(page.getByText('Matematiikka')).toBeVisible()
await expect(page.getByText('MAT-MAT').last()).toBeVisible()
await expect(page.getByText('On time: 9')).toBeVisible()
})
test('Shows correct average', async ({ page }) => {
await page.getByTestId('select-average').click()
const section = page.getByTestId('average-graduation-times-section')
await expect(section.getByText('2020 - 2021')).toBeVisible()
await section.getByText('19 graduated').hover()
await expect(page.getByText('From class of 2020 - 2021, 19/30 students have graduated')).toBeVisible()
await expect(page.getByText('average study time: 5.79 semesters')).toBeVisible()
await expect(page.getByText('15 graduated on time')).toBeVisible()
await expect(page.getByText('3 graduated max year overtime')).toBeVisible()
await expect(page.getByText('1 graduated over year late')).toBeVisible()
})
})
test.describe('Study track can be changed', () => {
test.beforeEach(async ({ page }) => {
// page.waitForTimeout('@stQuery')
await expect(page.getByText('All students of the programme')).toBeVisible()
await page.getByTestId('study-track-select').click()
await expect(page.getByText('Ekonometria')).toBeVisible()
await expect(page.getByText('Tietojenkäsittelyteoria')).toBeVisible()
await expect(page.getByText('Tilastotiede')).toBeVisible()
await page.getByText('Matematiikka').click()
})
test('Students of the study track are shown correctly', async ({ page }) => {
await expect(
page
.getByTestId('study-track-overview-section')
.getByText('Students of the study track MAT-MAT by starting year')
).toBeVisible()
const tableContents = [
// [Year, All, Started studying, Present, Absent, Passive, Graduated, Cancelled, Has recent attainment, Men, Women, Other/Unknown, Finland, Other]
['2022 - 2023', 3, 3, 0, 0, 1, 2, 1, 0, 2, 1, 0, 3, 0],
['2021 - 2022', 4, 1, 0, 0, 1, 3, 0, 0, 3, 1, 0, 4, 0],
['2020 - 2021', 13, 10, 0, 0, 1, 12, 0, 0, 7, 6, 0, 12, 3],
['2019 - 2020', 17, 14, 0, 0, 0, 17, 0, 0, 10, 7, 0, 17, 1],
['2018 - 2019', 24, 21, 0, 0, 2, 22, 1, 0, 11, 13, 0, 24, 0],
['2017 - 2018', 28, 24, 0, 0, 1, 27, 1, 0, 15, 13, 0, 28, 0],
['Total', 89, 73, 0, 0, 6, 83, 3, 0, 48, 41, 0, 88, 4],
]
await checkTableStats(page, tableContents, 'study-tracks-and-class-statistics')
})
test('Links to class statistics page with study track info included work', async ({ page }) => {
await page.getByTestId('2020-population-link-button').click()
await expect(page.getByText('Matemaattisten tieteiden kandiohjelma')).toBeVisible()
await expect(page.getByText('Studytrack MAT-MAT')).toBeVisible()
await expect(page.getByText('Class of 2020 - 2021, 30 students')).toBeVisible()
await expect(page.getByText('Showing 10 out of 30 students')).toBeVisible()
})
// FIXME: This feature was implemented. The data is no-good, so maybe reimplement this when it's clean.
test.fixme('Info message about missing progress stats is displayed', async ({ page }) => {
await expect(page.getByText('Progress of students of the study track MAT-MAT by starting year')).toBeVisible()
await expect(
page.getByText('Progress data is currently only available for all students of the degree programme.')
).toBeVisible()
})
})
})
test.describe('Programme courses tab works for basic user', () => {
test.beforeEach(async ({ page }) => {
await init(page, '/study-programme')
await page.getByRole('link', { name: 'Matemaattisten tieteiden kandiohjelma' }).click()
await page.getByTestId('ProgrammeCoursesTab').click()
await expect(page.getByTestId('by-credit-type-section')).toBeVisible()
await expect(page.getByTestId('by-credit-type-tab')).toBeVisible()
await expect(page.getByTestId('by-semester-tab')).toBeVisible()
})
test.describe('By credit type tab', () => {
test.beforeEach(async ({ page }) => {
await expect(page.getByText('Programme courses by credit type')).toBeVisible()
await expect(page.getByText('Loading content')).not.toBeVisible()
await page.getByText('Total students').click()
})
test('shows correct data', async ({ page }) => {
await expect(
page
.locator('tbody')
.locator('tr')
.filter({ hasText: 'Johdatus yliopistomatematiikkaan', hasNotText: 'Avoin yo' })
.locator('td')
).toHaveText(
['Course', 'MAT11001', 'Johdatus yliopistomatematiikkaan', 272, 253, 19, 240, 0, 4, 0, 0, 0, 1, 9].map(n =>
n.toString()
)
)
await expect(
page.locator('tbody').locator('tr').filter({ hasText: 'Raja-arvot', hasNotText: 'Avoin yo' }).locator('td')
).toHaveText(
['Course', 'MAT11003', 'Raja-arvot', 270, 249, 21, 237, 0, 4, 0, 0, 0, 1, 8].map(n => n.toString())
)
await expect(
page
.locator('tbody')
.locator('tr')
.filter({ hasText: 'Differentiaalilaskenta', hasNotText: 'Avoin yo' })
.locator('td')
).toHaveText(
['Course', 'MAT11004', 'Differentiaalilaskenta', 262, 248, 14, 230, 0, 4, 0, 0, 0, 1, 14].map(n =>
n.toString()
)
)
})
test('year toggle works', async ({ page }) => {
await page.getByTestId('year-toggle').click()
await expect(
page
.locator('tbody')
.locator('tr')
.filter({ hasText: 'Johdatus yliopistomatematiikkaan', hasNotText: 'Avoin yo' })
.locator('td')
).toHaveText(
['Course', 'MAT11001', 'Johdatus yliopistomatematiikkaan', 278, 259, 19, 240, 0, 4, 0, 0, 0, 1, 15].map(n =>
n.toString()
)
)
await expect(
page.locator('tbody').locator('tr').filter({ hasText: 'Raja-arvot', hasNotText: 'Avoin yo' }).locator('td')
).toHaveText(
['Course', 'MAT11003', 'Raja-arvot', 274, 253, 21, 237, 0, 4, 0, 0, 0, 1, 12].map(n => n.toString())
)
await expect(
page
.locator('tbody')
.locator('tr')
.filter({ hasText: 'Differentiaalilaskenta', hasNotText: 'Avoin yo' })
.locator('td')
).toHaveText(
['Course', 'MAT11004', 'Differentiaalilaskenta', 264, 250, 14, 230, 0, 4, 0, 0, 0, 1, 16].map(n =>
n.toString()
)
)
})
test('time range selection works', async ({ page }) => {
await page.getByTestId('from-year-select').click()
await page.getByTestId('from-year-select-option-2020').click()
await page.getByTestId('to-year-select').click()
await page.getByTestId('to-year-select-option-2021').click()
await expect(
page
.locator('tbody')
.locator('tr')
.filter({ hasText: 'Vektorianalyysi I', hasNotText: 'Avoin yo' })
.filter({ hasNotText: 'Vektorianalyysi II' })
.locator('td')
).toHaveText(
['Course', 'MAT21003', 'Vektorianalyysi I', 104, 97, 7, 89, 0, 1, 0, 0, 0, 0, 10].map(n => n.toString())
)
await expect(
page.locator('tbody').locator('tr').filter({ hasText: 'Sarjat', hasNotText: 'Avoin yo' }).locator('td')
).toHaveText(['Course', 'MAT21002', 'Sarjat', 83, 77, 6, 70, 0, 1, 0, 0, 0, 0, 7].map(n => n.toString()))
await expect(
page.locator('tbody').locator('tr').filter({ hasText: 'Raja-arvot', hasNotText: 'Avoin yo' }).locator('td')
).toHaveText(['Course', 'MAT11003', 'Raja-arvot', 80, 73, 7, 69, 0, 2, 0, 0, 0, 1, 1].map(n => n.toString()))
})
test("'Show credits/Show students' toggle works", async ({ page }) => {
const headers = page.getByRole('columnheader')
await expect(headers).toHaveCount(17)
await expect(headers).toHaveText([
'Type',
'Code',
'Name',
'Total students',
'Breakdown of total',
'Breakdown of passed',
'Not included in total or passed',
'Passed',
'Not completed',
'Degree students',
'Open university students (with hetu)',
'Open university students (without hetu)',
'Exchange students',
'Other university students',
'Separate studies',
'Other students',
'Students with transferred credits',
])
await page.getByTestId('show-credits-students-toggle').click()
await expect(headers).toHaveCount(13)
await expect(headers).toHaveText([
'Type',
'Code',
'Name',
'Total credits',
'Credits produced by',
'Transferred credits',
'Degree students',
'Open university (hetu)',
'Open university (no hetu)',
'Exchange students',
'Other universities',
'Separate studies',
'Other',
])
})
})
test.describe('By semester tab', () => {
test.beforeEach(async ({ page }) => {
await page.getByTestId('by-semester-tab').click()
})
// TODO: Implement proper tests
test('can be opened', async ({ page }) => {
await expect(page.getByText('Programme courses by semester')).toBeVisible()
await expect(page.getByText('From')).toBeVisible()
await expect(page.getByText('Until')).toBeVisible()
})
})
})
test.describe('Degree courses tab works for basic user', () => {
test.beforeEach(async ({ page }) => {
await init(page, '/study-programme')
await page.getByRole('link', { name: 'Matemaattisten tieteiden kandiohjelma' }).click()
await page.getByTestId('DegreeCoursesTab').click()
})
test.describe('Curriculum section', () => {
test('is shown in both settings tabs', async ({ page }) => {
for (const tab of ['criteria', 'visibility']) {
await page.getByTestId(`${tab}-tab`).click()
const panel = page.getByTestId(`${tab}-panel`)
await expect(panel.getByTestId('curriculum-section')).toBeVisible()
await expect(panel.getByText('Select curriculum to edit:')).toBeVisible()
await expect(panel.getByTestId('curriculum-picker').getByText('2023–2026')).toBeVisible()
}
})
test.skip('can be changed', () => {
return
})
})
test.describe('Credit criteria section', () => {
test('info box', async ({ page }) => {
await expect(page.getByTestId('credit-criteria-section')).toBeVisible()
await page.getByTestId('credit-criteria-info-box-button').click()
await expect(page.getByTestId('credit-criteria-info-box-content').getByText('Tässä asetetut')).toBeVisible()
})
test.skip('changing the credit criteria works', () => {
// TODO: Test using the inputs
// TODO: Button status
// TODO: Test "previously set to _" text changing when saving
})
})
test('Degree course table', async ({ page }) => {
await expect(page.getByTestId('criteria-panel').getByTestId('degree-course-table')).toBeVisible()
await page.getByTestId('visibility-tab').click()
await expect(page.getByTestId('visibility-panel').getByTestId('degree-course-table')).toBeVisible()
// TODO: Test clicking the arrow buttons
// TODO: Test toggling visibility for MODULES and COURSES
// TODO: Test changing criterion labels
})
})
test.describe('Tags tab works for basic user', () => {
test.beforeEach(async ({ page }) => {
await init(page, '/study-programme')
await page.getByRole('link', { name: 'Matemaattisten tieteiden kandiohjelma' }).click()
await page.getByTestId('TagsTab').click()
// We need to wait for tag page to load or there is no delete buttons
await expect(page.getByText('Create new tag')).toBeVisible()
// Delete tags until none remain
const deleteButtons = page.locator('[data-cy^=delete-tag-]')
while ((await deleteButtons.count()) > 0) {
const count = await deleteButtons.count()
await deleteButtons.first().click()
await page.getByTestId('confirm-delete-tag-button').click()
// Helps playwright to wait for the deleted tag acually being removed from DOM
await expect(deleteButtons).toHaveCount(count - 1)
}
})
test('info box', async ({ page }) => {
await page.getByTestId('create-new-tag-info-box-button').click()
await expect(page.getByTestId('create-new-tag-info-box-content').getByText('Here you can create')).toBeVisible()
})
test.describe('Adding tags to populations and removing them works', () => {
test.beforeEach(async ({ page }) => {
await expect(page.getByText('This degree programme does not have any tags yet')).toBeVisible()
await page.getByTestId('tag-name-text-field').getByRole('textbox').fill(tagName)
})
test.afterEach(async ({ page }) => {
await deleteTag(page, tagName)
await expect(page.getByText('This degree programme does not have any tags yet')).toBeVisible()
})
test('can create and delete tags without start year', async ({ page }) => {
await page.getByTestId('create-button').click()
await expect(page.getByText(tagName)).toBeVisible()
await expect(page.getByText('No associated start year')).toBeVisible()
})
test('can create and delete tags with start year', async ({ page }) => {
await selectYear(page, 2022)
await page.getByTestId('create-button').click()
await expect(page.getByText(tagName)).toBeVisible()
await expect(page.getByText('Associated start year 2022')).toBeVisible()
})
test.fixme('population link works', async ({ page }) => {
await selectYear(page, 2022)
await page.getByTestId('create-button').click()
await expect(page.getByText(tagName)).toBeVisible()
await page.getByTestId('population-link-button').click()
// FIXME: If no students are tagged with the tag, it shows "Invalid tag id"
await expect(page.getByText(`Tagged with: ${tagName}`)).toBeVisible()
await page.goBack()
})
test('can create personal tags', async ({ page }) => {
await page.getByTestId('personal-tag-toggle').click()
await page.getByTestId('create-button').click()
await expect(page.getByText(tagName)).toBeVisible()
await expect(page.getByTestId(`${tagName}-visibility-icon`)).toBeVisible()
})
})
test.describe('Adding tags to students and removing them works', () => {
const studentInput = '477806,478275; 478953 479239\n 480080'
const studentNumbers = studentInput.match(/[^\s,;]+/g)!
test.beforeEach(async ({ page, context }) => {
await init(page, '/study-programme', 'basic', context)
await page.getByRole('link', { name: 'Matemaattisten tieteiden kandiohjelma' }).click()
await page.getByTestId('TagsTab').click()
})
test('can add tags to students', async ({ page, context }) => {
// NOTE: Navigating to each students' page takes time, fix?
test.slow()
test.setTimeout(120_000) // 2min
await page.getByTestId('tag-name-text-field').getByRole('textbox').fill(tagName)
await selectYear(page, 2022)
await page.getByTestId('create-button').click()
await expect(page.getByText(tagName)).toBeVisible()
await page.getByTestId('add-students-button').click()
await page.getByTestId('add-students-text-field').getByRole('textbox').fill(studentInput)
await page.getByTestId('add-students-confirm-button').click()
await expect(page.getByText('Students added to tag')).toBeVisible()
await page.getByTestId('population-link-button').click()
await expect(page.getByText('Matemaattisten tieteiden kandiohjelma')).toBeVisible()
await expect(page.getByText('Class of 2022 - 2023')).toBeVisible()
await expect(page.getByText(`Tagged with: ${tagName}`)).toBeVisible()
await expect(page.getByText('Students (5)')).toBeVisible()
await page.getByText('Students (5)').click()
await Promise.all(studentNumbers.map(studentNumber => expect(page.getByText(studentNumber)).toBeVisible()))
await Promise.all(
studentNumbers.map(async studentNumber => {
const studentPage = await context.newPage()
await studentPage.goto(`/students/${studentNumber}`)
await expect(studentPage.getByRole('heading', { name: 'Tags' })).toBeVisible()
await expect(studentPage.getByText(tagName)).toBeVisible()
await studentPage.close()
})
)
await page.goBack()
await deleteTag(page, tagName)
})
test('deleting a tag from tag view also removes it from students', async ({ page, context }) => {
// NOTE: Navigating to each students' page takes time, fix?
test.slow()
test.setTimeout(180_000) // 4min
await expect(page.getByText(tagName)).not.toBeVisible()
await Promise.all(
studentNumbers.map(async studentNumber => {
const studentPage = await context.newPage()
await studentPage.goto(`/students/${studentNumber}`)
await expect(studentPage.getByRole('heading', { name: 'Tags' })).not.toBeVisible()
await expect(studentPage.getByText(tagName)).not.toBeVisible()
// await studentPage.close()
})
)
})
})
})
test.describe('IAM user', () => {
test.beforeEach(async ({ page }) => {
await init(page, '/study-programme', 'onlyiamrights')
await page.getByRole('link', { name: 'Matemaattisten tieteiden kandiohjelma' }).click()
})
test.skip('year selector', () => {
// TODO: Implement this test
// Things to test:
// - Clicking the button works
// - Selecting a study track works
// - The button is disabled if only one year is selected
// - The slider is not visible if the user does not have correct programme rights
})
test('can access programme and correct tabs are visible', async ({ page }) => {
await expect(page.getByText('Basic information')).toBeVisible()
await expect(page.getByText('Study tracks and class statistics')).toBeVisible()
await expect(page.getByText('Update statistics')).not.toBeVisible()
await expect(page.getByText('Degree courses')).not.toBeVisible()
})
test('can access basic information', async ({ page }) => {
await page.getByTestId('BasicInformationTab').click()
await expect(page.getByTestId('students-of-the-study-programme-section')).toBeVisible()
await expect(page.getByTestId('credits-produced-by-the-study-programme-section')).toBeVisible()
await expect(page.getByTestId('graduated-and-thesis-writers-of-the-programme-section')).toBeVisible()
await expect(page.getByTestId('average-graduation-times-section')).toBeVisible()
await expect(page.getByTestId('programmes-before-or-after-section')).toBeVisible()
})
test('can access study tracks', async ({ page }) => {
await page.getByTestId('StudyTracksAndClassStatisticsTab').click()
await expect(page.getByTestId('study-track-overview-section')).toBeVisible()
await expect(page.getByTestId('progress-of-students-section')).toBeVisible()
await expect(page.getByTestId('average-graduation-times-section')).toBeVisible()
})
test("doesn't see other tabs", async ({ page }) => {
await expect(page.getByTestId('DegreeCoursesTab')).not.toBeVisible()
await expect(page.getByTestId('TagsTab')).not.toBeVisible()
await expect(page.getByTestId('UpdateStatisticsTab')).not.toBeVisible()
})
})
})
-195
View File
@@ -1,195 +0,0 @@
import { expect, Page, test } from '@playwright/test'
import { init } from './support/commands'
const facultyName = 'Matemaattis-luonnontieteellinen tiedekunta'
test.use({ acceptDownloads: true })
const openFaculty = async (page: Page, userId?: 'admin' | 'basic') => {
await init(page, '/faculties', userId)
await page.getByRole('link', { name: facultyName }).click()
}
test.describe('Faculty statistics', () => {
test.describe('Faculty list', () => {
test.beforeEach(async ({ page }) => {
await init(page, '/faculties')
})
test('contains faculty names and faculty codes', async ({ page }) => {
const faculties = [
['Teologinen tiedekunta', 'H10'],
['Oikeustieteellinen tiedekunta', 'H20'],
['Lääketieteellinen tiedekunta', 'H30'],
['Humanistinen tiedekunta', 'H40'],
['Matemaattis-luonnontieteellinen tiedekunta', 'H50'],
['Farmasian tiedekunta', 'H55'],
['Bio- ja ympäristötieteellinen tiedekunta', 'H57'],
['Kasvatustieteellinen tiedekunta', 'H60'],
['Valtiotieteellinen tiedekunta', 'H70'],
['Svenska social- och kommunalhögskolan', 'H74'],
['Maatalous-metsätieteellinen tiedekunta', 'H80'],
['Eläinlääketieteellinen tiedekunta', 'H90'],
]
await expect(page.getByRole('heading', { name: 'Faculties', exact: true })).toBeVisible()
for (const [name, code] of faculties) {
await expect(page.getByText(name, { exact: true })).toBeVisible()
await expect(page.getByText(code, { exact: true })).toBeVisible()
}
})
test('contains a working link to faculty page', async ({ page }) => {
await page.getByRole('link', { name: 'Teologinen tiedekunta' }).click()
await expect(page).toHaveURL('/faculties/hy-org-1000000580')
await expect(page.getByText('Teologinen tiedekunta', { exact: true })).toBeVisible()
await expect(page.getByText('H10', { exact: true })).toBeVisible()
})
})
test.describe('Basic information tab', () => {
test('shows the correct tabs for an admin user', async ({ page }) => {
await openFaculty(page, 'admin')
const tabs = page.getByTestId('faculty-tabs')
await expect(tabs).toContainText('Basic information')
await expect(tabs).toContainText('Students by starting year')
await expect(tabs).toContainText('Graduation times')
await expect(tabs).toContainText('Update statistics')
})
test.describe('Basic user', () => {
test.beforeEach(async ({ page }) => {
await openFaculty(page)
})
test('info boxes contain the correct information', async ({ page }) => {
const infoBoxes = [
['students-of-the-faculty-info-box-button', 'students-of-the-faculty-info-box-content', 'Taulukon luvut on'],
[
'graduated-of-the-faculty-info-box-button',
'graduated-of-the-faculty-info-box-content',
'Sisältää kyseisenä',
],
[
'thesis-writers-of-the-faculty-info-box-button',
'thesis-writers-of-the-faculty-info-box-content',
'Sisältää kyseisenä',
],
[
'credits-produced-by-the-faculty-info-box-button',
'credits-produced-by-the-faculty-info-box-content',
'Sisältää opintopisteet',
],
]
for (const [button, content, text] of infoBoxes) {
await page.getByTestId(button).click()
await expect(page.getByTestId(content)).toContainText(text)
}
})
test('shows all graphs and tables', async ({ page }) => {
const sections = [
'students-of-the-faculty-line-graph-section',
'students-of-the-faculty-interactive-data-table',
'graduated-of-the-faculty-line-graph-section',
'graduated-of-the-faculty-interactive-data-table',
'thesis-writers-of-the-faculty-line-graph-section',
'thesis-writers-of-the-faculty-interactive-data-table',
'credits-produced-by-the-faculty-stacked-bar-chart-section',
'credits-produced-by-the-faculty-interactive-data-table',
]
for (const section of sections) await expect(page.getByTestId(section)).toBeVisible()
})
test('shows the correct tabs', async ({ page }) => {
const tabs = page.getByTestId('faculty-tabs')
await expect(tabs).toContainText('Basic information')
await expect(tabs).toContainText('Students by starting year')
await expect(tabs).toContainText('Graduation times')
await expect(tabs).not.toContainText('Update statistics')
})
test('year toggle works', async ({ page }) => {
const tables = [
'credits-produced-by-the-faculty-interactive-data-table',
'thesis-writers-of-the-faculty-interactive-data-table',
'students-of-the-faculty-interactive-data-table',
'graduated-of-the-faculty-interactive-data-table',
]
for (const table of tables) await expect(page.getByTestId(table)).toContainText('2022 - 2023')
await page.getByTestId('year-toggle').click()
for (const table of tables) await expect(page.getByTestId(table)).toContainText('2022')
})
test('programme toggle works', async ({ page }) => {
await expect(page.getByTestId('faculty-programmes-shown-info')).not.toBeVisible()
await page.getByTestId('programme-toggle').click()
await expect(page.getByTestId('faculty-programmes-shown-info')).toBeVisible()
})
test('study rights toggle works', async ({ page }) => {
await expect(page.getByTestId('faculty-exclude-specials-info')).not.toBeVisible()
await page.getByTestId('study-right-toggle').click()
await expect(page.getByTestId('faculty-exclude-specials-info')).toBeVisible()
})
})
})
test.describe('Students by starting year tab', () => {
test.beforeEach(async ({ page }) => {
await openFaculty(page)
await page.getByText('Students by starting year', { exact: true }).click()
})
test.skip('export button downloads an Excel file', async ({ page }) => {
// Firefox does not trigger playwright's download event when downloading a file
const downloadPromise = page.waitForEvent('download')
await page.getByTestId('faculty-student-table-export-button').click()
const download = await downloadPromise
expect(download.suggestedFilename()).toMatch(/^oodikone_H50_programme_stats_\d{4}-\d{2}-\d{2}\.xlsx$/)
})
test('export button exists and works', async ({ page }) => {
const exportButton = page.getByTestId('faculty-student-table-export-button')
await expect(exportButton).toBeVisible()
await expect(exportButton).toBeEnabled()
await exportButton.click()
})
test('info box contains the correct information', async ({ page }) => {
await page.getByTestId('faculty-student-table-info-box-button').click()
await expect(page.getByTestId('faculty-student-table-info-box-content')).toContainText('Opiskelijat, joiden')
})
test('percentage toggle works', async ({ page }) => {
const table = page.getByTestId('faculty-student-stats-table')
await expect(table).toBeVisible()
await expect(table).not.toContainText('92.3 %')
await page.getByTestId('percentage-toggle').click()
await expect(table).toContainText('92.3 %')
})
})
test.describe('Graduation times tab', () => {
test.beforeEach(async ({ page }) => {
await openFaculty(page)
await page.getByText('Graduation times', { exact: true }).click()
})
test('info box contains the correct information', async ({ page }) => {
await page.getByTestId('average-graduation-times-info-box-button').click()
await expect(page.getByTestId('average-graduation-times-info-box-content')).toContainText(
'Opiskelijoiden keskimääräiset'
)
})
test('graphs are visible', async ({ page }) => {
await expect(page.getByTestId('bachelor-graduation-times-section')).toBeVisible()
await expect(page.getByTestId('master-graduation-times-section')).toBeVisible()
await expect(page.getByTestId('doctor-graduation-times-section')).toBeVisible()
})
})
})
-198
View File
@@ -1,198 +0,0 @@
import { expect, test, type Page } from '@playwright/test'
import { init } from './support/commands'
const checkFilteredStudentCount = async (page: Page, count: number) => {
await expect(page.getByText(`Students (${count})`)).toBeVisible()
}
const notYetImplemented = () => {
test.skip(true, 'TODO')
}
const selectDropdownOption = async (page: Page, selector: string, option: string | number) => {
// Only open the filter card if it is closed
const dropdown = page.getByTestId(`${selector}-selector`)
if (!(await dropdown.isVisible())) {
await page.getByTestId(`${selector}-header`).click()
await expect(dropdown).toBeVisible()
}
await dropdown.click()
const options = page.getByRole('listbox').getByRole('option')
if (typeof option === 'number') await options.nth(option).click()
else await options.filter({ hasText: option }).click()
await page.keyboard.press('Escape') // Close the search box
}
const setRange = async (page: Page, filter: string, min: number, max: number, expected: number) => {
const card = page.getByTestId(`${filter}-filter-card`)
await card.getByTestId('FilterRangeStart').locator('input').fill(String(min))
await card.getByTestId('FilterRangeEnd').locator('input').fill(String(max))
await checkFilteredStudentCount(page, expected)
}
const pathToMathMSc2020 = '/populations?years=2020&programme=MH50_001&semesters=FALL&semesters=SPRING'
const defaultMScStudents = 26
test.describe("Population statistics with a master's programme", () => {
test.beforeEach(async ({ page }) => {
await init(page, pathToMathMSc2020)
})
test('Study track filter works', async ({ page }) => {
await selectDropdownOption(page, 'studyTrackFilter', 'Matematiikka ja soveltava matematiikka (MAST-MSM)')
await checkFilteredStudentCount(page, 15)
await page.getByTestId('studyTrackFilter-clear').click()
await checkFilteredStudentCount(page, defaultMScStudents)
})
test('Study right type filter works', async ({ page }) => {
const filter = page.getByTestId('studyRightTypeFilter-header')
await filter.click()
for (const [value, count] of [
['Bachelor + master', 24],
['Master only', 2],
['All', defaultMScStudents],
] as const) {
const option = page
.getByTestId('studyRightTypeFilter-filter-card')
.getByRole('radio', { name: value, exact: true })
await expect(option).not.toBeChecked()
await option.click()
await expect(option).toBeChecked()
await checkFilteredStudentCount(page, count)
}
await filter.click()
})
})
test.describe('Population Statistics', () => {
test.describe.skip('TODO', () => {
test('Study right type filter is not visible', async ({ page }) => {
await init(page, '/populations?years=2020&programme=KH50_001&semesters=FALL&semesters=SPRING')
await expect(page.getByText('Study right type')).not.toBeVisible()
})
test('Transfer filter works and is set to "not transferred" by default', notYetImplemented)
test('Graduation filter works', notYetImplemented)
test('Enrollment filter works', notYetImplemented)
test('Credit filter works', notYetImplemented)
test('Age filter works', notYetImplemented)
test('Gender filter works', notYetImplemented)
test('Starting year filter works', notYetImplemented)
test('Admission type filter works', notYetImplemented)
test('Courses filter works', notYetImplemented)
test('Filter combinations work', notYetImplemented)
test('"Reset All Filters" button works', notYetImplemented)
})
})
test.describe('Population Statistics with Bachelor + Master', () => {
test.beforeEach(async ({ page }) => {
await init(
page,
'/populations?years=2017&programme=KH50_001&showBachelorAndMaster=true&semesters=FALL&semesters=SPRING'
)
})
test('Graduation filter works', async ({ page }) => {
await page.getByTestId('graduatedFromProgrammeFilter-header').click()
for (const [value, count] of [
["Graduated with Bachelor's", 42],
["Graduated with Master's", 18],
["Not graduated with Bachelor's", 5],
["Not graduated with Master's", 29],
] as const) {
const option = page
.getByTestId('graduatedFromProgrammeFilter-filter-card')
.getByRole('radio', { name: value, exact: true })
await expect(option).not.toBeChecked()
await option.click()
await expect(option).toBeChecked()
await checkFilteredStudentCount(page, count)
}
})
test('Study right status filter works', async ({ page }) => {
await page.getByTestId('studyRightStatusFilter-header').click()
for (const [value, count] of [
['Active Bachelor study right', 0],
['Active Master study right', 0],
['Passive Bachelor study right', 5],
['Passive Master study right', 24],
] as const) {
const option = page
.getByTestId('studyRightStatusFilter-filter-card')
.getByRole('radio', { name: value, exact: true })
await expect(option).not.toBeChecked()
await option.click()
await expect(option).toBeChecked()
await checkFilteredStudentCount(page, count)
}
})
})
test.describe('Course Statistics', () => {
test('Grade filter works', notYetImplemented)
test('Programme filter works and defaults to "Attainment"', notYetImplemented)
test('Age filter works', notYetImplemented)
test('Gender filter works', notYetImplemented)
test('Starting year filter works', notYetImplemented)
test('Filter combinations work', notYetImplemented)
})
test.describe('Custom Population Statistics', () => {
test.beforeEach(async ({ page }) => {
await init(page, '/custompopulation')
const students = ['433237', '457144', '458090', '465136', '474270', '479440', '470391', '474789']
await page.getByTestId('student-number-input').getByRole('textbox').pressSequentially(students.join('\n'))
await page.getByTestId('search-button').click()
await expect(page.getByText('Credit accumulation')).toBeVisible()
})
test('Age filter works', async ({ page }) => {
await page.getByTestId('ageFilter-header').click()
await setRange(page, 'ageFilter', 24, 28, 4)
})
test('Programme filter works and defaults to "Active Study Right"', async ({ page }) => {
await page.getByTestId('programmeFilter-header').click()
await expect(page.getByTestId('programmeFilter-mode-selector').getByRole('combobox')).toHaveText(
'Active Study Right'
)
await selectDropdownOption(page, 'programmeFilter', 'Matemaattisten tieteiden kandiohjelma')
await checkFilteredStudentCount(page, 5)
})
test('Gender filter works', async ({ page }) => {
for (const [value, count] of [
['Female (5)', 5],
['Male (3)', 3],
['Other (0)', 0],
['Unknown (0)', 0],
] as const) {
await selectDropdownOption(page, 'genderFilter', value)
await checkFilteredStudentCount(page, count)
}
})
test('Starting year filter works', async ({ page }) => {
await selectDropdownOption(page, 'startYearAtUniFilter', 0)
await checkFilteredStudentCount(page, 2)
})
test('Courses filter works', async ({ page }) => {
const filter = page.getByTestId('courseFilter-header')
await filter.click()
await page.getByRole('combobox').fill('MAT11005 - Integraalilaskenta')
await page.getByRole('option', { name: 'MAT11005 - Integraalilaskenta' }).click()
await checkFilteredStudentCount(page, 7)
// MAT11005 = hy-CU-117375607
await page.getByTestId('courseFilter-hy-CU-117375607-selector').click()
await page.getByRole('option', { name: 'Passed' }).click()
await checkFilteredStudentCount(page, 6)
})
})
-81
View File
@@ -1,81 +0,0 @@
import { expect, test, Page } from '@playwright/test'
import { init } from './support/commands'
const chooseSemester = async (page: Page, semester: string, fromOrTo: 'from' | 'to') => {
await page.getByTestId(`semester-${fromOrTo}`).click()
await page.getByTestId(`select-opt-${semester}`).click()
// The dropdown unmounts its options on close, so wait for it to be gone before reading the table
await expect(page.getByRole('listbox')).toHaveCount(0)
}
const checkNumbers = async (page: Page, numbers: number[], numberOfColumns: number, mode: string) => {
const totalRow = page.getByTestId(`ooditable-${mode}`).locator('table tbody tr').first()
const cells = totalRow.locator('td')
await expect(cells).toHaveCount(numberOfColumns)
await expect(cells).toHaveText(['Total' + 'All courses total'].concat(numbers.map(number => String(number))))
}
test.describe('When language center is opened', () => {
test.describe('as an admin user', () => {
test.beforeEach(async ({ page }) => {
await init(page, '/languagecenterview', 'admin')
await page.getByTestId('completions-button').click()
})
test.describe('Faculties tab', () => {
test.beforeEach(async ({ page }) => {
await chooseSemester(page, 'Syksy 2017', 'from')
await chooseSemester(page, 'Kevät 2024', 'to')
})
test('Initial view is correct', async ({ page }) => {
await expect(page.getByText('All courses total')).toBeVisible()
await expect(
page.getByRole('cell', { name: 'AYKK-ENG Academic and Professional Communication in English' })
).toBeVisible()
await expect(page.getByText('AYKK-RUKIRJ')).toBeVisible()
})
// FIXME: This changes per updater run
// Example case: 550789 - KK-JAOK15
test.skip('Faculties tab shows numbers', async ({ page }) => {
await checkNumbers(page, [2074, 36, 9, 33, 758, 15, 40, 6, 51, 1, 24, 1059, 42, 0], 15, 'faculties')
})
test.skip('Faculties tab "exceeding" button works', async ({ page }) => {
await page.getByTestId('difference-button').click()
await checkNumbers(page, [66, 3, 0, 4, 18, 0, 1, 1, 4, 0, 0, 31, 3, 1], 15, 'faculties')
})
test.skip('Faculties tab semester selector changes numbers', async ({ page }) => {
await chooseSemester(page, 'Syksy 2020', 'from')
await checkNumbers(page, [1182, 28, 6, 27, 498, 4, 10, 2, 36, 0, 9, 535, 27, 0], 15, 'faculties')
})
})
test.describe('Semester tab', () => {
test.beforeEach(async ({ page }) => {
await page.getByText('By semesters').click()
await chooseSemester(page, 'Syksy 2017', 'from')
await chooseSemester(page, 'Kevät 2024', 'to')
})
test.skip('Semester tab shows numbers', async ({ page }) => {
await checkNumbers(page, [2074, 69, 26, 298, 58, 343, 98, 437, 138, 310, 90, 123, 73, 10, 1], 16, 'semesters')
})
})
})
test.describe('with a user with no rights', () => {
test('"Access denied" is shown', async ({ page }) => {
await init(page, '/languagecenterview', 'norights')
await expect(page.getByRole('heading', { name: 'Access denied' })).toBeVisible()
await expect(page.getByText("You don't currently have permission to view this page.")).toBeVisible()
await page.getByText('Special populations').click()
await expect(page.getByText('Language center view')).not.toBeVisible()
})
})
})
-252
View File
@@ -1,252 +0,0 @@
import { expect, test, type Page } from '@playwright/test'
import { init } from './support/commands'
type CourseData = [
courseOrModule: string,
code: string,
groupId: string,
name: string,
total: number,
passed: number,
notCompleted: number,
failed: number,
enrolledNoGrade: number,
]
const courseData: CourseData[] = [
['Course', 'MAT11001', 'hy-CU-117375151', 'Johdatus yliopistomatematiikkaan', 272, 253, 1 + 18, 1, 18],
['Course', 'MAT11003', 'hy-CU-117375413', 'Raja-arvot', 270, 249, 1 + 20, 1, 20],
['Course', 'MAT11004', 'hy-CU-117375487', 'Differentiaalilaskenta', 262, 248, 1 + 13, 1, 13],
['Course', 'MAT21002', 'hy-CU-117375793', 'Sarjat', 256, 250, 0 + 6, 0, 6],
['Course', 'MAT11005', 'hy-CU-117375607', 'Integraalilaskenta', 251, 234, 0 + 17, 0, 17],
['Course', 'MAT11002', 'hy-CU-117375394', 'Lineaarialgebra ja matriisilaskenta I', 247, 242, 1 + 4, 1, 4],
['Course', 'MAT12003', 'hy-CU-117376940', 'Todennäköisyyslaskenta I', 234, 227, 3 + 4, 3, 4],
['Course', 'MAT21001', 'hy-CU-117375754', 'Lineaarialgebra ja matriisilaskenta II', 230, 216, 4 + 10, 4, 10],
['Course', 'MAT21003', 'hy-CU-117375829', 'Vektorianalyysi I', 228, 202, 3 + 23, 3, 23],
['Course', 'MAT20005', 'hy-CU-120230575', 'Akateemiset taidot', 201, 178, 0 + 23, 0, 23],
['Module', 'MAT110', 'hy-SM-117412902', 'Matematiikka, perusopinnot', 195, 195, 0 + 0, 0, 0],
['Course', 'MAT21014', 'hy-CU-117377242', 'Johdatus logiikkaan I', 188, 170, 0 + 18, 0, 18],
]
const MAT21003_GROUP_ID = 'hy-CU-117375829'
const yearlyData = [
[2017, 1, 0, 1, 0, 1 + 0],
[2018, 31, 27, 4, 0, 4 + 0],
[2019, 58, 56, 2, 0, 2 + 0],
[2020, 63, 62, 1, 0, 1 + 0],
[2021, 41, 35, 0, 6, 0 + 6],
[2022, 28, 21, 0, 7, 0 + 7],
[2023, 21, 1, 0, 20, 0 + 20],
] as const
// Copied from services/backend/src/services/studyProgramme/studyProgrammeHelpers.ts (almost)
const getPercentage = (value: any, total: any) => {
if (typeof value !== 'number' || typeof total !== 'number') return 'NA'
if (total === 0) return 'NA'
if (value === 0) return '0.00 %'
return `${((value / total) * 100).toFixed(2)} %`
}
const selectCourseStatus = async (page: Page, courseGroupId: string, status: string) => {
await page.getByTestId(`courseFilter-${courseGroupId}-selector`).click()
await page.getByRole('listbox').getByRole('option', { name: status }).click()
}
const selectYear = async (page: Page, selector: string, option: string) => {
await page.getByTestId(selector).click()
await page.getByTestId(option).click()
}
test.describe('Numbers should match between', () => {
test.slow()
test.setTimeout(180_000) // 3min
test.describe('Programme courses, Course statistics and Course population (no substitutions, all years)', () => {
test('in Programme courses', async ({ page }) => {
await init(page, '/study-programme')
await page.getByRole('link', { name: 'Matemaattisten tieteiden kandiohjelma' }).click()
await page.getByTestId('ProgrammeCoursesTab').click()
await expect(page.getByText('Programme courses by credit type')).toBeVisible()
await page.getByRole('columnheader', { name: 'Total students' }).click()
const rows = page.locator('tbody tr')
for (const [index, course] of courseData.entries()) {
const row = rows.nth(index)
const [courseOrModule, code, , name, total, passed, notCompleted] = course
// Not optimal, but not checking all locator("td") matches directly with toHaveText is hard.
for (const [fieldIndex, field] of [courseOrModule, code, name, total, passed, notCompleted].entries()) {
await expect(row.locator('td').nth(fieldIndex)).toHaveText(String(field))
}
}
})
test('in Course statistics', async ({ page }) => {
for (const [
_courseOrModule,
_code,
groupId,
name,
total,
passed,
_notCompleted,
failed,
enrolledNoGrade,
] of courseData) {
await init(page, `/coursestatistics?courses=${groupId}&substitutions=false`, 'basic')
await expect(page).toHaveURL(
url =>
url.pathname === '/coursestatistics' &&
url.searchParams.get('courses') === groupId &&
url.searchParams.get('substitutions') === 'false'
)
await expect(page.getByText('Course statistics')).toBeVisible()
await expect(page.getByText(name)).toBeVisible()
await selectYear(page, 'FromYearSelector', 'FromYearSelectorOption2017-2018')
const row = page.locator('tbody tr').first()
await expect(row.locator('td')).toHaveText(
[
'Total',
total,
passed,
failed,
enrolledNoGrade,
getPercentage(passed, total),
getPercentage(failed + enrolledNoGrade, total),
].map(String)
)
}
})
test('in Course population', async ({ page }) => {
for (const [
_courseOrModule,
code,
groupId,
name,
total,
passed,
_notCompleted,
failed,
enrolledNoGrade,
] of courseData) {
await init(
page,
`/coursepopulation?from=${2017 - 1949}&to=${2023 - 1949}&courses=${groupId}&substitutions=false`,
'basic'
)
await expect(page).toHaveURL(
url =>
url.pathname === '/coursepopulation' &&
url.searchParams.get('courses') === groupId &&
url.searchParams.get('substitutions') === 'false'
)
await expect(page.getByText(name)).toBeVisible()
await page.getByTestId('courseFilter-filter-card').click()
await page.getByTestId('courseFilter-selector').click()
await page
.getByTestId('courseFilter-popper')
.getByRole('option', { name: `${code} - ${name}` })
.click()
for (const [status, expected] of [
['All', total],
['Passed', passed],
['Failed', failed],
['Enrolled, No Grade', enrolledNoGrade],
] as const) {
await selectCourseStatus(page, groupId, status)
await expect(page.getByText(`Students (${expected})`)).toBeVisible()
}
}
})
})
test.describe('Programme courses, Course statistics and Course population (no substitutions, single years, single course)', () => {
test('in Programme courses', async ({ page }) => {
await init(page, '/study-programme')
await page.getByRole('link', { name: 'Matemaattisten tieteiden kandiohjelma' }).click()
await page.getByTestId('ProgrammeCoursesTab').click()
for (const [year, total, passed, _failed, _enrolledNoGrade, notCompleted] of yearlyData) {
await selectYear(page, 'to-year-select', `to-year-select-option-${year}`)
await selectYear(page, 'from-year-select', `from-year-select-option-${year}`)
const row = page.locator('tbody tr').filter({ hasText: 'MAT21003' })
await expect(row.locator('td').nth(3)).toContainText(String(total))
await expect(row.locator('td').nth(4)).toContainText(String(passed))
await expect(row.locator('td').nth(5)).toContainText(String(notCompleted))
}
})
test('in Course statistics', async ({ page }) => {
for (const [year, total, passed, failed, enrolledNoGrade] of yearlyData) {
await init(page, `/coursestatistics?courses=${MAT21003_GROUP_ID}&substitutions=false`, 'basic')
await expect(page).toHaveURL(
url =>
url.pathname === '/coursestatistics' &&
url.searchParams.get('courses') === MAT21003_GROUP_ID &&
url.searchParams.get('substitutions') === 'false'
)
const yearString = `${year}-${year + 1}`
await selectYear(page, 'ToYearSelector', `ToYearSelectorOption${yearString}`)
await selectYear(page, 'FromYearSelector', `FromYearSelectorOption${yearString}`)
await expect(page.getByText('Student statistics')).toBeVisible()
await expect(page.locator('tbody tr')).toHaveCount(2)
const row = page.locator('tbody tr').filter({ hasText: 'Total' })
await expect(row.locator('td')).toHaveText(
[
'Total',
total,
passed,
failed,
enrolledNoGrade,
getPercentage(passed, total),
getPercentage(failed + enrolledNoGrade, total),
].map(String)
)
}
})
test('in Course population', async ({ page }) => {
for (const [year, _total, passed, failed, enrolledNoGrade] of yearlyData) {
await init(
page,
`/coursepopulation?from=${year - 1949}&to=${year - 1949}&courses=${MAT21003_GROUP_ID}&substitutions=false`,
'basic'
)
await expect(page).toHaveURL(
url =>
url.pathname === '/coursepopulation' &&
url.searchParams.get('courses') === MAT21003_GROUP_ID &&
url.searchParams.get('substitutions') === 'false'
)
await expect(page.getByText('Vektorianalyysi I')).toBeVisible()
await page.getByTestId('courseFilter-filter-card').click()
await page.getByTestId('courseFilter-selector').click()
await page
.getByTestId('courseFilter-popper')
.getByRole('option', { name: 'MAT21003 - Vektorianalyysi I' })
.click()
for (const [status, expected] of [
['All', _total],
['Passed', passed],
['Failed', failed],
['Enrolled, No Grade', enrolledNoGrade],
] as const) {
await selectCourseStatus(page, MAT21003_GROUP_ID, status)
await expect(page.getByText(`Students (${expected})`)).toBeVisible()
}
}
})
})
})
-493
View File
@@ -1,493 +0,0 @@
import { test, expect, Page } from '@playwright/test'
import { init } from './support/commands'
// Now "Class statistics" in UI
const selectStudyProgramme = async (page: Page, programme: string) => {
await expect(page.getByTestId('population-programme-selector').locator('input')).toHaveAttribute(
'placeholder',
'Select degree programme'
)
await page.getByTestId('population-programme-selector-parent').click()
await page.getByTestId('population-programme-selector-parent').getByText(programme).click()
}
const selectStudyTrack = async (page: Page, studyTrack: string) => {
await expect(page.getByTestId('population-studytrack-selector').locator('input')).toHaveAttribute(
'placeholder',
'Select study track'
)
await page.getByTestId('population-studytrack-selector-parent').click()
await page.getByTestId('population-studytrack-selector-parent').getByText(studyTrack).click()
}
const getPath = (programme: string) => {
return `/populations?months=49&semesters=FALL&semesters=SPRING&programme=${programme}&years=2020`
}
const pathToMathBSc2020 = getPath('KH50_001')
const pathToMathMSc2020 = getPath('MH50_001')
test.describe('Population statistics tests', () => {
test.describe('When using basic user', () => {
test.beforeEach(async ({ page }) => {
await init(page, '/populations')
})
test.describe('Population search', () => {
test('Info box works', async ({ page }) => {
await expect(page.getByTestId('PopulationSearch-info-box-content')).not.toBeVisible()
await page.getByTestId('PopulationSearch-info-box-button').hover()
await expect(page.getByTestId('PopulationSearch-info-box-content')).toBeVisible()
await expect(
page.getByTestId('PopulationSearch-info-box-content').getByText('Tässä osiossa voi tarkastella')
).toBeVisible()
await page.getByTestId('PopulationSearch-info-box-button').dispatchEvent('mouseout')
await expect(page.getByTestId('PopulationSearch-info-box-content')).not.toBeVisible()
})
test('Form is usable', async ({ page }) => {
await expect(page.getByText('Search for class')).toBeVisible()
await expect(page.getByText('See class')).toBeDisabled()
const yearSelect = page.getByTestId('population-year-selector')
const yearDecrement = page.getByTestId('population-year-decrement')
const yearIncrement = page.getByTestId('population-year-increment')
await yearSelect.click()
await page.getByText('2018 - 2019').click()
await expect(yearSelect.getByText('2018 - 2019')).toBeVisible()
await yearDecrement.click()
await expect(yearSelect.getByText('2017 - 2018')).toBeVisible()
await yearIncrement.click()
await expect(yearSelect.getByText('2018 - 2019')).toBeVisible()
await expect(page.getByTestId('population-studytrack-selector').locator('input')).toHaveAttribute(
'placeholder',
'No study tracks available'
)
await selectStudyProgramme(page, 'Matematiikan ja tilastotieteen maisteriohjelma')
await expect(page.getByText('See class')).toBeEnabled()
await selectStudyTrack(page, 'Matematiikka ja soveltava matematiikka')
await expect(page.getByText('See class')).toBeEnabled()
})
test.describe('Correct population is shown for programme', () => {
test('without study tracks', async ({ page }) => {
await selectStudyProgramme(page, 'Matematiikan ja tilastotieteen maisteriohjelma')
// Select year 2020 - 2021
const yearSelect = page.getByTestId('population-year-selector')
await yearSelect.click()
await page.getByText('2020 - 2021').click()
await page.getByText('See class').click()
await expect(page.getByText('Matematiikan ja tilastotieteen maisteriohjelma')).toBeVisible()
await expect(page.getByText('Class of 2020 - 2021, 26 students')).toBeVisible()
await expect(page.getByText('Showing 26 out of 26 students')).toBeVisible()
await expect(page.getByText('Studytrack MAST-MSM')).not.toBeVisible()
})
test('with study tracks', async ({ page }) => {
await selectStudyProgramme(page, 'Matematiikan ja tilastotieteen maisteriohjelma')
// Select year 2020 - 2021
const yearSelect = page.getByTestId('population-year-selector')
await yearSelect.click()
await page.getByText('2020 - 2021').click()
await selectStudyTrack(page, 'Matematiikka ja soveltava matematiikka')
await page.getByText('See class').click()
await expect(page.getByText('Matematiikan ja tilastotieteen maisteriohjelma')).toBeVisible()
await expect(page.getByText('Class of 2020 - 2021, 26 students')).toBeVisible()
await expect(page.getByText('Showing 15 out of 26 students')).toBeVisible()
await expect(page.getByText('Studytrack MAST-MSM')).toBeVisible()
})
})
})
test('Population statistics is usable on general level', async ({ page }) => {
await page.goto(pathToMathBSc2020)
await expect(page.getByTestId('filtered-students')).toBeVisible()
await expect(page.getByText('Matemaattisten tieteiden kandiohjelma')).toBeVisible()
await expect(page.getByText('Class of 2020 - 2021, 30 students')).toBeVisible()
await expect(page.getByText('Showing 27 out of 30 students')).toBeVisible()
await expect(page.getByText('Excludes exchange students')).toBeVisible()
await expect(page.getByText('Excludes students with non-degree study right')).toBeVisible()
await expect(page.getByText('Excludes students who have transferred out of this programme')).toBeVisible()
await expect(page.getByTestId('filtered-students')).toBeVisible()
})
test('Advanced settings work', async ({ page }) => {
await page.goto(pathToMathMSc2020)
await expect(page.getByText('Credit accumulation (for 26 students)')).toBeVisible()
await expect(page).toHaveURL(/.*semesters=FALL&semesters=SPRING.*/)
await page.getByTestId('advanced-toggle').click()
// only spring
await page.getByTestId('toggle-fall').click()
await page.getByText('Fetch class').click()
await expect(page.getByText('Credit accumulation (for 17 students)')).toBeVisible()
await expect(page).not.toHaveURL(/.*semesters=FALL.*/)
await page.getByTestId('advanced-toggle').click()
await page.getByTestId('toggle-fall').click()
await page.getByTestId('toggle-spring').click()
await page.getByText('Fetch class').click()
await expect(page.getByText('Credit accumulation (for 9 students)')).toBeVisible()
await expect(page).not.toHaveURL(/.*semesters=SPRING.*/)
await page.getByTestId('advanced-toggle').click()
await page.getByTestId('toggle-spring').click()
await page.getByText('Fetch class').click()
await expect(page.getByText('Credit accumulation (for 26 students)')).toBeVisible()
await expect(page).toHaveURL(/.*semesters=FALL&semesters=SPRING.*/)
})
test.describe('Credit statistics', () => {
test("'Credits gained' tab shows correct statistics for all students of the class and also students grouped by admission type", async ({
page,
}) => {
await page.goto(pathToMathBSc2020)
const totalStudents = 27
await expect(page.getByText('Credit statistics')).toBeVisible()
await page.getByText('Credit statistics').click()
await expect(page.getByTestId('credits-gained-tab')).toBeVisible()
await page.getByTestId('credits-gained-tab').click()
const limits = [1, 45, 90, 135, 180, null]
const ranges = limits.map((limit, i) => (i === 0 ? [null, 0] : [limits[i - 1], limit])).reverse()
const getTableData = (selector: string, numbersOfStudents: number[]) => {
const studentsInCategory = numbersOfStudents.reduce((acc, val) => acc + val, 0)
return ranges.map((range, index) => ({
selector: `credits-gained-table-${selector}`,
start: range[0],
end: range[1],
students: numbersOfStudents[index],
percentage: `${((numbersOfStudents[index] / studentsInCategory) * 100).toFixed(1)}%`,
}))
}
await expect(page.getByTestId('credits-gained-table-All students of the class')).toBeVisible()
await expect(page.getByTestId('credits-gained-table-All students of the class').locator('th')).toHaveCount(4)
await expect(page.getByTestId('credits-gained-table-All students of the class').locator('th')).toContainText([
'',
'Credits gained between 01.08.2020 and ', // Current date
`Number of students` + `(n = ${totalStudents})`,
'Percentage of population',
])
await Promise.all(
[
getTableData('All students of the class', [9, 5, 7, 4, 2, 0]),
getTableData('Avoin väylä', [0, 1, 3, 2, 0, 0]),
getTableData('Muu', [0, 1, 1, 1, 0, 0]),
getTableData('Todistusvalinta', [9, 3, 3, 1, 2, 0]),
].map(category =>
Promise.all(
category.map(async ({ selector, start, end, students, percentage }, index) => {
let value
if (start === null) {
value = '0 credits'
} else if (end === null) {
value = `${start} ≤ credits`
} else {
value = `${start} ≤ credits < ${end}`
}
const bodyLocator = page.getByTestId(selector).getByTestId('credits-gained-table-body')
await expect(bodyLocator).toBeVisible()
const row = bodyLocator.locator('tr').nth(index)
await expect(row.locator('td')).toHaveCount(4)
await expect(row.locator('td')).toHaveText(['' /* Icon */, value, students.toString(), percentage])
})
)
)
)
const numberOfStudentCounts = await page
.getByTestId('credits-gained-table-All students of the class')
.getByTestId('credits-gained-table-body')
.locator('td:nth-child(3)')
.allTextContents()
const sum = numberOfStudentCounts.reduce((acc, t) => acc + parseInt(t, 10), 0)
expect(sum).toBe(totalStudents)
})
test("'Statistics' tab shows correct statistics for all students of the class and also students grouped by admission type", async ({
page,
}) => {
await page.goto(pathToMathBSc2020)
await page.getByText('Credit statistics').click()
await page.getByTestId('credit-statistics-tab').click()
const rows = ['Total credits', 'Average', 'Median', 'Standard deviation', 'Minimum', 'Maximum']
const categories = [
{
selector: 'All students of the population',
data: ['6136.50', '227.28', '197.00', '117.94', '60', '533'],
size: 27,
},
{
selector: 'Muu',
data: ['580.00', '193.33', '192.00', '36.75', '149', '239'],
size: 3,
},
{
selector: 'Todistusvalinta',
data: ['3657.50', '203.19', '193.50', '98.35', '60', '460'],
size: 18,
},
{
selector: 'Avoin väylä',
data: ['1899.00', '316.50', '257.00', '150.43', '150', '533'],
size: 6,
},
]
await Promise.all(
categories.map(async ({ selector, data, size }) => {
const tableLocator = page.getByTestId(`statistics-table-${selector}`)
await expect(tableLocator).toBeVisible()
await expect(tableLocator.getByRole('heading', { level: 5, name: selector })).toBeVisible()
await expect(tableLocator.getByTestId('credit-stats-population-size')).toContainText(`n = ${size}`)
await Promise.all(
rows.map((_, index) => {
const row = tableLocator.locator('table tbody tr').nth(index)
return expect(row.locator('td')).toHaveText([rows[index], data[index]])
})
)
})
)
})
})
test.describe('Courses of class', () => {
test('Is displayed and link to individual course stats page works and has substitutions enabled', async ({
page,
}) => {
await page.goto(pathToMathBSc2020)
await page.getByText('Courses of class').click()
await page.getByTestId('toggle-group-module-MAT110').click()
await page.getByRole('row', { name: 'MAT11001' }).getByRole('link').click()
await expect(page).toHaveURL(/.*coursestatistics.*/)
await expect(page.getByText('MAT11001', { exact: true })).toBeVisible()
await expect(page.getByText('AYMAT11001')).toBeVisible()
await expect(page.getByText('57033', { exact: true })).toBeVisible()
await expect(page.getByText('A57033')).toBeVisible()
})
test('Curriculum selection works', async ({ page }) => {
await page.goto(pathToMathBSc2020)
await page.getByText('Courses of class').click()
await expect(page.getByTestId('curriculum-picker')).toContainText('2020–2023')
await expect(page.getByTestId('toggle-group-module-MAT-tyo')).toBeVisible()
await page.getByTestId('curriculum-picker').click()
await page.getByText('2023–2026').click()
await expect(page.getByTestId('toggle-group-module-MAT-tyo')).not.toBeVisible()
})
test('Courses data is changed when curriculum is changed', async ({ page }) => {
await page.goto(pathToMathBSc2020)
await page.getByText('Courses of class').click()
const curriculumPicker = page.getByTestId('curriculum-picker')
await expect(curriculumPicker.getByText('2020–2023')).toBeVisible()
await expect(page.getByText('Students (27)')).toBeVisible()
await expect(page.getByTestId('toggle-group-module-DIGI-k')).toBeVisible()
await page.getByTestId('toggle-group-module-DIGI-k').click()
await expect(page.getByText('DIGI-100')).toBeVisible()
await expect(page.getByTestId('toggle-group-module-DIGI-k')).toBeVisible()
await curriculumPicker.click()
await page.getByText('2023–2026').click()
await expect(page.getByTestId('toggle-group-module-DIGI-k')).toBeVisible()
await expect(page.getByText('DIGI-100')).not.toBeVisible()
})
test('Courses data is changed when filtered students change', async ({ page }) => {
await page.goto(pathToMathBSc2020)
const filterCard = page.getByTestId('graduatedFromProgrammeFilter-filter-card')
await filterCard.getByTestId('graduatedFromProgrammeFilter-header').click()
await page.getByRole('radio', { name: 'Graduated', exact: true }).click()
await expect(page.getByRole('radio', { name: 'Graduated', exact: true })).toBeChecked()
await expect(page.getByText('Students (16)')).toBeVisible()
})
})
test.describe('Students', () => {
test.beforeEach(async ({ page }) => {
await page.goto(pathToMathBSc2020)
const studentsHeading = page.getByText('Students (27)')
const parentHasActive = await studentsHeading.evaluate(el => el.parentElement?.classList.contains('active'))
if (!parentHasActive) await page.getByText('Students (27)').click()
})
test("'General tab' is usable", async ({ page }) => {
const general = page.getByTestId('ooditable-general')
await expect(general).toBeVisible()
const row = general.locator('tbody').locator('tr').filter({ hasText: '489963' })
await expect(row.locator('td')).toHaveCount(24)
await expect(row.locator('td')).toHaveText(
[
'489963' + 'Sisu',
'Graduated',
241,
0,
169,
0,
0,
2020,
'2020-08-01',
'2020-08-01',
'2022-06-13',
23,
'',
'Matematiikka',
'Matematiikan ja tilastotieteen maisteriohjelma',
'Matematiikan ja tilastotieteen maisteriohjelma',
'',
'Avoin väylä',
'Male',
'Suomi',
'',
'',
'',
'',
].map(n => n.toString())
)
})
test("'Courses tab' is usable", async ({ page }) => {
await page.getByTestId('student-table-tabs').getByText('Courses').click()
const courses = page.getByTestId('ooditable-courses')
await expect(courses.getByText('MAT11001')).toBeVisible()
await expect(courses.getByText('MAT11004')).toBeVisible()
})
test("'Modules tab' Displays correct modules based on the selected programme", async ({ page }) => {
await page.getByTestId('student-table-tabs').getByText('Modules').click()
const modules = page.getByTestId('ooditable-modules')
await expect(modules.getByText('MAT011')).toBeVisible()
await expect(modules.getByText('MAT110')).toBeVisible()
await page.getByText('Courses of class').click()
await page.getByTestId('curriculum-picker').click()
await page.getByText('2023–2026').click()
await page.getByText('Courses of class').click()
await expect(page.getByTestId('ooditable-modules').getByText('MAT011')).not.toBeVisible()
await expect(page.getByTestId('ooditable-modules').getByText('MAT110')).toBeVisible()
})
test("Empty 'tags' tab has a link to the page where tags can be created", async ({ page }) => {
await page.getByTestId('student-table-tabs').getByText('Tags').click()
await expect(page.getByText('No tags defined. You can define them')).toBeVisible()
await page.getByRole('link', { name: 'here' }).click()
await expect(page).toHaveURL(/.*study-programme\/KH50_001\?tab=4.*/)
await expect(page.getByText('Matemaattisten tieteiden kandiohjelma')).toBeVisible()
await expect(page.getByText('Create new tag')).toBeVisible()
await expect(page.getByTestId('create-button')).toBeDisabled()
})
})
})
test.describe('When using admin', () => {
test('Student list checking works as intended', async ({ page }) => {
await init(page, pathToMathBSc2020, 'admin')
const existing = '433237'
const nonExisting = '550004'
await expect(page.getByText('Students (27)')).toBeVisible()
await page.getByText('Students (27)').click()
await expect(page.getByText(existing)).toBeVisible()
await expect(page.getByText(nonExisting)).not.toBeVisible()
await page.getByRole('button', { name: 'Check student numbers' }).click()
await expect(page.getByText('Check for student numbers')).toBeVisible()
await page.getByTestId('check-student-numbers').getByRole('textbox').fill(`${existing}\n${nonExisting}`)
await page.getByRole('button', { name: 'Check students' }).click()
const results = page.locator('#checkstudentsresults')
await results.getByTestId('found-title').click()
await expect(results.getByTestId('found-data').getByText(existing)).toBeVisible()
await results.getByTestId('not-found-title').click()
await expect(results.getByTestId('not-found-data').getByText(nonExisting)).toBeVisible()
await results.getByTestId('not-searched-title').click()
await expect(results.getByTestId('not-searched-data').getByText('457144')).toBeVisible()
await page.getByRole('button', { name: 'Close' }).click()
await expect(page.getByText('Student numbers in list and in Sisu')).not.toBeVisible()
})
})
test.describe('When using IAM user', () => {
test.beforeEach(async ({ page }) => {
await init(page, pathToMathBSc2020, 'onlyiamrights')
await expect(page.getByText('Matemaattisten tieteiden kandiohjelma')).toBeVisible()
await expect(page.getByText('Class of 2020 - 2021, 30 students')).toBeVisible()
})
test('Population statistics is visible', async ({ page }) => {
const card = page.getByTestId('PopulationQueryCard')
await expect(card.getByText('Excludes exchange students')).toBeVisible()
await expect(card.getByText('Excludes students with non-degree study right')).toBeVisible()
await expect(card.getByText('Excludes students who have transferred out of this programme')).toBeVisible()
})
test('Only correct panels are visible', async ({ page }) => {
const panelParent = page.getByTestId('panelview-parent')
await expect(panelParent.getByText('Credit accumulation (for 27 students)')).toBeVisible()
await expect(panelParent.getByText('Credit statistics')).toBeVisible()
await expect(panelParent.getByText('Age distribution')).toBeVisible()
await expect(panelParent.getByText('Courses of class')).toBeVisible()
})
test('Ages cannot be ungrouped', async ({ page }) => {
await expect(page.getByText('Age distribution')).toBeVisible()
await page.getByText('Age distribution').click()
await expect(page.getByText('Group ages')).not.toBeVisible()
})
test('Age filter is not visible', async ({ page }) => {
await expect(page.locator("[data-cy='filtered-students']")).toBeVisible()
await expect(page.locator("[data-cy='Age-filter-card']")).not.toBeVisible()
})
test('Students tab is not available', async ({ page }) => {
await expect(page.getByText('Students (27)')).not.toBeVisible()
})
})
})
-126
View File
@@ -1,126 +0,0 @@
import { expect, test } from '@playwright/test'
import { userHeaders } from './support/commands'
type RouteRequirements = {
userId?: string
roles?: string[]
requireProgrammeRights?: boolean
programmeRights?: string[]
iamGroups?: string[]
}
type UserInfo = {
userId?: string
roles?: string[]
programmeRights?: unknown[]
iamGroups?: string[]
}
const baseRoutes: [string, RouteRequirements][] = [
['/', {}],
['/changelog', {}],
['/completedcoursessearch', {}],
['/university', {}],
['/feedback', {}],
['/populations', { roles: ['fullSisuAccess'], programmeRights: [] }],
['/study-programme', { roles: ['fullSisuAccess'], programmeRights: [] }],
// TODO: This should probably be allowed for user with programmeRight, not only fullSisuAccess
['/study-programme/KH50_001', { roles: ['fullSisuAccess'], programmeRights: [] }],
['/coursepopulation', { roles: ['fullSisuAccess'], programmeRights: [] }],
['/users', { roles: ['admin'] }],
['/users/334', { roles: ['admin'] }],
['/updater', { roles: ['admin'] }],
['/coursestatistics', { roles: ['fullSisuAccess', 'courseStatistics'], programmeRights: [] }],
['/faculties', { roles: ['fullSisuAccess', 'facultyStatistics'] }],
['/faculties/hy-org-1000000911', { roles: ['fullSisuAccess', 'facultyStatistics'] }],
['/students', { roles: ['fullSisuAccess', 'studyGuidanceGroups'], programmeRights: [] }],
['/students/545400', { roles: ['fullSisuAccess', 'studyGuidanceGroups'], programmeRights: [] }],
['/custompopulation', { roles: ['fullSisuAccess', 'studyGuidanceGroups'], programmeRights: [] }],
['/teachers', { roles: ['teachers'] }],
['/teachers/hy-hlo-1463447', { roles: ['teachers'] }],
['/studyguidancegroups', { roles: ['studyGuidanceGroups'] }],
['/studyguidancegroups/otm-b3b52ff5-88f6-4d52-bd18-0588023b6322', { userId: '', roles: ['studyGuidanceGroups'] }],
['/close-to-graduation', { roles: ['fullSisuAccess', 'studyGuidanceGroups'] }],
['/languagecenterview', { iamGroups: ['grp-kielikeskus-esihenkilot'] }],
]
for (const user of userHeaders) {
if (user.uid === 'admin') continue
test.describe(`${user.displayname} has access only to views that were intended`, () => {
let userInfo: UserInfo | undefined
test.beforeAll(async ({ request }) => {
const response = await request.get('/api/login', { headers: user })
userInfo = (await response.json())?.user
})
test.beforeEach(async ({ page }) => {
expect(userInfo).toBeDefined()
// Inject user headers into all API calls and fail if any of them results in a server error
await page.route('**/api/**', async route => {
const response = await route.fetch({
headers: {
...route.request().headers(),
...user,
},
})
expect(response.status(), `${route.request().method()} ${route.request().url()}`).toBeLessThan(500)
await route.fulfill({ response })
})
})
test.afterEach(async ({ page }) => {
await page.unrouteAll({ behavior: 'ignoreErrors' })
})
for (const [route, requirements] of baseRoutes) {
test(`Checking access ${route}`, async ({ page }) => {
// TODO: JAMI mock needed to make this test deterministic
test.skip(user.uid === 'onlyiamrights' && route === '/coursestatistics')
await page.goto(route)
const { userId, roles = [], programmeRights = [], iamGroups = [] } = userInfo ?? {}
const hasProgrammeRights = programmeRights.length > 0
const requiredUserId = !requirements.userId || requirements.userId === userId
const requiredRoles = !requirements.roles || requirements.roles.some(role => roles.includes(role))
const requiredIamGroups =
!requirements.iamGroups || requirements.iamGroups.some(group => iamGroups.includes(group))
let requiredRights = !requirements.requireProgrammeRights || hasProgrammeRights
if (requirements.programmeRights) {
requiredRights =
requirements.programmeRights.length === 0
? hasProgrammeRights
: roles.includes('fullSisuAccess') ||
requirements.programmeRights.every(right => programmeRights.includes(right))
}
// This is defined in frontend ProtectedRoute component.
const specialNeedsRoutes = [
'populations',
'study-programme',
'coursepopulation',
'coursestatistics',
'students',
'custompopulation',
]
const mankeli = specialNeedsRoutes.some(path => route.includes(path))
? requiredRoles || requiredRights
: requiredRoles && requiredRights
const shouldHaveAccess = mankeli && requiredUserId && requiredIamGroups
if (shouldHaveAccess) {
await expect(page.getByRole('heading', { name: 'Something broke' })).not.toBeVisible()
await expect(page.getByRole('heading', { name: 'Access denied' })).not.toBeVisible()
} else {
await expect(page.getByRole('heading', { name: 'Access denied' })).toBeVisible()
}
})
}
})
}
-253
View File
@@ -1,253 +0,0 @@
import { expect, test, type Page } from '@playwright/test'
import { init } from './support/commands'
const student = {
firstNames: 'Varpu Roope',
lastName: 'Mårtensson',
studentNumber: '550003',
sisPersonId: 'hy-hlo-115926826',
email: 'sisutestidata134902@testisisudata.fi',
}
const searchFor = async (page: Page, value: string) => {
await page.getByTestId('student-search').getByRole('textbox').fill(value)
}
const openStudent = async (page: Page, studentNumber: string) => {
await searchFor(page, studentNumber)
await page.getByRole('cell', { name: studentNumber }).click()
}
const expectNamesHidden = async (page: Page) => {
await expect(page.getByText(student.firstNames)).toHaveCount(0)
await expect(page.getByText(student.lastName)).toHaveCount(0)
}
test.describe('Students tests', () => {
test.describe('When using basic user', () => {
test.beforeEach(async ({ page }) => {
await init(page, '/students')
await expect(page).toHaveURL(/\/students/)
await expect(page.getByText('Show student names')).toBeVisible()
})
test('Students search form is usable', async ({ page }) => {
await searchFor(page, student.lastName)
await expect(page.getByText('Student number')).toBeVisible()
await expect(page.getByText('Last name')).toHaveCount(0)
await expect(page.getByText('First names')).toHaveCount(0)
await expect(page.getByText('Started')).toBeVisible()
await expect(page.getByText('Credits')).toBeVisible()
await expect(page.getByText('Active study rights')).toBeVisible()
await expectNamesHidden(page)
await page.getByTestId('toggleStudentNames').click()
await expect(page.getByText(student.firstNames)).toBeVisible()
await expect(page.getByText('Last name')).toBeVisible()
await expect(page.getByText('First names')).toBeVisible()
await expect(page.getByText(student.lastName)).toBeVisible()
await page.getByTestId('toggleStudentNames').click()
await expectNamesHidden(page)
})
test('Search term must be at least 4 characters long', async ({ page }) => {
await searchFor(page, student.lastName.slice(0, 3))
await expect(page.getByText('Search term is not accurate enough')).toBeVisible()
await page.getByTestId('student-search').getByRole('textbox').pressSequentially(student.lastName.slice(3)) // Appends to input
await expect(page.locator('table tbody tr')).toHaveCount(1)
})
test('Can search with student number too', async ({ page }) => {
await searchFor(page, student.studentNumber)
await expect(page.getByText(student.studentNumber)).toBeVisible()
})
test('Can get student specific page by clicking student', async ({ page }) => {
await openStudent(page, student.studentNumber)
await expect(page.getByText('Matemaattisten tieteiden kandiohjelma (01.08.2020–31.07.2027)')).toBeVisible()
await expectNamesHidden(page)
await expect(page.getByText(student.email, { exact: true })).toHaveCount(0)
await page.getByTestId('toggleStudentNames').click()
await expect(page.getByText(student.lastName)).toBeVisible()
await expect(page.getByText(student.firstNames)).toBeVisible()
await expect(page.getByText(student.email)).toBeVisible()
await page.getByTestId('toggleStudentNames').click()
await expectNamesHidden(page)
await expect(page.getByText(student.email)).toHaveCount(0)
})
test("'Update student' button is not shown", async ({ page }) => {
await openStudent(page, student.studentNumber)
await expect(page.getByTestId('student-info-card').getByRole('button', { name: 'Update student' })).toHaveCount(0)
})
test('Can get back to search menu', async ({ page }) => {
await openStudent(page, student.studentNumber)
await page.goBack()
await expect(page.getByText('Student number')).toHaveCount(0)
await expect(page.getByText('Credits')).toHaveCount(0)
})
test('Can jump to course', async ({ page }) => {
await openStudent(page, student.studentNumber)
const courseRow = page.locator('tr').filter({ hasText: 'Tilastollinen päättely I (MAT12004)' })
await courseRow.locator('a').click()
await expect(page).toHaveURL(/\/coursestatistics/)
await expect(page.getByText('MAT12004', { exact: true })).toBeVisible()
await expect(page.getByText('AYMAT12004')).toBeVisible()
await expect(page.getByText('57046', { exact: true })).toBeVisible()
})
test('Has correct Sisu link', async ({ page }) => {
await openStudent(page, student.studentNumber)
await expect(page.getByTestId('sisu-link')).toHaveAttribute(
'href',
`https://sisu.helsinki.fi/tutor/role/staff/student/${student.sisPersonId}/basic/basic-info`
)
})
test('Semester enrollments can be toggled', async ({ page }) => {
await openStudent(page, student.studentNumber)
const programmes = [
'Oikeustieteen tohtoriohjelma',
'Matemaattisten tieteiden kandiohjelma',
'Oikeustieteen maisterin koulutusohjelmaOikeusnotaarin koulutusohjelma',
]
const card = page.getByTestId('student-info-card')
await card.getByText('Enrollments').click()
await expect(card.locator('table tbody tr')).toHaveCount(3)
await expect(card.locator('table tbody tr td:first-child')).toHaveText(programmes)
})
test("Searching with bad name doesn't yield results", async ({ page }) => {
await searchFor(page, 'SWAG LITTINEN')
await expect(page.getByText('Student number')).toHaveCount(0)
})
test("Searching with bad student number doesn't yield results", async ({ page }) => {
await searchFor(page, '01114')
await expect(page.getByText('Student number')).toHaveCount(0)
})
test('Can jump to population page', async ({ page }) => {
await openStudent(page, student.studentNumber)
await page
.getByTestId('study-rights-section')
.getByText('Matemaattisten tieteiden kandiohjelma')
.getByRole('link')
.click()
await expect(page.getByText('Class of 2020 - 2021, 30 students')).toBeVisible()
})
test('Grade graph works in all three different modes', async ({ page }) => {
await openStudent(page, student.studentNumber)
await page.getByRole('tab', { name: 'Grade graph' }).click()
await page.getByRole('button', { name: 'Show group mean' }).click()
const groupSize = page.getByTestId('group-size-input')
await expect(groupSize.getByLabel('Group size')).toBeVisible()
await expect(groupSize.locator('input')).toHaveValue('5')
await groupSize.locator('input').fill('10')
await expect(groupSize.locator('input')).toHaveValue('10')
await page.getByRole('button', { name: 'Show semester mean' }).click()
await expect(groupSize.getByLabel('Group size')).not.toBeVisible()
})
test.describe('Bachelor Honours section', () => {
test('Shows qualified student details', async ({ page }) => {
await page.goto('/students/495976')
await expect(page.getByRole('heading', { name: 'Bachelor Honours' })).toBeVisible()
await expect(page.getByTestId('honours-chip-qualified')).toContainText('Qualified for Honours')
await page.getByText('Study modules').click()
const rows = page.getByTestId('main-modules').locator('tbody tr')
await expect(rows).toHaveCount(3)
await expect(
rows.filter({ hasText: 'Matemaattisten tieteiden kandiohjelma (KH50_001)' }).locator('td')
).toHaveText(['07.05.2020', 'Matemaattisten tieteiden kandiohjelma (KH50_001)', 'Hyv.'])
await expect(rows.filter({ hasText: 'Matematiikka, perusopinnot (MAT110)' }).locator('td')).toHaveText([
'08.05.2018',
'Matematiikka, perusopinnot (MAT110)',
'4',
])
await expect(rows.filter({ hasText: 'Matematiikka, aineopinnot (MAT210)' }).locator('td')).toHaveText([
'25.02.2020',
'Matematiikka, aineopinnot (MAT210)',
'4',
])
})
test('Shows Did not graduate in time', async ({ page }) => {
await page.goto('/students/540355')
await expect(page.getByTestId('honours-chip-not-qualified')).toHaveText('Not qualified for Honours')
await expect(page.getByTestId('honours-chip-error')).toHaveText('Did not graduate in time')
})
test('Shows Module grades too low', async ({ page }) => {
await page.goto('/students/547934')
await expect(page.getByTestId('honours-chip-not-qualified')).toHaveText('Not qualified for Honours')
await expect(page.getByTestId('honours-chip-error')).toHaveText('Module grades too low')
})
test('Shows Might need further inspection', async ({ page }) => {
await page.goto('/students/478837')
await expect(page.getByTestId('honours-chip-inspection')).toHaveText('Might need further inspection')
})
test('Shows Has not graduated', async ({ page }) => {
await page.goto(`/students/${student.studentNumber}`)
await expect(page.getByTestId('honours-chip-not-qualified')).toHaveText('Not qualified for Honours')
await expect(page.getByTestId('honours-chip-error')).toHaveText('Has not graduated')
})
})
})
test.describe('When using admin user', () => {
test.beforeEach(async ({ page }) => {
await init(page, '/students', 'admin')
})
test('Does not crash if student has no study rights or courses', async ({ page }) => {
await openStudent(page, '450730')
await expect(page.getByText('Credits: 0')).toBeVisible()
})
test("'Update student' button is shown", async ({ page }) => {
await openStudent(page, student.studentNumber)
await expect(page.getByTestId('student-info-card').getByRole('button', { name: 'Update student' })).toBeVisible()
})
test('Bachelor Honours section is not shown outside Faculty of Science', async ({ page }) => {
await page.goto('/students/453146')
await expect(page.getByRole('heading', { name: 'Bachelor Honours' })).toHaveCount(0)
})
test('Study plan courses are highlighted with a blue background', async ({ page }) => {
await page.goto('/students/550789')
const studyPlan = page.getByTestId('KH40_005-0')
await expect(studyPlan).toContainText('Kulttuurien tutkimuksen kandiohjelma')
await studyPlan.locator('td').filter({ hasText: 'Kulttuurien tutkimuksen kandiohjelma' }).click()
await expect(page.locator('table tbody tr').filter({ hasText: 'Kandidaatintutkielma (KUKA-LIS222)' })).toHaveCSS(
'background-color',
'rgb(232, 244, 255)'
)
})
test('Completed credits are displayed for a matching study plan', async ({ page }) => {
await page.goto('/students/550789')
const studyPlan = page.getByTestId('KH40_005-0')
await expect(studyPlan).toContainText('Kulttuurien tutkimuksen kandiohjelma')
await expect(studyPlan).toContainText('185 cr')
})
test('Completion percentage is displayed for an unfinished matching study plan', async ({ page }) => {
await page.goto('/students/458723')
const studyPlan = page.getByTestId('KH20_001-0')
await expect(studyPlan).toContainText('Oikeusnotaarin koulutusohjelma')
await expect(studyPlan).toContainText('69% (125 cr)')
})
})
})
-193
View File
@@ -1,193 +0,0 @@
import { expect, Page, test } from '@playwright/test'
import { init, type UserId } from './support/commands'
const studyGuidanceGroups = [
{
id: 'sgg-cypress-1',
name: { fi: 'MAT kandit 2020' },
members: [{ personStudentNumber: '433237' }, { personStudentNumber: '457144' }, { personStudentNumber: '458090' }],
tags: { studyProgramme: 'KH50_001', year: '2020' },
},
{
id: 'sgg-cypress-2',
name: { fi: 'Oma ohjausryhmä' },
members: [{ personStudentNumber: '474270' }, { personStudentNumber: '479440' }],
tags: { studyProgramme: null, year: null },
},
]
const initWithGroups = async (page: Page, path: string, userId: UserId = 'onlystudyguidancegroups') => {
await init(page, path, userId)
await page.route('**/api/studyguidancegroups', async route => await route.fulfill({ json: studyGuidanceGroups }))
await page.reload()
}
test.describe('Study guidance group tests', () => {
test.describe('Study guidance group overview page', () => {
test('without study guidance groups shows the correct notification', async ({ page }) => {
await init(page, '/studyguidancegroups', 'admin')
await expect(page.getByRole('heading', { name: 'Study guidance groups' })).toBeVisible()
await expect(page.getByText('You do not have access to any study guidance groups.')).toBeVisible()
})
test('without correct rights shows the correct notification', async ({ page }) => {
await init(page, '/studyguidancegroups', 'basic')
await expect(page.getByRole('heading', { name: 'Access denied' })).toBeVisible()
await expect(
page.getByText(
"You don't currently have permission to view this page. If you believe this is a mistake, please contact oodikone@helsinki.fi."
)
).toBeVisible()
})
test.describe('with study guidance groups', () => {
test.beforeEach(async ({ page }) => {
await initWithGroups(page, '/studyguidancegroups')
await expect(page.getByRole('heading', { name: 'Study guidance groups' })).toBeVisible()
await expect(page.getByText('You do not have access to any study guidance groups')).not.toBeVisible()
})
test('shows the correct data', async ({ page }) => {
const row = page.getByTestId('study-guidance-group-overview-data-table').locator('tr')
// NOTE: Only MAT kandit 2020 can be edited
await expect(row.filter({ hasText: 'MAT kandit 2020' }).locator('td')).toHaveText([
'MAT kandit 2020',
'3',
'Matemaattisten tieteiden kandiohjelma' + 'Edit',
'2020 - 2021' + 'Edit',
])
await expect(row.filter({ hasText: 'Oma ohjausryhmä' }).locator('td')).toHaveText([
'Oma ohjausryhmä',
'2',
'Add degree programme',
'Add starting year',
])
})
test('has working links to single study guidance groups', async ({ page }) => {
await page.getByTestId('study-guidance-group-link-sgg-cypress-1').click()
await expect(page).toHaveURL('/studyguidancegroups/sgg-cypress-1')
})
})
})
test.describe('Page for single study guidance group', () => {
test.describe('without associated degree programme and year', () => {
test.beforeEach(async ({ page }) => {
await initWithGroups(page, '/studyguidancegroups/sgg-cypress-2')
})
test('shows the correct panes', async ({ page }) => {
await expect(page.getByRole('heading', { name: 'Oma ohjausryhmä' })).toBeVisible()
await expect(page.getByTestId('Credit accumulation (for 2 students)')).toBeVisible()
await expect(page.getByTestId('Age distribution')).toBeVisible()
await expect(page.getByTestId('Courses of population')).toBeVisible()
await expect(page.getByText('Students (2)')).toBeVisible()
await expect(page.getByTestId('Credit statistics')).not.toBeVisible()
})
test('students table has the correct tabs', async ({ page }) => {
await page.getByText('Students (2)').click()
await expect(page.getByTestId('student-table-tabs').getByRole('tab', { name: 'General' })).toBeVisible()
})
test('general tab of the students table has the correct columns', async ({ page }) => {
const columns = [
'Student number',
'Credits',
'Start date',
'Degree programmes',
'TVEX',
'Tags',
'Total',
'Since 1.1.1970',
'University',
]
await page.getByText('Students (2)').click()
await expect(page.getByTestId('ooditable-general').locator('table thead th')).toHaveText(columns)
})
})
test.describe('with associated degree programme and year', () => {
test.beforeEach(async ({ page }) => {
await initWithGroups(page, '/studyguidancegroups/sgg-cypress-1')
})
test('shows the correct labels', async ({ page }) => {
await expect(page.getByRole('heading', { name: 'MAT kandit 2020' })).toBeVisible()
await expect(page.getByLabel('Associated degree programme')).toHaveText('Matemaattisten tieteiden kandiohjelma')
await expect(page.getByLabel('Associated starting academic')).toHaveText('2020 - 2021')
})
test('shows the correct panes', async ({ page }) => {
await expect(page.getByTestId('Credit accumulation (for 3 students)')).toBeVisible()
await expect(page.getByTestId('Credit statistics')).toBeVisible()
await expect(page.getByTestId('Age distribution')).toBeVisible()
await expect(page.getByTestId('Courses of population')).toBeVisible()
await expect(page.getByText('Students (3)')).toBeVisible()
})
test("clicking the 'Show credits starting from the associated academic year' filter", async ({ page }) => {
const filter = page.getByTestId('ParticipationDate-filter-card')
await expect(filter).toHaveAttribute('data-active', 'false')
await page.getByTestId('Credit accumulation (for 3 students)').click()
await page.getByText('Show credits starting from the associated academic year').click()
await expect(filter).toHaveAttribute('data-active', 'true')
await expect(page.locator('.date-picker').first().locator('input')).toHaveValue('01.08.2020')
await expect(page.getByText('Reset All Filters')).toBeVisible()
await page.getByText('Show credits starting from the associated academic year').click()
await expect(filter).toHaveAttribute('data-active', 'false')
})
test('students table has the correct tabs', async ({ page }) => {
await page.getByText('Students (3)').click()
await expect(page.getByTestId('student-table-tabs').getByRole('tab')).toHaveText([
'General',
'Courses',
'Modules',
'Progress',
])
})
test('general tab of the students table has the correct columns', async ({ page }) => {
const columns = [
'Student number',
'Status',
'Credits',
'Prior to bachelor',
'Start date',
'Graduation date',
'Semesters present',
'Study track',
'Other programmes',
'Transferred From',
'Admission Type',
'Gender',
'Citizenships',
'Curriculum period',
'Latest attainment date',
'TVEX',
'Tags',
'Total',
'In study plan',
'Since start in programme',
'Credits',
'Courses',
'University',
'Study right',
'Programme',
]
await page.getByText('Students (3)').click()
await expect(page.getByTestId('ooditable-general').locator('table thead th')).toHaveText(columns)
})
})
test('has a working back button', async ({ page }) => {
await initWithGroups(page, '/studyguidancegroups/sgg-cypress-1')
await page.getByRole('button', { name: 'Back to groups' }).click()
await expect(page).toHaveURL('/studyguidancegroups')
})
})
})
-85
View File
@@ -1,85 +0,0 @@
import { test, expect } from '@playwright/test'
import { init } from './support/commands'
test.describe('Teachers page tests', () => {
test.beforeEach(async ({ page }) => {
// login as admin = has teacher rights
await init(page, '/teachers', 'admin')
expect(page.url()).toContain('/teachers')
})
const teacher1 = 'Luokkanen Liisa Viljami'
const teacher2 = 'Perälä Juhani Susanna'
test('Check Statistics', async ({ page }) => {
await page.getByTestId('semester-start').click()
await page.getByText('Syksy 2020').click()
await page.getByTestId('course-providers').click()
await page.getByText('Matemaattisten tieteiden kandiohjelma').click()
await page.keyboard.press('Escape')
await page.getByTestId('search-statistics').click()
await expect(page.getByRole('columnheader')).toHaveCount(4)
await expect(page.getByText('Export to Excel')).toBeVisible()
const teacher1Row = page.locator('tr').filter({ hasText: teacher1 })
await expect(teacher1Row.getByText(teacher1)).toBeVisible()
await expect(teacher1Row.getByText('235')).toBeVisible()
await expect(teacher1Row.getByText('97.40%')).toBeVisible()
const teacher2Row = page.locator('tr').filter({ hasText: teacher2 })
await expect(teacher2Row.getByText(teacher2)).toBeVisible()
await expect(teacher2Row.getByText('395')).toBeVisible()
await expect(teacher2Row.getByText('98.78%')).toBeVisible()
})
test('Teacher search works', async ({ page }) => {
await page.getByTestId('Search').click()
await page
.getByTestId('teacher-search')
.getByPlaceholder('Search by entering a name or an id')
.fill(teacher1.split(' ')[0])
await expect(page.getByText(teacher1)).toBeVisible()
await expect(page.locator('tbody').locator('tr')).toHaveCount(4)
})
test('Can check teacher page', async ({ page, context }) => {
// Re-init with context so the tab opened from the teacher link gets mocked headers too.
await init(page, '/teachers', 'admin', context)
await page.getByTestId('Search').click()
await page.getByTestId('teacher-search').getByPlaceholder('Search by entering a name or an id').fill(teacher2)
await expect(page.getByText(teacher2)).toBeVisible()
expect(await page.getByText(teacher2).getAttribute('href')).toBe('/teachers/hy-hlo-49026530')
// Change to other tab
const pagePromise = context.waitForEvent('page')
await page.getByRole('link', { name: teacher2 }).click()
const newPage = await pagePromise
await newPage.waitForURL('**/teachers/hy-hlo-49026530')
await expect(newPage.getByRole('heading', { name: teacher2 })).toBeVisible()
await newPage.getByText('Syksy 2023').click()
await newPage.getByText('Kevät 2019').click()
await expect(newPage.getByText('MAT12004')).toBeVisible()
await expect(newPage.getByText('MAT22002')).toBeVisible()
await expect(newPage.getByText('MAT22003')).toBeVisible()
const courseRow = newPage.locator('tr').filter({ hasText: 'MAT12004' })
const rowContent = ['MAT12004', 'Tilastollinen päättely I', '120', '0', '92.31%']
await expect(courseRow.locator('td')).toHaveText(rowContent)
})
test('Check leaderboad works', async ({ page }) => {
await page.getByTestId('Leaderboard').click()
await page.getByTestId('academic-year').click()
await page.getByText('2020-2021').click()
await expect(page.getByRole('columnheader')).toHaveCount(4)
await expect(page.getByText('Export to Excel')).toBeVisible()
await expect(page.getByText(teacher1)).toBeVisible()
})
})
-90
View File
@@ -1,90 +0,0 @@
import { expect, test } from '@playwright/test'
import { init } from './support/commands'
const progressLevels = ['bachelors', 'bachelor-masters', 'masters', 'doctoral']
const graduationTimesLevels = ['bachelor', 'bcMsCombo', 'master', 'doctor']
const checkProgressBarCharts = async (page: Parameters<typeof init>[0]) => {
for (const level of progressLevels) {
await expect(page.getByTestId(`faculty-${level}-progress-bar-chart-section`)).toBeVisible({ timeout: 60_000 })
}
}
const checkProgressTables = async (page: Parameters<typeof init>[0]) => {
for (const level of progressLevels) {
await expect(page.getByTestId(`${level}-faculty-progress-table`)).toBeVisible()
}
}
const checkGraduationCharts = async (page: Parameters<typeof init>[0], mode: string) => {
for (const level of graduationTimesLevels) {
const chart = page.getByTestId(`${level}-${mode}-bar-chart`)
await expect(chart).toBeVisible()
await expect(chart.getByText('Loading content')).not.toBeVisible()
}
}
test.describe('University view', () => {
test.slow()
test.beforeEach(async ({ page }) => {
await init(page, '/university')
await expect(page.getByRole('heading', { name: 'University', exact: true })).toBeVisible()
})
test.describe('Faculty progress tab', () => {
test('contains all the correct progress bar charts', async ({ page }) => {
await checkProgressBarCharts(page)
})
test('contains all the correct progress tables', async ({ page }) => {
await checkProgressTables(page)
})
test("'All study rights / Special study rights excluded' toggle works", async ({ page }) => {
await page.getByTestId('study-right-toggle').click()
await checkProgressBarCharts(page)
await checkProgressTables(page)
})
test('info boxes contain correct information', async ({ page }) => {
await page.getByTestId('faculty-progress-info-box-button').click()
await expect(page.getByTestId('faculty-progress-info-box-content')).toContainText('Kuvaa tiedekuntaan kuuluvien')
await page.getByTestId('faculty-bachelor-masters-progress-info-box-button').click()
await expect(page.getByTestId('faculty-bachelor-masters-progress-info-box-content')).toContainText(
'The starting year is the'
)
})
})
test.describe('Faculty graduations tab', () => {
test.beforeEach(async ({ page }) => {
await page.getByTestId('faculty-graduations-tab').click()
})
test.describe('Different modes work', () => {
test('Breakdown', async ({ page }) => {
await page.getByTestId('graduation-mode-selector').getByTestId('select-breakdown').click()
await checkGraduationCharts(page, 'breakdown')
})
test('Median', async ({ page }) => {
await page.getByTestId('graduation-mode-selector').getByTestId('select-median').click()
await checkGraduationCharts(page, 'median')
})
test('Average', async ({ page }) => {
await page.getByTestId('graduation-mode-selector').getByTestId('select-average').click()
await checkGraduationCharts(page, 'average')
})
})
test('info boxes contain correct information', async ({ page }) => {
await page.getByTestId('average-graduation-times-info-box-button').hover()
await expect(page.getByTestId('average-graduation-times-info-box-content')).toContainText(
'Opiskelijoiden keskimääräiset valmistumisajat'
)
})
})
})
-100
View File
@@ -1,100 +0,0 @@
import { test, expect, Page } from '@playwright/test'
import { init } from './support/commands'
const visibleLinks = {
norights: ['University', 'Faculties', 'Special populations', 'Feedback'],
onlycoursestatistics: ['University', 'Courses', 'Special populations', 'Feedback'],
basic: [] as string[],
admin: [] as string[],
}
visibleLinks.basic = [...visibleLinks.onlycoursestatistics, 'Faculties', 'Programmes', 'Students']
visibleLinks.admin = [...visibleLinks.basic, 'Teachers', 'Admin']
const containsLinks = async (page: Page, links: string[]) => {
const navBar = page.getByTestId('nav-bar')
await Promise.all(links.map(link => expect(navBar.getByText(link)).toBeVisible()))
}
const userButtonWorks = async (page: Page, username: string, mocking = false) => {
await page.getByTestId('nav-bar-user-button').click()
await expect(page.getByText(mocking ? `Mocking as ${username}` : `Logged in as ${username}`)).toBeVisible()
await expect(page.getByText('Language')).toBeVisible()
await expect(page.getByText('suomi')).toBeVisible()
await expect(page.getByText('English')).toBeVisible()
await expect(page.getByText('svenska')).toBeVisible()
await expect(page.getByText(mocking ? 'Stop mocking' : 'Log out')).toBeVisible()
}
test.describe('Users tests', () => {
test.describe('Using as user with just grp-oodikone-user, no other rights', () => {
test('shows correct tabs', async ({ page }) => {
await init(page, '', 'norights')
await containsLinks(page, visibleLinks.norights)
await userButtonWorks(page, 'norights')
})
})
test.describe('Using as coursestatistics user', () => {
test('shows correct tabs', async ({ page }) => {
await init(page, '', 'onlycoursestatistics')
await containsLinks(page, visibleLinks.onlycoursestatistics)
await userButtonWorks(page, 'onlycoursestatistics')
})
})
test.describe('Using as basic user', () => {
test('shows correct tabs', async ({ page }) => {
await init(page, '', 'basic')
await containsLinks(page, visibleLinks.basic)
await userButtonWorks(page, 'basic')
})
})
test.describe('Using as admin', () => {
test.beforeEach(async ({ page }) => {
await init(page, '/users', 'admin')
})
test('should see more stuff than others', async ({ page }) => {
await containsLinks(page, visibleLinks.admin)
await userButtonWorks(page, 'admin')
})
test.describe('can mock as other users', () => {
test.beforeEach(async ({ page }) => {
await page.getByTestId('user-page-button-basic').click()
await expect(page.getByText('Basic User')).toBeVisible()
await expect(page.getByText('IAM Groups')).toBeVisible()
await expect(page.getByText('Roles')).toBeVisible()
await expect(page.getByText('Degree programme rights')).toBeVisible()
await page.getByTestId('mock-button').click()
})
test('user button shows mocked user', async ({ page }) => {
await userButtonWorks(page, 'basic', true)
})
test("only the mocked user's programmes are visible", async ({ page }) => {
await page.route('**/api/populationstatistics/studyprogrammes', async route => {
const response = await route.fetch()
expect(response.status()).toBeLessThanOrEqual(304)
await route.fulfill({ response })
})
await init(page, '/populations', 'basic')
await expect(page.getByRole('heading', { name: 'Degree programme' })).toBeVisible()
const populationProgrammeSelector = page.getByTestId('population-programme-selector')
await expect(populationProgrammeSelector.getByPlaceholder('Select degree programme')).toBeVisible()
const programmeSelectorParent = page.getByTestId('population-programme-selector-parent')
await programmeSelectorParent.click()
await expect(programmeSelectorParent).toContainText('Matematiikan ja tilastotieteen maisteriohjelma')
})
})
})
})
-109
View File
@@ -1,109 +0,0 @@
// ! IMPORTANT: here we need to set keys to be all lowercase, since
// ! we're replacing headers after they've left browser / frontend.
import { BrowserContext, expect, Page } from '@playwright/test'
// 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;grp-toska',
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',
mail: 'grp-toska+mockbasicuser@helsinki.fi',
hypersonsisuid: 'hy-hlo-1234567',
}
const onlycoursestatisticsUserHeaders = {
uid: 'onlycoursestatistics',
displayname: 'Onlycoursestatistics User',
'shib-session-id': 'mock-cypress-session',
hygroupcn: 'grp-oodikone-basic-users',
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',
mail: 'grp-toska+mocknorightuser@helsinki.fi',
hypersonsisuid: 'hy-hlo-0000000',
}
const onlyIamRightsUserHeaders = {
uid: 'onlyiamrights',
displayname: 'Only IAM rights user',
'shib-session-id': 'mock-cypress-session',
hygroupcn: 'hy-employees;hy-mltdk-mat-jory',
mail: 'grp-toska+mockonlyiamrightsuser@helsinki.fi',
hypersonsisuid: 'hy-hlo-1111111',
}
// Also has element detail of "KH50_005" for the study guidance groups to work
const onlyStudyGuidanceGroupsUser = {
uid: 'onlystudyguidancegroups',
displayname: 'Study Guidance Groups User',
'shib-session-id': 'mock-cypress-session',
hygroupcn: 'grp-oodikone-users',
mail: 'grp-toska+mockonlystudyguidancegroupsuser@helsinki.fi',
hypersonsisuid: 'hy-hlo-2222222',
}
export const userHeaders = [
adminUserHeaders,
basicUserHeaders,
onlycoursestatisticsUserHeaders,
norightsUserHeaders,
onlyIamRightsUserHeaders,
onlyStudyGuidanceGroupsUser,
]
export const testMockTime = '2026-03-06'
export type UserId =
| 'admin'
| 'basic'
| 'onlycoursestatistics'
| 'norights'
| 'onlyiamrights'
| 'onlystudyguidancegroups'
export const init = async (page: Page, path: string, userId: UserId = 'basic', context?: BrowserContext) => {
const headersToUse = userHeaders.find(({ uid }) => uid === userId)
if (!headersToUse) throw Error(`${userId} is not valid user id!`)
// Prefer context-level routing when provided so new tabs inherit header injection.
const routeTarget = context ?? page
await routeTarget.route('**/api/**', async route => {
await route.continue({
headers: {
...route.request().headers(),
...headersToUse,
'x-test-now': testMockTime,
},
})
})
await page.clock.install({ time: testMockTime })
await page.goto(path || '/')
}
export const checkTableStats = async (page: Page, correctStats: (string | number)[][], tableName: string) => {
const testId = tableName + '-data-table'
await expect(page.getByTestId(testId).getByText('Loading content')).not.toBeVisible()
const table = page.getByTestId(testId).locator('tbody')
await expect(table).toBeVisible()
await expect(table.locator('tr > td')).toHaveText(correctStats.flatMap(stats => stats.map(value => value.toString())))
}
-20
View File
@@ -1,20 +0,0 @@
events { worker_connections 1024; }
http {
rewrite_log on;
error_log /var/log/nginx/localhost.error_log notice;
access_log off;
server {
listen 80;
location / {
proxy_pass http://frontend:5000/;
}
location /api/ {
proxy_pass http://backend:8080/;
}
}
}
-399
View File
@@ -1,399 +0,0 @@
#!/usr/bin/env bash
# Oodikone CLI tool (revision 2.0)
# Bash template provided by:
# https://betterdev.blog/minimal-safe-bash-script-template/
set -Eueo pipefail
trap cleanup SIGINT SIGTERM ERR EXIT
# Constants
# Resolve relative to the script's own location (not the caller's cwd), so
# `ok.sh` behaves the same whether it's run as `./ok.sh` from the repo or
# invoked with an absolute/symlinked path from anywhere (e.g. the `ok`
# shell function installed via `./ok.sh install`).
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" &> /dev/null && pwd)"
PROJECT_ROOT="$(cd "$SCRIPT_DIR" && git rev-parse --show-toplevel)"
cd "$PROJECT_ROOT"
# Include scripts from other sources
source "$PROJECT_ROOT"/scripts/functions_for_setup.sh
usage() {
cat << EOF # remove the space between << and EOF, this is due to web plugin issue
Usage: $(basename "${BASH_SOURCE[0]}") [command]
Run without a command to open the interactive menu.
Available commands:
up, u, r, real Start Oodikone with real data
rs, rl Restart Oodikone with real data (down, then up)
anon, a Start Oodikone with anonymous data
down, d Stop Oodikone
logs, l Follow oodikone service logs
logs:updater, lu Follow sis-updater service logs
lint, li Lint the project or the path given as parameter
format, fmt, f Format the project or the path given as parameter
test:backend, tb Run backend unit tests
test:frontend, tf Run frontend unit tests
test:updater, tu Run updater (worker) unit tests
test:services, ts Run backend service tests
test:api, ta Run backend api tests
test:component, tc Run frontend component tests tests
test:e2e, te Run all e2e tests (add --ui to run in headed mode)
importer, i Open a psql/pgcli shell to the importer database
updater Open a psql/pgcli shell to the updater (sis) database
install Add the 'ok' shell function to your shell rc file
-h, --help Print this help and exit
EOF
exit
}
cleanup() {
trap - SIGINT SIGTERM ERR EXIT
msg ""
errormsg "(╯°□°)╯︵ ┻━┻"
}
setup_colors() {
if [[ -t 2 ]] && [[ -z "${NO_COLOR-}" ]] && [[ "${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
}
die() {
local msg=$1
local code=${2-1} # default exit status 1
msg "$msg"
exit "$code"
}
get_pgclient() {
if command -v pgcli >/dev/null 2>&1; then
echo "pgcli"
else
warningmsg "Couldn't verify pgcli installation, using psql as fallback" >&2
echo "psql"
fi
}
show_welcome() {
local cashmoneyyellow
cashmoneyyellow=$(tput setaf 221)
local normal
normal=$(tput sgr0)
if [ "$(tput cols)" -gt "76" ]; then
while IFS="" read -r p || [ -n "$p" ]; do
printf '%40s\n' "${cashmoneyyellow}$p${normal}"
done < "$PROJECT_ROOT"/scripts/assets/logo.txt
fi
msg "Welcome to Oodikone CLI!"
msg ""
}
main_menu_opts() {
msg "1) Start Oodikone 2) Databases 3) Data refresh 4) Setup q) Quit\n"
}
database_menu_opts() {
msg "\nView local databases ($datasource data)"
msg "suffix: $dbsuffix"
msg "1) Oodikone"
msg "2) Importer"
msg "3) Kone"
msg "4) Toggle data source (test/real)"
msg "5) Go back"
msg ""
}
database_menu() {
# If a single mandatory service is down, an assumption is made that they all are.
if ! docker compose ps 2>/dev/null | grep -q 'oodikone-backend'; then
msg ""
warningmsg "Can't access databases, oodikone is not running!\nPlease run option 1 first with preferred data source."
return
fi
declare dbsuffix="-real"
declare datasource="real"
toggle_environment() {
if [[ $dbsuffix = "" ]];
then
dbsuffix="-real"
datasource="real"
else
dbsuffix=""
datasource="test"
fi
}
local pgclient
pgclient="$(get_pgclient)"
msg "${GREEN}\nUsing $pgclient for database connections${NOFORMAT}"
database_menu_opts $datasource
local userinput
while true; do
read -r -s -n 1 -p "Please select an option: " userinput
case ${userinput} in
1|o|O)
msg "Oodikone-db"
$pgclient -h localhost -p 12345 -U postgres -d "sis-db$dbsuffix" -w
database_menu_opts
;;
2|i|I)
msg "Importer-db"
$pgclient -h localhost -p 12346 -U postgres -d "sis-importer-db$dbsuffix" -w
database_menu_opts
;;
3|k|K)
msg "Kone-db"
$pgclient -h localhost -p 4321 -U postgres -d "kone-db$dbsuffix" -w
database_menu_opts
;;
4|t|T)
msg "Toggling environment..."
database_menu_opts
;;
5|q|Q)
msg ""
break
;;
*)
true
;;
esac
done
}
main_loop() {
local userinput
while true; do
main_menu_opts
read -r -s -n 1 -p "Please select an option: " userinput
case ${userinput} in
1)
msg "\nTODO: todo\n";;
2)
database_menu;;
3)
msg "\nTODO: todo\n";;
4)
msg "\nTODO: todo\n";;
5|q|Q)
break 2;;
*)
errormsg "${RED}Invalid option:${NOFORMAT} $userinput";;
esac
done
}
# === Direct (non-interactive) commands ===
run_up() {
infomsg "Starting Oodikone with real data..."
npm run both:real "$@"
}
run_restart() {
infomsg "Restarting Oodikone with real data..."
npm run docker:down
npm run both:real
}
run_anon() {
infomsg "Starting Oodikone with anonymous data..."
npm run both "$@"
}
run_down() {
infomsg "Stopping Oodikone..."
npm run docker:down "$@"
}
run_logs() {
npm run logs "$@"
}
run_lint() {
npm run lint -- "$@"
}
run_format() {
npm run fmt -- "$@"
}
run_unit_tests() {
npm run test:unit --prefix "$@"
}
run_component_tests() {
npm run test:component --prefix services/frontend "$@"
}
run_service_tests() {
npm run test:services --prefix services/backend "$@"
}
run_api_tests() {
npm run test:api --prefix services/backend "$@"
}
run_e2e_tests() {
npm run playwright -- "$@"
}
run_logs_updater() {
docker compose --file docker-compose.yml logs sis-updater-worker sis-updater-scheduler -f
}
run_importer_db() {
local pgclient
pgclient="$(get_pgclient)"
msg "Importer-db"
$pgclient -h localhost -p 12346 -U postgres -d sis-importer-db-real -w
}
run_updater_db() {
local pgclient
pgclient="$(get_pgclient)"
msg "Updater-db"
$pgclient -h localhost -p 12345 -U postgres -d sis-db-real -w
}
# Add (or refresh) an `ok` shell function in the user's shell rc file, which
# simply forwards all arguments to this script. This is the one-time setup
# step referenced above the shell function no longer needs its own copy of
# oodikone-specific logic.
cmd_install() {
local shell_name
shell_name="$(basename "${SHELL:-bash}")"
local shell_rc
case "$shell_name" in
zsh) shell_rc="$HOME/.zshrc" ;;
*) shell_rc="$HOME/.bashrc" ;;
esac
local ok_path="$PROJECT_ROOT/ok.sh"
local marker_start="# >>> oodikone ok.sh >>>"
local marker_end="# <<< oodikone ok.sh <<<"
if [ -f "$shell_rc" ] && grep -qF "$marker_start" "$shell_rc"; then
warningmsg "ok.sh integration already present in $shell_rc, skipping."
infomsg "To reinstall (e.g. after moving the repo), remove the block between\n'$marker_start' and '$marker_end' in $shell_rc and re-run 'ok.sh install'."
return 0
fi
{
echo ""
echo "$marker_start"
echo "ok() { \"$ok_path\" \"\$@\"; }"
echo "$marker_end"
} >> "$shell_rc"
successmsg "Added the 'ok' function to $shell_rc"
infomsg "Run 'source $shell_rc' or restart your shell, then try 'ok up'."
}
# Main
setup_colors
case "${1-}" in
up|u|r|real)
shift
run_up "$@"
;;
rs|rl)
shift
run_restart "$@"
;;
anon|a)
shift
run_anon "$@"
;;
down|d)
shift
run_down "$@"
;;
logs|l)
shift
run_logs "$@"
;;
lint|li)
shift
run_lint "$@"
;;
format|fmt|f)
shift
run_format "$@"
;;
test:backend|tb)
shift
run_unit_tests services/backend "$@"
;;
test:frontend|tf)
shift
run_unit_tests services/frontend "$@"
;;
test:updater|tu)
shift
run_unit_tests updater/sis-updater-worker "$@"
;;
test:services|ts)
shift
run_service_tests "$@"
;;
test:api|ta)
shift
run_api_tests "$@"
;;
test:component|tc)
shift
run_component_tests "$@"
;;
test:e2e|te)
shift
run_e2e_tests "$@"
;;
logs:updater|lu)
shift
run_logs_updater "$@"
;;
importer|i)
shift
run_importer_db
;;
updater)
shift
run_updater_db
;;
install)
shift
cmd_install
;;
-h|--help)
usage
;;
"")
show_welcome
get_s3_config
main_loop
;;
*)
errormsg "Unknown command: $1"
usage
;;
esac
+16295 -6460
View File
File diff suppressed because it is too large Load Diff
+92 -53
View File
@@ -1,61 +1,100 @@
{
"name": "oodikone",
"description": "An application for analyzing university data.",
"engines": {
"node": ">=24.0.0"
},
"scripts": {
"playwright": "playwright test -c playwright.config.ts",
"playwright:ui": "playwright test --ui -c playwright.config.ts",
"playwright:headed": "playwright test --headed -c playwright.config.ts",
"lint": "oxlint",
"lint:fix": "oxlint --fix",
"lint:quiet": "oxlint --quiet",
"fmt": "oxfmt",
"fmt:check": "oxfmt --check",
"stylelint": "stylelint",
"cli": "./cli.sh",
"logs": "docker compose --file docker-compose.yml logs backend frontend sis-updater-worker sis-updater-scheduler -f",
"logs:all": "docker compose --file docker-compose.yml --profile oodikone --profile updater logs -f",
"logs:ci": "docker compose --file docker-compose.ci.yml logs backend frontend sis-updater-worker sis-updater-scheduler jami",
"oodikone": "npm run docker:down && ./run.sh oodikone anon up --detach",
"oodikone:real": "./run.sh oodikone real up --detach",
"oodikone:test": "npm run docker:down && docker compose --profile oodikone up --detach && docker compose --file docker-compose.test.yml up --detach",
"oodikone:ci": "npm run docker:down && docker compose --file docker-compose.ci.yml --profile oodikone up",
"updater": "./run.sh updater anon up --detach",
"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 anon down --remove-orphans; ./run.sh both real down --remove-orphans",
"testupdater": "docker compose --file docker-compose.ci.yml up --detach --no-recreate --wait sis-updater-worker && SCHEDULE_IMMEDIATE=weekly EXIT_AFTER_IMMEDIATES=yes docker compose --file docker-compose.ci.yml up sis-updater-scheduler --exit-code-from sis-updater-scheduler",
"flushredis": "docker exec -it oodikone-redis redis-cli flushall",
"prepare": "husky",
"install:local": "npm i && npm i --prefix services/shared && npm i --prefix services/frontend && npm i --prefix services/backend && npm i --prefix updater/sis-updater-scheduler && npm i --prefix updater/sis-updater-worker",
"ci:local": "npm ci && npm ci --prefix services/shared && npm ci --prefix services/frontend && npm ci --prefix services/backend && npm ci --prefix updater/sis-updater-scheduler && npm ci --prefix updater/sis-updater-worker",
"ci:docker": "docker exec oodikone-frontend sh -lc \"npm ci --prefix /opt/app-root/shared && npm ci\" && docker exec oodikone-backend npm ci && docker exec oodikone-updater-scheduler npm ci && docker exec oodikone-updater-worker npm ci",
"ci:both": "npm run ci:local && npm run ci:docker"
},
"name": "oodikone2-frontend",
"version": "0.1.0",
"private": true,
"repository": {
"type": "git",
"url": "git+https://github.com/UniversityOfHelsinkiCS/oodikone.git"
"url": "git+https://github.com/UniversityOfHelsinkiCS/oodikone2-frontend.git"
},
"license": "ISC",
"bugs": {
"url": "https://github.com/UniversityOfHelsinkiCS/oodikone/issues"
"scripts": {
"dev": "webpack-dev-server --config webpack.config.local.js",
"dist": "rimraf dist && webpack --config webpack.config.production.js",
"lint": "concurrently 'npm run lint:js' 'npm run lint:css'",
"lint:css": "stylelint 'src/**/*.css'",
"lint:js": "eslint server ./ --ext .jsx,.js",
"lintfix": "eslint server ./ --ext .jsx,.js --fix",
"stats": "webpack --json --config webpack.config.production.js > stats.json",
"prepush": "npm run lint",
"pretest": "npm run lint",
"test": ""
},
"homepage": "https://github.com/UniversityOfHelsinkiCS/oodikone#readme",
"licence": "",
"main": "src/app.jsx",
"devDependencies": {
"@playwright/test": "1.61.0",
"eslint-plugin-import-x": "^4.16.1",
"eslint-plugin-react": "^7.37.5",
"eslint-plugin-react-hooks": "^5.2.0",
"globals": "^17.4.0",
"husky": "^9.1.7",
"lint-staged": "^16.1.0",
"oxfmt": "0.58.0",
"oxlint": "1.73.0",
"oxlint-tsgolint": "0.25.0",
"stylelint": "^17.6.0",
"stylelint-config-standard": "^40.0.0"
"babel-core": "6.26.0",
"babel-eslint": "8.2.1",
"babel-loader": "7.1.4",
"babel-plugin-inline-json-import": "^0.2.1",
"babel-plugin-transform-async-to-generator": "^6.24.1",
"babel-plugin-transform-object-rest-spread": "^6.26.0",
"babel-preset-latest": "^6.24.1",
"babel-preset-react": "^6.24.1",
"babel-runtime": "6.26.0",
"case-sensitive-paths-webpack-plugin": "2.1.1",
"concurrently": "^3.5.1",
"css-loader": "0.28.7",
"eslint": "^4.19.1",
"eslint-config-airbnb": "^16.1.0",
"eslint-config-react-app": "^2.0.1",
"eslint-loader": "1.9.0",
"eslint-plugin-babel": "^4.1.2",
"eslint-plugin-flowtype": "2.39.1",
"eslint-plugin-import": "^2.10.0",
"eslint-plugin-jsx-a11y": "^6.0.3",
"eslint-plugin-prefer-object-spread": "^1.2.1",
"eslint-plugin-react": "^7.7.0",
"extract-text-webpack-plugin": "^v4.0.0-beta.0",
"file-loader": "1.1.11",
"glob": "^7.1.2",
"html-webpack-plugin": "3.2.0",
"html-webpack-template": "^6.2.0",
"husky": "^0.14.3",
"mini-css-extract-plugin": "^0.4.0",
"optimize-css-assets-webpack-plugin": "^3.2.0",
"postcss-cssnext": "^3.0.2",
"postcss-flexbugs-fixes": "3.2.0",
"postcss-loader": "^2.1.2",
"react-dev-utils": "^4.2.1",
"style-loader": "^0.19.1",
"stylelint": "^8.2.0",
"stylelint-config-standard": "^18.2.0",
"sw-precache-webpack-plugin": "0.11.5",
"uglifyjs-webpack-plugin": "^1.2.4",
"webpack": "^4.4.1",
"webpack-cli": "^2.0.13",
"webpack-dev-server": "3.1.1",
"webpack-manifest-plugin": "^2.0.0-rc.2"
},
"dependencies": {
"axios": "^0.18.0",
"babel-plugin-transform-class-properties": "^6.24.1",
"babel-polyfill": "^6.26.0",
"dotenv": "4.0.0",
"highcharts": "^6.1.0",
"highcharts-react-official": "^1.1.0",
"jStat": "^1.7.1",
"jwt-decode": "^2.2.0",
"lodash": "^4.17.5",
"moment": "^2.22.0",
"prop-types": "^15.6.1",
"react": "^16.4.0",
"react-datetime": "^2.14.0",
"react-dom": "^16.4.0",
"react-highcharts": "^16.0.2",
"react-hot-loader": "^4.3.4",
"react-localize-redux": "^2.16.0",
"react-redux": "^5.0.7",
"react-router": "^4.2.0",
"react-router-dom": "^4.2.2",
"react-scroll-to-component": "^1.0.2",
"recharts": "^1.0.0-beta.10",
"redux": "^3.7.2",
"redux-logger": "^3.0.6",
"redux-thunk": "^2.2.0",
"reselect": "^3.0.1",
"rimraf": "^2.6.2",
"semantic-ui-css": "^2.3.1",
"semantic-ui-react": "^0.77.2",
"uuid": "^3.2.1"
}
}
-32
View File
@@ -1,32 +0,0 @@
import { defineConfig, devices } from '@playwright/test'
// oxlint-disable-next-line no-default-export
export default defineConfig({
testDir: './e2e',
testMatch: ['**/*.spec.ts'],
testIgnore: ['**/support/**', '**/plugins/**'],
// Fail the build on CI if you accidentally left test.only in the source code.
forbidOnly: !!process.env.CI,
timeout: 30_000,
expect: { timeout: 10_000 },
reporter: [['list', { printFailuresInline: true }]],
retries: process.env.CI ? 1 : 0,
workers: process.env.CI ? 4 : undefined,
use: {
baseURL: process.env.CI ? 'http://localhost:1337' : process.env.PLAYWRIGHT_BASE_URL || 'http://localhost:3000',
viewport: { width: 1920, height: 1080 },
trace: 'on-first-retry',
testIdAttribute: 'data-cy',
},
projects: [
{
name: 'firefox',
use: { ...devices['Desktop Firefox'] },
},
],
})
+14
View File
@@ -0,0 +1,14 @@
const cssNextPlugin = require('postcss-cssnext') // eslint-disable-line import/no-extraneous-dependencies
const variables = require('./src/styles/variables')
module.exports = {
plugins: [
cssNextPlugin({
browsers: 'last 2 versions',
features: {
customProperties: { variables }
}
})
]
}
-23
View File
@@ -1,23 +0,0 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": ["config:recommended"],
"packageRules": [
{
"groupName": "all non-major dependencies",
"groupSlug": "all-minor-patch",
"matchUpdateTypes": ["minor", "patch"],
"matchCurrentVersion": "!/^0/",
"matchPackageNames": ["*"]
},
{
"matchDatasources": ["docker"],
"enabled": false
}
],
"rebaseWhen": "never",
"vulnerabilityAlerts": {
"labels": ["security"]
},
"addLabels": ["dependencies"],
"rangeStrategy": "bump"
}
-94
View File
@@ -1,94 +0,0 @@
#!/usr/bin/env bash
# This script is used to run oodikone with different setups and is mainly used by
# scripts in package.json.
# Base for script: https://betterdev.blog/minimal-safe-bash-script-template/
# === Config ===
# Fail immediately if script fails, unbound variables are referenced
# or command inside pipe fails. -E ensures cleanup trap fires in rare ERR cases.
set -euoE pipefail
# Set up constants
PROJECT_ROOT="$(git rev-parse --show-toplevel)"
# Set up messages and exiting
source "$PROJECT_ROOT"/scripts/utils.sh
usage() {
cat <<EOF
Usage: $(basename "${BASH_SOURCE[0]}") option [version] command --flag
Parameters:
* Option: oodikone/updater/both
* Version: anon/real. Not necessary when running down/logs/pull.
* Command: will be passed to docker compose.
EOF
}
# Parse parameters. If arguments are not correct, print usage and exit with error.
parse_params() {
args=("$@")
[[ ${#args[@]} -eq 0 ]] && usage && die "Wrong number of arguments"
option=${args[0]}
# parse arguments
[[ ("$option" != "oodikone" && "$option" != "updater" && "$option" != "both" && "$option" != "tests") ]] && \
usage && die "Wrong option: $option"
[[ ${#args[@]} -eq 1 ]] && usage && die "Wrong number of arguments"
# Some arguments can be passed without version. Otherwise parse version and then pass
# rest to compose
if [[ "${args[1]}" == "down" || "${args[1]}" == "logs" || "${args[1]}" == "pull" ]]; then
version=""
compose_command=${args[*]:1}
else
version=${args[1]}
[[ "$version" != "anon" && "$version" != "real" && "$version" != "api" ]] && usage && \
die "Wrong version $version"
compose_command=${args[*]:2}
fi
return 0
}
# Set which profile to use to launch correct services
parse_profiles() {
if [[ "$option" == "both" ]]; then
profiles="--profile oodikone --profile updater"
else
profiles="--profile $option"
fi
return 0
}
# Set which docker-compose files to use based on version.
parse_env() {
env=""
if [[ "$version" == "real" ]]; then
env="--file docker-compose.yml --file docker-compose.real.yml"
fi
if [[ "$version" == "api" ]]; then
env="--file docker-compose.ci.yml"
fi
return 0
}
# === Run script ===
parse_params "$@"
# Create command that will be run. Empty command and "down" command will be handled
# differently.
parse_profiles
parse_env
if [[ "$compose_command" == "" ]]; then
final_command="docker compose ${env}"
else
final_command="docker compose ${env} ${profiles} ${compose_command}"
fi
msg "${BLUE}Running: ${final_command}${NOFORMAT}"
eval "$final_command"
-10
View File
@@ -1,10 +0,0 @@
______ __ __ __
/ \ | \| \| \
| $$$$$$\ ______ ____| $$ \$$| $$ __ ______ _______ ______
| $$ | $$ / \ / $$| \| $$ / \ / \ | \ / \
| $$ | $$| $$$$$$\| $$$$$$$| $$| $$_/ $$| $$$$$$\| $$$$$$$\| $$$$$$\
| $$ | $$| $$ | $$| $$ | $$| $$| $$ $$ | $$ | $$| $$ | $$| $$ $$
| $$__/ $$| $$__/ $$| $$__| $$| $$| $$$$$$\ | $$__/ $$| $$ | $$| $$$$$$$$
\$$ $$ \$$ $$ \$$ $$| $$| $$ \$$\ \$$ $$| $$ | $$ \$$ \
\$$$$$$ \$$$$$$ \$$$$$$$ \$$ \$$ \$$ \$$$$$$ \$$ \$$ \$$$$$$$
-38
View File
@@ -1,38 +0,0 @@
::---..``` `ohmMMMMMNNNdy.
/MMMMMMMMMMMMMMMMMMMMmyMMMMd
`sddmmmNNNNNMMMMMMMMMMMMMMMMmdddddhhhhhhy+`
``````mMMMMMMMMMMMMMMNNMMMMMMMMMMMN-
.yMMMMMMMMMMMMo``````...----.
:MMMMMMMMMMMMm-
hMMNNMMMMMMMMMNy- `.--.`
-MMMdMMMMMMMMMMMMNdo:.`` ``..://ossyhdmmNNNNNNmhys+:.
` -mMMmdMMMMMMMMMMMMMMMNNNmmmmddmmmNNNNNMMMMMMMMMMMMMMMMMMMMMMNmh/.
.mds+:-.` ` -+hNMMNsNNNNNNNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMNy-
.`.-/+syyy+:. -ddymNMMMMNmNNNNdNNNNNNNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMNo`
``-oydmmMMMMMMMMNNNNNNNNNNmNNmNNNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMm:
/NMMMMMMMMMNNMMNNNNNNmNNmNNNNMMMMMMMMMMMMMNdmMMMMMMMMMMMMMMMMMMhmMMMMMMMMMMMMMMN/
`oNMMMMMMMMMMMMMMMNNNNNNmdNNNMMNNMMMMMMMMMMMd`/MMMMMMMMMMMMMMMMMM`+MMMMMMMMMMMMMMMN-
yMMMMMMMMMMMMMMMMMNNNNmNdmNMMNNNMMMMMMMMMMMMN .MMMMMMMMMMMMMMMMMM.:MMMMMMNMMMMMMMMMh
oMMMMMMMMMMMMMdyyyhmNNNmmdMMMMNNMMMMMMMMMMMNmm` ddddhhhyyhdNMmyo+o..MMMd+:::/mMMMMMMM:
-NMMMMMMMMMMMm+sdmdy/hNNNNNMMMMMMMMMMMMMMMMMy-:` -::. -///--o.-syo. NNo.+dmmosMMMMMMMm
`/osyhmMMMMMMMMMMMM+mMMMMMN:MMMMMMNMMMMMMMMMMMMMMMNmN/ yNd`.hNMMMNs` oNMMs y:-hNdys+oMMMMMMMM`
:mMMMhmMMMMMMMMMMMMMhmMMMMMhyMMMMMMNMMMMMMMMMMMMMMMMMMo sM: hMMMMMMMo .-::: . oh-:oyh+MMMMMMMM`
NMMMMNMMMMMMMMMNNNMMMmmNmNmmMMMMMMMMMMMMMMMMMMMMMMMMMMy +M- mMMMMMMMy sddh+ os.`.NMMm:MMMMMMMM
NMMMMMMMMMMMMMMMNNNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMd -Ms +NMMMMMN: sNMNy sMm/.oys:/MMMMMMMh
+NMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMN `NN+`:ydmdy:.+-+ss+oNMMMddmNNMMMMMMMN-
.+yhdmdMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM/.+ss++oyhhddNMNMMMMMMMMMMMMMMMMMMMMM+
` hMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMNMMMMMMMMMMMMMMMMMMMMMMMMMMMNmmNNMMy
.NMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMmmMMMMMMm.
/NMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMmyNMMMMMMMMh
+MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMdhMMMMMMMMMMM`
/NMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMmyMMMMMMMMMMMM.
yMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMyMMMMMMMMMNMMM`
hMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMNNNNhMMMMMMMMMMMMh
yMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMhMMMMMMMMMMMMMMMMMMMMMMMNNmhys+/-...oMMMMMMMMMMMN:
:MMMMMMMMMMMMMMMNddmNMMMMMMMMMMMMMhMMMMMMMMMNmmMMMMN++/++/- `dMMMMMMMMMMs`
hMMMMMMMMMMMMMN: .:/+osyyhddmmmdMMMMMMMMMMMMMMMMy `omMMMMMMNmy-
yMMMMMMMMMMMm- `+-`mMMMMMMMMMMMMMMMM. ````````.``.-osyo//`
`sNMMMMMMMMo.`` ``` `````` ```+MMMMMMMMMMMMMMMy``` `````
`yy.shmMMmh- ` ``. ``hMMMMMMMMMMMMN+-.``
`.`...` `...``.```....`.-://mMMMMMMMMMMhy/
`+hNMMMNmhs:-.
-312
View File
@@ -1,312 +0,0 @@
#!/usr/bin/env bash
# This file includes functions that are used to setup Oodikone
# File doesn't run anything by itself and should be sourced from other scripts
# === Config ===
# Set up constants
## Folders
PROJECT_ROOT="$(git rev-parse --show-toplevel)"
DUMP_DIR="$PROJECT_ROOT/.databasedumps"
S3_CONFIG_FILE=~/.s3cfg
S3_BUCKET="s3://psyduck"
## Following the naming convention in docker compose, these are names for database
## services. The real data databases inside service will have names suffixed by "-real".
KONE_DB_NAME="kone-db"
SIS_DB_NAME="sis-db"
SIS_IMPORTER_DB_NAME="sis-importer-db"
USER_DB_NAME="user-db"
JAMI_DB_NAME="jami-db"
DATABASES=("$KONE_DB_NAME" "$SIS_DB_NAME" "$SIS_IMPORTER_DB_NAME" "$USER_DB_NAME" "$JAMI_DB_NAME")
## Urls should be in same order as databases as both are iterated through by indexes.
KONE_DB_S3_PATH="oodikone_kone"
SIS_DB_S3_PATH="oodikone_updater"
SIS_IMPORTER_DB_S3_PATH="sis_importer"
USER_DB_S3_PATH="oodikone_user"
JAMI_DB_S3_PATH="jami"
S3_PATHS=("$KONE_DB_S3_PATH" "$SIS_DB_S3_PATH" "$SIS_IMPORTER_DB_S3_PATH" "$USER_DB_S3_PATH" "$JAMI_DB_S3_PATH")
# Source utility functions
source "$PROJECT_ROOT"/scripts/utils.sh
# === Function ===
draw_mopo() {
# Some hacks to print mopo as green, since colours from common config don't work when
# catting images. Please feel free to fix this. And tell otahontas how you did it!
local mopogreen
mopogreen=$(tput setaf 34)
local normal
normal=$(tput sgr0)
if [ "$(tput cols)" -gt "100" ]; then
while IFS="" read -r p || [ -n "$p" ]; do
printf '%40s\n' "${mopogreen}$p${normal}"
done < "$PROJECT_ROOT"/scripts/assets/mopo.txt
fi
}
retry() {
sleep 5
for i in {1..60}; do
"$@" && break || warningmsg "Retry attempt $i failed, waiting..." && sleep 5;
done
}
download_real_dump() {
local database=$1
local dump_destination="$DUMP_DIR/$database.sql.gz"
local s3_path=$2
local selection_flag=$3
rm -f "$dump_destination"
if [ "$selection_flag" = "true" ]; then
local backup_files
mapfile -t backup_files < <(s3cmd -c "$S3_CONFIG_FILE" ls "$S3_BUCKET/$s3_path/" | sort -rk1,2 | awk '{print $4}')
if [ ${#backup_files[@]} -eq 0 ]; then
die "No backup files found in $S3_BUCKET bucket $s3_path path!"
fi
infomsg "Available backups:"
PS3="Select backup (or 0 to go back): "
select chosen_backup in "${backup_files[@]}"; do
if [ "$REPLY" = "0" ] || [ "$chosen_backup" = "Go back" ]; then
infomsg "Going back to previous menu."
break
elif [ -n "$chosen_backup" ]; then
infomsg "You selected: $chosen_backup"
local FILE_NAME
FILE_NAME=$(basename "$chosen_backup")
infomsg "Fetching the selected dump: $FILE_NAME"
s3cmd -c "$S3_CONFIG_FILE" get "$chosen_backup" "$dump_destination"
if [ ! -f "$dump_destination" ]; then
die "Download failed or file not found: $dump_destination"
fi
echo "$dump_destination"
break
else
warningmsg "Invalid selection. Please select a valid backup number."
fi
done
else
local current_date
current_date=$(date +"%Y%m%d")
s3cmd -c "$S3_CONFIG_FILE" get "s3://psyduck/${s3_path}/${s3_path}_${current_date}.sql.gz" "$dump_destination"
fi
}
check_if_postgres_is_ready() {
local container=$1
local database=$2
retry docker exec -u postgres "$container" pg_isready --dbname="$database"
}
reset_databases() {
local databases=("$@")
local database_dump_dir=$DUMP_DIR
infomsg "Restoring PostgreSQL dumps from backups. This might take a while."
npm run docker:down
docker compose up -d "${databases[@]}"
for database in "${databases[@]}"; do
local database_dump="$database_dump_dir/$database.sql.gz"
local database_container="oodikone-$database"
local database_name="$database-real"
infomsg "Attempting to create database $database_name from dump $database_dump inside container $database_container"
infomsg "Trying to remove possibly existing previous version of database"
check_if_postgres_is_ready "$database_container" "$database_name"
docker exec -u postgres "$database_container" dropdb "$database_name" || warningmsg "This is okay, continuing"
infomsg "Trying to create new database"
check_if_postgres_is_ready "$database_container" "$database_name"
docker exec -u postgres "$database_container" createdb "$database_name" || warningmsg "This is okay, continuing"
infomsg "Restoring database from dump database"
msg "Writing database..."
docker exec -i "$database_container" /bin/bash -c "gunzip | psql -U postgres -d $database_name" < "$database_dump" 2> /dev/null
msg ""
done
successmsg "Database setup finished"
}
reset_all_data() {
infomsg "Downloading real data dumps"
for i in ${!DATABASES[*]}; do
local database="${DATABASES[$i]}"
local url="${S3_PATHS[$i]}"
download_real_dump "$database" "$url" false
done
reset_databases "${DATABASES[@]}"
}
restore_data_from_dumps() {
infomsg "Restoring earlier-downloaded dumps to databases"
reset_databases "${DATABASES[@]}"
}
reset_single_database() {
# Define custom shell prompt for the nested interactive select loop
PS3="Please enter which database to reset: "
options=(
"kone-db"
"sis-db"
"sis-importer-db"
"user-db"
"jami-db"
"Return to main menu."
)
local database=""
local url=""
local selection_flag=true
while true; do
select opt in "${options[@]}"; do
case $opt in
"kone-db")
local database=$KONE_DB_NAME
local url=$KONE_DB_S3_PATH
break 2;;
"sis-db")
local database=$SIS_DB_NAME
local url=$SIS_DB_S3_PATH
break 2;;
"sis-importer-db")
local database=$SIS_IMPORTER_DB_NAME
local url=$SIS_IMPORTER_DB_S3_PATH
break 2;;
"user-db")
local database=$USER_DB_NAME
local url=$USER_DB_S3_PATH
break 2;;
"jami-db")
local database=$JAMI_DB_NAME
local url=$JAMI_DB_S3_PATH
break 2;;
"Return to main menu.")
break 2;;
*) msg "${RED}Invalid option:${NOFORMAT} $REPLY
";;
esac
break
done
done
if [ -n "$database" ]; then
infomsg "Downloading $database dump"
download_real_dump $database $url $selection_flag
reset_databases $database
fi
}
nuke_node_modules() {
folders_to_set_up=(
"$PROJECT_ROOT"
"$PROJECT_ROOT/services/frontend"
"$PROJECT_ROOT/services/backend"
"$PROJECT_ROOT/services/shared"
"$PROJECT_ROOT/updater/sis-updater-scheduler"
"$PROJECT_ROOT/updater/sis-updater-worker"
)
named_volumes=(
"oodikone-shared-nodemod"
"oodikone-frontend-nodemod"
"oodikone-backend-nodemod"
"oodikone-updater-worker-nodemod"
"oodikone-updater-scheduler-nodemod"
)
infomsg "Running docker compose down"
"$PROJECT_ROOT"/run.sh both down --remove-orphans || infomsg "brr"
for folder in "${folders_to_set_up[@]}"; do
cd "$folder" || die "Couldn't change directory to folder $folder"
rm -rf node_modules || die "Couldn't remove node_modules in folder $folder"
done
for volume in "${named_volumes[@]}"; do
docker volume rm "$volume" || infomsg "Couldn't delete volume $volume (already gone?)"
done
infomsg "Installing npm packages locally to root and each subfolder to enable linting and formatting (and LSP)"
for folder in "${folders_to_set_up[@]}"; do
cd "$folder" || die "Couldn't change directory to folder $folder"
([[ -d node_modules ]] && die "$folder already has node_modules, exiting!") || npm ci
done
cd "$PROJECT_ROOT" || die "Couldn't change directory to project root"
successmsg "Node_module nuke performed"
}
set_up_oodikone_from_scratch() {
draw_mopo
folders_to_set_up=(
"$PROJECT_ROOT"
"$PROJECT_ROOT/services/frontend"
"$PROJECT_ROOT/services/backend"
"$PROJECT_ROOT/services/shared"
"$PROJECT_ROOT/updater/sis-updater-scheduler"
"$PROJECT_ROOT/updater/sis-updater-worker"
)
infomsg "Cleaning up docker setup and node_modules from possible previous installations"
"$PROJECT_ROOT"/run.sh both down --remove-orphans --volumes --rmi all || infomsg "Cleaning errored, but will continue"
for folder in "${folders_to_set_up[@]}"; do
cd "$folder" || die "Couldn't change directory to folder $folder"
rm -rf node_modules || die "Couldn't remove node_modules in folder $folder"
done
infomsg "Installing npm packages locally to root and each subfolder to enable linting and formatting"
for folder in "${folders_to_set_up[@]}"; do
cd "$folder" || die "Couldn't change directory to folder $folder"
([[ -d node_modules ]] && die "$folder already has node_modules, exiting!") || npm ci
done
cd "$PROJECT_ROOT" || die "Couldn't change directory to project root"
infomsg "Setting up husky for pre-commit linting"
npm run prepare
infomsg "Pulling images"
"$PROJECT_ROOT"/run.sh both anon pull
infomsg "Building images"
"$PROJECT_ROOT"/run.sh both anon build
successmsg "Setup ready, Oodikone can be started! See README for more info."
}
docker_prune() {
infomsg "Running: docker system prune -a && docker volume prune -a"
docker system prune -a && docker volume prune -a
}
init_dirs() {
if [[ ! -d "$DUMP_DIR" ]]; then
infomsg "Creating directory for dumps"
mkdir "$DUMP_DIR"
fi
}
# If username is not set, get username from data file.
# Ask user to provide username, if username was not found from data file.
get_s3_config() {
if [ ! -f "$S3_CONFIG_FILE" ]; then
warningmsg "No username provided, config file is required. The \".s3cfg\" config file can be found on toska/dokumentaatio repo."
return 0
fi
infomsg "Using your config $S3_CONFIG_FILE - for getting database dumps"
}
-43
View File
@@ -1,43 +0,0 @@
#!/usr/bin/env bash
# This file includes some utility functions that are used in Oodikone scripts
# Functions are based on https://betterdev.blog/minimal-safe-bash-script-template/
# 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}
"
}
# Quit and exit with given message and error code
# By default uses exit code 1, message must be given
die() {
local msg=$1
local code=${2-1}
msg "${RED}${msg}${NOFORMAT}"
exit "$code"
}
# 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' PURPLE='\033[0;35m' CYAN='\033[0;36m' YELLOW='\033[1;33m'
else
NOFORMAT='' RED='' GREEN='' ORANGE='' BLUE='' PURPLE='' CYAN='' YELLOW=''
fi
-117
View File
@@ -1,117 +0,0 @@
# disallow directory named "private", it can be used to store things that should not end up in version control or images
# NOTE: needs to be at the root of the project
private/
gitignored/
# disallow version control, package managment, IDEs, tools etc
**/node_modules
**/.venv
**/.git
**/.github
**/.vscode
**/.idea
**/.zed
**/coverage/
**/.eslintcache
**/.DS_Store
# do not include any data by default (except json)
**/*.csv
**/*.tsv
**/*.db
**/*.sqlite
**/*.sql
**/*.psql
**/*.sqlz
**/*.sqz
**/*.sql.gz
**/*.bson
**/*.jsonl
**/*.bin
**/*.dat
**/*.data
**/*.swp
**/*.tmp
**/.~lock*
**/*.pid
**/*.seed
**/*.lock
**/*.gz
**/*.tgz
**/*.xz
**/*.zip
**/*.gzip
**/*.tar
**/*.xz
**/*.7z
**/*.rar
**/*.docx
**/*.doc
**/*.xlsx
**/*.xls
**/*.pptx
**/*.ppt
**/*.odt
**/*.odp
**/*.ods
**/*.pdf
# disallow logs
**/*.log
**/*.log*
**/logs/
**/log/
**/*.cpuprofile
**/*.heapprofile
**/*.heapsnapshot
# disallow configuration
**/*.yml
**/*.yaml
**/*.toml
**/*.conf
**/*.ini
**/toska_login/
**/toska_login/*
**/cypress.env.json
cypress/
# disallow secrects
**/.env
**/.env*
**/*.env.*
**/.secrects
**/*.s3cfg
# disallow backups
**/backups/*
**/backups
**/backup/*
**/backup
**/dump/
**/dump
**/dumps/
**/dumps
**/databasedumps/
**/privateData
**/temp
**/minio-data/*
# additional inclusions here
# exclusions here
**/.dockerignore
**/.gitignore
**/.eslintrc.json
**/*Dockerfile
**/build
**/dist
**/.npm
-12
View File
@@ -1,12 +0,0 @@
CI='true'
NODE_ENV='test'
DB_URL_KONE=postgres://postgres@localhost:4321/kone-db
DB_URL_USER=postgres://postgres@localhost:4320/user-db
FRONT_URL=http://localhost:3000
REDIS=localhost
SECRET_TOKEN=test
SIS_DB_URL=postgres://postgres@localhost:12345/sis-db
SIS_UPDATER_URL=http://localhost:8082
JAMI_URL='http://localhost:3003/'
IMPORTER_DB_API_URL='https://api-toska.apps.ocp-prod-0.k8s.it.helsinki.fi/importer'
CRYPT_KEY='e16730fce263d5270ba8dcf84d068a32556542455e71331880d6c1c5e4db9b4e'

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