Compare commits

...
777 Commits
Author SHA1 Message Date
joonas-a 2a89f37984 [Docker][Dev] fix permissions with node_modules................... 2026-07-16 16:32:20 +03:00
joonas-a 05a9c1f0d1 [Docker] madness. ubi9->ubi10. 2026-07-16 14:39:55 +03:00
Santeri Rajamäki 31bb53b885 [META] Update cli scripts 2026-07-16 12:41:36 +03:00
joonas-a f1120cc6ce [Meta][Frontend] allow root level package(s) from remote rather than npm
We use xlsx which is distributed by their own cdn rather than npm. Npm
v12 by defaults blocks this installation.
2026-07-16 11:27:46 +03:00
joonas-a 4754feb6af [Meta][Frontend] Vite v8, react plugin swc->oxc 2026-07-16 11:27:46 +03:00
Santeri Rajamäki 2ebfb44571 [Frontend][CloseToGraduation] Remove function pointers from memo 2026-07-15 17:09:40 +03:00
Santeri Rajamäki df71cc955c [Frontend][CloseToGraduation] Remove early exit from semesterEnrollment map 2026-07-15 17:08:44 +03:00
Santeri Rajamäki 373f870854 [Frontend][Export] Remove unneccesary array spreads from faculty students by year 2026-07-15 17:07:19 +03:00
Santeri Rajamäki 2ac369daa4 [Frontend] Cleanup semestersPresent call sites 2026-07-15 16:54:24 +03:00
Santeri Rajamäki 018a73e760 [Frontend] Cleanup semestersPresent format util 2026-07-15 16:53:03 +03:00
Santeri Rajamäki f43a4e4397 [Frontend] Convert semestersPresent helper to use formtting utils like the rest of the views 2026-07-15 16:52:20 +03:00
Santeri Rajamäki 958258ce01 [Dockerfile] Use --include and --omit correctly 2026-07-15 16:03:03 +03:00
Santeri Rajamäki e039c78f9e [OT] Detach header mapping 2026-07-15 14:50:39 +03:00
Santeri Rajamäki 6784b6e682 [OT] Rework SortIcons 2026-07-15 14:48:24 +03:00
Santeri Rajamäki cc85b5994a [Frontend][Redux] Drop TODO as not needed 2026-07-15 14:21:40 +03:00
Santeri Rajamäki d5af6977c5 [META] Remove deprecated language center switch 2026-07-15 13:19:51 +03:00
joonas-a dc0b5eb890 [React] enable StrictMode 2026-07-15 12:30:27 +03:00
joonas-a e47db461bb [Infobox][Programme overview] fix text formatting on combined programme(s) 2026-07-15 11:45:42 +03:00
joonas-a 429980e883 [Infobox][By starting year] mention "has recent attainment" column 2026-07-15 11:44:24 +03:00
joonas-a a8f4600875 [Graduation times] reword helper text 2026-07-15 11:33:15 +03:00
joonas-a 6cb063d717 [Frontend] inconsistent layout fixes 2026-07-15 11:30:21 +03:00
joonas-a a79cf865e4 [Langcenterview] make great again
Big UI tweaks.
2026-07-14 16:15:35 +03:00
joonas-a d8342371eb [Section] enhance
do not render jank when no need
2026-07-14 15:51:56 +03:00
joonas-a a2f9450eb5 [Overview][Lang center] fix atrocious layouts 2026-07-14 15:35:05 +03:00
joonas-a d4253141e6 [Programme overview] layout / padding / overlap tweaks 2026-07-14 14:21:28 +03:00
joonas-a 07dc603460 [Faculties] more UI tweaks and consistency 2026-07-14 12:38:27 +03:00
joonas-a 1877be265e [StyledTable] add nowrap as a flag 2026-07-14 12:34:21 +03:00
joonas-a a47c3c809d [Faculties][By starting year] table layout tweaks 2026-07-14 12:03:03 +03:00
joonas-a 7ce91ca38d [Faculties][Basic info] increase rendering performance 1000x 2026-07-14 10:43:33 +03:00
joonas-a 26cc77e68e [Banners] cleanup 2026-07-14 10:09:56 +03:00
joonas-a 183bdbfa45 [Frontend][Navigation] make BanMan accessible to admins 2026-07-13 16:49:38 +03:00
joonas-a 4bdbf4ad4d [Frontend] new admin view to manage banners (BanMan) 2026-07-13 16:49:38 +03:00
joonas-a 826dd15fd3 [BannerView] load + show existing banners at the top 2026-07-13 16:49:38 +03:00
joonas-a faf8fb5d01 [RTKapi] inject endpoints for banner queries/mutations 2026-07-13 16:49:38 +03:00
joonas-a 975959f1a5 [Backend] use banner routes + include banners in /login response 2026-07-13 16:49:38 +03:00
joonas-a 249b5ff94e [Backend] create routes/services for banners 2026-07-13 16:49:38 +03:00
joonas-a fdc860ffd7 [Kone] add model/migration for upcoming banners 2026-07-13 16:49:38 +03:00
joonas-a 15cd86cc47 [Theme] add "dark" (black) color 2026-07-13 16:49:38 +03:00
Santeri Rajamäki 3c5d76bc43 [UPDATER][WORKER] Strip initStartYearToSemesters 2026-07-13 15:41:10 +03:00
Santeri Rajamäki 6dba913e0c [UPDATER][WORKER] Cleanup initStartYearToSemesters 2026-07-13 15:38:52 +03:00
Santeri Rajamäki 3ea337e43e [Frontend][DegreeCourses] Fix. 2026-07-13 15:20:08 +03:00
Santeri Rajamäki 513b15fed5 [CLI] Reword option 2 2026-07-13 14:12:35 +03:00
Santeri Rajamäki 52d672b99b [Frontend][StudyProgramme] Collapse if init 2026-07-13 13:26:45 +03:00
Santeri Rajamäki 855121b5af [Frontend][Students] Collapse if init 2026-07-13 13:26:45 +03:00
Santeri Rajamäki 27b812f8cf [Frontend][CourseStats] Collapse if init 2026-07-13 13:26:45 +03:00
Santeri Rajamäki 3225fc1006 [Backend][Teachers] Collapse if init 2026-07-13 13:26:45 +03:00
Santeri Rajamäki ea40c2a56b [Backend][StudyProgramme] Collapse if init 2026-07-13 13:26:45 +03:00
Santeri Rajamäki 342048619a [Backend][University] Collapse if init 2026-07-13 13:26:45 +03:00
Santeri Rajamäki 8c7c130672 [Updater] Collapse if init 2026-07-13 13:26:45 +03:00
Santeri Rajamäki 681263aac4 [Frontend][Faculty] Collapse if init 2026-07-13 13:26:45 +03:00
Aaro Koivukangas 686438992f [E2E][Filters] Search study track by name, not by index 2026-07-13 13:00:42 +03:00
Aaro Koivukangas 4dcccdfab3 [E2E][University] Use the config variable name 2026-07-13 11:52:28 +03:00
Santeri Rajamäki ef5985d290 [E2E] Add access right checking to yaml 2026-07-13 11:50:05 +03:00
Santeri Rajamäki 0bfe4bace5 [E2E][Routes] Forbid "Something broke" when user has access 2026-07-13 11:36:41 +03:00
Santeri Rajamäki a7f285c5cb [E2E][Routes] Add server error catcher 2026-07-13 11:36:41 +03:00
Santeri Rajamäki 324db4c837 [E2E] Add access checking 2026-07-13 11:36:41 +03:00
Aaro Koivukangas 150b86145d [E2E][University] Add hella-long timeout to university e2e tests 2026-07-13 10:50:44 +03:00
Aaro Koivukangas 18f61cdb90 [E2E][University] Add same kind of check for Average graduation times breakdown chart 2026-07-12 17:04:12 +03:00
Aaro Koivukangas f99b882c4a [GithubActions] Still cannot enable updater update action 2026-07-12 14:17:34 +03:00
Aaro Koivukangas d525bc8673 [GithubActions] Add shorter retention dates to artifact uploads to use less storage 2026-07-11 09:54:39 +03:00
Aaro Koivukangas 1afbd6f2b4 [E2E][University] Add check that data has loaded 2026-07-11 09:50:21 +03:00
Aaro Koivukangas e3110219b6 [E2E] Fix degree programme overview test numbers 2026-07-11 09:48:39 +03:00
Aaro Koivukangas fb16335c81 [LanguageCenter] Fix sorting study rights with null end dates 2026-07-11 09:32:33 +03:00
Aaro Koivukangas f3fb9bc6da [Lint] Lint fix 2026-07-10 21:27:08 +03:00
Aaro Koivukangas a153b20fae [GithubActions] Re-enable updater update action acually updating sis-db 2026-07-10 21:11:14 +03:00
Aaro Koivukangas 159e904792 [E2E] Fix language center test numbers (for the last time... I hope) 2026-07-10 21:10:44 +03:00
Aaro Koivukangas 2b5b10b38b [LanguageCenter] Make language center view show correct faculty for students 2026-07-10 21:10:22 +03:00
Aaro Koivukangas 92fa9c3d2a [E2E] Fix faculty test numbers 2026-07-10 13:38:24 +03:00
Aaro Koivukangas 5d8b3e76ab [E2E] Fix course statistic test numbers 2026-07-10 13:38:07 +03:00
Aaro Koivukangas aacb2b2749 [Faculty] Fix test data studytrackstats calculations 2026-07-10 13:13:56 +03:00
Aaro Koivukangas fbe6fd4315 [Updater][Worker] Update all students by default locally 2026-07-10 12:52:43 +03:00
joonas-a 25e31c308a [Faculties] fix colspan to account for new column 2026-07-10 12:50:53 +03:00
Aaro Koivukangas 6034d885a0 [DockerCompose] Change ci-jami to use test NODE_ENV (even though it is not supported, just changing away from development) 2026-07-10 10:25:14 +03:00
Aaro Koivukangas bd9cfb9a7e [GithubActions][E2E] Run e2e tests in firefox 2026-07-10 10:17:54 +03:00
Aaro Koivukangas 24eb3137e9 [GithubActions] Add backend service-tests to gh actions 2026-07-10 10:12:07 +03:00
Aaro Koivukangas 5829f5c434 [Tests][Backend] Move unit tests that require db out of unit tests 2026-07-10 10:11:32 +03:00
joonas-a 9260a7d290 [Student page] Additionally highlight module and degree programme completions when a study right is selected 2026-07-09 16:11:20 +03:00
joonas-a 3adcb48e15 [Backend][Student details] Fetch modules in addition to courses for studyplans 2026-07-09 16:11:20 +03:00
Aaro Koivukangas b494af6344 [E2E] Add Course population tests to Numbers match 2026-07-09 15:41:50 +03:00
Aaro Koivukangas 6357960541 [ApiTests] Add api tests for Course population student logic 2026-07-09 15:41:50 +03:00
Aaro Koivukangas 73994181c8 [Backend][CourseStatistics] Change CoursePopulation student calculation logic to match other course calculations 2026-07-09 15:41:50 +03:00
Aaro Koivukangas 3bcab31d8e [Filters] Make Course-filter to respect includeSubstitutions 2026-07-09 15:41:49 +03:00
Aaro Koivukangas 951948e26d [Dockerfile] Ensure devDependencies are downloaded in updater dev environment 2026-07-09 15:41:49 +03:00
Aaro Koivukangas 06dc27e4fb [Vitest] Improve api test performance 2026-07-09 15:41:49 +03:00
Aaro Koivukangas 35872d6d26 [Updater][SubstitutionGroups] Remove possibly empty substitution groups 2026-07-09 15:41:48 +03:00
Aaro Koivukangas b142ed7339 [DockerCompose] Fix jami node_env to the correct one 2026-07-09 15:41:48 +03:00
joonas-a 2ffcfe7466 [StudyTrackStats] Scale colspans to account for new column 2026-07-09 15:19:41 +03:00
joonas-a 2a6dadf3dc [Backend][StudyTrackStats] add "Has recent attainment" to response 2026-07-09 15:19:04 +03:00
joonas-a 06c4b4cfae [Backend][StudyRightsFinders] include more data with studyrights when "include credits" mode enabled 2026-07-09 15:08:30 +03:00
Aaro Koivukangas 065922564c [GithubActions] Fix typo in frontend release action 2026-07-08 09:25:33 +03:00
Aaro Koivukangas c7d0e39eeb [E2E] Test numbers fix (pls be the last...) 2026-07-08 09:10:49 +03:00
Aaro Koivukangas 25418f700b [E2E] Fix e2e test numbers 2026-07-07 18:02:54 +03:00
Aaro Koivukangas 2439fc4475 [GithubActions] Fix hanging api test 2026-07-07 17:41:04 +03:00
Aaro Koivukangas df4601f0bf [Scripts] Improve oodikone:ci script + add comment about nginx in ci compose 2026-07-07 17:15:15 +03:00
Aaro Koivukangas 80516dd6a8 [E2E][LanguageCenter] Revert previous edits 2026-07-07 17:14:07 +03:00
Aaro Koivukangas 9d22511895 [GithubActions] Disable updater update pushing image to toska hub 2026-07-07 16:02:18 +03:00
Aaro Koivukangas 2bec5a0de1 [E2E][LanguageCenter] Fix broken e2e test 2026-07-07 15:38:10 +03:00
Aaro Koivukangas 734442c43c [GithubActions] Add missing failure + cancelled checks 2026-07-07 15:37:53 +03:00
Aaro Koivukangas 6ae1a05f27 [GithubActions] Remove log-flood from api tests 2026-07-07 15:19:57 +03:00
Aaro Koivukangas f11f2f240c [Scripts+DockerCompose] Update scripts used to run tests 2026-07-07 14:32:10 +03:00
Aaro Koivukangas 86d7f74cb2 [E2E][LanguageCenter] Fix broken e2e test 2026-07-07 13:57:23 +03:00
Aaro Koivukangas ceb7a733d7 [E2E][LanguageCenter] Fix broken e2e tests 2026-07-07 12:37:01 +03:00
Aaro Koivukangas c0246550bb [E2E][Teachers] Fix broken test 2026-07-07 12:14:26 +03:00
Aaro Koivukangas cb21c675c2 [E2E][DegreeProgrammeOverview] Fix tests broken by updater update 2026-07-07 12:13:44 +03:00
Aaro Koivukangas 49da59655e [E2E][Students] Fix tests broken by updater update 2026-07-07 10:27:11 +03:00
Aaro Koivukangas 39d9d6f664 [GithubActions] Make dependencies more clean 2026-07-06 16:51:15 +03:00
Aaro Koivukangas e3bd0d1226 [ApiTests] Update tests to match newly updated data 2026-07-06 16:50:27 +03:00
Aaro Koivukangas 795cb50a1f [GithubActions] Remove prepurge and purge actions, they never did anything 2026-07-06 14:44:07 +03:00
Aaro Koivukangas ba75b0828f [DockerCompose] Add env variables to scheduler to trigger updater updates locally easier 2026-07-06 13:58:16 +03:00
Aaro Koivukangas 0a0225bc77 [Scripts] Add a script to get all logs, including db 2026-07-06 13:58:15 +03:00
Aaro Koivukangas f14028145b [GithubActions] Restore previously deleted flag from healthchecks 2026-07-06 13:38:07 +03:00
Aaro Koivukangas 3cd7f05280 [GithubActions] Add log dump to updater update in case of failure 2026-07-06 13:06:25 +03:00
Aaro Koivukangas bc5769e89e [GithubActions] Last fixes to dependency structure 2026-07-06 12:59:00 +03:00
Aaro Koivukangas 85365e2636 [GithubActions] Fix permission error on updater update 2026-07-06 12:54:26 +03:00
Aaro Koivukangas b4deae4343 [DockerCompose] Fix healthchecks to all work the same way 2026-07-06 12:54:09 +03:00
Aaro Koivukangas 669e1b6ab9 [GithubActions] Fix actions dependency structure even more 2026-07-06 12:53:41 +03:00
Aaro Koivukangas 35c11c3e20 [GithubActions] Update dependency structure 2026-07-06 10:40:45 +03:00
Aaro Koivukangas fc61414f4b [GithubActions] Change to use more simple if block 2026-07-06 10:39:07 +03:00
Aaro Koivukangas 130f38c171 [Updater][Worker] Add throws to all errors + add a proper wait when running in EXIT_AFTER_IMMEDIATES 2026-07-06 10:27:01 +03:00
Aaro Koivukangas 98d32cfafc [DockerCompose] Fix database healthcheck logs No role for "root" 2026-07-06 10:25:36 +03:00
Aaro Koivukangas 8c85333cd6 [GithubActions] Add updater update to test workflow 2026-07-06 10:24:40 +03:00
Aaro Koivukangas febef1b7b8 [GithubActions] Remove unnecessary steps from build action 2026-07-05 11:15:52 +03:00
Aaro Koivukangas c9465da33d [GithubActions] Fix backend+frontend push to quay 2026-07-05 11:02:17 +03:00
Aaro Koivukangas 3a85da4573 [GithubActions] Add more missing quotes from build 2026-07-04 16:11:13 +03:00
Aaro Koivukangas 57d0aa6870 [GithubActions] Add missing quotes to backend+frontend build action 2026-07-04 15:48:39 +03:00
Aaro Koivukangas f00855f1bf [GithubActions] Export buildah image and tag name from action 2026-07-04 15:27:18 +03:00
Aaro Koivukangas 2c2393c422 [GithubActions] Update external action version 2026-07-04 15:03:23 +03:00
Aaro Koivukangas 677c0f2b59 [GithubActions] Fix simple typo in frontend and backend build action 2026-07-04 15:03:02 +03:00
Aaro Koivukangas 7ae1a84bd5 [UnitTesting] Add placeholder unit tests for backend to stop actions from failing 2026-07-04 14:48:33 +03:00
Aaro Koivukangas 2463ffa771 [GithubActions] Add missig checkout 2026-07-04 14:46:11 +03:00
Aaro Koivukangas 2fff045861 [GithubActions] Remove unnecesary logs from pulling images 2026-07-04 14:45:19 +03:00
Aaro Koivukangas 02aa86ae47 [GithubActions] Remove image pull caching so that the correct version for dbs are always used 2026-07-04 14:39:04 +03:00
Aaro Koivukangas 304da32284 [GithubActions] Fix multiple action errors 2026-07-04 14:37:56 +03:00
Aaro Koivukangas 76071c9f00 [GithubActions] Fix tests waiting for backend to start inside docker-compose.ci.yml 2026-07-04 14:35:16 +03:00
Aaro Koivukangas 6d4aeced89 [GithubActions] Fix e2e test action path change 2026-07-04 14:02:18 +03:00
Aaro Koivukangas ec32982ada [GithubActions] Add shell variable to all run: action steps 2026-07-04 14:00:34 +03:00
Aaro Koivukangas e68bffe8ec [GithubActions] Frontend build typo fix 2026-07-04 13:54:25 +03:00
Aaro Koivukangas 20875c6184 [GithubActions] Fix action error and use correct release_version var name 2026-07-04 13:51:05 +03:00
Aaro Koivukangas 67e67323de [GithubActions] Fix one more typo 2026-07-04 13:47:32 +03:00
Aaro Koivukangas 5d6b6b71ab [GithubActions] Fix typo and add names to workflow steps 2026-07-04 13:46:32 +03:00
Aaro Koivukangas 438267d552 [GithubActions] Handle checkouts correctly 2026-07-04 13:41:26 +03:00
Aaro Koivukangas dc02443986 [GithubActions] Fix multiple typos and errors 2026-07-04 13:35:36 +03:00
Aaro Koivukangas e3e7468f14 [GithubActions] Change backend action to use correct runner 2026-07-04 13:29:52 +03:00
Aaro Koivukangas fb0b4d01c4 [GithubActions] Fix typos 2026-07-04 13:22:34 +03:00
Aaro Koivukangas f0ebd6de93 [GithubActions] Rework github actions to be more consistent 2026-07-04 13:19:15 +03:00
Aaro Koivukangas f734c0db38 [DockerCompose] Add healthchecks for all applicable containers 2026-07-04 10:41:26 +03:00
joonas-a d8d34a58b1 [BullMQ][Backend] Fix logger not logging
There is no need to have a try/catch or logger instance in the
processor. The errors are already propagated back to where we had
existing event listeners.
2026-07-03 19:39:14 +03:00
Aaro Koivukangas 20433e72b7 [ApiTests] Skip few tests, will be fixed later 2026-07-03 18:27:49 +03:00
joonas-a 63988f7d6a [Updater][StudyRightElements] new logic to combat corrupted "most recent" snapshots 2026-07-03 18:13:14 +03:00
Aaro Koivukangas b1433ee4a2 [ApiTests] Update tests to match new test data 2026-07-03 17:38:12 +03:00
Aaro Koivukangas f0082e5dd0 [ApiTests] Refactor flaky api test 2026-07-03 16:58:18 +03:00
Aaro Koivukangas f8d2097f24 [ApiTests] Move api tests to api dir 2026-07-03 16:31:48 +03:00
Aaro Koivukangas 0bedc21a57 [Scripts] Add silent testig mode for backend api tests 2026-07-03 16:15:21 +03:00
Aaro Koivukangas 8a119485de [CI] Change ci to use vitest for backend api tests 2026-07-03 16:06:33 +03:00
Aaro Koivukangas 2f94bfd474 [CI] Unify normal and ci compose.yamls to use same ports 2026-07-03 16:01:41 +03:00
Aaro Koivukangas 5082ec9deb [ApiTests] Migrated changelog tests 2026-07-03 15:51:20 +03:00
Aaro Koivukangas 11c75181c3 [ApiTests] Migrated programme course stats tests 2026-07-03 15:51:20 +03:00
Aaro Koivukangas 02935f7da8 [ApiTests] Migrated populationStudyProgramme tests 2026-07-03 15:51:19 +03:00
Aaro Koivukangas 9bb43caa57 [ApiTests] Migrated populationStatisticsByStudentNumbers tests 2026-07-03 15:51:19 +03:00
Aaro Koivukangas 9995e61939 [ApiTests] Migrated populationStatisticsByCourse tests 2026-07-03 15:51:19 +03:00
Aaro Koivukangas 7fbeb34e6d [ApiTests] Migrated populationstatsitics tests 2026-07-03 15:51:18 +03:00
Aaro Koivukangas eda0069e30 [ApiTests] Change to use vitest 2026-07-03 15:51:18 +03:00
joonas-a dcf7ef3e23 [Backend][Graduation times] No longer send grad time arrays to client as they are not needed and pollute the response 2026-07-03 15:42:17 +03:00
joonas-a 55474d5de9 Remove empty file 2026-07-03 15:27:46 +03:00
joonas-a eb843ff43d [E2E][Programme overview] fix tests 2026-07-03 15:02:14 +03:00
joonas-a de32410cc7 [E2E][University] fix tests 2026-07-03 14:18:53 +03:00
Aaro Koivukangas fd8c67384e [CI] Remove watch from logs:ci command, tests hang 2026-07-03 13:48:19 +03:00
Aaro Koivukangas 931b6632f2 [ApiTests] Remove unused db connection 2026-07-03 13:46:30 +03:00
Aaro Koivukangas 5769d7b996 [ApiTests] Add a possibility to execute tests locally 2026-07-03 13:46:30 +03:00
joonas-a f81e8e59da [Scheduler] remove duplicate log entry
The exact same thing is logged two lines in a row
2026-07-03 12:40:05 +03:00
Aaro Koivukangas 85830efce5 [CI] Remove bloat from logging 2026-07-03 12:20:21 +03:00
Aaro Koivukangas 55f48c4b77 [E2E] Add admin privileges to one test 2026-07-03 10:57:50 +03:00
Aaro Koivukangas 119073a87a [E2E] Add e2e test to check if numbers match between Programme courses and Course stats 2026-07-02 19:22:52 +03:00
Aaro Koivukangas 5863cc90be [ApiTests] Add Programme course tests and make numbers match to Course stats 2026-07-02 19:22:51 +03:00
Aaro Koivukangas b794d9d778 [CourseStats] Improve tests + Fix Enrolled no grade calculations 2026-07-02 19:22:51 +03:00
Aaro Koivukangas 8d79a68a81 [ApiTests] By default silence logging inside tests 2026-07-02 19:22:51 +03:00
Aaro Koivukangas 5ca04fea1a [Faculties] Fix type name typo 2026-07-02 19:22:51 +03:00
Aaro Koivukangas 369ff207cb [Dockerignore] Change to toska-standard .dockerignore files 2026-07-02 19:22:50 +03:00
Aaro Koivukangas a863aa798c [CI] Trigger updater workflows on pushes, not only on releases 2026-07-02 19:20:08 +03:00
Aaro KoivukangasandAaro Koivukangas 7c15472c17 [ApiTests] Add more thorough testing for /courseyearlystats 2026-07-02 19:20:07 +03:00
joonas-a 2a9509ed90 [Charts][Graduations] Improve UI 2026-07-02 15:43:24 +03:00
joonas-a 237d47c22e [University][Graduations] add averages 2026-07-02 15:27:05 +03:00
joonas-a c1340daf3d [Faculties][Graduations] add averages 2026-07-02 15:25:15 +03:00
joonas-a f899572eaf [Programme overview][Graduations] add averages to charts 2026-07-02 15:18:02 +03:00
joonas-a c16d18b699 [Degree programme graduations] Add "average" to respose and make typings consistent with faculty/uni views 2026-07-02 13:22:59 +03:00
joonas-a 5f31e03ae1 [Updater][StudyRightElements] Consider phase graduations earlier in the mankeli
Now it does not grant phase 2 elements before phase 1 graduation, and
does not allow modifying elements if there is a graduation already
2026-07-02 12:44:54 +03:00
joonas-a 70120f27bc [Updater][StudyRightElements] fix error when student has many phase 2 educations before phase 1 completion 2026-07-01 13:15:22 +03:00
joonas-a 96889eca28 [Logging] Ignore logs to /updater/jobs endpoint as it is polled very often 2026-07-01 12:53:13 +03:00
joonas-a 0336c27aac [Updater] add logs for studyrightelementmapper errors 2026-07-01 12:40:16 +03:00
Santeri Rajamäki 5e6a320c88 [Frontend] Move PopulationComponent Students Table component tree 2026-06-30 12:12:18 +03:00
Santeri Rajamäki 092aefb177 [Frontend] Move PopulationStudents component tree 2026-06-30 12:12:18 +03:00
Santeri Rajamäki 8ce01807c2 [CoursePopulation] Add fetch skip on invalid mainCodes array 2026-06-30 12:12:18 +03:00
Santeri Rajamäki 8331e462f1 [Frontend] Remove "index" from path subfixes 2026-06-30 12:12:18 +03:00
Santeri Rajamäki 22a672bbe8 [Frontend] Consolidate ProgrammeDist infobox 2026-06-30 12:12:18 +03:00
Santeri Rajamäki d21f383b17 [Frontend] Convert relative paths to absolute \@ paths 2026-06-30 12:12:16 +03:00
Santeri Rajamäki ca826a387b [Frontend] Move ProgrammeDist component 2026-06-30 12:10:34 +03:00
Santeri Rajamäki 17cbaaa024 [Frontend] Move CourseTableModeSelector component 2026-06-30 12:10:34 +03:00
Santeri Rajamäki 9aa8e84dbc [Frontend] Move PopulationCourses component 2026-06-30 12:10:34 +03:00
Santeri Rajamäki f69c96e807 [Frontend] Move AgeStats component 2026-06-30 12:10:34 +03:00
Santeri Rajamäki 6ec7153c6f [Frontend] Move CreditGainStats component 2026-06-30 12:10:34 +03:00
Santeri Rajamäki 98bf2de51f [Frontend] Move CreditAccumulation component 2026-06-30 12:10:34 +03:00
joonas-a 34cb0c83d9 [Update][Studyrights] prevent creating phase 2 elements before if there is no phase 1 graduation 2026-06-29 16:39:13 +03:00
Aaro Koivukangas 0b76807706 [E2E] Test number fix 2026-06-29 14:24:55 +03:00
Aaro Koivukangas e6676044a9 [ProgrammeCourses+CourseStats+ApiTests] Fix calculations and make numbers match between the two pages 2026-06-29 13:52:15 +03:00
joonas-a 4d98abcb39 [Frontend][Graduation times] Guarantee years sorted correctly 2026-06-26 16:56:49 +03:00
joonas-a 463541a08c [Frontend][Graduation times] Show infotext if no data for current level 2026-06-26 16:41:26 +03:00
joonas-a be835b7ed5 [Backend][Graduation times] make faculties not send each individual graduation time to client 2026-06-26 16:31:29 +03:00
joonas-a db0e6ebb63 [Backend][Graduation times] Fix undefined programme names 2026-06-26 15:15:22 +03:00
joonas-a 5f3f3caf3b [Infobox][Graduation times] clarify what groups are included in numbers
Further clarification possibly needed
2026-06-26 14:57:15 +03:00
joonas-a 3cb4828eb4 [Frontend][Graduation times] add hints about chart level breakdown feature 2026-06-26 14:47:44 +03:00
joonas-a 418747e6c6 [Updater] Fix "ACTIVE" enrolments on "DELETED" CURs causing an error
There was no document_state check on CURs, and deleted CURs seem to set
validity period to year 1900 which is outside of DKN's semester logic ->
error
2026-06-26 12:54:23 +03:00
joonas-a 57fab874bc [DX] Add button for admins to open page in devbuild in prod 2026-06-26 11:28:24 +03:00
joonas-a c000d1a57d [Faculty graduations] simplify logic
Fixes bug where by start year switch caused a random program to be
counted twice
2026-06-25 22:14:23 +03:00
Aaro Koivukangas b6e5b92bdc [CompontentTests] Replace few more e2e tests with component tests 2026-06-25 15:13:16 +03:00
Aaro Koivukangas 11819d223a [CI] Also install shared's node_modules during component testing 2026-06-25 14:31:18 +03:00
Aaro Koivukangas bb68879566 [ComponentTests] Add proof-of-concept component test with api calls 2026-06-25 14:22:57 +03:00
joonas-a 4acee84b14 [DX] Click dev chip to open current page in prod 2026-06-25 13:21:15 +03:00
Aaro Koivukangas 8c261458ba [Docker] Change user 1001 to the correct place for dev.Dockerfiles 2026-06-25 12:18:37 +03:00
Aaro Koivukangas eb634285eb [DockerCompose] Acually acually change incorrect volume name 2026-06-25 11:53:39 +03:00
Aaro Koivukangas 5de3afe2ae [DockerCompose] Acually change incorrect volume name 2026-06-25 11:50:40 +03:00
Aaro Koivukangas 12a9396b8f [DockerCompose] Change incorrect volume name 2026-06-25 11:48:38 +03:00
Aaro Koivukangas 40a37e001f [ProgrammeOverview][ProgrammeCourses] Change default value of credit/students toggle to be students 2026-06-25 09:23:36 +03:00
joonas-a 8e9b0f7844 [Updater] studyRightElement rework
Should fix bugs related to elements ending before their start date, and
dates being a mess in general.
2026-06-24 19:30:28 +03:00
joonas-a ba93dc8927 [Faculty Graduation times] fix semesters labelled as months in excel export 2026-06-24 19:30:27 +03:00
joonas-a 4935d84842 [Graduation times] make medians chart target marker ignore mouse events
it would trigger an invalid tooltip
2026-06-24 19:30:27 +03:00
Aaro Koivukangas 0b207c899a [E2E] Fix broken e2e test 2026-06-24 18:09:21 +03:00
Aaro Koivukangas 99dcdcd5b3 [Scripts] Add docker:down to oodikone npm script 2026-06-24 17:56:56 +03:00
Aaro Koivukangas ed330f70a7 [Meta] Change enrollments to by default be excluded before OK sisu-migration 2021-05-31 2026-06-24 17:55:25 +03:00
Aaro Koivukangas e06239f8a0 [CI] Trigger staging gh action workflow also if docker-compose.ci.yml changes 2026-06-24 15:46:54 +03:00
Aaro Koivukangas fcdf2b8fc6 [DockerCompose] Exclude shared node_modules from being overridden 2026-06-24 15:44:01 +03:00
Aaro Koivukangas dd6e44d6c6 [Dockerfile] Add shared node_modules to also dev.Dockerfiles 2026-06-24 15:34:13 +03:00
Aaro Koivukangas 6d838a9acc [Dockerfile] Fix previous fix, add shared's package*.json to backend docker image 2026-06-24 15:18:01 +03:00
Aaro Koivukangas 629ea3a516 [Dockerfile] Add missing shared npm ci step to both dockerfiles 2026-06-24 15:14:13 +03:00
Aaro Koivukangas 980168eb9e [E2E] Fix broken e2e tests 2026-06-24 15:02:55 +03:00
Aaro Koivukangas df88a88550 [E2E] Skipping many e2e tests, writing these in the future as api tests 2026-06-24 14:53:44 +03:00
Aaro Koivukangas ccfb09eab4 [CourseStats] Calculate Enrolled, no grade properly by enrollment_date_time, not semesterCode 2026-06-24 14:49:21 +03:00
Aaro Koivukangas 582637942e [ApiTests] Add api tests for /courseyearlystats, used by Course statistics 2026-06-24 10:48:35 +03:00
Aaro Koivukangas 742e12201c [E2E] Wait for a toggle to change data 2026-06-23 16:35:08 +03:00
Aaro Koivukangas ff015d04bf [ApiTests] Move to use nested tests + fix handleQueryArrays to work with undefined values 2026-06-23 16:07:42 +03:00
Aaro Koivukangas 9b59f53f1b [Meta] Remove winston-gelf-transporter 2026-06-23 16:04:55 +03:00
Aaro Koivukangas f2c1181a1d [Programme overview][Programme courses] Change default value of combineSubstitutions to be false 2026-06-22 15:38:05 +03:00
Aaro Koivukangas 2b42935d2d [E2E] Fix broken test 2026-06-22 15:32:40 +03:00
Aaro Koivukangas 8deea058af [E2E] Fix broken e2e tests 2026-06-22 15:21:53 +03:00
Aaro Koivukangas a53e46568e [Programme overview][Programme courses] Course links now include substitutions by default 2026-06-22 15:09:45 +03:00
Aaro Koivukangas cf4ae69ae4 [CI] Re-enable removed action correctly 2026-06-22 14:57:47 +03:00
Aaro Koivukangas 6a3ddb865c [Meta] In frontend default to Academic year instead of Calendar year 2026-06-22 14:51:42 +03:00
Aaro Koivukangas cec8dc0c0b [CI] Remove unused action 2026-06-22 14:45:55 +03:00
Aaro Koivukangas 7fa14c10f8 [CI] More typo fixes 2026-06-22 14:36:11 +03:00
Aaro Koivukangas 96a9770fbf [CI] Revert back to ubuntu-latest 2026-06-22 14:22:49 +03:00
Aaro Koivukangas 4b1e416a18 [CI] Fix typo 2026-06-22 13:58:50 +03:00
Aaro Koivukangas 258996ea17 [CI] Change staging to use runner-scaleset, add frontend unit and component tests to actions 2026-06-22 13:57:52 +03:00
Aaro Koivukangas 5c542f8a91 [CI] Change tests to use self-hosted runner, disable dependabot triggering workflow runs 2026-06-22 13:21:01 +03:00
Aaro Koivukangas ab69044d79 [Meta] Add frontend component testing 2026-06-22 10:51:43 +03:00
joonas-a 2eded61803 [Lint] fix 2026-06-17 17:47:43 +03:00
joonas-a 3fe589811e [Backend][Refactor] unify graduation time typing and fix bugs 2026-06-17 17:42:47 +03:00
Aaro Koivukangas 20d5884722 [Scripts] Handpick logs-script containers 2026-06-17 09:44:21 +03:00
joonas-a 8a924487ce [Infobox] update 2026-06-16 17:03:49 +03:00
joonas-a cf77473e77 [Percentiles] Add credit gain target diagonal line
60 credits per academic year
2026-06-16 17:03:49 +03:00
joonas-a 9383a8bb73 [Percentiles] rotate item order and introduce new color scheme 2026-06-16 17:03:49 +03:00
Aaro Koivukangas 21de703da9 [Docker] Fix broken commands from oodikone- prefix addition 2026-06-16 16:58:43 +03:00
Aaro Koivukangas b58dd5f25c [Docker] Add oodikone- prefix to container and volume names 2026-06-16 15:54:44 +03:00
Aaro Koivukangas dc10aeb14c [E2E] Update broken e2e test values 2026-06-15 17:29:17 +03:00
Aaro Koivukangas 4b38d6dd96 [ClassStats][Students][Progress] Fix single course progress in Excel export 2026-06-15 17:25:55 +03:00
Aaro Koivukangas deaa41565d [ProgrammeOverview][ProgrammeCourses] Remove 95% of (duplicate) 'Not completed' entries 2026-06-15 15:33:03 +03:00
joonas-a 5359eae68d [Updater] Fix studyplan curriculum periods
Previously shown was the hops creation date, not which curriculum the
hops for each "phase" shows
2026-06-12 18:11:12 +03:00
Aaro Koivukangas 0717fc0703 [ClassStats][CreditStats][DistributionDevel] Fix Divide by Calendar year's start date 2026-06-11 17:41:27 +03:00
Aaro Koivukangas 508cce0b36 [ClassStats][CreditStats][DistributionDevel] Fix non-cumulative graduated count 2026-06-11 17:41:27 +03:00
joonas-a d2fed921e4 [Regression] fix infobox formatting 2026-06-11 15:04:31 +03:00
Aaro Koivukangas 514407abf8 [ClassStatistics][Students][Courses] Fix Total planned and passed courses 2026-06-11 14:42:59 +03:00
Santeri Rajamäki af9f5b15d7 [DOCS] Add grafana entry to README 2026-06-10 13:53:56 +03:00
Santeri Rajamäki ea9b61d473 [Updater] Cleanup db connections 2026-06-10 13:53:56 +03:00
joonas-a 7171bf198e [Completed courses] fix regression + add types 2026-06-10 13:42:07 +03:00
joonas-a 14c22af2ef [Completed courses] add substitutions to infobox 2026-06-10 13:42:07 +03:00
Aaro Koivukangas 2f05de3258 [Infobox] Fix incorrect formatting in a infobox 2026-06-10 12:32:36 +03:00
joonas-a a6d69cbff5 [Backend][Updater] Add way to flush redis completely in prod
Vastuuvapauslauseke: käyttö omalla vastuulla
2026-06-08 16:10:51 +03:00
Aaro Koivukangas c0b3decaf7 [CompletedCoursesSearch] Refactor substitutions => substitution_groups 2026-06-06 13:41:42 +03:00
joonas-a 1258c0c55c [Backend][Study track stats] discard study rights that do not belong in any category
If viewing bs+ms study right, bs study rights granted before 2017 do not
belong to any category -> do not show them
2026-06-05 12:08:05 +03:00
Aaro Koivukangas 3a0cb6f0ef [PreCommit] Restore previous behaviour for husky pre-commit hook 2026-06-04 14:57:39 +03:00
joonas-a 33e8fb7a39 [Docker] Enable nodemon for dev-updater + fix error 2026-06-04 14:50:48 +03:00
joonas-a 39c78791c2 [Updater panel] layout tweak and fix eslint breaking functionality 2026-06-03 19:13:51 +03:00
joonas-a 54e0469a02 [Updater panel] Improvements
- Convert to TS
- Show status of ongoing and queued jobs
2026-06-03 19:08:39 +03:00
Aaro Koivukangas 9c9e120d8b [Meta] Remove one more instance of substitutions 2026-06-03 17:38:00 +03:00
Aaro Koivukangas 2466aab3a4 [StudyProgrammes] Replace one instance of substitutions with substitution_groups 2026-06-03 16:51:54 +03:00
Santeri Rajamäki 0ac0d8befa [Frontend] Use full width with ErrorBackground as it doesn't have a parent to fill 2026-06-03 13:24:46 +03:00
Santeri Rajamäki e8b46a8b56 [Frontend] FilterView improvements 2026-06-03 13:12:37 +03:00
Santeri Rajamäki 73ee57b6bc [Frontend] Filter rework 2026-06-03 13:11:14 +03:00
joonas-a 4b60f985ac [Curriculum picker] add translation to infobox 2026-06-03 12:33:39 +03:00
Aaro Koivukangas b975714c97 [Infoboxes] Refactor population infobox text object 2026-06-03 12:30:39 +03:00
joonas-a f7c22f4d6e [Filters] Clarify date of course part. infobox text 2026-06-03 12:19:45 +03:00
joonas-a f8cfe9c23a [Percentiles] add english infobox translation 2026-06-03 11:50:54 +03:00
Aaro Koivukangas e818ffa03a [Infoboxes] Refactor faculty infobox text object 2026-06-03 10:39:13 +03:00
Aaro Koivukangas 571a51f09f [Infoboxes] Refactor studyprogramme infobox text object 2026-06-03 10:14:05 +03:00
joonas-a 64deef0c27 [Programme overview][Percentiles] Use proper class sizes
... instead of the amount of months in the range oops.
2026-06-02 18:47:49 +03:00
joonas-a 0e7746184c [Percentiles chart] Fix HTML tag use 2026-06-02 18:39:20 +03:00
joonas-a 628a93e6df [Backend] add proper class sizes to studytrackstats response 2026-06-02 18:37:59 +03:00
joonas-a e4091d6b45 [Percentiles] Add data view feature to chart 2026-06-02 13:45:43 +03:00
joonas-a f57dce2259 [Percentiles][prog overview] add infobox
finnish only :d
2026-06-02 13:45:43 +03:00
Aaro Koivukangas 853912f25b [InfoBoxes] Fix remaining broken info-boxes 2026-06-02 12:37:22 +03:00
Aaro Koivukangas a55198464c [Meta] Remove unused 'substitutions' from types 2026-06-01 15:13:13 +03:00
Aaro Koivukangas b8e39aa29a [Faculty] Change Faculty-view, Update credit data -function to use substitution_groups 2026-06-01 15:09:02 +03:00
Aaro Koivukangas 94d0e54e8b [E2E] Course filter e2e fix 2026-06-01 13:22:02 +03:00
joonas-a ed90d13846 [META] remove safechain in favor of npmrc in docker images 2026-06-01 13:13:39 +03:00
joonas-a 7731a4c699 [META] add npmrcs 2026-06-01 12:52:51 +03:00
Aaro Koivukangas 486613febd [Filters][Courses] Course filter calculates enrolled, no grade correctly 2026-06-01 12:48:27 +03:00
Aaro Koivukangas d7e26d22d0 [Meta] Remove unused unifyOpenUniCourses query param from links 2026-05-29 15:25:54 +03:00
Santeri Rajamäki e069c315a5 [META] Remove open uni search 2026-05-29 11:54:02 +03:00
Aaro Koivukangas f4ac2b3806 [Class statistics][Filters] Course filter respects substitution_groups 2026-05-28 16:08:18 +03:00
Santeri Rajamäki 03756dfce0 [Frontend] Introduce expanding infoboxes 2026-05-28 13:00:36 +03:00
Santeri Rajamäki 1c26912ac7 [Frontend] Add HTML support to react markdown using "an official plugin" 2026-05-28 12:59:36 +03:00
Aaro Koivukangas 8a7b9b4894 [Navbar] Make dev version easily distinguishable from production 2026-05-28 12:40:22 +03:00
Aaro Koivukangas b4103ca779 [CourseSearch] Implement hover for Course search substitution groups 2026-05-27 15:55:15 +03:00
Aaro Koivukangas 4a68fe19e6 [CI] Disable using prebuilt frontend version 2026-05-27 13:52:15 +03:00
Santeri Rajamäki 69365473a4 [META] Use node user in local dev environments to avoid root devour 2026-05-27 13:37:18 +03:00
Aaro Koivukangas 2cfe0d1f6c [CI] Fix correct image name for frontend 2026-05-27 13:36:42 +03:00
Aaro Koivukangas 26d33c1201 [CI] Add missing uses from gh action 2026-05-27 13:20:10 +03:00
Aaro Koivukangas b1c8396098 [CI] Use same config for frontend as backend 2026-05-27 13:18:10 +03:00
Santeri Rajamäki 9d608dbbcc [META] Update docker dev builds 2026-05-27 13:09:02 +03:00
Aaro Koivukangas ae52189689 [CI] Utilize prebuild frontend image 2026-05-26 13:59:20 +03:00
Aaro Koivukangas ecfc8053a0 [CI] Use prebuilt backend image during e2e 2026-05-26 13:31:08 +03:00
Aaro Koivukangas 71bff0214e [CI] Don't pull buildable images eg. backend 2026-05-26 13:19:35 +03:00
Aaro Koivukangas 58bbe00171 [CI] Try acually utilizing the built backend image 2026-05-26 13:13:52 +03:00
Aaro Koivukangas 75302bdc14 [CI] Use more docker-image caching during testing 2026-05-26 12:54:52 +03:00
Aaro Koivukangas 55135030e0 [CI] Fix docker-image missing 2026-05-26 12:42:46 +03:00
Santeri Rajamäki a8b87eed3c [SGG] Remove extra Paper wrapper from Courses of population 2026-05-26 12:34:56 +03:00
Santeri Rajamäki 95f36ef7e8 [PopulationStatistics] Move curriculum selection to the top-level details panel 2026-05-26 12:34:56 +03:00
Aaro Koivukangas f8107bc475 [CI] Increase test parallelization 2026-05-26 12:14:07 +03:00
Aaro Koivukangas 391ea1cbaf [E2E] More fixes 2026-05-26 11:54:31 +03:00
Aaro Koivukangas 5f1d3a12b7 [E2E] e2e test typo fixes 2026-05-26 11:39:26 +03:00
Santeri Rajamäki c533c25bfd [Backend] Remove redundant FD user code 2026-05-26 11:11:31 +03:00
Santeri Rajamäki d4e2d856e8 [Frontend] Use better font 2026-05-26 11:11:31 +03:00
Aaro Koivukangas c5b3147f04 [CourseStats] Update tests to match page title changes 2026-05-26 11:09:04 +03:00
Aaro Koivukangas 8977c52225 [CoursePopulation] Add includeSubstitutions flag to links to Course populations instead of massive coursecode arrays 2026-05-25 22:45:58 +03:00
Aaro Koivukangas a8f48f24d1 [Meta] Add curl to alpine images 2026-05-24 22:31:34 +03:00
Aaro Koivukangas 714682c6c5 [CI] Properly skip e2e if api fails 2026-05-24 21:54:34 +03:00
Aaro Koivukangas 0659d162b5 [Meta] Change alpine images to use ash instead of bash 2026-05-24 21:54:34 +03:00
Aaro KoivukangasandAaro Koivukangas 7057e00ee2 [E2E] Disable the broken test entirely 2026-05-24 21:53:26 +03:00
Aaro KoivukangasandAaro Koivukangas adf64a3d1d [E2E] Disable incorrect check in test 2026-05-24 21:53:26 +03:00
Aaro KoivukangasandAaro Koivukangas 36421cf5e7 [E2E] Now acually scroll the bar into view 2026-05-24 21:53:26 +03:00
Aaro KoivukangasandAaro Koivukangas a81b569ab9 [E2E] Scroll bar chart to view in e2e test 2026-05-24 21:53:26 +03:00
Aaro KoivukangasandAaro Koivukangas 7641ca3ba1 [E2E] Fix remaining broken tests with new data 2026-05-24 21:53:26 +03:00
Aaro KoivukangasandAaro Koivukangas 9515adb1be [ApiTests] Update data used in api tests 2026-05-24 21:53:26 +03:00
Aaro KoivukangasandAaro Koivukangas 55668cc313 [E2E][CourseStats] Fix incorrect e2e test values 2026-05-24 21:53:26 +03:00
Aaro KoivukangasandAaro Koivukangas 28b7cdfe6b [CI] Increase sleep time on failure logs 2026-05-24 21:53:26 +03:00
Aaro KoivukangasandAaro Koivukangas 4b6bba653a [CourseStats] Safeguard all length checks 2026-05-24 21:53:26 +03:00
Aaro KoivukangasandAaro Koivukangas eea167df9b [CourseStats] Handle courses with incorrect substitution_groups better 2026-05-24 21:53:26 +03:00
Aaro KoivukangasandAaro Koivukangas ce946bcb0d [CI] Pull api-test docker images quietly, less logs 2026-05-24 21:53:26 +03:00
Aaro KoivukangasandAaro Koivukangas 6580f42eab [CI] Wait for docker to output all error logs to stdout 2026-05-24 21:53:26 +03:00
Aaro KoivukangasandAaro Koivukangas 043b0d2cbd [CourseStats] Add better handling for invalid values 2026-05-24 21:53:26 +03:00
Aaro KoivukangasandAaro Koivukangas 25fb4bad84 [CourseStats][E2E] Fix broken e2e tests 2026-05-24 21:53:26 +03:00
Aaro KoivukangasandAaro Koivukangas 53f3955427 [CourseStats][E2E] Fix e2e tests 2026-05-24 21:53:26 +03:00
Aaro KoivukangasandAaro Koivukangas 49390cf92f [CourseStats] Lint fixes 2026-05-24 21:53:26 +03:00
Aaro KoivukangasandAaro Koivukangas 128ee465ff [CourseStats] Fix provider organization selector 2026-05-24 21:53:26 +03:00
Aaro KoivukangasandAaro Koivukangas 6ed84bee52 [CourseStats] Fix population link course lists 2026-05-24 21:53:26 +03:00
Aaro KoivukangasandAaro Koivukangas fe665cc8ad [CourseStats] Fix enrollments 2026-05-24 21:53:26 +03:00
Aaro KoivukangasandAaro Koivukangas 413f2b4fda [CourseStats] Fix substitution group chips on combineSubstitutions=true 2026-05-24 21:53:26 +03:00
Aaro KoivukangasandAaro Koivukangas 8959204a37 [CourseStats] Fix attempts calculations 2026-05-24 21:53:26 +03:00
Aaro KoivukangasandAaro Koivukangas f069d705cb [CourseStats] Fix student statistics 2026-05-24 21:53:26 +03:00
Aaro KoivukangasandAaro Koivukangas adb8c9322b [CourseStats] Fix substitution calculations WIP 2026-05-24 21:53:26 +03:00
Aaro Koivukangas 3e83b63324 [Meta] Add safe-chain to all actions and Dockerfiles 2026-05-24 21:34:23 +03:00
joonas-a 4dcb6b4e37 [Percentiles] Add class size indicator to chart 2026-05-20 16:31:20 +03:00
joonas-a 7830a8abc3 [Backend][Math] fix interpolation logic 2026-05-20 16:23:40 +03:00
joonas-a 34e006e12a [Backend][Percentiles] guarantee obj keys in order
failsafe
2026-05-20 16:14:31 +03:00
joonas-a a33fe22bac [Percentiles] fix credit calculation for bsc+msc combos 2026-05-20 15:46:19 +03:00
joonas-a 4ac467fa7a [lint] moar fix lint error 2026-05-20 14:45:02 +03:00
joonas-a 6aad36d06a [lint] run eslint --fix 2026-05-20 14:32:35 +03:00
Santeri Rajamäki 8413369b89 [CoursePopulation] Fetch course details instead of relying on the fetched population 2026-05-20 13:51:45 +03:00
Santeri Rajamäki 4f2780bf00 [CoursePopulation] Clean up table utils 2026-05-20 13:51:45 +03:00
joonas-a 59120fa69c [Study tracks and class stats] Implement credit acc percentile view 2026-05-20 13:50:54 +03:00
joonas-a 5e502fa23a [Year selector] add slim option 2026-05-19 17:00:35 +03:00
joonas-a 28eec2db0f [Styled message] enhance 2026-05-19 17:00:09 +03:00
joonas-a 9df131b326 [Backend][Study tracks and class stats] include student credit gain percentiles in response 2026-05-19 15:58:57 +03:00
Santeri Rajamäki b9bd916b91 [META] Less lodash 2026-05-19 13:50:02 +03:00
Santeri Rajamäki c99034575f [CoursesOfClass] orderBy in the OT instance instead of setModules hook 2026-05-19 12:26:40 +03:00
Santeri Rajamäki dc12b0fd04 [CoursesOfClass] Add warning message when there are no courses/modules to show with the current student amount limit 2026-05-19 11:51:57 +03:00
Santeri Rajamäki 3ec2b5e5d1 [E2E] Use hover 2026-05-18 15:56:03 +03:00
Santeri Rajamäki 26461f5277 [E2E] Add hover command 2026-05-18 15:40:50 +03:00
Santeri Rajamäki 17b83a8574 [E2E][Students] Remove highcharts specific code 2026-05-18 14:06:40 +03:00
Santeri Rajamäki ec2e203e6b [E2E][DegreeProgramme] Fix mouseover for the rest of the failing tests 2026-05-18 14:00:33 +03:00
Santeri Rajamäki 6f6a732e7f [E2E][DegreeProgramme] Fix mouseover for split view 2026-05-18 13:37:51 +03:00
Santeri Rajamäki fbbf3830b3 [E2E][DegreeProgramme] Fix mouseover 2026-05-18 13:12:14 +03:00
Santeri Rajamäki 4cbccd29ba Merge remote-tracking branch 'origin/chart-refresh' 2026-05-16 01:30:37 +03:00
joonas-a 3d0fd3563f Delete highcharts 2026-05-15 17:49:11 +03:00
joonas-a a929613e83 [Course stats] enable data view on chart + cleanup 2026-05-15 17:48:36 +03:00
joonas-a d64aca5285 [Course stats] convert grade dist to echarts 2026-05-15 17:46:38 +03:00
joonas-a 9e1307595c [Course stats] convert pass rate chart to echarts 2026-05-15 17:37:15 +03:00
joonas-a 0e9ba8490d Delete old files 2026-05-15 17:17:47 +03:00
joonas-a 50b4166a7f [Charts] cleanup 2026-05-15 17:01:03 +03:00
joonas-a 3843937429 [Graduation times chart] more refactor 2026-05-15 15:55:42 +03:00
joonas-a b60b1dd88b [Graduation times chart] refactor 2026-05-15 14:56:49 +03:00
Santeri Rajamäki 4f4868a32b [E2E] Skip now implemented feature test 2026-05-15 13:04:17 +03:00
Santeri Rajamäki d45bddbeab [DegreeProgramme][StudyTracks] Add ability to view progress by study track 2026-05-15 12:35:29 +03:00
Santeri Rajamäki 2abda1de50 [Frontend] StudyTrack is study track 2026-05-13 14:47:48 +03:00
Santeri Rajamäki 333944d377 [Programme][StudyTracks] Add export option to tables, mirroring faculties view 2026-05-13 13:55:00 +03:00
Santeri Rajamäki 50c301fe82 [Degree programme] Replace study programme with degree programme 2026-05-13 11:34:11 +03:00
joonas-a ed513c14bf [Error background] put veterans to rest 2026-05-12 16:23:38 +03:00
joonas-a 8e2b8e5443 [University][Backend] fix graduation targets
from months to semesters like frontend expects
2026-05-12 15:39:58 +03:00
joonas-a bb215bb785 [Charts] bring back missing legends oops 2026-05-12 15:38:27 +03:00
Santeri Rajamäki 35ea22e125 [Frontend] Convert ColorizedCoursesTable to typescript 2026-05-12 15:13:38 +03:00
Santeri Rajamäki c3c7b5b479 [Frontend] Rework useSemesters 2026-05-12 14:59:50 +03:00
joonas-a 635fb97b25 [Charts] final tweaks 2026-05-12 14:54:07 +03:00
joonas-a a5334c8e2f [Progres bar chart] Change emphasis strategy
fixes incorrect highlighting bug when clicking categories from the
legend
2026-05-12 14:25:18 +03:00
joonas-a 18b8daabff [Charts] convert horizontal stacked bar chart 2026-05-12 14:24:26 +03:00
Santeri Rajamäki b6494f88d0 [Frontend] Cleanup ColorizedCourseTable CSS rules 2026-05-12 14:14:09 +03:00
Santeri Rajamäki 87bc8b24a9 [Frontend] Rework StudentsByStartingYear StudentDataTable and exportStudentTable 2026-05-12 14:14:09 +03:00
joonas-a b181fda7fc [Charts] Fix styling issues 2026-05-12 13:27:23 +03:00
joonas-a 747c37697c [Median graduations] add guard for undefined data and fix style 2026-05-12 12:05:12 +03:00
joonas-a 76d8fef4eb [Dist devlpmnt] Update infobox 2026-05-12 11:51:06 +03:00
Aaro Koivukangas e08d99c801 [Semesters] Remove type assertion and use reduce<type> 2026-05-12 11:47:11 +03:00
Aaro Koivukangas b5f7a8bd86 [ApiTests] Add initial test and rename incorrect test name 2026-05-12 11:45:32 +03:00
Aaro Koivukangas 7d7d71b6be [Admin][Updater] Re-enable all buttons in staging 2026-05-12 11:36:46 +03:00
Aaro Koivukangas c1bacd11cc [Updater] Add hack fix to mainCourseCode being array 2026-05-12 11:36:46 +03:00
joonas-a 743ccb6797 [Credit accumulation] infobox update 2026-05-11 18:00:13 +03:00
joonas-a dab81f994d [Charts][Crdt accmltn] bring back click to open individual student 2026-05-11 16:51:42 +03:00
joonas-a b17fa2a11d [Charts][Graduation times] Convert breakdown chart 2026-05-11 15:25:09 +03:00
joonas-a ea8a84420f [Charts][Graduation times] Invert order to match previous behaviour 2026-05-11 15:24:43 +03:00
joonas-a 56d98e16cd [Charts][Graduation times] convert median chart 2026-05-11 13:56:52 +03:00
Santeri Rajamäki 110994ea46 [Filters] Remove view restriction from actions 2026-05-06 12:27:01 +03:00
Santeri Rajamäki 11bde27811 [Filters] Apply students length filter to Credit Statistics filter buttons 2026-05-06 12:24:51 +03:00
Santeri Rajamäki 0f4ef4a3cd [ClassStatistics][Students] Add emails to courses table 2026-05-06 12:02:47 +03:00
Santeri Rajamäki a3bb84e169 [Frontend] Demangle FilterView 2026-05-06 11:50:20 +03:00
joonas-a b9b90876f1 [Charts][Year expansion chart] first attempt at echarts conversion 2026-04-29 16:08:35 +03:00
joonas-a 777a19b291 [Charts][LineGraph] Add emphasis strategy
Will make it so when hovering only one item blurs all the others
2026-04-29 11:13:15 +03:00
joonas-a 362930c481 [Student tab] change credit acc graph dot icon 2026-04-28 11:42:52 +03:00
joonas-a 8ca1636517 [Charts][Progress of students] convert to echarts 2026-04-27 17:22:29 +03:00
joonas-a d6beafa13a [Charts][Graduated/thesis writers] convert graph to echart 2026-04-27 14:03:49 +03:00
joonas-a f92d72b77a [Charts][StackedBarChart] echart conversion 2026-04-27 13:42:46 +03:00
joonas-a dca144d58b [Charts] Replace students of x with echarts 2026-04-27 13:08:21 +03:00
Santeri Rajamäki 915d5c097c [Backend] Remove unused eslint disable 2026-04-24 09:58:35 +03:00
joonas-a 9a9f723b46 [Charts][Distribution development] Migrate to echarts 2026-04-21 12:41:39 +03:00
Santeri Rajamäki 523f6aee3e [Backend] Add rudimentary types to studyProgramme and programmeModules routes 2026-04-21 12:28:52 +03:00
Santeri Rajamäki e874454e40 [Backend] Remove reflect-metadata from haunting us 2026-04-21 12:04:24 +03:00
Santeri Rajamäki 45289702d5 [Backend] Update express types 2026-04-21 11:23:17 +03:00
Aaro Koivukangas 1ab6366fd1 [CI] Can't docker:down without .env, reverting adding docker:down 2026-04-21 10:16:52 +03:00
Aaro Koivukangas a01b06b8bc [CI] Remove updater from api-test compose, missing .env 2026-04-21 10:00:27 +03:00
Aaro Koivukangas 11dfa4c6f1 [Updater] Fix logger typo 2026-04-20 15:43:49 +03:00
Aaro Koivukangas b94e4fc25d [Updater] Remove cron jobs in staging environment 2026-04-20 15:37:46 +03:00
Aaro Koivukangas c1bb44bd55 [CI] Restart docker when starting api-ci environment 2026-04-20 15:37:19 +03:00
Aaro Koivukangas 1d8f92c663 [ApiTests] Add api tests for /populationstatisticsbycourse 2026-04-20 15:34:04 +03:00
joonas-a e6686c024a [Charts][Student details] final touches 2026-04-20 14:12:21 +03:00
joonas-a 2aa6e9c549 [Student grade graph] Move legend to right for consistency 2026-04-20 12:39:44 +03:00
joonas-a 898a2b1f1b [Charts][Student details] Renew grade graphs 2026-04-20 12:25:39 +03:00
joonas-a 6a9a49952b [Frontend] Further enhance StyledMessage 2026-04-20 12:02:49 +03:00
joonas-a 0f76e50e4b [Frontend] Enhance StyledMessage 2026-04-20 11:59:16 +03:00
Aaro Koivukangas b5ee1f2190 [Eslint] Correct ignore rules for backend/tests 2026-04-17 13:28:37 +03:00
Aaro Koivukangas c5649e9d39 [ApiTests] Restructure tests 2026-04-17 13:25:14 +03:00
Aaro Koivukangas 2a979af974 [Lint] Lint fixes 2026-04-17 12:40:12 +03:00
Aaro Koivukangas 06964526dc [Husky] Enable front+back linting again :D 2026-04-17 12:37:12 +03:00
Aaro Koivukangas b2b471dab5 [ApiTests] Add api tests for /populationstatistics/studyprogrammes 2026-04-17 12:32:52 +03:00
joonas-a d9695901b2 [Charts][Student details] remake credit graph 2026-04-16 16:08:15 +03:00
joonas-a b85edaf6f0 [Charts][Custom pop] Use new credit acc
Also add graduations if programme set.
2026-04-16 14:30:53 +03:00
joonas-a b165ece770 [Lint] fix 2026-04-16 14:19:51 +03:00
joonas-a a90e82a31e [Charts][SGG] use new credit acc 2026-04-16 14:18:58 +03:00
joonas-a 651e37051d [Charts][Credit acc] fix combined programme graduation markers 2026-04-16 13:55:21 +03:00
joonas-a 77b6d1cb16 [Charts] cleanup and edge case handling 2026-04-16 12:48:30 +03:00
Aaro Koivukangas 15b9551f7a [Husky] Lint all files in pre-commit hook and don't lint them twice 2026-04-15 17:17:13 +03:00
Aaro Koivukangas c6f4e6066b [Meta] Remove rapodiff + unused npm commands 2026-04-15 17:10:33 +03:00
Aaro Koivukangas 7ebb60e564 [Staging] Disable updater update buttons in staging, breaks things 2026-04-15 16:32:48 +03:00
Aaro Koivukangas b641cf9ca6 [Scripts] Add script to update staging databases with test data 2026-04-15 16:11:51 +03:00
Santeri Rajamäki 686697f202 [Frontend] Convert LanguageCenterView to tsx 2026-04-14 16:32:00 +03:00
Santeri Rajamäki 296299e635 [Frontend] Simplify ColorizedCoursesTable top-level usage 2026-04-14 16:30:24 +03:00
joonas-a afa6804d09 [Charts] Credit acc done for non bscmsc classes 2026-04-14 16:19:46 +03:00
Santeri Rajamäki c8b8b033ed [Frontend] Drop ProgressBar support 2026-04-14 15:43:45 +03:00
Santeri Rajamäki f64f34b971 [Frontend] Convert DateSelector and DateRangeSelector to tsx 2026-04-14 15:31:22 +03:00
Santeri Rajamäki 9b4f689dd3 [Frontend] Convert LogOutButton to tsx 2026-04-14 15:14:48 +03:00
Santeri Rajamäki cd32860300 [Frontend] Convert FilterDegreeCoursesModal to tsx 2026-04-14 15:10:28 +03:00
Santeri Rajamäki 7ed58eb38a [Frontend] Default CreditCriteriaSection to previous value instead of showing "help text" 2026-04-14 15:06:04 +03:00
Santeri Rajamäki 291082e6c3 [Frontend] Convert ExternalGradeFilterToggle to tsx 2026-04-14 14:44:31 +03:00
Santeri Rajamäki 54f363be6e [META] Drop version numbers from api routes 2026-04-14 14:41:52 +03:00
Santeri Rajamäki 539ed1e3b8 [Backend] Actually return after sending response for close to graduation 2026-04-14 13:24:48 +03:00
Santeri Rajamäki 9d61b50db5 [Frontend] Better type integration for redux users path 2026-04-14 13:22:52 +03:00
Santeri Rajamäki 8ede9de315 [Shared] Move user typings and route types to shared from backend 2026-04-14 13:07:11 +03:00
Santeri Rajamäki 1cb43cee6b [Teachers] Add more handling 2026-04-14 12:38:06 +03:00
Santeri Rajamäki b2617c8933 [Backend] Add handleQueryArrays to other backend routes that crash without it 2026-04-14 12:19:03 +03:00
Santeri Rajamäki beb1130050 [Backend] Add handleQueryArrays to courseYearlyStats 2026-04-14 11:52:38 +03:00
Santeri Rajamäki 1c75204854 [Filters] Map "Koepisteet" to "Valintakoe" as intended 2026-04-14 10:46:14 +03:00
Santeri Rajamäki 80bba6e0fc [Backend] Upgrade from express v4 to v5 2026-04-13 17:27:45 +03:00
Santeri Rajamäki 47b0dabe75 [OT] Remove pre-emptive null returns
OT pagination now supports changing data models which makes these early returns redundant
2026-04-13 15:52:50 +03:00
Santeri Rajamäki da45c8c150 [Backend] Cull user route types 2026-04-13 15:34:57 +03:00
joonas-a a57f2f01ca [Charts][Credit accmltn] hops filter functionality 2026-04-13 14:40:44 +03:00
Aaro Koivukangas 8fd7383217 [Staging] Remove unnecessary staging iamgroups 2026-04-13 12:00:04 +03:00
Santeri Rajamäki baa6d87c2c [Filters] Fix student number filter allowlist 2026-04-13 11:48:13 +03:00
Santeri Rajamäki 5ae4c8c7a0 [OT] Change rowsPerPage provider 2026-04-10 14:15:51 +03:00
Santeri Rajamäki cbb7e82d23 [OT] Apply ellipsis to cells table-wide 2026-04-10 14:09:07 +03:00
Aaro KoivukangasandSanteri Rajamäki 2768e95920 [Class stats][Courses of class] Show Master-level modules by fixing incorrect nullish coalesing 2026-04-10 13:39:10 +03:00
Aaro KoivukangasandSanteri Rajamäki 5b7d641ad1 [E2E] More page title e2e test fixing 2026-04-10 13:39:10 +03:00
Aaro KoivukangasandSanteri Rajamäki 37a71d7580 [E2E] Fix changed titles in tests 2026-04-10 13:39:10 +03:00
Aaro KoivukangasandSanteri Rajamäki aaa7b5de27 [E2E] Fix tests 2026-04-10 13:39:10 +03:00
Aaro KoivukangasandSanteri Rajamäki 40bcbc59dc [Class stats][Courses of class] Change empty fields to be 0, not null 2026-04-10 13:39:10 +03:00
Aaro KoivukangasandSanteri Rajamäki 5e46ce0cb0 [Class stats] Improve page title 2026-04-10 13:39:10 +03:00
Santeri Rajamäki 517c08091e [META] Update @types/pg 2026-04-10 13:39:09 +03:00
Santeri Rajamäki e25dfb2a36 [META] Update lodash-es 2026-04-10 13:38:24 +03:00
Santeri Rajamäki aa4e0abb44 [META] Update cypress 2026-04-10 13:37:13 +03:00
Santeri Rajamäki 3e5dc9a9f0 [META] Update globals 2026-04-10 13:37:13 +03:00
Santeri Rajamäki afd9541151 [META] Update eslint-plugin-import 2026-04-10 13:37:13 +03:00
Santeri Rajamäki a5ab3b972c [META] Update stylelint-config-standard 2026-04-10 13:37:13 +03:00
Santeri Rajamäki 4f3350e4f9 [META] Update stylelint 2026-04-10 13:37:13 +03:00
Santeri Rajamäki 2b8e1ed98f [Backend] Convert oriProvider to use module exports instead of CJS module.exports 2026-04-10 13:37:13 +03:00
joonas-a 1b4bdff37e [Charts][poc] credit accumulation 2026-04-02 16:14:04 +03:00
Santeri Rajamäki 3f311dee69 [Frontend] Remove legacy RTKApi middleware 2026-04-01 13:57:39 +03:00
Santeri Rajamäki eb6b683296 [Frontend] Convert apiConnections to typescript 2026-04-01 13:56:02 +03:00
Aaro Koivukangas b2153ec3dd [ClassStats][Courses] Add better handling for null substitution_groups (only in test-data) 2026-04-01 13:34:40 +03:00
Aaro Koivukangas 8949e7baf0 [Class stats][Courses] Better substitution group type handling 2026-03-31 15:18:47 +03:00
joonas-a 1fa10f7812 [Scripts] fix incorrect container name 2026-03-31 14:47:33 +03:00
Aaro Koivukangas fbe5db8a43 [E2E] Fix lint errors and e2e tests 2026-03-31 14:42:57 +03:00
Aaro Koivukangas d4c7a4014b [Class stats][Courses] Handle substitution groups correctly + fix
include substitutions button
2026-03-31 14:42:57 +03:00
Santeri Rajamäki 1177c2f848 [OT] Use 0th index in pagination when table data changes 2026-03-31 14:40:53 +03:00
Santeri Rajamäki 0f8d72fd1d [Frontend] Remove manual pagination from OT implementations 2026-03-31 14:36:45 +03:00
Santeri Rajamäki 47e3e2e26e [OT] Fix pagination 2026-03-31 14:36:05 +03:00
Santeri Rajamäki a312a14944 [Frontend] Disallow prop forwarding for all internal StyledTable attributes 2026-03-31 12:01:33 +03:00
joonas-a e3ec33f8dc [Course stat tables] try to add 0s 2026-03-30 16:10:22 +03:00
joonas-a 9c55d3b2ab [Highcharts] fix median time bar chart threshold lines 2026-03-30 16:07:51 +03:00
joonas-a 17ce83660c [e2e] programme overview graduation time fixes 2026-03-30 15:40:59 +03:00
joonas-a 22d3877b8e [Faculty][Graduation times] update graduation time targets in highcharts 2026-03-30 14:58:20 +03:00
joonas-a c60bee34fd [Backend][Faculty] update graduation time targets 2026-03-30 14:51:32 +03:00
joonas-a 15240050b7 [Faculty] graduation time infobox update 2026-03-30 14:31:18 +03:00
joonas-a 7da09c8b02 [Programme overview] update graduation time infoboxes 2026-03-30 14:31:18 +03:00
joonas-a cfb2d4d5c0 [Frontend][Graduation times] replace months with semesters 2026-03-30 14:31:18 +03:00
joonas-a 39b594d795 [Backend] Use semesters with graduation times 2026-03-30 14:31:18 +03:00
Santeri Rajamäki ff5bce1713 [E2E] Scroll coursestatistics data table into view 2026-03-30 10:26:56 +03:00
Santeri Rajamäki 8e288f3b26 [Frontend] Remove columnOrder from course statistics tables 2026-03-30 10:07:06 +03:00
Santeri Rajamäki 4134c8b64b [Frontend] Use manualPagination on course stats tables 2026-03-27 15:47:17 +02:00
Santeri Rajamäki ddc1a22492 [Frontend] Cleanup 2026-03-27 15:47:17 +02:00
Santeri Rajamäki 8ff1cb54dc [Frontend] Convert AttemptsTable to OT 2026-03-27 15:47:17 +02:00
Santeri Rajamäki 777de520f9 [Frontend] Convert StudentsTable to OT 2026-03-27 15:47:17 +02:00
joonas-a a9e325fe72 [Updater] Add transfer info to study rights 2026-03-26 17:41:17 +02:00
joonas-a befc3db3a8 [Filters][Studentnumber] Fix blacklist blacklisting all students always 2026-03-26 17:41:17 +02:00
Santeri Rajamäki e1b8ebd17f [Frontend] Readd EquivalenceGroupChip 2026-03-26 13:13:05 +02:00
Santeri Rajamäki 883129bee0 [Frontend] Unify course page secondary chip with course search table chips 2026-03-26 12:13:51 +02:00
Santeri Rajamäki 18eb04beeb [Frontend] Rework course search table and equivalence group chips 2026-03-26 12:12:19 +02:00
Santeri Rajamäki eb12d7e5ec [OT] Add cell width under size attribute 2026-03-26 12:08:56 +02:00
Santeri Rajamäki 26ca0a3bc1 [META] Remove redundant reset_jami_data script from setup script 2026-03-25 17:34:47 +02:00
Santeri Rajamäki b79c97780b [META] Update JAMI url in docker-compose 2026-03-25 17:29:04 +02:00
Santeri Rajamäki 0cd3bf28ff [Frontend] Replace MRT with StyledTable in StudyProgrammeTable 2026-03-25 16:20:55 +02:00
Santeri Rajamäki 20b0ba5a30 [Frontend] Enhance StyledTable 2026-03-25 16:19:12 +02:00
Aaro Koivukangas 67001a9db1 [Updater] Remove limit of 1000 entries from update-meta in dev environment 2026-03-23 14:15:51 +02:00
Aaro Koivukangas 8c268395b0 [E2E] Fix broken test 2026-03-20 16:32:41 +02:00
Aaro Koivukangas 235d2b03b2 [CourseStat][Search] Use substitution_groups when searching for courses 2026-03-20 15:53:11 +02:00
Aaro Koivukangas cbe20f1ad3 [Updater] Handle substitution_groups with null/undefined codes better 2026-03-20 15:53:11 +02:00
Matti Luukkainen 47211dcf89 [faculty/study programme] moar compact credits produced table (fits in screen) 2026-03-20 12:48:41 +02:00
joonas-a 26c6c653e5 [Custom pop] Enable hetu filter 2026-03-19 15:52:56 +02:00
Aaro Koivukangas 13bdf698cf [CI] Skip very flaky test 2026-03-19 15:29:25 +02:00
Aaro Koivukangas fd6a65925a [CI][DegreeProgrammes] Fix hetu-e2e tests once again 2026-03-19 15:03:14 +02:00
Aaro Koivukangas 435d2920fb [CI][DegreeProgramme] More e2e test fixes 2026-03-19 14:47:27 +02:00
Aaro Koivukangas 4ae93e0b7e [Updater] Add latest executed migration to starting logs 2026-03-19 14:45:18 +02:00
Aaro Koivukangas dc5200da2f [CI] Fix broken hetu-tests 2026-03-19 14:27:20 +02:00
joonas-a 373c8f9de8 [CI] remove debug print 2026-03-19 13:34:00 +02:00
joonas-a 34cc022830 [Custom pop][General tab] Fetch hops credits from acual hops
Also integrated CI test
2026-03-19 13:32:03 +02:00
Aaro Koivukangas ab311ddc66 [CI] Don't trigger all workflows if lint fails 2026-03-19 13:27:50 +02:00
Aaro Koivukangas 833fdad3bb [CI] Fix lint errors 2026-03-19 13:21:52 +02:00
joonas-a 4b634e032d [Eslint] Disable root imports
These cause silent backend crashes which are hard to debug
2026-03-19 13:18:41 +02:00
Aaro Koivukangas a16fd13413 [CI] Fix broken import in mailServices 2026-03-19 13:11:46 +02:00
Aaro Koivukangas 58913b4684 [Docker] Make .dockerignore work by moving it to context root 2026-03-19 12:58:55 +02:00
joonas-a 05c8993ba3 [Backend] Fix path import 2026-03-19 12:52:49 +02:00
joonas-a d3090ae725 [Ooditable] Highlight hovered row 2026-03-19 12:18:49 +02:00
joonas-a c870061fec [Excel exports] Show acual values in preview
No more empty strings
2026-03-19 12:00:49 +02:00
Aaro Koivukangas ec43699c3c [CI] Add backend and frontend logs to e2e tests 2026-03-19 11:57:56 +02:00
joonas-a 1b1e380a13 [Frontend] make lowercasing consistent 2026-03-17 17:21:14 +02:00
joonas-a 5c13b5c0ab [Programme overview][Faculties] hetu/ton credits produced 2026-03-17 17:18:02 +02:00
joonas-a f35fd41bfb [Backend] Include hetu/ton split in provider credits computation 2026-03-17 17:15:31 +02:00
joonas-a 6548cdd42b [Course population] Enable HETU filter 2026-03-17 15:16:30 +02:00
joonas-a 49b0da760d [Filters] New filter: Personal identity code 2026-03-17 15:16:30 +02:00
joonas-a 34e9cf5667 [Backend] Add "has hetu" to students 2026-03-17 15:16:30 +02:00
Aaro Koivukangas 288d32c591 [Backend][MailService] Backend doesn't crash if it tries to send emails in non-production environments 2026-03-17 13:16:04 +02:00
joonas-a 5001d55843 [Programme courses] Display hetuttomat/hetulliset 2026-03-17 12:25:41 +02:00
joonas-a eaedfe99db [Backend][Programme courses] Split open uni students (hetuton/hetu on) 2026-03-17 12:25:41 +02:00
Aaro Koivukangas 6cf375936a [Meta] Lint fixes 2026-03-17 12:02:12 +02:00
Aaro Koivukangas 8bc5c3ea65 [Updater][Substitutions] Add og. substitutions back and move the new substitutions to substitution_groups 2026-03-17 11:49:49 +02:00
Santeri RajamäkiandAaro Koivukangas 3e4e749625 [CoursesSearch] Rebuild substitution combination 2026-03-17 11:49:49 +02:00
Santeri RajamäkiandAaro Koivukangas 114af7522d [Backend] Try apply fix for courseFinder 2026-03-17 11:49:49 +02:00
Santeri RajamäkiandAaro Koivukangas c0b29d69c7 [Updater] Fix substitution .map() for modules 2026-03-17 11:49:49 +02:00
Santeri RajamäkiandAaro Koivukangas fb541ffa3a [Updater] Build course unit substitutions as substitution groups/arrays 2026-03-17 11:49:20 +02:00
Santeri RajamäkiandAaro Koivukangas 0ed4ce7bb4 [Updater] Clarify updateCourses function 2026-03-17 11:49:20 +02:00
Santeri RajamäkiandAaro Koivukangas 87e1e332a8 [Frontend] Convert sentry util to typescript 2026-03-17 11:49:20 +02:00
joonas-a 914440cef9 [Programme courses] Enable stripes in table 2026-03-17 10:52:06 +02:00
Aaro Koivukangas 901a4de21c [Updater] Allow updater workflow to trigger 2026-03-12 13:35:45 +02:00
Aaro Koivukangas 2bc28c7dd3 [Student] Convert BachelorHonors to ts 2026-03-12 13:00:57 +02:00
Aaro Koivukangas 2dd43b1ccd [StudyGuidanceGroups] Typo fix 2026-03-12 10:51:59 +02:00
Aaro Koivukangas 3fc1bc161b [Models] Add substitution_groups to course table in sis-db 2026-03-11 18:54:38 +02:00
Aaro Koivukangas 1edffd437e [ClassStats][CoursesOfPopulation] Remove module stats from Show all view 2026-03-11 17:14:49 +02:00
Aaro Koivukangas 4ed5e610bf [StudyGuidanceGroups] Change close-to-graduation link once more 2026-03-11 13:09:54 +02:00
Aaro Koivukangas 90ab1e80f7 [ClassStats][CoursesOfPopulation] Remove unecessary fields from module stats 2026-03-11 12:31:54 +02:00
Santeri Rajamäki 5b9f3c0184 [Meta] Update docker CI actions 2026-03-10 13:35:17 +02:00
Santeri Rajamäki 3f81fff6aa [Meta] Update swc plugin for vite 2026-03-10 13:32:17 +02:00
Santeri Rajamäki 5058a54099 [Meta] Update Vite for frontend 2026-03-10 13:27:10 +02:00
Santeri Rajamäki c5fb781f50 [Meta] Remove Vite from backend 2026-03-10 13:26:03 +02:00
Santeri Rajamäki 6c45c5cd76 [Meta] Upgrade Redux toolkit
This upgrade should bring noticeable improvments to frontend mankelointi times
2026-03-10 13:11:43 +02:00
Santeri Rajamäki b7e3df2e6b [Meta] Update sentry packages 2026-03-10 12:58:32 +02:00
Santeri Rajamäki 35ce0388eb [Meta] Update sequelize
Patch for CVE in JSON where parsing
2026-03-10 12:31:04 +02:00
Santeri Rajamäki 5252f3b816 [Meta] Update node-postgres and add type package for redundancy 2026-03-10 12:31:04 +02:00
Aaro Koivukangas 48f108c41d [StudyGuidanceGroup] Move filtering to the correct component 2026-03-10 12:16:07 +02:00
Santeri Rajamäki bade936f3c [Meta] Upgrade actions checkout, upload-artifact and download-artifact 2026-03-10 12:00:50 +02:00
Aaro Koivukangas c17721bd54 [StudyGuidanceGroup] Remove duplicate entries from Associated programme selector 2026-03-10 11:53:45 +02:00
Santeri Rajamäki a2e9064376 [Frontend] Move primitive functions to primitive file 2026-03-10 11:33:04 +02:00
Aaro Koivukangas 4b92ce4bd9 [StudyGuidanceGroup] Add better description to a link to close to graduation 2026-03-10 11:05:01 +02:00
Aaro Koivukangas f2348db941 [StudyGuidanceGroup] Fix empty table and add courses <-> modules switch to the sgg population view 2026-03-06 15:14:42 +02:00
Aaro Koivukangas b828904c3d [StudyGuidanceGroup] Migrate SGG population courses to ts 2026-03-06 14:49:14 +02:00
Aaro Koivukangas ed82a26e45 [Meta] Update remaining useGetProgrammesQuery users 2026-03-06 11:24:17 +02:00
Aaro Koivukangas b87ee36ebf [Students] Fix style 2026-03-05 17:10:15 +02:00
Aaro Koivukangas 7695315843 [Meta] keyBy is now used, letsgoo! 2026-03-05 16:31:09 +02:00
Aaro Koivukangas 0a292057cb [StudyGuidanceGroups] Add all degree programmes as options when selecting associated programmes for SGG 2026-03-05 16:24:12 +02:00
Aaro Koivukangas 0f5c672f5d [StudyGuidanceGroups] Change getCombinedProgrammeName to use switch case 2026-03-04 17:42:17 +02:00
joonas-a 8ec509aeed [General tab] before starting -> prior to x 2026-03-03 20:57:14 +02:00
joonas-a 7f4eab62a7 [General tab] Rephrase start date col 2026-03-03 20:10:35 +02:00
Aaro Koivukangas 6d82475696 [StudyGuidance] Correctly set initial year when adding starting year to group 2026-03-03 11:36:31 +02:00
Santeri RajamäkiandAaro Koivukangas bdcdd43230 [META] Remove toska sub- and prefixes from function names 2026-02-27 16:36:24 +02:00
Santeri RajamäkiandAaro Koivukangas 80e91d7bfb [Chore] Remove unused modules re-emerged after rebase 2026-02-27 16:36:24 +02:00
Santeri RajamäkiandAaro Koivukangas b15a56b177 [Frontend] Remove unused modules 2026-02-27 16:36:24 +02:00
Santeri RajamäkiandAaro Koivukangas c2684a4e8e [META] Remove apparent service provider specific code 2026-02-27 16:36:24 +02:00
Aaro Koivukangas 4ed8b3d2e9 [LanguageCenter] Add a course name/code filter 2026-02-27 14:34:59 +02:00
Santeri Rajamäki d6c55ed4f9 [Filters] Fix courses filter .courses being read-only 2026-02-27 13:58:13 +02:00
Aaro Koivukangas 97804b645e [ClassStats][CoursesOfClass] Fix filter buttons not working 2026-02-26 14:31:45 +02:00
Aaro Koivukangas 6b1c3e358a [CloseToGraduation] Add placeholders for text filters 2026-02-25 14:47:54 +02:00
Aaro Koivukangas 1935e36a3a [Meta] Fix graduated count in ProgressBars 2026-02-25 13:49:26 +02:00
Aaro Koivukangas 15d0acae13 [ClassStats][ProgressTab] Remove accidental text from link 2026-02-25 13:02:53 +02:00
Aaro Koivukangas ca31e0ed31 [ClassStats][ProgressTab] Add link to create progress criteria if it doesn't exist 2026-02-25 12:57:53 +02:00
joonas-a ab4c7c9ec3 [Cleanup] Replace HOPS -> studyplan and fix typo 2026-02-24 18:29:18 +02:00
joonas-a 6cd3e1bf4f [General tab] Enable study time months col for bsc programmes 2026-02-24 18:28:18 +02:00
joonas-a f836b3dacd [General tab] Better column grouping and ordering 2026-02-24 17:54:00 +02:00
joonas-a 066e1267f1 [Custom pop General tab] Add new cols if programme explicitly set 2026-02-24 17:11:17 +02:00
joonas-a eb29a4d261 [SGG General tab] Show new cols if programme set 2026-02-24 17:05:04 +02:00
joonas-a 85fc25eec7 [General tab] New cols: credits and courses attained before starting in programme
* that are in study plan for current programme
2026-02-24 16:57:39 +02:00
Santeri Rajamäki 27d8d73e3d [Frontend] Move color lists to theme palette 2026-02-24 11:08:31 +02:00
Aaro Koivukangas 4df339fa25 [PopulationStatistics][Courses of class] Rename 'Show all courses or modules' 2026-02-24 10:48:31 +02:00
Aaro Koivukangas 0b2349d1c1 [ClassStats][Progress] Fix missing course names/codes 2026-02-20 13:35:48 +02:00
Aaro Koivukangas 55f18a4cef [ClassStats][Progress] Table filtering works on all columns 2026-02-20 13:35:48 +02:00
Aaro Koivukangas 022efc22bd [ClassStats][Progress] Implement types to ProgressTable 2026-02-20 13:35:48 +02:00
Aaro Koivukangas a02e4adfa7 [ClassStats][Progress] Convert Progress tab to typescript 2026-02-20 13:35:48 +02:00
joonas-a 92db3af85f [General tab] remove excess Date casting 2026-02-19 17:07:21 +02:00
joonas-a e112bd3482 [General tab] New col: Study time in months for masters programmes 2026-02-19 16:44:16 +02:00
joonas-a 205a3079ad [Courses of class] quick and dirty too narrow column fix 2026-02-18 15:10:21 +02:00
Aaro Koivukangas 29a0447b3f [ClassStats][CoursesOfX] Change default Link visibility 2026-02-18 15:00:32 +02:00
Aaro Koivukangas ee65aac42a [ClassStats][CoursesOfPopulation] Add stats to modules 2026-02-18 14:52:02 +02:00
joonas-a 56c59cb86b [Frontend / Ooditable] Fix too large boxshadow in multiline headers 2026-02-18 13:42:59 +02:00
Aaro Koivukangas e404fe09e3 [E2E][ProgrammeCourses] Change more header names in tests 2026-02-18 10:27:23 +02:00
Aaro Koivukangas 613164b3da [E2E][ProgrammeCourses] Fix header name in e2e test 2026-02-18 10:01:53 +02:00
joonas-a 7446760798 [Frontend][Programme courses] Add final info texts 2026-02-17 20:01:56 +02:00
joonas-a a86a99ef96 [Programme courses] Update infobox 2026-02-17 19:46:57 +02:00
joonas-a df7f2b75da [Programme courses] Add docstring and sanity check 2026-02-17 18:32:54 +02:00
joonas-a 3f40896f3b [E2E] adjust numbers 2026-02-17 16:29:59 +02:00
joonas-a 975d37142a [DX][Nodemon] Fix backend hotreload on helix/nvim 2026-02-17 14:55:49 +02:00
Matti Luukkainen 6026f04fcf try optimize workflow 2026-02-16 00:50:41 +02:00
Matti Luukkainen 12182193e1 feat: if course population covers only one semester, indicate it in the name 2026-02-16 00:20:03 +02:00
Aaro Koivukangas ca8ad3ea04 [PopulationStatistics][Courses of class] Show all courses/modules changes text based on Course <-> Module toggle 2026-02-14 12:57:42 +02:00
Aaro Koivukangas b21693ba1d [PopulationStatistics][Courses of class] Fix table dissapearing if Course <-> Module tab returns 0 rows 2026-02-14 12:57:42 +02:00
Aaro Koivukangas b99d3ff649 [PopulationStatistics][Courses of class] Add a toggle to show only courses or modules in "Show all courses" view 2026-02-14 12:57:42 +02:00
joonas-a 2ca49bbe32 [Faculty][Degree programme overview] Remove special category switch
Instead show everything by default, also add "other" category.

Commit powered by AI btw
2026-02-13 18:31:47 +02:00
joonas-a 9015be8458 [Programme courses] Remove merging of AY courses 2026-02-13 17:26:20 +02:00
joonas-a 8dc95c7a89 [Programme courses] Rework credit categories 2026-02-13 16:56:39 +02:00
Santeri Rajamäki 4537bd792b [Frontend] Hide progress tab (once again) from multi year populations 2026-02-13 15:09:27 +02:00
Santeri Rajamäki fcea050b9d [FilterView] Cleanup 2026-02-06 16:35:42 +02:00
Santeri Rajamäki 75049cc641 [Frontend] Fix progress criteria calculation source 2026-02-06 15:59:40 +02:00
Santeri Rajamäki 58a8c38c53 [Frontend] FilterDegreeCoursesModal "Save & Close" is actually "Close" 2026-02-06 13:48:19 +02:00
Aaro Koivukangas 29723b15aa [Population][ClassStats] Handle class stat query-arrays better and add more tests 2026-02-06 12:39:22 +02:00
Aaro Koivukangas 77c3271c25 [ApiTests] Add tests for ClassStats with years and programme 2026-02-04 17:48:36 +02:00
Aaro Koivukangas eaee2bac5a [ApiTests] Add tests PopulationStatistics and a skipped test semester bug #4959 2026-02-04 17:05:02 +02:00
Santeri Rajamäki 5a2e8ed4b1 [Frontend] Cull icons from @/theme 2026-02-03 14:07:09 +02:00
Santeri Rajamäki 128303cc70 [Backend] Import logger to worker correctly 2026-02-03 13:32:30 +02:00
Santeri Rajamäki 92dea1c19d [Frontend] Rename SellIcon to TagIcon to reflect use case 2026-02-03 13:01:23 +02:00
Santeri Rajamäki 94669ef98a [Frontend] Cull icons from @/theme 2026-02-03 13:00:24 +02:00
Santeri Rajamäki 7036d1be28 [Frontend] Consolidate icons under @/theme folder 2026-02-03 12:30:55 +02:00
Aaro Koivukangas 0aff1afcd6 [Api-tests] Add basic permission tests for populations 2026-02-02 17:14:38 +02:00
Aaro KoivukangasandAaro Koivukangas 6db47e0568 [CI] Update toska registry login envs in pull_request action 2026-02-02 14:30:46 +02:00
Aaro KoivukangasandAaro Koivukangas 2b855654a6 [CI] Login to toksa docker registry in pull_request action 2026-02-02 14:30:46 +02:00
Aaro KoivukangasandAaro Koivukangas 5acf0d3e2c [CI] Remove incorrect path from pull_request action 2026-02-02 14:30:46 +02:00
Aaro KoivukangasandAaro Koivukangas 76f80c7828 [CI] Rename npm script to the correct one in pull_request action 2026-02-02 14:30:46 +02:00
Aaro KoivukangasandAaro Koivukangas c0f4acf681 [Meta] Update docker compose container_names to use '-' as separator 2026-02-02 14:30:46 +02:00
Santeri Rajamäki 31348527a0 [META] Use correct entry for dev tsx --watch 2026-02-02 12:35:29 +02:00
joonas-aandSanteri Rajamäki 773c768805 [E2E][Deprecation] Remove coverage plugin requires 2026-02-02 11:40:52 +02:00
joonas-aandSanteri Rajamäki f335d38d27 [Meta][Deprecation] Remove instrumentation stuff 2026-02-02 11:40:52 +02:00
Aaro KoivukangasandSanteri Rajamäki 892f64f492 [CI] Action typo fix 2026-02-02 11:40:52 +02:00
Aaro KoivukangasandSanteri Rajamäki a12a86ad0e [CI] Run api+e2e tests even when previous jobs are skipped 2026-02-02 11:40:52 +02:00
Aaro KoivukangasandSanteri Rajamäki 3ed549cc75 [CI] Run frontend and backend while e2e testing 2026-02-02 11:40:52 +02:00
Aaro KoivukangasandSanteri Rajamäki 809a42d947 [CI] Login to toska docker registry while setting up api tests 2026-02-02 11:40:52 +02:00
Aaro KoivukangasandSanteri Rajamäki ceabbd0628 [CI] Fixing the same error 3rd time 2026-02-02 11:40:52 +02:00
Aaro KoivukangasandSanteri Rajamäki d4485ee3dd [CI] Fix the same error again 2026-02-02 11:40:52 +02:00
Aaro KoivukangasandSanteri Rajamäki 214df46c4d [CI] Typo fix 2026-02-02 11:40:52 +02:00
Aaro KoivukangasandSanteri Rajamäki 39709d41c7 [CI] Api test and e2e tests now run even when previous steps are skipped 2026-02-02 11:40:52 +02:00
joonas-aandSanteri Rajamäki 54464f12da [Lint] fix import in wrong order 2026-02-02 11:40:52 +02:00
joonas-aandSanteri Rajamäki 3a752a1f83 [TS/lint] betterment and fix 2026-02-02 11:40:52 +02:00
Aaro KoivukangasandSanteri Rajamäki 12003a9c0b [CI] Multiple ci fixes 2026-02-02 11:40:52 +02:00
Aaro KoivukangasandSanteri Rajamäki d5f3f86b2b [CI] Change api tests to use docker-compose.ci.yml 2026-02-02 11:40:52 +02:00
Aaro KoivukangasandSanteri Rajamäki f37617754d [CI] Add check services run correctly as a dependency in actions to api+e2e 2026-02-02 11:40:52 +02:00
joonas-aandSanteri Rajamäki 1a41d3d8fc [Backend][Programme courses] Refactor
Preparing for upcoming change in logic
2026-02-02 11:40:32 +02:00
Aaro Koivukangas b83dae4ead [Meta] Change run.sh to use docker compose, not docker-compose 2026-01-28 18:13:44 +02:00
Aaro Koivukangas b1d75d53d1 [CI] Remove working-directory, not run from test action 2026-01-28 18:07:42 +02:00
Aaro Koivukangas 52569cb402 [CI] Fix incorrect path in test action 2026-01-28 18:06:15 +02:00
Aaro Koivukangas cd88b6c41c [CI, Updater] Trigger updater workflow only on release 2026-01-28 18:00:06 +02:00
Aaro Koivukangas ea5543f712 [CI] Typo fix 2026-01-28 17:58:47 +02:00
Aaro Koivukangas e0fb0ab788 [CI] Add node/npm to api-tests action 2026-01-28 17:57:56 +02:00
Aaro Koivukangas adda1c5020 [CI] E2E acually checks if needed jobs succeed or are skipped 2026-01-28 17:45:45 +02:00
Aaro Koivukangas c15a113bbd [CI] Typo fix 2026-01-28 17:36:50 +02:00
Aaro Koivukangas d52be3e425 [CI] Add api tests to pull requests 2026-01-28 17:33:37 +02:00
Aaro Koivukangas 5643fe0c07 [CI] Remove duplicate setup-node from test actions 2026-01-28 17:18:39 +02:00
Aaro Koivukangas fb673950b7 [CI] Add api-tests to ci 2026-01-28 17:18:39 +02:00
Aaro Koivukangas 4b052a1b13 [Dev] Add a script to run api-tests in docker 2026-01-28 17:18:39 +02:00
Aaro Koivukangas 34d0509572 [api-tests] Add @types/supertest 2026-01-28 17:18:39 +02:00
Aaro Koivukangas 5e8a5022cd [api-test] Add basic api test for changelog 2026-01-28 17:18:39 +02:00
Aaro Koivukangas a989f98524 [Changelog] Change to fake changelogs in test env 2026-01-28 17:18:39 +02:00
Santeri Rajamäki 6076b3d45a [Faculties] Relax programme and yearIndex requirements for ExpandableRow data 2026-01-27 16:40:22 +02:00
Santeri Rajamäki 3d4a09f2ef [OT] Add basic documentation for features 2026-01-27 15:02:41 +02:00
Santeri Rajamäki c6e40d27a4 [Frontend] Commit the rest of the changes 2026-01-27 15:00:59 +02:00
Santeri Rajamäki 016d2b6ec8 [Frontend] Remove lodash chain from project 2026-01-27 12:51:30 +02:00
joonas-a 2754874e67 [Eslint] Unban all 1 char long identifiers
Harmful and inconsistent rule making some reduces/maps/filters bloated
for absolutely no reason
2026-01-27 00:23:47 +02:00
joonas-a 64352a6e24 [Chore] remove few lodash chains
lodash-es chain method does not work well with vite in prod/e2e
2026-01-27 00:19:43 +02:00
Santeri Rajamäki 4f290d45eb [CI] Use node 24 before docker-compose in run-tests/action.yml 2026-01-26 20:31:17 +02:00
Santeri Rajamäki 27a75877bd [Backend] Use imports instead of require in languageCenterData 2026-01-26 20:31:17 +02:00
santeri0200andGitHub 3f9af3b5f6 Merge pull request #4930 from UniversityOfHelsinkiCS/renovate/major-github-artifact-actions
Update GitHub Artifact Actions (major)
2026-01-26 20:20:30 +02:00
Santeri Rajamäki 359a18be92 [Frontend] Remove already replaced package immer 2026-01-26 19:58:14 +02:00
Santeri Rajamäki b6a1e07fb4 [Frontend] Unify data passing in Courses of Class tabs 2026-01-26 19:49:29 +02:00
Santeri Rajamäki 63d8b12273 [Backend] Convert logger to typescript 2026-01-26 17:56:43 +02:00
Aaro Koivukangas 124b203629 [META] Update remaining package-lock.jsons 2026-01-26 16:13:52 +02:00
Aaro Koivukangas ada19dc3a8 [META] Update package-lock.json to node 24 2026-01-26 16:07:32 +02:00
Aaro Koivukangas eba2a775ae [CI] Acual change cypress node version to 24 2026-01-26 15:47:25 +02:00
Aaro Koivukangas f74342d25d [CI] Update cypress node version to 24 2026-01-26 15:31:24 +02:00
Santeri Rajamäki 8b133fae64 [Backend] Use correct import style for local files 2026-01-26 12:22:31 +02:00
Santeri Rajamäki 6100aaaa0b [META] Update node env from 22 to 24 2026-01-23 17:20:23 +02:00
joonas-a 5a2f7196a2 [Programme overview] Fix sorting by type col flaky
sometimes there could be undefined which messes things up
2026-01-23 17:19:53 +02:00
Santeri Rajamäki 53684200ca [E2E] Remove deprecated option experimentalStudio
The option was deprecated in 15.4.0, where the studio was made available for everyone.
2026-01-23 16:50:41 +02:00
joonas-a c3b24d3d5d [E2E] Fix degree programme overview tests 2026-01-23 16:45:02 +02:00
joonas-a 20b40ec5c8 [RTK api] remove unnecessary ampersand from route 2026-01-23 16:45:02 +02:00
Santeri Rajamäki 4244e70d68 [META] Update imports from lodash to lodash-es 2026-01-23 16:43:25 +02:00
Santeri Rajamäki 7480a063c3 [META] Change type packages from to 2026-01-23 16:42:23 +02:00
Santeri Rajamäki 37b4b31a9a [META] Upgrade lodash and lodash-es to the latest lodash-es variant 2026-01-23 16:33:39 +02:00
Santeri Rajamäki 159ccc3655 [META] Remove lodash from global dependencies 2026-01-23 16:22:00 +02:00
Santeri Rajamäki 380168b425 [Courses of class] Simplify module/course parse logic 2026-01-23 16:13:43 +02:00
Santeri Rajamäki 14ed9fa99a [Filters] Reword studyRightType filter logic 2026-01-23 15:58:50 +02:00
Santeri Rajamäki adcac058a6 [Filters] Reword graduatedFromProgramme filter logic 2026-01-23 15:58:50 +02:00
Santeri Rajamäki 03efb92936 [Filters] Simplify studentNumber filter actions 2026-01-23 15:58:50 +02:00
Santeri Rajamäki a0f3a1d4ab [Frontend] Simplify getStudentTotalCredits 2026-01-23 15:58:50 +02:00
joonas-a 27c3291049 [Meta] Delete unused file 2026-01-23 15:58:21 +02:00
joonas-a 706ada2b6f [Programme courses] Remove default sort as unneeded 2026-01-23 15:58:21 +02:00
joonas-a 41ff70cd45 [Programme overview] Add excel export 2026-01-23 15:58:21 +02:00
Aaro Koivukangas c065cfd2ed [E2E] Remove deprecated test 2026-01-23 15:36:40 +02:00
Aaro Koivukangas 9ac6b3db84 [Meta][ProgressCharts] Remove Include graduated toggle from all views 2026-01-23 15:27:51 +02:00
Santeri Rajamäki b3d7722d79 [Filters] Use precompute in admissionType filter instead of recalculating the same value each render 2026-01-23 15:00:50 +02:00
Santeri Rajamäki 197fee44ad [Docs] Partial update to resources.md 2026-01-23 15:00:50 +02:00
joonas-a 5c6f6ec689 [Programme courses] Default sort by total credits 2026-01-23 14:31:52 +02:00
Santeri Rajamäki 6fbd93b97d [META] Update Cypress minor version to 15.9 2026-01-23 14:20:22 +02:00
joonas-a 95b3695fc5 [Programme overview][Programme courses] Hide text overflow in name col 2026-01-23 14:15:44 +02:00
joonas-a bb547e2f51 [Programme overview][Programme courses] ditch MRT 2026-01-23 14:09:23 +02:00
Santeri Rajamäki 858f82d93e [Filters] Use mutate attribute instead of mutating in filter attribute 2026-01-23 13:54:24 +02:00
Aaro KoivukangasandGitHub 33cea662b9 Merge pull request #4948 from UniversityOfHelsinkiCS/progress-chart-graduated-category
Progress chart graduated category
2026-01-23 12:46:20 +02:00
Aaro Koivukangas fc75dfd2dd [StudyTrackStats+E2E] Include graduated back in all students in charts 2026-01-23 12:37:23 +02:00
Santeri Rajamäki f0fc96f07f [Faculties] Fix bcMs credit intervals to take in count the SRE based timings 2026-01-22 18:05:54 +02:00
joonas-a df08a30f2d [Programme overview] Programme courses show credits correctly
remove default sort for now until ooditable migration
2026-01-22 17:49:57 +02:00
Aaro Koivukangas 97d332d50e [E2E] maybe soon 2026-01-22 17:45:14 +02:00
Aaro Koivukangas 860488977f [E2E] 4th e2e fix 2026-01-22 17:26:46 +02:00
Aaro Koivukangas 6b1ca59559 [E2E] Fix e2e 3rd time 2026-01-22 17:15:29 +02:00
Aaro Koivukangas 071038a1fa [E2E] More e2e fixes 2026-01-22 17:01:17 +02:00
Aaro Koivukangas 392e28010d [E2E] Fix e2e test 2026-01-22 16:38:58 +02:00
Aaro Koivukangas c1c7d3d254 [StudyTrackStats] Exclude graduated from credit counts 2026-01-22 16:34:02 +02:00
Aaro Koivukangas c35701bb5d [University+Faculty] Add graduated as category to ProgressCharts 2026-01-22 12:00:14 +02:00
Aaro Koivukangas b7e8b6fad1 [CI] manual lint fixes 2026-01-19 14:13:25 +02:00
Aaro Koivukangas f355577c8a [StudyProgramme] Add graduated as category to ProgressCharts 2026-01-19 13:22:29 +02:00
joonas-a 2bd78a587a [E2E] Fix graduation time calculation 2026-01-12 17:01:54 +02:00
joonas-a 294b624e5a [Graduations] Allow 4 absences if study right start before 1-8-2015 2026-01-12 16:20:20 +02:00
joonas-a 2eacb56ce2 [Graduations] Add 2 free absences
First two non-statutory absences will no longer drain study time
2026-01-12 16:20:20 +02:00
Santeri Rajamäki e384d734a6 [Filters][Grade] Use fallback if selectedGrade fails to show up 2026-01-12 14:29:17 +02:00
joonas-a a9ab7621bf [Programme overview] Fix labels in studies after programme chart 2026-01-12 13:19:47 +02:00
joonas-a 98ea010d64 [Graduations] Route typings 2026-01-12 13:11:45 +02:00
Aaro Koivukangas 304b16c73c [CourseStats] Fix incorrect precomputed use 2026-01-12 12:40:30 +02:00
Aaro Koivukangas 82a726190e [CourseStats] Get course name for title from FilterView 2026-01-12 12:15:02 +02:00
Santeri Rajamäki af21972e88 [Faculty] Progrss tab credit accumulation table for bachelor+master now shows credit limits correctly 2026-01-09 18:39:52 +02:00
Santeri Rajamäki cf7e34f4ce [Faculty] Add programme level color coding 2026-01-09 17:30:57 +02:00
Santeri Rajamäki 57cff95ba3 [Frontend] Swap AssignmentCheckedIcon with SwapHorizIcon 2026-01-09 16:43:20 +02:00
Santeri Rajamäki b7c7a24a3f [PopulationStats][Courses] Add transferred icon to APPROVED credits 2026-01-09 16:43:20 +02:00
joonas-a 0769b38b98 [Routes/programme overview] Type creditstats 2026-01-09 15:18:44 +02:00
Santeri Rajamäki 6557f7db55 [Populations] Add enrollments to course stats before credits 2026-01-09 14:42:39 +02:00
joonas-a 9e6b9be0e2 [Backend] Type creditstats 2026-01-09 14:32:43 +02:00
joonas-a 52b92e4cb6 [Routes] Type basicstats 2026-01-09 12:35:16 +02:00
Santeri Rajamäki e23cb37121 [Backend] Rewrite faculty route types 2026-01-07 18:31:29 +02:00
Aaro Koivukangas 1c6b9a60ed [StudyTrack] Add programme code and study track name to programme {sub,}title 2026-01-07 18:04:08 +02:00
Santeri Rajamäki 30e7f832f7 [Filters][StudyTrack] Add ability to read student study tracks 2026-01-07 15:48:18 +02:00
Aaro Koivukangas 62b5009391 [CI] Fix slackbot link url 2026-01-07 13:06:43 +02:00
joonas-a 6dd7dbc803 [Faculties/University] Remove legacy datafields
lastUpdated and Status have not been in use for a long time
2026-01-07 12:10:07 +02:00
Aaro Koivukangas e8f4ed8ae6 replace studytrack status 'inactive' to 'passive' 2026-01-07 11:26:44 +02:00
renovate[bot]andGitHub 67ab0db6d1 Update GitHub Artifact Actions 2025-12-12 22:57:34 +00:00
652 changed files with 25872 additions and 20340 deletions
+41
View File
@@ -0,0 +1,41 @@
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 }}
outputs:
image:
description: 'Image name provided by buildah action'
value: ${{ steps.build-image-action.outputs.image }}
tags:
description: 'Tag names provided by buildah action'
value: ${{ steps.build-image-action.outputs.tags }}
runs:
using: composite
steps:
- uses: actions/checkout@v6
- name: Build backend image
id: build-image-action
uses: redhat-actions/buildah-build@v2
with:
image: oodikone-backend
tags: ${{ inputs.environment }} ${{ github.sha }}
context: services
containerfiles: |
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 }}
@@ -0,0 +1,50 @@
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: ''
outputs:
image:
description: 'Image name provided by buildah action'
value: ${{ steps.build-image-action.outputs.image }}
tags:
description: 'Tag names provided by buildah action'
value: ${{ steps.build-image-action.outputs.tags }}
runs:
using: composite
steps:
- name: Get current time
id: get_current_time
shell: bash
run: echo "NOW=$(date --iso-8601=seconds)" >> $GITHUB_OUTPUT
- name: Build frontend image
id: build-image-action
uses: redhat-actions/buildah-build@v2
with:
image: oodikone-frontend
tags: ${{ inputs.environment }} ${{ github.sha }}
context: services
containerfiles: |
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, '') }}
@@ -0,0 +1,40 @@
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 }}
outputs:
image:
description: 'Image name provided by buildah action'
value: ${{ steps.build-image-action.outputs.image }}
tags:
description: 'Tag names provided by buildah action'
value: ${{ steps.build-image-action.outputs.tags }}
runs:
using: composite
steps:
- uses: actions/checkout@v6
- name: Build scheduler image
id: build-image-action
uses: redhat-actions/buildah-build@v2
with:
image: updater-scheduler
tags: ${{ inputs.environment }} ${{ github.sha }}
context: updater/sis-updater-scheduler
containerfiles: |
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 }}
+40
View File
@@ -0,0 +1,40 @@
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 }}
outputs:
image:
description: 'Image name provided by buildah action'
value: ${{ steps.build-image-action.outputs.image }}
tags:
description: 'Tag names provided by buildah action'
value: ${{ steps.build-image-action.outputs.tags }}
runs:
using: composite
steps:
- uses: actions/checkout@v6
- name: Build worker image
id: build-image-action
uses: redhat-actions/buildah-build@v2
with:
image: updater-worker
tags: ${{ inputs.environment }} ${{ github.sha }}
context: updater/sis-updater-worker
containerfiles: |
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 }}
+37
View File
@@ -0,0 +1,37 @@
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
@@ -0,0 +1,31 @@
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
@@ -19,7 +19,7 @@ runs:
using: composite
steps:
- name: 'Login to toska Docker registry'
uses: docker/login-action@v3
uses: docker/login-action@v4
with:
registry: registry-toska.ext.ocp-prod-0.k8s.it.helsinki.fi
username: ${{ inputs.username }}
@@ -27,24 +27,25 @@ runs:
- name: 'Run services, pull rest of the needed images'
shell: bash
run: docker compose --file docker-compose.ci.yml up --detach --quiet-pull
run: docker compose --file docker-compose.ci.yml --profile oodikone up --detach --quiet-pull
- name: 'Run cypress'
uses: cypress-io/github-action@v6
uses: cypress-io/github-action@v7
with:
browser: chrome
browser: firefox
spec: ${{ inputs.spec }}
env:
CYPRESS_baseUrl: http://localhost:1337
- name: 'Upload video if tests fail'
uses: actions/upload-artifact@v5
uses: actions/upload-artifact@v7
if: failure()
with:
name: 'Cypress videos - ${{ inputs.test_name }}'
path: ./cypress/videos/*
retention-days: 7
- name: 'Print logs on failure'
shell: bash
if: failure()
run: docker compose --file docker-compose.ci.yml logs
run: npm run logs:ci && sleep 15
+37
View File
@@ -0,0 +1,37 @@
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
+22
View File
@@ -0,0 +1,22 @@
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 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
+20 -37
View File
@@ -5,35 +5,23 @@ on:
types: [published]
jobs:
release_frontend:
name: 'Build and release frontend'
release_backend:
name: 'Build and release backend'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- name: 'Get current time'
id: get_current_time
run: echo "NOW=$(date --iso-8601=seconds)" >> $GITHUB_OUTPUT
- uses: actions/checkout@v6
- name: Extract release version
id: extract_version
run: echo "VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT
- name: Build frontend image
- name: 'Build backend'
id: build-image
uses: redhat-actions/buildah-build@v2
uses: ./.github/actions/build/backend
with:
image: oodikone-frontend
tags: production ${{ github.sha }}
context: services
containerfiles: |
services/frontend/Dockerfile
build-args: |
BUILT_AT=${{ steps.get_current_time.outputs.NOW }}
SENTRY_RELEASE=${{ steps.extract_version.outputs.VERSION }}
SENTRY_ENVIRONMENT=production
SENTRY_DSN=${{ secrets.OODIKONE_FRONTEND_SENTRY_DSN }}
environment: 'production'
release_version: ${{ steps.extract_version.outputs.VERSION }}
- name: Push to quay.io
uses: redhat-actions/push-to-registry@v2
@@ -44,29 +32,24 @@ jobs:
username: toska+github
password: ${{ secrets.QUAY_IO_TOKEN }}
release_backend:
name: 'Build and release backend'
release_frontend:
name: 'Build and release frontend'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- name: Extract release version
id: extract_version
run: echo "VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT
- name: Build backend image
- name: 'Build frontend'
id: build-image
uses: redhat-actions/buildah-build@v2
uses: ./.github/actions/build/frontend
with:
image: oodikone-backend
tags: production ${{ github.sha }}
context: services
containerfiles: |
services/backend/Dockerfile
build-args: |
SENTRY_RELEASE=${{ steps.extract_version.outputs.VERSION }}
SENTRY_ENVIRONMENT=production
environment: 'production'
release_version: ${{ steps.extract_version.outputs.VERSION }}
sentry_dns: ${{ secrets.OODIKONE_FRONTEND_SENTRY_DSN }} # Used only in production
- name: Push to quay.io
uses: redhat-actions/push-to-registry@v2
@@ -85,7 +68,7 @@ jobs:
- release_backend
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- name: Extract release version
id: extract_version
@@ -119,7 +102,7 @@ jobs:
- release_backend
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- name: Extract release version
id: extract_version
@@ -157,13 +140,13 @@ jobs:
if: success()
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- name: 'Send notification to Slack'
uses: UniversityOfHelsinkiCS/slackbot-action@v1.5
with:
webhook-url: ${{ secrets.WEBHOOK_URL }}
message-type: deployment
softa-url: https://oodikone.cs.helsinki.fi/
softa-url: https://oodikone.helsinki.fi/
deployment-target: production
report_failure:
@@ -178,7 +161,7 @@ jobs:
if: failure()
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- name: 'Send notification to Slack'
uses: UniversityOfHelsinkiCS/slackbot-action@v1.5
with:
+43 -11
View File
@@ -5,28 +5,60 @@ on:
branches:
- master
paths:
- '.github/actions/**'
- '.github/workflows/pull_request.yaml'
- '.github/workflows/test.yaml'
- 'services/**'
- 'updater/**'
- 'docker-compose.ci.yml'
- 'docker-compose.test.yml'
- 'package*.json'
- 'cypress/**'
- 'cypress.config.json'
- 'services/**'
- '!services/backend/src/rapodiff/**'
- 'docker-compose.ci.yml'
- 'package*.json'
jobs:
lint_packages:
name: 'Lint packages'
runs-on: ubuntu-latest
if: ${{ github.event.pull_request.user.login != 'renovate[bot]' }}
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- name: Lint backend, frontend and shared
uses: ./.github/actions/lint
test_end_to_end:
test_unit:
name: 'Run unit tests'
runs-on: ubuntu-latest
if: ${{ github.event.pull_request.user.login != 'renovate[bot]' }}
needs:
- lint_packages
steps:
- uses: actions/checkout@v6
- uses: ./.github/actions/test/unit
test_api:
name: 'Test backend api'
runs-on: ubuntu-latest
if: ${{ github.event.pull_request.user.login != 'renovate[bot]' }}
needs:
- lint_packages
steps:
- uses: actions/checkout@v6
- uses: ./.github/actions/test/api
test_end_to_end:
name: ${{ matrix.name }}
runs-on: ubuntu-latest
if: ${{ github.event.pull_request.user.login != 'renovate[bot]' }}
needs:
- lint_packages
- test_api
strategy:
fail-fast: false
matrix:
include:
- name: 'Test course statistics'
@@ -49,19 +81,19 @@ jobs:
cypress/e2e/Users.js
- name: 'Test faculty statistics'
spec: cypress/e2e/Faculty_statistics.js
- name: 'Test open uni search'
spec: cypress/e2e/Openuni.js
- name: 'Test language center view'
spec: cypress/e2e/Language_center.js
- name: 'Test study guidance groups'
spec: cypress/e2e/Study_guidance_groups.js
- name: 'Test university'
spec: cypress/e2e/University.js
name: ${{ matrix.name }}
- name: 'Test numbers match'
spec: cypress/e2e/Numbers_match.js
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- name: 'Run tests'
uses: ./.github/actions/run-tests
uses: ./.github/actions/test/e2e
with:
username: ${{ secrets.TOSKAREGISTRY_USERNAME }}
password: ${{ secrets.TOSKAREGISTRY_PASSWORD }}
+75 -43
View File
@@ -8,13 +8,13 @@ on:
- '.github/actions/**'
- '.github/workflows/staging.yaml'
- '.github/workflows/test.yaml'
- 'services/**'
- 'updater/**'
- 'docker-compose.ci.yml'
- 'docker-compose.test.yml'
- 'package*.json'
- 'cypress/**'
- 'cypress.config.json'
- 'services/**'
- '!services/backend/src/rapodiff/**'
- 'updater/sis-updater-worker/src/db/migrations/**'
- 'docker-compose.ci.yml'
- 'package*.json'
jobs:
test:
@@ -23,31 +23,20 @@ jobs:
username: ${{ secrets.TOSKAREGISTRY_USERNAME }}
password: ${{ secrets.TOSKAREGISTRY_PASSWORD }}
release_frontend:
name: 'Build and release frontend'
release_backend:
name: 'Build and release backend'
runs-on: ubuntu-latest
needs: test
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- name: Get current time
id: get_current_time
run: echo "NOW=$(date --iso-8601=seconds)" >> $GITHUB_OUTPUT
- name: Build frontend image
- name: 'Build backend'
id: build-image
uses: redhat-actions/buildah-build@v2
uses: ./.github/actions/build/backend
with:
image: oodikone-frontend
tags: staging ${{ github.sha }}
context: services
containerfiles: |
services/frontend/Dockerfile
build-args: |
BUILT_AT=${{ steps.get_current_time.outputs.NOW }}
BASE_PATH=/oodikone
STAGING=true
environment: 'staging'
release_version: ${{ github.sha }}
- name: Push to quay.io
uses: redhat-actions/push-to-registry@v2
@@ -58,27 +47,68 @@ jobs:
username: toska+github
password: ${{ secrets.QUAY_IO_TOKEN }}
release_backend:
name: 'Build and release backend'
release_frontend:
name: 'Build and release frontend'
runs-on: ubuntu-latest
needs: test
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- name: Build backend image
- name: 'Build frontend'
id: build-image
uses: redhat-actions/buildah-build@v2
uses: ./.github/actions/build/frontend
with:
image: oodikone-backend
tags: staging ${{ github.sha }}
context: services
containerfiles: |
services/backend/Dockerfile
build-args: |
BASE_PATH=/oodikone
GIT_SHA=${{ github.sha }}
STAGING=true
environment: 'staging'
release_version: ${{ github.sha }}
- name: Push to quay.io
uses: redhat-actions/push-to-registry@v2
with:
image: ${{ steps.build-image.outputs.image }}
tags: ${{ steps.build-image.outputs.tags }}
registry: quay.io/toska
username: toska+github
password: ${{ secrets.QUAY_IO_TOKEN }}
release_updater_worker:
name: 'Build and release updater worker'
runs-on: ubuntu-latest
needs: test
steps:
- uses: actions/checkout@v6
- name: 'Build worker'
id: build-image
uses: ./.github/actions/build/worker
with:
environment: 'staging'
release_version: ${{ github.sha }}
- name: Push to quay.io
uses: redhat-actions/push-to-registry@v2
with:
image: ${{ steps.build-image.outputs.image }}
tags: ${{ steps.build-image.outputs.tags }}
registry: quay.io/toska
username: toska+github
password: ${{ secrets.QUAY_IO_TOKEN }}
release_updater_scheduler:
name: 'Build and release updater scheduler'
runs-on: ubuntu-latest
needs: test
steps:
- uses: actions/checkout@v6
- name: 'Build scheduler'
id: build-image
uses: ./.github/actions/build/scheduler
with:
environment: 'staging'
release_version: ${{ github.sha }}
- name: Push to quay.io
uses: redhat-actions/push-to-registry@v2
@@ -92,14 +122,15 @@ jobs:
report_success:
name: 'Report deployment success to the "oodikone_notifications" channel'
runs-on: ubuntu-latest
if: success()
needs:
- release_frontend
- release_backend
if: success()
- release_updater_worker
- release_updater_scheduler
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- name: 'Send notification to Slack'
uses: UniversityOfHelsinkiCS/slackbot-action@v1.5
with:
@@ -111,14 +142,15 @@ jobs:
report_failure:
name: 'Report deployment failure to the "oodikone_notifications" channel'
runs-on: ubuntu-latest
if: failure()
needs:
- release_frontend
- release_backend
if: failure()
- release_updater_worker
- release_updater_scheduler
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- name: 'Send notification to Slack'
uses: UniversityOfHelsinkiCS/slackbot-action@v1.5
with:
+213 -94
View File
@@ -16,7 +16,7 @@ jobs:
name: 'Lint packages'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- name: Lint backend, frontend and shared
uses: ./.github/actions/lint
@@ -25,49 +25,25 @@ jobs:
name: 'Build backend'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: docker/setup-buildx-action@v3
- name: 'Build image and export it'
uses: docker/build-push-action@v6
- uses: actions/checkout@v6
- name: Build backend
uses: ./.github/actions/build/backend
with:
build-args: |
SENTRY_RELEASE=${{ github.sha }}
SENTRY_ENVIRONMENT=staging
context: services
file: services/backend/Dockerfile
push: false
tags: oodikone-backend:staging
outputs: type=docker,dest=/tmp/backend.tar
env:
DOCKER_BUILD_SUMMARY: false
- name: 'Upload image for other jobs'
uses: actions/upload-artifact@v5
with:
name: backend-image
path: /tmp/backend.tar
environment: 'staging'
release_version: ${{ github.sha }}
build_frontend:
name: 'Build frontend'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- name: Get current time
id: get_current_time
run: echo "NOW=$(date --iso-8601=seconds)" >> $GITHUB_OUTPUT
- name: Build frontend image
id: build-image
uses: redhat-actions/buildah-build@v2
- uses: actions/checkout@v6
- name: Build frontend
uses: ./.github/actions/build/frontend
with:
image: oodikone-frontend
tags: staging ${{ github.sha }}
context: services
containerfiles: |
services/frontend/Dockerfile
build-args: |
BUILT_AT=${{ steps.get_current_time.outputs.NOW }}
BASE_PATH=/oodikone
STAGING=true
environment: 'staging'
release_version: ${{ github.sha }}
# === Update databases ===
check_migration_files:
name: Check if new migrations have been added
runs-on: ubuntu-latest
@@ -75,11 +51,12 @@ jobs:
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@v5
- uses: actions/checkout@v6
- name: Check if new migrations have been added
id: check_migrations
uses: dorny/paths-filter@v3
uses: dorny/paths-filter@v4
with:
filters: |
kone_db_migrations:
@@ -94,94 +71,231 @@ jobs:
build_and_push_anon_sis_db:
name: 'Build anon sis-db and push to toska hub if new migrations have been added'
needs: check_migration_files
runs-on: ubuntu-latest
if: needs.check_migration_files.outputs.sis_db_migrations == 'true'
if: |
!cancelled() && !failure() &&
needs.check_migration_files.outputs.sis_db_migrations == 'true'
needs:
- check_migration_files
- build_backend
- build_frontend
- lint_packages
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- name: 'Login to toska docker registry'
uses: docker/login-action@v3
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'
run: |
docker compose --file docker-compose.ci.yml up --detach sis-updater-worker
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
./anonyymioodi/commit_and_push_to_toska_hub.sh sis-db
build_and_push_anon_kone_db:
name: 'Build anon kone-db and push to toska hub if new migrations have been added'
runs-on: ubuntu-latest
if: |
!cancelled() && !failure() &&
needs.check_migration_files.outputs.kone_db_migrations == 'true'
needs:
- check_migration_files
- build_backend
- build_frontend
- check_migration_files
runs-on: ubuntu-latest
if: needs.check_migration_files.outputs.kone_db_migrations == 'true'
- lint_packages
steps:
- uses: actions/checkout@v5
- name: 'Download backend image'
uses: actions/download-artifact@v6
with:
name: backend-image
- name: 'Load built backend image'
run: docker load --input backend.tar
- uses: actions/checkout@v6
- name: 'Login to toska docker registry'
uses: docker/login-action@v3
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'
run: |
docker compose --file docker-compose.ci.yml up --detach backend
docker compose --file docker-compose.ci.yml up --detach --quiet-pull backend --wait
sleep 30 # wait for backend to start and run migrations
./anonyymioodi/commit_and_push_to_toska_hub.sh kone-db
build_and_push_anon_user_db:
name: 'Build anon user-db and push to toska hub if new migrations have been added'
runs-on: ubuntu-latest
if: |
!cancelled() && !failure() &&
needs.check_migration_files.outputs.user_db_migrations == 'true'
needs:
- check_migration_files
- build_backend
- build_frontend
- check_migration_files
runs-on: ubuntu-latest
if: needs.check_migration_files.outputs.user_db_migrations == 'true'
- lint_packages
steps:
- uses: actions/checkout@v5
- uses: dorny/paths-filter@v3
id: check_migrations
with:
filters: |
migrations:
- 'services/backend/src/database/migrations_user/**'
- '.github/workflows/test.yaml'
- name: 'Download backend image'
uses: actions/download-artifact@v6
with:
name: backend-image
- name: 'Load built backend image'
run: docker load --input backend.tar
- uses: actions/checkout@v6
- name: 'Login to toska docker registry'
uses: docker/login-action@v3
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'
run: |
docker compose --file docker-compose.ci.yml up --detach backend
docker compose --file docker-compose.ci.yml up --detach --quiet-pull backend --wait
sleep 30 # wait for backend to start and run migrations
./anonyymioodi/commit_and_push_to_toska_hub.sh user-db
# === End-to-end tests ===
test_end_to_end:
# === Update sis-db with updater ===
check_updater_changed:
name: 'Check if updater has been updated'
runs-on: ubuntu-latest
if: |
!cancelled() && !failure()
outputs:
has_updater_changed: ${{ steps.check_updater_changed.outputs.has_updater_changed }}
steps:
- uses: actions/checkout@v6
- 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: ubuntu-latest
if: |
!cancelled() && !failure() &&
(needs.check_updater_changed.outputs.has_updater_changed == 'true' ||
needs.check_migration_files.outputs.kone_db_migrations == 'true' ||
needs.check_migration_files.outputs.user_db_migrations == 'true' ||
needs.check_migration_files.outputs.sis_db_migrations == 'true')
needs:
- check_migration_files
- check_updater_changed
- build_and_push_anon_sis_db
- build_and_push_anon_kone_db
- build_and_push_anon_user_db
# Always run the tests, even if some of the needed jobs failed or were skipped
if: always()
steps:
- uses: actions/checkout@v6
- 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 --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()
# run: ./anonyymioodi/commit_and_push_to_toska_hub.sh sis-db
- name: Dump logs on failure
if: failure()
run: npm run logs:ci
# === Backend api-tests ===
test_api:
name: 'Run backend api tests'
runs-on: ubuntu-latest
if: |
!cancelled() && !failure()
needs:
- run_updater_updates
strategy:
fail-fast: false
steps:
- uses: actions/checkout@v6
- 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: ubuntu-latest
if: |
!cancelled() && !failure()
needs:
- run_updater_updates
strategy:
fail-fast: false
steps:
- uses: actions/checkout@v6
- 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: ubuntu-latest
if: |
!cancelled() && !failure()
needs:
- run_updater_updates
steps:
- uses: actions/checkout@v6
- name: Run unit tests (frontend and backend)
uses: ./.github/actions/test/unit
# === Frontend component testing ===
test_components:
name: 'Test frontend components'
runs-on: ubuntu-latest
if: |
!cancelled() && !failure()
needs:
- run_updater_updates
steps:
- uses: actions/checkout@v6
- name: Run frontend component tests
uses: ./.github/actions/test/component
# === End-to-end tests ===
test_end_to_end:
runs-on: ubuntu-latest
if: |
!cancelled() && !failure()
needs:
- run_updater_updates
strategy:
fail-fast: false
matrix:
@@ -206,23 +320,28 @@ jobs:
cypress/e2e/Users.js
- name: 'Test faculty statistics'
spec: cypress/e2e/Faculty_statistics.js
- name: 'Test open uni search'
spec: cypress/e2e/Openuni.js
- name: 'Test language center view'
spec: cypress/e2e/Language_center.js
- name: 'Test study guidance groups'
spec: cypress/e2e/Study_guidance_groups.js
- name: 'Test university'
spec: cypress/e2e/University.js
- name: 'Test numbers match'
spec: cypress/e2e/Numbers_match.js
- name: 'Test access rights'
spec: cypress/e2e/Routes.js
name: ${{ matrix.name }}
runs-on: ubuntu-24.04
steps:
- name: Checkout
uses: actions/checkout@v5
uses: actions/checkout@v6
- name: Install node-24 for cypress
uses: actions/setup-node@v6
with:
node-version: 24
- name: 'Run tests'
uses: ./.github/actions/run-tests
uses: ./.github/actions/test/e2e
with:
username: ${{ secrets.username }}
password: ${{ secrets.password }}
@@ -232,23 +351,23 @@ jobs:
# === Other tests ===
test_service_exit_statuses:
name: 'Check services run correctly'
needs:
- build_and_push_anon_sis_db
- build_and_push_anon_kone_db
- build_and_push_anon_user_db
if: always()
runs-on: ubuntu-latest
if: |
!cancelled() && !failure()
needs:
- run_updater_updates
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- name: 'Login to toska docker registry'
uses: docker/login-action@v3
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, pull rest of the needed images'
run: docker compose --file docker-compose.ci.yml up --detach --quiet-pull
- 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: |
@@ -260,4 +379,4 @@ jobs:
- name: 'Print logs on failure'
if: failure()
run: docker compose --file docker-compose.ci.yml logs
run: npm run logs:ci && sleep 15
+45 -52
View File
@@ -1,29 +1,22 @@
name: 'Test (and release) Updater'
on:
push:
branches:
- master
release:
types: [published]
paths:
- '.github/workflows/updater.yaml'
- 'updater/sis-updater-worker/**'
- 'updater/sis-updater-scheduler/**'
pull_request:
branches:
- master
paths:
- 'updater/sis-updater-worker/**'
- 'updater/sis-updater-scheduler/**'
- 'updater/**'
jobs:
lint_scheduler_and_worker:
name: 'Lint scheduler and worker'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
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'
@@ -32,11 +25,12 @@ jobs:
build_worker:
name: 'Build Updater worker'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: docker/setup-buildx-action@v3
- uses: actions/checkout@v6
- uses: docker/setup-buildx-action@v4
- name: 'Build the image'
uses: docker/build-push-action@v6
uses: docker/build-push-action@v7
with:
context: updater/sis-updater-worker/
push: false
@@ -44,20 +38,23 @@ jobs:
tags: toska/sis-updater-worker:latest
env:
DOCKER_BUILD_SUMMARY: false
- name: 'Upload image for other jobs'
uses: actions/upload-artifact@v5
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: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: docker/setup-buildx-action@v3
- uses: actions/checkout@v6
- uses: docker/setup-buildx-action@v4
- name: 'Build the image'
uses: docker/build-push-action@v6
uses: docker/build-push-action@v7
with:
context: updater/sis-updater-scheduler/
push: false
@@ -65,11 +62,13 @@ jobs:
tags: toska/sis-updater-scheduler:latest
env:
DOCKER_BUILD_SUMMARY: false
- name: 'Upload image for other jobs'
uses: actions/upload-artifact@v5
uses: actions/upload-artifact@v7
with:
name: updater-scheduler-image
path: /tmp/scheduler.tar
retention-days: 7
test_updater:
name: 'Test Updater'
@@ -78,42 +77,50 @@ jobs:
- lint_scheduler_and_worker
- build_worker
- build_scheduler
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- name: 'Login to toska docker registry'
uses: docker/login-action@v3
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@v6
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 --no-recreate sis-updater-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 --file docker-compose.ci.yml run sis-updater-scheduler npm start
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 --file docker-compose.ci.yml run sis-updater-scheduler npm start
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 --file docker-compose.ci.yml run sis-updater-scheduler npm start
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 --file docker-compose.ci.yml run sis-updater-scheduler npm start
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
@@ -122,22 +129,15 @@ jobs:
name: 'Build and release Updater worker'
runs-on: ubuntu-latest
needs: test_updater
if: github.event_name == 'push'
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- name: Build worker image
- name: 'Build updater worker'
id: build-image
uses: redhat-actions/buildah-build@v2
uses: ./.github/actions/build/worker
with:
image: updater-worker
tags: staging production ${{ github.sha }}
context: updater/sis-updater-worker
containerfiles: |
updater/sis-updater-worker/Dockerfile
build-args: |
SENTRY_RELEASE=${{ github.sha }}
SENTRY_ENVIRONMENT=production
environment: 'production'
release_version: ${{ github.sha }}
- name: Push to quay.io
uses: redhat-actions/push-to-registry@v2
@@ -152,22 +152,15 @@ jobs:
name: 'Build and release Updater scheduler'
runs-on: ubuntu-latest
needs: test_updater
if: github.event_name == 'push'
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- name: Build scheduler image
- name: 'Build udpater scheduler'
id: build-image
uses: redhat-actions/buildah-build@v2
uses: ./.github/actions/build/scheduler
with:
image: updater-scheduler
tags: staging production ${{ github.sha }}
context: updater/sis-updater-scheduler
containerfiles: |
updater/sis-updater-scheduler/Dockerfile
build-args: |
SENTRY_RELEASE=${{ github.sha }}
SENTRY_ENVIRONMENT=production
environment: 'production'
release_version: ${{ github.sha }}
- name: Push to quay.io
uses: redhat-actions/push-to-registry@v2
+3 -4
View File
@@ -56,7 +56,7 @@ cypress/videos/
coverage/
instrumented/
# Pycharm
# Pycharm
.idea
# VS Code
@@ -65,9 +65,8 @@ instrumented/
.vscode/launch.json
.vscode/terminals.json
# Funidata specific
environments/
# Target of shared symlink
services/**/src/shared
!services/backend/.env.test
+3
View File
@@ -0,0 +1,3 @@
ignore-scripts=true
min-release-age=7
save-prefix=
-5
View File
@@ -1,5 +0,0 @@
{
"all": true,
"include": ["services/frontend/src/**/*.js", "services/frontend/src/**/*.jsx"],
"extends": "./nyc-config.js"
}
+20 -6
View File
@@ -1,4 +1,5 @@
# Oodikone
[![Maintainability](https://qlty.sh/gh/UniversityOfHelsinkiCS/projects/oodikone/maintainability.svg)](https://qlty.sh/gh/UniversityOfHelsinkiCS/projects/oodikone)
An application for analyzing university data, running at [https://oodikone.helsinki.fi/](https://oodikone.helsinki.fi/).
@@ -122,6 +123,8 @@ Some useful commands are defined in `package.json` and can be run with `npm run
- `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
@@ -129,7 +132,6 @@ Some useful commands are defined in `package.json` and can be run with `npm run
- `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 rapodiff`: runs [Rapodiff](./services/backend/src/rapodiff/)
- `npm run install:local`: installs `node_modules` for each package (backend, frontend, sis-updater-scheduler and sis-updater-worker) locally
- `npm run install:docker`: installs `node_modules` for each package inside containers
- `npm run install:both`: runs both `npm run install:local` and `npm run install:docker`
@@ -149,8 +151,8 @@ It is recommended to spend some time to become familiar with `docker` and `docke
- `docker compose ps`: view the containers in the running environment
- `docker compose logs frontend`: print logs for just frontend
- `docker compose logs --follow --tail 100 backend`: print last hundred rows of backend logs and begin to follow them in your terminal window
- `docker exec -it backend sh`: open bash terminal inside backend container
- `docker exec -it sis-db psql -U postgres sis-db-real`: open psql client to investigate sis real data database
- `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
@@ -187,13 +189,11 @@ Continuous integration (CI) works with Github actions and is defined in workflow
- Updater is deployed to production when pushed if tests pass
- Test databases can be updated, instructions in [anonyymioodi README](anonyymioodi/README.md)
[Rapodiff](./services/backend/src/rapodiff/) can be used to check differences between Rapo and Oodikone data.
## ❓FAQ
### Modules are missing after updating package.json
You should always install the dependencies **inside** the container to have the application **inside** the container access them. Module might be missing for example when someone else installs a new library and you only pull the changes in package.json. Use `docker exec <service> npm ci` to install modules inside the container or `npm run install:docker` to install modules inside all packages (backend, frontend, sis-updater-scheduler and sis-updater-worker).
You should always install the dependencies **inside** the container to have the application **inside** the container access them. Module might be missing for example when someone else installs a new library and you only pull the changes in package.json. Prefer `npm run ci:docker` or `npm run install:docker`, which also fix `node_modules` ownership for the non-root runtime user. If you run installs manually, use root inside the container and restore ownership afterwards (`chown -R 1001:0 node_modules`) to avoid permission issues.
### Study guidance groups don't work on my machine
@@ -240,6 +240,20 @@ npm run cypress:run
Remember to revert the `baseUrl` in [cypress.config.js](./cypress.config.js) to its original value when you're done.
## 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
+3 -3
View File
@@ -6,14 +6,14 @@ The quick guide should be enough for most purposes: updating schema, data, or th
## 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.
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_1`).
- 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:
@@ -67,6 +67,6 @@ The quick guide should be enough for most purposes: updating schema, data, or th
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_1`). If you see any errors, try clicking the buttons again.
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.
+1 -1
View File
@@ -43,6 +43,6 @@ fi
infomsg "Committing and pushing to registry"
database=${args[0]}
docker commit "$database" "registry-toska.ext.ocp-prod-0.k8s.it.helsinki.fi/$database"
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"
+5 -5
View File
@@ -52,13 +52,13 @@ 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="${args[0]}"
docker compose down --remove-orphans
docker compose up -d "$database"
database_dump="$database.sqz"
infomsg "Attempting to restore database $database from dump $database_dump"
retry docker-compose exec "$database" pg_isready --dbname="$database"
docker exec -i "$database" /bin/bash -c "pg_restore --username=postgres \
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"
+29 -12
View File
@@ -1,40 +1,57 @@
x-defaults: &defaults
restart: unless-stopped
x-healthcheck-defaults: &healthcheck-defaults
interval: 1s
timeout: 3s
retries: 30
services:
kone-db:
<<: *defaults
container_name: kone-db
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: sis-db
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: sis-importer-db
container_name: oodikone-sis-importer-db
environment:
PGDATA: /data
POSTGRES_DB: sis-importer-db
POSTGRES_HOST_AUTH_METHOD: trust
image: postgres:15.10
user-db:
<<: *defaults
container_name: 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
+103
View File
@@ -0,0 +1,103 @@
#!/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
+6 -6
View File
@@ -18,11 +18,11 @@ 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
# 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
warningmsg "Trying to run docker compose down and remove orphans"
docker compose --profile oodikone --profile updater down --remove-orphans
}
# === CLI ===
@@ -49,7 +49,7 @@ set_custom_select_prompt() {
options=(
"Set up Oodikone from scratch"
"Reset all real data"
"Reset all data"
"Reset single database"
"Restore data from dumps"
"Docker system prune"
@@ -68,8 +68,8 @@ while true; do
case $opt in
"Set up Oodikone from scratch")
set_up_oodikone_from_scratch;;
"Reset all real data")
reset_all_real_data;;
"Reset all data")
reset_all_data;;
"Reset single database")
reset_single_database
set_custom_select_prompt # Set prompt back to initial values
-2
View File
@@ -3,7 +3,6 @@
const { defineConfig } = require('cypress')
module.exports = defineConfig({
experimentalStudio: process.env.NODE_ENV === 'development',
projectId: 'c3jsph',
defaultCommandTimeout: 30000,
requestTimeout: 30000,
@@ -14,7 +13,6 @@ module.exports = defineConfig({
// We've imported your old cypress plugins here.
// You may want to clean this up later by importing these.
setupNodeEvents(on, config) {
require('@cypress/code-coverage/task')(on, config)
require('./cypress/plugins/index.js')(on, config)
return config
},
+305 -281
View File
@@ -33,6 +33,7 @@ const checkGradeTable = gradesTableContents => {
}
const checkTableContents = contents => {
cy.get('table tbody').scrollIntoView()
cy.get('table tbody').within(() => {
contents.forEach((values, trIndex) => {
cy.get('tr')
@@ -91,17 +92,17 @@ const clickAway = () => {
const getCourseSearchHooksForSingleYear = code => {
cy.intercept({
pathname: '/api/v2/coursesmulti',
pathname: '/api/coursesmulti',
query: { code },
}).as('courses')
cy.intercept({
pathname: '/api/v3/courseyearlystats',
pathname: '/api/courseyearlystats',
query: { 'codes[]': code },
}).as('yearlystats')
cy.intercept({
pathname: '/api/v3/populationstatisticsbycourse',
pathname: '/api/populationstatisticsbycourse',
}).as('coursestats')
}
@@ -184,13 +185,16 @@ describe('Basic user', () => {
cy.wait('@yearlystats').its('response.statusCode').should('be.oneOf', [200, 304])
cy.url().should('contain', `courseCodes=%5B%22${coursecode}%22%5D`)
cy.contains('50131 • Pro gradu -tutkielma tietojenkäsittelytieteessä')
cy.contains('50131') // Pro gradu -tutkielma tietojenkäsittelytieteessä
cy.contains('Show population').should('be.enabled')
cy.contains('Show population').click()
cy.wait('@coursestats').its('response.statusCode').should('be.oneOf', [200, 304])
cy.contains('Population of course Pro gradu -tutkielma tietojenkäsittelytieteessä 2007-2020 (open and normal)')
cy.contains('Pro gradu -tutkielma tietojenkäsittelytieteessä')
cy.contains('Class of 2007-2020, 10 students')
cy.contains('Include substitutions, Open and normal')
cy.contains('Showing 10 out of 10 students')
const gradesTableContents = [
@@ -213,17 +217,15 @@ describe('Basic user', () => {
cy.wait('@yearlystats').its('response.statusCode').should('be.oneOf', [200, 304])
cy.url().should('contain', `courseCodes=%5B%22${coursecode}%22%5D`)
cy.contains(
'ON-310 • Tieteellisen kirjoittamisen seminaarin alkuopetus: Tieteellisen kirjallisen työn ja tiedonhankinnan perustaidot'
)
cy.contains('200012 • Tieteellisen kirjallisen työn ja tiedonhankinnan perustaidot')
cy.contains('ON-310') // Tieteellisen kirjoittamisen seminaarin alkuopetus: Tieteellisen kirjallisen työn ja tiedonhankinnan perustaidot
cy.contains('200012') // Tieteellisen kirjallisen työn ja tiedonhankinnan perustaidot
cy.contains('Show population').should('be.enabled')
cy.contains('Show population').click()
cy.wait('@coursestats').its('response.statusCode').should('be.oneOf', [200, 304])
cy.contains(
'Population of course Tieteellisen kirjallisen työn ja tiedonhankinnan perustaidot 2011-2018 (open and normal)'
)
cy.contains('Tieteellisen kirjallisen työn ja tiedonhankinnan perustaidot')
cy.contains('Class of 2011-2018, 4 students')
cy.contains('Include substitutions, Open and normal')
cy.contains('Showing 4 out of 4 students')
const gradesTableContents = [
@@ -262,17 +264,17 @@ describe('Basic user', () => {
it('Searching multiple courses having substitution mappings shows course statistics', () => {
cy.intercept({
pathname: '/api/v2/coursesmulti',
pathname: '/api/coursesmulti',
query: { code: 'TKT' },
}).as('courses')
cy.intercept({
pathname: '/api/v3/courseyearlystats',
pathname: '/api/courseyearlystats',
query: { 'codes[]': 'TKT20001', 'codes[]': 'TKT10002' },
}).as('yearlystats')
cy.intercept({
pathname: '/api/v3/populationstatisticsbycourse',
pathname: '/api/populationstatisticsbycourse',
}).as('coursestats')
cy.cs('select-multiple-courses-toggle').should('not.have.class', 'Mui-checked')
@@ -308,17 +310,17 @@ describe('Basic user', () => {
it('On searches with multiple courses, has correct links on the Course tab', () => {
cy.intercept({
pathname: '/api/v2/coursesmulti',
pathname: '/api/coursesmulti',
query: { code: 'BSCS' },
}).as('courses')
cy.intercept({
pathname: '/api/v3/courseyearlystats',
pathname: '/api/courseyearlystats',
query: { 'codes[]': 'BSCS1003', 'codes[]': 'BSCS1001' },
}).as('yearlystats')
cy.intercept({
pathname: '/api/v3/populationstatisticsbycourse',
pathname: '/api/populationstatisticsbycourse',
}).as('coursestats')
cy.cs('select-multiple-courses-toggle').should('not.have.class', 'Mui-checked')
@@ -337,7 +339,9 @@ describe('Basic user', () => {
cy.url().should('contain', 'courseCodes=%5B%22BSCS1001%22%2C%22BSCS1003%22%5D')
cy.cs('course-population-for-2023-2024').click()
cy.contains('Population of course Introduction to Programming 2023-2024 (open and normal)')
cy.contains('Introduction to Programming')
cy.contains('Class of 2023-2024')
cy.contains('Include substitutions, Open and normal')
// NOTE: These should also have `cy.intercept`s.
// The calls should be either cached or cheap so this is only
@@ -348,7 +352,9 @@ describe('Basic user', () => {
// Check the link has updated correctly
cy.cs('course-population-for-2022-2023').invoke('attr', 'href').should('include', 'BSCS1003')
cy.cs('course-population-for-2022-2023').click()
cy.contains('Population of course Data Structures and Algorithms 2022-2023 (open and normal)')
cy.contains('Data Structures and Algorithms')
cy.contains('Class of 2022-2023')
cy.contains('Include substitutions, Open and normal')
})
it('Searching course by name displays right courses', () => {
@@ -360,11 +366,11 @@ describe('Basic user', () => {
cy.contains('Search for courses').should('not.exist')
cy.contains('TKT10004 • Tietokantojen perusteet')
cy.contains('AYTKT10004 • Avoin yo: Tietokantojen perusteet')
cy.contains('BSCS2001 • Introduction to Databases')
cy.contains('581328 • Tietokantojen perusteet')
cy.contains('A581328 • Avoin yo: Tietokantojen perusteet')
cy.contains('TKT10004') // Tietokantojen perusteet
cy.contains('AYTKT10004') // Avoin yo: Tietokantojen perusteet
cy.contains('BSCS2001') // Introduction to Databases
cy.contains('581328') // Tietokantojen perusteet
cy.contains('A581328') // Avoin yo: Tietokantojen perusteet
clickNewQueryButton()
cy.contains('Please enter at least 5 characters for course name or 2 characters for course code.')
})
@@ -376,27 +382,26 @@ describe('Basic user', () => {
cy.cs(`course-TKT20001`).click()
cy.contains('Search for courses').should('not.exist')
cy.contains('TKT20001 • Tietorakenteet ja algoritmit')
cy.contains('AYTKT20001 • Avoin yo: Tietorakenteet ja algoritmit')
cy.contains('BSCS1003 • Data Structures and Algorithms')
cy.contains('58131 • Tietorakenteet')
cy.contains('TKT20001') // Tietorakenteet ja algoritmit
cy.contains('AYTKT20001') // Avoin yo: Tietorakenteet ja algoritmit
cy.contains('BSCS1003') // Data Structures and Algorithms
cy.contains('58131') // Tietorakenteet
clickNewQueryButton()
cy.contains('Please enter at least 5 characters for course name or 2 characters for course code.')
searchByCourseName('tietorakenteet ja algoritmit')
cy.cs(`course-TKT20001`).click()
cy.contains('Search for courses').should('not.exist')
cy.contains('TKT20001 • Tietorakenteet ja algoritmit')
cy.contains('AYTKT20001 • Avoin yo: Tietorakenteet ja algoritmit')
cy.contains('BSCS1003 • Data Structures and Algorithms')
cy.contains('58131 • Tietorakenteet')
cy.contains('TKT20001') // Tietorakenteet ja algoritmit
cy.contains('AYTKT20001') // Avoin yo: Tietorakenteet ja algoritmit
cy.contains('BSCS1003') // Data Structures and Algorithms
cy.contains('58131') // Tietorakenteet
})
it('"Select all search results" button is not showing unless "Select multiple courses" toggle is on', () => {
cy.contains('Search for courses')
searchByCourseCode('TKT')
cy.cs('select-multiple-courses-toggle').should('not.have.class', 'Mui-checked')
cy.contains('TKT10004, BSCS2001, 581328, A581328, AYTKT10004')
cy.contains('Select all search results').should('not.exist')
cy.cs('select-multiple-courses-toggle').click()
cy.cs('select-multiple-courses-toggle').should('have.class', 'Mui-checked')
@@ -410,11 +415,11 @@ describe('Basic user', () => {
cy.cs(`course-TKT10004`).click()
cy.contains('Search for courses').should('not.exist')
cy.contains('TKT10004 • Tietokantojen perusteet')
cy.contains('AYTKT10004 • Avoin yo: Tietokantojen perusteet')
cy.contains('BSCS2001 • Introduction to Databases')
cy.contains('581328 • Tietokantojen perusteet')
cy.contains('A581328 • Avoin yo: Tietokantojen perusteet')
cy.contains('TKT10004') // Tietokantojen perusteet
cy.contains('AYTKT10004') // Avoin yo: Tietokantojen perusteet
cy.contains('BSCS2001') // Introduction to Databases
cy.contains('581328') // Tietokantojen perusteet
cy.contains('A581328') // Avoin yo: Tietokantojen perusteet
cy.cs('ProviderOrganizationSelect').click()
cy.cs('ProviderOrganizationSelectOptionBoth').should('have.class', 'Mui-selected')
@@ -438,10 +443,14 @@ describe('Basic user', () => {
cy.contains('Search for courses')
searchByCourseCode('TKT20003')
cy.contains('tr', 'TKT20003').click()
cy.contains('TKT20003 • Käyttöjärjestelmät')
cy.contains('582219 • Käyttöjärjestelmät')
cy.contains('TKT20003') // Käyttöjärjestelmät
cy.contains('582219') // Käyttöjärjestelmät
cy.cs('course-population-for-2020-2021').click()
cy.contains('Population of course Käyttöjärjestelmät 2020-2021 (open and normal')
cy.contains('Käyttöjärjestelmät')
cy.contains('Class of 2020-2021, 19 students')
cy.contains('Include substitutions, Open and normal')
cy.contains('TKT20003')
cy.contains('Students (19)').click()
@@ -452,13 +461,17 @@ describe('Basic user', () => {
it('Population of course shows grades for each student', () => {
searchByCourseCode('TKT20001')
cy.cs(`course-TKT20001`).click()
cy.contains('TKT20001 • Tietorakenteet ja algoritmit')
cy.contains('AYTKT20001 • Avoin yo: Tietorakenteet ja algoritmit')
cy.contains('BSCS1003 • Data Structures and Algorithms')
cy.contains('58131 • Tietorakenteet')
cy.contains('TKT20001') // Tietorakenteet ja algoritmit
cy.contains('AYTKT20001') // Avoin yo: Tietorakenteet ja algoritmit
cy.contains('BSCS1003') // Data Structures and Algorithms
cy.contains('58131') // Tietorakenteet
cy.cs('course-population-for-2019-2020').click()
cy.contains('Population of course Tietorakenteet ja algoritmit 2019-2020 (open and normal)')
cy.contains('Students (33)').click()
cy.contains('Tietorakenteet ja algoritmit')
cy.contains('Class of 2019-2020, 32 students')
cy.contains('Include substitutions, Open and normal')
cy.contains('Students (32)').click()
cy.contains('td', '394776').siblings().eq(2).contains('3')
cy.contains('td', '497388').siblings().eq(2).contains('2')
})
@@ -466,13 +479,17 @@ describe('Basic user', () => {
it("In 'Course population' view, student numbers of students that the user isn't allowed to see are hidden", () => {
searchByCourseCode('TKT20001')
cy.cs(`course-TKT20001`).click()
cy.contains('TKT20001 • Tietorakenteet ja algoritmit')
cy.contains('AYTKT20001 • Avoin yo: Tietorakenteet ja algoritmit')
cy.contains('BSCS1003 • Data Structures and Algorithms')
cy.contains('58131 • Tietorakenteet')
cy.contains('TKT20001') // Tietorakenteet ja algoritmit
cy.contains('AYTKT20001') // Avoin yo: Tietorakenteet ja algoritmit
cy.contains('BSCS1003') // Data Structures and Algorithms
cy.contains('58131') // Tietorakenteet
cy.cs('course-population-for-2019-2020').click()
cy.contains('Population of course Tietorakenteet ja algoritmit 2019-2020 (open and normal)')
cy.contains('Students (33)').click()
cy.contains('Tietorakenteet ja algoritmit')
cy.contains('Class of 2019-2020, 32 students')
cy.contains('Include substitutions, Open and normal')
cy.contains('Students (32)').click()
cy.get('table tbody td').filter(':contains("Hidden")').should('have.length', 9)
})
})
@@ -480,10 +497,14 @@ describe('Basic user', () => {
it('Language distribution is correct', () => {
searchByCourseCode('TKT20003')
cy.cs(`course-TKT20003`).click()
cy.contains('TKT20003 • Käyttöjärjestelmät')
cy.contains('582219 • Käyttöjärjestelmät')
cy.contains('TKT20003') // Käyttöjärjestelmät
cy.contains('582219') // Käyttöjärjestelmät
cy.cs('course-population-for-2021-2022').click()
cy.contains('Population of course Käyttöjärjestelmät 2021-2022 (open and normal)')
cy.contains('Käyttöjärjestelmät')
cy.contains('Class of 2021-2022')
cy.contains('Include substitutions, Open and normal')
cy.contains('Language distribution').click()
cy.contains('td', 'finnish').siblings().eq(0).contains('5')
cy.contains('td', 'english').siblings().eq(0).contains('2')
@@ -499,7 +520,7 @@ describe('Basic user', () => {
searchByCourseCode('TKT10002')
cy.cs(`course-TKT10002`).click()
cy.contains('Search for courses').should('not.exist')
cy.contains('TKT10002 • Ohjelmoinnin perusteet')
cy.contains('TKT10002') // Ohjelmoinnin perusteet
})
it('Time range', () => {
@@ -516,7 +537,8 @@ describe('Basic user', () => {
cy.contains('Show population').should('be.enabled')
})
describe('Students tab', () => {
// TODO: Writing these as backend api tests
describe.skip('Students tab', () => {
describe('Info boxes', () => {
it('Student statistics table', () => {
cy.cs('StudentStatistics-info-box-button').click()
@@ -538,14 +560,14 @@ describe('Basic user', () => {
it('Show grades off, Separate by semesters off', () => {
const tableContents = [
// [Time, Total students, Passed, Failed, Enrolled no grade, Pass rate, Fail rate]
['Total', 187, 140, 11, 36, '74.87 %', '25.13 %'],
['Total', 183, 140, 6, 37, '76.50 %', '23.50 %'],
['2023-2024', 7, 1, 0, 6, '14.29 %', '85.71 %'],
['2022-2023', 33, 27, 0, 6, '81.82 %', '18.18 %'],
['2021-2022', 39, 15, 0, 24, '38.46 %', '61.54 %'],
['2020-2021', 23, 23, 0, null, '100.00 %', '0.00 %'],
['2022-2023', 32, 26, 0, 6, '81.25 %', '18.75 %'],
['2021-2022', 39, 14, 0, 25, '35.90 %', '64.10 %'],
['2020-2021', 21, 21, 0, null, '100.00 %', '0.00 %'],
['2019-2020', 28, 28, 0, null, '100.00 %', '0.00 %'],
['2018-2019', 30, 26, 4, null, '86.67 %', '13.33 %'],
['2017-2018', 26, 19, 7, null, '73.08 %', '26.92 %'],
['2018-2019', 29, 27, 2, null, '93.10 %', '6.90 %'],
['2017-2018', 26, 22, 4, null, '84.62 %', '15.38 %'],
['2016-2017', 1, 1, 0, null, '100.00 %', '0.00 %'],
]
checkTableContents(tableContents)
@@ -554,21 +576,21 @@ describe('Basic user', () => {
it('Show grades off, Separate by semesters on', () => {
const tableContents = [
// [Time, Total students, Passed, Failed, Enrolled no grade, Pass rate, Fail rate]
['Total', 191, 140, 13, 38, '73.30 %', '26.70 %'],
['Total', 185, 140, 6, 39, '75.68 %', '24.32 %'],
['Syksy 2023', 7, 1, 0, 6, '14.29 %', '85.71 %'],
['Kevät 2023', 10, 9, 0, 1, '90.00 %', '10.00 %'],
['Syksy 2022', 23, 18, 0, 5, '78.26 %', '21.74 %'],
['Syksy 2022', 22, 17, 0, 5, '77.27 %', '22.73 %'],
['Kevät 2022', 26, 9, 0, 17, '34.62 %', '65.38 %'],
['Syksy 2021', 15, 6, 0, 9, '40.00 %', '60.00 %'],
['Kevät 2021', 4, 4, 0, null, '100.00 %', '0.00 %'],
['Syksy 2020', 19, 19, 0, null, '100.00 %', '0.00 %'],
['Kevät 2020', 4, 4, 0, null, '100.00 %', '0.00 %'],
['Syksy 2019', 25, 24, 1, null, '96.00 %', '4.00 %'],
['Kevät 2019', 8, 8, 0, null, '100.00 %', '0.00 %'],
['Syksy 2018', 22, 18, 4, null, '81.82 %', '18.18 %'],
['Kevät 2018', 19, 12, 7, null, '63.16 %', '36.84 %'],
['Syksy 2017', 8, 7, 1, null, '87.50 %', '12.50 %'],
['Kevät 2017', 0, 0, 0, null, '–', '–'],
['Syksy 2021', 15, 5, 0, 10, '33.33 %', '66.67 %'],
['Kevät 2021', 3, 3, 0, null, '100.00 %', '0.00 %'],
['Syksy 2020', 18, 18, 0, null, '100.00 %', '0.00 %'],
['Kevät 2020', 3, 3, 0, null, '100.00 %', '0.00 %'],
['Syksy 2019', 25, 25, 0, null, '100.00 %', '0.00 %'],
['Kevät 2019', 7, 7, 0, null, '100.00 %', '0.00 %'],
['Syksy 2018', 22, 20, 2, null, '90.91 %', '9.09 %'],
['Kevät 2018', 18, 14, 4, null, '77.78 %', '22.22 %'],
['Syksy 2017', 8, 8, 0, null, '100.00 %', '0.00 %'],
['Kevät 2017', 0, 0, 0, 0, '–', '–'],
['Syksy 2016', 1, 1, 0, null, '100.00 %', '0.00 %'],
]
toggleSeparateBySemesters()
@@ -578,14 +600,14 @@ describe('Basic user', () => {
it('Show grades on, Separate by semesters off', () => {
const tableContents = [
// [Time, Total students, Failed, 1, 2, 3, 4, 5, Other passed, Enrolled no grade, Pass rate, Fail rate]
['Total', 187, 11, 3, 8, 5, 24, 99, 1, 36, '74.87 %', '25.13 %'],
['Total', 183, 6, 3, 8, 5, 24, 99, 1, 37, '76.50 %', '23.50 %'],
['2023-2024', 7, 0, 0, 0, 0, 0, 1, 0, 6, '14.29 %', '85.71 %'],
['2022-2023', 33, 0, 0, 0, 0, 6, 21, 0, 6, '81.82 %', '18.18 %'],
['2021-2022', 39, 0, 0, 0, 0, 2, 13, 0, 24, '38.46 %', '61.54 %'],
['2020-2021', 23, 0, 0, 2, 0, 2, 18, 1, null, '100.00 %', '0.00 %'],
['2022-2023', 32, 0, 0, 0, 0, 6, 20, 0, 6, '81.25 %', '18.75 %'],
['2021-2022', 39, 0, 0, 0, 0, 2, 12, 0, 25, '35.90 %', '64.10 %'],
['2020-2021', 21, 0, 0, 1, 0, 2, 17, 1, null, '100.00 %', '0.00 %'],
['2019-2020', 28, 0, 1, 4, 1, 5, 17, 0, null, '100.00 %', '0.00 %'],
['2018-2019', 30, 4, 2, 1, 2, 3, 18, 0, null, '86.67 %', '13.33 %'],
['2017-2018', 26, 7, 0, 1, 2, 5, 11, 0, null, '73.08 %', '26.92 %'],
['2018-2019', 29, 2, 2, 1, 2, 3, 19, 0, null, '93.10 %', '6.90 %'],
['2017-2018', 26, 4, 0, 2, 2, 5, 13, 0, null, '84.62 %', '15.38 %'],
['2016-2017', 1, 0, 0, 0, 0, 1, 0, 0, null, '100.00 %', '0.00 %'],
]
toggleShowGrades()
@@ -595,21 +617,21 @@ describe('Basic user', () => {
it('Show grades on, Separate by semesters on', () => {
const tableContents = [
// [Time, Total students, Failed, 1, 2, 3, 4, 5, Other passed, Enrolled no grade, Pass rate, Fail rate]
['Total', 191, 13, 3, 8, 5, 24, 99, 1, 38, '73.30 %', '26.70 %'],
['Total', 185, 6, 3, 8, 5, 24, 99, 1, 39, '75.68 %', '24.32 %'],
['Syksy 2023', 7, 0, 0, 0, 0, 0, 1, 0, 6, '14.29 %', '85.71 %'],
['Kevät 2023', 10, 0, 0, 0, 0, 3, 6, 0, 1, '90.00 %', '10.00 %'],
['Syksy 2022', 23, 0, 0, 0, 0, 3, 15, 0, 5, '78.26 %', '21.74 %'],
['Syksy 2022', 22, 0, 0, 0, 0, 3, 14, 0, 5, '77.27 %', '22.73 %'],
['Kevät 2022', 26, 0, 0, 0, 0, 2, 7, 0, 17, '34.62 %', '65.38 %'],
['Syksy 2021', 15, 0, 0, 0, 0, 0, 6, 0, 9, '40.00 %', '60.00 %'],
['Kevät 2021', 4, 0, 0, 0, 0, 0, 4, 0, null, '100.00 %', '0.00 %'],
['Syksy 2020', 19, 0, 0, 2, 0, 2, 14, 1, null, '100.00 %', '0.00 %'],
['Kevät 2020', 4, 0, 1, 0, 1, 0, 2, 0, null, '100.00 %', '0.00 %'],
['Syksy 2019', 25, 1, 0, 4, 0, 5, 15, 0, null, '96.00 %', '4.00 %'],
['Kevät 2019', 8, 0, 0, 0, 1, 1, 6, 0, null, '100.00 %', '0.00 %'],
['Syksy 2018', 22, 4, 2, 1, 1, 2, 12, 0, null, '81.82 %', '18.18 %'],
['Kevät 2018', 19, 7, 0, 1, 0, 3, 8, 0, null, '63.16 %', '36.84 %'],
['Syksy 2017', 8, 1, 0, 0, 2, 2, 3, 0, null, '87.50 %', '12.50 %'],
['Kevät 2017', 0, 0, 0, 0, 0, 0, 0, 0, null, '–', '–'],
['Syksy 2021', 15, 0, 0, 0, 0, 0, 5, 0, 10, '33.33 %', '66.67 %'],
['Kevät 2021', 3, 0, 0, 0, 0, 0, 3, 0, null, '100.00 %', '0.00 %'],
['Syksy 2020', 18, 0, 0, 1, 0, 2, 14, 1, null, '100.00 %', '0.00 %'],
['Kevät 2020', 3, 0, 0, 0, 1, 0, 2, 0, null, '100.00 %', '0.00 %'],
['Syksy 2019', 25, 0, 1, 4, 0, 5, 15, 0, null, '100.00 %', '0.00 %'],
['Kevät 2019', 7, 0, 0, 0, 1, 1, 5, 0, null, '100.00 %', '0.00 %'],
['Syksy 2018', 22, 2, 2, 1, 1, 2, 14, 0, null, '90.91 %', '9.09 %'],
['Kevät 2018', 18, 4, 0, 2, 0, 3, 9, 0, null, '77.78 %', '22.22 %'],
['Syksy 2017', 8, 0, 0, 0, 2, 2, 4, 0, null, '100.00 %', '0.00 %'],
['Kevät 2017', 0, 0, 0, 0, 0, 0, 0, 0, 0, '–', '–'],
['Syksy 2016', 1, 0, 0, 0, 0, 1, 0, 0, null, '100.00 %', '0.00 %'],
]
toggleShowGrades()
@@ -618,7 +640,8 @@ describe('Basic user', () => {
})
})
describe('Attempts tab', () => {
// TODO: Writing these as backend api tests
describe.skip('Attempts tab', () => {
beforeEach(() => {
openAttemptsTab()
})
@@ -644,15 +667,15 @@ describe('Basic user', () => {
it('Show grades off, Separate by semesters off', () => {
const tableContents = [
// [Time, Total attempts, Passed, Failed, Pass rate, Enrollments]
['Total', 186, 140, 16, '89.74 %', 73],
['2023-2024', 6, 1, 0, '16.67 %', 6],
['2022-2023', 26, 27, 0, '100.00 %', 26],
['2021-2022', 41, 15, 0, '36.59 %', 41],
['2020-2021', 23, 23, 0, '100.00 %', null],
['2019-2020', 29, 28, 1, '96.55 %', null],
['2018-2019', 32, 26, 6, '81.25 %', null],
['2017-2018', 28, 19, 9, '67.86 %', null],
['2016-2017', 1, 1, 0, '100.00 %', null],
['Total', 178, 140, 6, '95.89 %', 73],
['2023-2024', 6, 1, null, '16.67 %', 6],
['2022-2023', 26, 26, null, '100.00 %', 26],
['2021-2022', 41, 14, null, '34.15 %', 41],
['2020-2021', 21, 21, null, '100.00 %', null],
['2019-2020', 28, 28, null, '100.00 %', null],
['2018-2019', 29, 27, 2, '93.10 %', null],
['2017-2018', 26, 22, 4, '84.62 %', null],
['2016-2017', 1, 1, null, '100.00 %', null],
]
checkTableContents(tableContents)
})
@@ -660,20 +683,20 @@ describe('Basic user', () => {
it('Show grades off, Separate by semesters on', () => {
const tableContents = [
// [Time, Total attempts, Passed, Failed, Pass rate, Enrollments]
['Total', 186, 140, 16, '89.74 %', 73],
['Total', 178, 140, 6, '95.89 %', 73],
['Syksy 2023', 6, 1, 0, '16.67 %', 6],
['Kevät 2023', 11, 9, 0, '81.82 %', 11],
['Syksy 2022', 15, 18, 0, '100.00 %', 15],
['Syksy 2022', 15, 17, 0, '100.00 %', 15],
['Kevät 2022', 29, 9, 0, '31.03 %', 29],
['Syksy 2021', 12, 6, 0, '50.00 %', 12],
['Kevät 2021', 4, 4, 0, '100.00 %', null],
['Syksy 2020', 19, 19, 0, '100.00 %', null],
['Kevät 2020', 4, 4, 0, '100.00 %', null],
['Syksy 2019', 25, 24, 1, '96.00 %', null],
['Kevät 2019', 8, 8, 0, '100.00 %', null],
['Syksy 2018', 24, 18, 6, '75.00 %', null],
['Kevät 2018', 20, 12, 8, '60.00 %', null],
['Syksy 2017', 8, 7, 1, '87.50 %', null],
['Syksy 2021', 12, 5, 0, '41.67 %', 12],
['Kevät 2021', 3, 3, 0, '100.00 %', null],
['Syksy 2020', 18, 18, 0, '100.00 %', null],
['Kevät 2020', 3, 3, 0, '100.00 %', null],
['Syksy 2019', 25, 25, 0, '100.00 %', null],
['Kevät 2019', 7, 7, 0, '100.00 %', null],
['Syksy 2018', 22, 20, 2, '90.91 %', null],
['Kevät 2018', 18, 14, 4, '77.78 %', null],
['Syksy 2017', 8, 8, 0, '100.00 %', null],
['Kevät 2017', 0, 0, 0, '–', 0],
['Syksy 2016', 1, 1, 0, '100.00 %', null],
]
@@ -684,14 +707,14 @@ describe('Basic user', () => {
it('Show grades on, Separate by semesters off', () => {
const tableContents = [
// [Time, Total attempts, 0, 1, 2, 3, 4, 5, Other passed]
['Total', 186, 16, 3, 8, 5, 24, 99, 1],
['Total', 178, 6, 3, 8, 5, 24, 99, 1],
['2023-2024', 6, 0, 0, 0, 0, 0, 1, 0],
['2022-2023', 26, 0, 0, 0, 0, 6, 21, 0],
['2021-2022', 41, 0, 0, 0, 0, 2, 13, 0],
['2020-2021', 23, 0, 0, 2, 0, 2, 18, 1],
['2019-2020', 29, 1, 1, 4, 1, 5, 17, 0],
['2018-2019', 32, 6, 2, 1, 2, 3, 18, 0],
['2017-2018', 28, 9, 0, 1, 2, 5, 11, 0],
['2022-2023', 26, 0, 0, 0, 0, 6, 20, 0],
['2021-2022', 41, 0, 0, 0, 0, 2, 12, 0],
['2020-2021', 21, 0, 0, 1, 0, 2, 17, 1],
['2019-2020', 28, 0, 1, 4, 1, 5, 17, 0],
['2018-2019', 29, 2, 2, 1, 2, 3, 19, 0],
['2017-2018', 26, 4, 0, 2, 2, 5, 13, 0],
['2016-2017', 1, 0, 0, 0, 0, 1, 0, 0],
]
toggleShowGrades()
@@ -701,20 +724,20 @@ describe('Basic user', () => {
it('Show grades on, Separate by semesters on', () => {
const tableContents = [
// [Time, Total attempts, 0, 1, 2, 3, 4, 5, Other passed]
['Total', 186, 16, 3, 8, 5, 24, 99, 1],
['Total', 178, 6, 3, 8, 5, 24, 99, 1],
['Syksy 2023', 6, 0, 0, 0, 0, 0, 1, 0],
['Kevät 2023', 11, 0, 0, 0, 0, 3, 6, 0],
['Syksy 2022', 15, 0, 0, 0, 0, 3, 15, 0],
['Syksy 2022', 15, 0, 0, 0, 0, 3, 14, 0],
['Kevät 2022', 29, 0, 0, 0, 0, 2, 7, 0],
['Syksy 2021', 12, 0, 0, 0, 0, 0, 6, 0],
['Kevät 2021', 4, 0, 0, 0, 0, 0, 4, 0],
['Syksy 2020', 19, 0, 0, 2, 0, 2, 14, 1],
['Kevät 2020', 4, 0, 1, 0, 1, 0, 2, 0],
['Syksy 2019', 25, 1, 0, 4, 0, 5, 15, 0],
['Kevät 2019', 8, 0, 0, 0, 1, 1, 6, 0],
['Syksy 2018', 24, 6, 2, 1, 1, 2, 12, 0],
['Kevät 2018', 20, 8, 0, 1, 0, 3, 8, 0],
['Syksy 2017', 8, 1, 0, 0, 2, 2, 3, 0],
['Syksy 2021', 12, 0, 0, 0, 0, 0, 5, 0],
['Kevät 2021', 3, 0, 0, 0, 0, 0, 3, 0],
['Syksy 2020', 18, 0, 0, 1, 0, 2, 14, 1],
['Kevät 2020', 3, 0, 0, 0, 1, 0, 2, 0],
['Syksy 2019', 25, 0, 1, 4, 0, 5, 15, 0],
['Kevät 2019', 7, 0, 0, 0, 1, 1, 5, 0],
['Syksy 2018', 22, 2, 2, 1, 1, 2, 14, 0],
['Kevät 2018', 18, 4, 0, 2, 0, 3, 9, 0],
['Syksy 2017', 8, 0, 0, 0, 2, 2, 4, 0],
['Kevät 2017', 0, 0, 0, 0, 0, 0, 0, 0],
['Syksy 2016', 1, 0, 0, 0, 0, 1, 0, 0],
]
@@ -732,11 +755,11 @@ describe('Basic user', () => {
searchByCourseCode('TKT10002')
cy.cs(`course-TKT10002`).click()
cy.contains('Search for courses').should('not.exist')
cy.contains('TKT10002 • Ohjelmoinnin perusteet')
cy.contains('AYTKT10002 • Avoin yo: Ohjelmoinnin perusteet')
cy.contains('BSCS1001 • Introduction to Programming')
cy.contains('581325 • Ohjelmoinnin perusteet')
cy.contains('A581325 • Avoin yo: Ohjelmoinnin perusteet')
cy.contains('TKT10002') // Ohjelmoinnin perusteet
cy.contains('AYTKT10002') // Avoin yo: Ohjelmoinnin perusteet
cy.contains('BSCS1001') // Introduction to Programming
cy.contains('581325') // Ohjelmoinnin perusteet
cy.contains('A581325') // Avoin yo: Ohjelmoinnin perusteet
})
it('Time range', () => {
@@ -753,22 +776,23 @@ describe('Basic user', () => {
cy.contains('Show population').should('be.enabled')
})
describe('Students tab', () => {
// TODO: Writing these as backend api tests
describe.skip('Students tab', () => {
it('Show grades off, Separate by semesters off', () => {
const tableContents = [
// [Time, Total students, Passed, Failed, Enrolled no grade, Pass rate, Fail rate]
['Total', 284, 237, 16, 31, '83.45 %', '16.55 %'],
['Total', 277, 237, 8, 32, '85.56 %', '14.44 %'],
['2023-2024', 8, 2, 0, 6, '25.00 %', '75.00 %'],
['2022-2023', 35, 28, 0, 7, '80.00 %', '20.00 %'],
['2021-2022', 45, 27, 0, 18, '60.00 %', '40.00 %'],
['2020-2021', 33, 33, 0, null, '100.00 %', '0.00 %'],
['2019-2020', 57, 56, 1, null, '98.25 %', '1.75 %'],
['2018-2019', 42, 38, 4, null, '90.48 %', '9.52 %'],
['2017-2018', 32, 25, 7, null, '78.13 %', '21.88 %'],
['2016-2017', 7, 6, 1, null, '85.71 %', '14.29 %'],
['2015-2016', 4, 3, 1, null, '75.00 %', '25.00 %'],
['2014-2015', 6, 6, 0, null, '100.00 %', '0.00 %'],
['2013-2014', 2, 1, 1, null, '50.00 %', '50.00 %'],
['2022-2023', 34, 27, 0, 7, '79.41 %', '20.59 %'],
['2021-2022', 45, 26, 0, 19, '57.78 %', '42.22 %'],
['2020-2021', 31, 31, 0, null, '100.00 %', '0.00 %'],
['2019-2020', 56, 56, 0, null, '100.00 %', '0.00 %'],
['2018-2019', 41, 39, 2, null, '95.12 %', '4.88 %'],
['2017-2018', 32, 28, 4, null, '87.50 %', '12.50 %'],
['2016-2017', 6, 5, 1, null, '83.33 %', '16.67 %'],
['2015-2016', 4, 4, 0, null, '100.00 %', '0.00 %'],
['2014-2015', 5, 5, 0, null, '100.00 %', '0.00 %'],
['2013-2014', 2, 2, 0, null, '100.00 %', '0.00 %'],
['2012-2013', 4, 4, 0, null, '100.00 %', '0.00 %'],
['2011-2012', 1, 1, 0, null, '100.00 %', '0.00 %'],
['2010-2011', 1, 1, 0, null, '100.00 %', '0.00 %'],
@@ -790,28 +814,28 @@ describe('Basic user', () => {
it('Show grades off, Separate by semesters on', () => {
const tableContents = [
// [Time, Total students, Passed, Failed, Enrolled no grade, Pass rate, Fail rate]
['Total', 293, 239, 19, 35, '81.57 %', '18.43 %'],
['Total', 282, 237, 9, 36, '84.04 %', '15.96 %'],
['Syksy 2023', 8, 2, 0, 6, '25.00 %', '75.00 %'],
['Kevät 2023', 12, 10, 0, 2, '83.33 %', '16.67 %'],
['Syksy 2022', 23, 18, 0, 5, '78.26 %', '21.74 %'],
['Syksy 2022', 22, 17, 0, 5, '77.27 %', '22.73 %'],
['Kevät 2022', 28, 11, 0, 17, '39.29 %', '60.71 %'],
['Syksy 2021', 21, 16, 0, 5, '76.19 %', '23.81 %'],
['Kevät 2021', 8, 8, 0, null, '100.00 %', '0.00 %'],
['Syksy 2020', 25, 25, 0, null, '100.00 %', '0.00 %'],
['Kevät 2020', 26, 26, 0, null, '100.00 %', '0.00 %'],
['Syksy 2019', 33, 31, 2, null, '93.94 %', '6.06 %'],
['Kevät 2019', 17, 16, 1, null, '94.12 %', '5.88 %'],
['Syksy 2018', 27, 23, 4, null, '85.19 %', '14.81 %'],
['Kevät 2018', 24, 17, 7, null, '70.83 %', '29.17 %'],
['Syksy 2017', 9, 8, 1, null, '88.89 %', '11.11 %'],
['Syksy 2021', 21, 15, 0, 6, '71.43 %', '28.57 %'],
['Kevät 2021', 7, 7, 0, null, '100.00 %', '0.00 %'],
['Syksy 2020', 24, 24, 0, null, '100.00 %', '0.00 %'],
['Kevät 2020', 24, 24, 0, null, '100.00 %', '0.00 %'],
['Syksy 2019', 32, 32, 0, null, '100.00 %', '0.00 %'],
['Kevät 2019', 15, 14, 1, null, '93.33 %', '6.67 %'],
['Syksy 2018', 27, 25, 2, null, '92.59 %', '7.41 %'],
['Kevät 2018', 23, 19, 4, null, '82.61 %', '17.39 %'],
['Syksy 2017', 9, 9, 0, null, '100.00 %', '0.00 %'],
['Kevät 2017', 4, 3, 1, null, '75.00 %', '25.00 %'],
['Syksy 2016', 3, 3, 0, null, '100.00 %', '0.00 %'],
['Kevät 2016', 3, 2, 1, null, '66.67 %', '33.33 %'],
['Syksy 2016', 2, 2, 0, null, '100.00 %', '0.00 %'],
['Kevät 2016', 3, 3, 0, null, '100.00 %', '0.00 %'],
['Syksy 2015', 1, 1, 0, null, '100.00 %', '0.00 %'],
['Kevät 2015', 3, 3, 0, null, '100.00 %', '0.00 %'],
['Syksy 2014', 3, 3, 0, null, '100.00 %', '0.00 %'],
['Syksy 2014', 2, 2, 0, null, '100.00 %', '0.00 %'],
['Kevät 2014', 1, 1, 0, null, '100.00 %', '0.00 %'],
['Syksy 2013', 1, 0, 1, null, '0.00 %', '100.00 %'],
['Syksy 2013', 1, 1, 0, null, '100.00 %', '0.00 %'],
['Kevät 2013', 0, 0, 0, 0, '–', '–'],
['Syksy 2012', 4, 4, 0, null, '100.00 %', '0.00 %'],
['Kevät 2012', 1, 1, 0, null, '100.00 %', '0.00 %'],
@@ -848,18 +872,18 @@ describe('Basic user', () => {
it('Show grades on, Separate by semesters off', () => {
const tableContents = [
// [Time, Total students, Failed, 1, 2, 3, 4, 5, Other passed, Enrolled no grade, Pass rate, Fail rate]
['Total', 284, 16, 4, 14, 9, 37, 168, 5, 31, '83.45 %', '16.55 %'],
['Total', 277, 8, 4, 14, 9, 37, 168, 5, 32, '85.56 %', '14.44 %'],
['2023-2024', 8, 0, 0, 0, 0, 0, 2, 0, 6, '25.00 %', '75.00 %'],
['2022-2023', 35, 0, 0, 0, 0, 6, 22, 0, 7, '80.00 %', '20.00 %'],
['2021-2022', 45, 0, 0, 1, 0, 2, 24, 0, 18, '60.00 %', '40.00 %'],
['2020-2021', 33, 0, 0, 2, 1, 2, 27, 1, null, '100.00 %', '0.00 %'],
['2019-2020', 57, 1, 1, 5, 3, 9, 38, 0, null, '98.25 %', '1.75 %'],
['2018-2019', 42, 4, 3, 2, 2, 6, 25, 0, null, '90.48 %', '9.52 %'],
['2017-2018', 32, 7, 0, 2, 2, 6, 15, 0, null, '78.13 %', '21.88 %'],
['2016-2017', 7, 1, 0, 0, 1, 1, 4, 0, null, '85.71 %', '14.29 %'],
['2015-2016', 4, 1, 0, 1, 0, 0, 1, 1, null, '75.00 %', '25.00 %'],
['2014-2015', 6, 0, 0, 1, 0, 0, 2, 3, null, '100.00 %', '0.00 %'],
['2013-2014', 2, 1, 0, 0, 0, 0, 1, 0, null, '50.00 %', '50.00 %'],
['2022-2023', 34, 0, 0, 0, 0, 6, 21, 0, 7, '79.41 %', '20.59 %'],
['2021-2022', 45, 0, 0, 1, 0, 2, 23, 0, 19, '57.78 %', '42.22 %'],
['2020-2021', 31, 0, 0, 1, 1, 2, 26, 1, null, '100.00 %', '0.00 %'],
['2019-2020', 56, 0, 1, 5, 3, 9, 38, 0, null, '100.00 %', '0.00 %'],
['2018-2019', 41, 2, 3, 2, 2, 6, 26, 0, null, '95.12 %', '4.88 %'],
['2017-2018', 32, 4, 0, 3, 2, 6, 17, 0, null, '87.50 %', '12.50 %'],
['2016-2017', 6, 1, 0, 0, 1, 1, 3, 0, null, '83.33 %', '16.67 %'],
['2015-2016', 4, 0, 0, 1, 0, 0, 2, 1, null, '100.00 %', '0.00 %'],
['2014-2015', 5, 0, 0, 1, 0, 0, 1, 3, null, '100.00 %', '0.00 %'],
['2013-2014', 2, 0, 0, 0, 0, 0, 2, 0, null, '100.00 %', '0.00 %'],
['2012-2013', 4, 0, 0, 0, 0, 3, 1, 0, null, '100.00 %', '0.00 %'],
['2011-2012', 1, 0, 0, 0, 0, 0, 1, 0, null, '100.00 %', '0.00 %'],
['2010-2011', 1, 0, 0, 0, 0, 1, 0, 0, null, '100.00 %', '0.00 %'],
@@ -882,28 +906,28 @@ describe('Basic user', () => {
it('Show grades on, Separate by semesters on', () => {
const tableContents = [
// [Time, Total students, Failed, 1, 2, 3, 4, 5, Other passed, Enrolled no grade, Pass rate, Fail rate]
['Total', 293, 19, 5, 15, 9, 37, 168, 5, 35, '81.57 %', '18.43 %'],
['Total', 282, 9, 4, 14, 9, 37, 168, 5, 36, '84.04 %', '15.96 %'],
['Syksy 2023', 8, 0, 0, 0, 0, 0, 2, 0, 6, '25.00 %', '75.00 %'],
['Kevät 2023', 12, 0, 0, 0, 0, 3, 7, 0, 2, '83.33 %', '16.67 %'],
['Syksy 2022', 23, 0, 0, 0, 0, 3, 15, 0, 5, '78.26 %', '21.74 %'],
['Syksy 2022', 22, 0, 0, 0, 0, 3, 14, 0, 5, '77.27 %', '22.73 %'],
['Kevät 2022', 28, 0, 0, 0, 0, 2, 9, 0, 17, '39.29 %', '60.71 %'],
['Syksy 2021', 21, 0, 0, 1, 0, 0, 15, 0, 5, '76.19 %', '23.81 %'],
['Kevät 2021', 8, 0, 0, 0, 0, 0, 8, 0, null, '100.00 %', '0.00 %'],
['Syksy 2020', 25, 0, 0, 2, 1, 2, 19, 1, null, '100.00 %', '0.00 %'],
['Kevät 2020', 26, 0, 1, 1, 2, 4, 18, 0, null, '100.00 %', '0.00 %'],
['Syksy 2019', 33, 2, 1, 4, 1, 5, 20, 0, null, '93.94 %', '6.06 %'],
['Kevät 2019', 17, 1, 1, 1, 1, 2, 11, 0, null, '94.12 %', '5.88 %'],
['Syksy 2018', 27, 4, 2, 2, 1, 4, 14, 0, null, '85.19 %', '14.81 %'],
['Kevät 2018', 24, 7, 0, 2, 0, 4, 11, 0, null, '70.83 %', '29.17 %'],
['Syksy 2017', 9, 1, 0, 0, 2, 2, 4, 0, null, '88.89 %', '11.11 %'],
['Syksy 2021', 21, 0, 0, 1, 0, 0, 14, 0, 6, '71.43 %', '28.57 %'],
['Kevät 2021', 7, 0, 0, 0, 0, 0, 7, 0, null, '100.00 %', '0.00 %'],
['Syksy 2020', 24, 0, 0, 1, 1, 2, 19, 1, null, '100.00 %', '0.00 %'],
['Kevät 2020', 24, 0, 0, 1, 2, 4, 17, 0, null, '100.00 %', '0.00 %'],
['Syksy 2019', 32, 0, 1, 4, 1, 5, 21, 0, null, '100.00 %', '0.00 %'],
['Kevät 2019', 15, 1, 1, 1, 1, 2, 9, 0, null, '93.33 %', '6.67 %'],
['Syksy 2018', 27, 2, 2, 1, 1, 4, 17, 0, null, '92.59 %', '7.41 %'],
['Kevät 2018', 23, 4, 0, 3, 0, 4, 12, 0, null, '82.61 %', '17.39 %'],
['Syksy 2017', 9, 0, 0, 0, 2, 2, 5, 0, null, '100.00 %', '0.00 %'],
['Kevät 2017', 4, 1, 0, 0, 1, 0, 2, 0, null, '75.00 %', '25.00 %'],
['Syksy 2016', 3, 0, 0, 0, 0, 1, 2, 0, null, '100.00 %', '0.00 %'],
['Kevät 2016', 3, 1, 0, 1, 0, 0, 1, 0, null, '66.67 %', '33.33 %'],
['Syksy 2016', 2, 0, 0, 0, 0, 1, 1, 0, null, '100.00 %', '0.00 %'],
['Kevät 2016', 3, 0, 0, 1, 0, 0, 2, 0, null, '100.00 %', '0.00 %'],
['Syksy 2015', 1, 0, 0, 0, 0, 0, 0, 1, null, '100.00 %', '0.00 %'],
['Kevät 2015', 3, 0, 0, 0, 0, 0, 0, 3, null, '100.00 %', '0.00 %'],
['Syksy 2014', 3, 0, 0, 1, 0, 0, 2, 0, null, '100.00 %', '0.00 %'],
['Syksy 2014', 2, 0, 0, 1, 0, 0, 1, 0, null, '100.00 %', '0.00 %'],
['Kevät 2014', 1, 0, 0, 0, 0, 0, 1, 0, null, '100.00 %', '0.00 %'],
['Syksy 2013', 1, 1, 0, 0, 0, 0, 0, 0, null, '0.00 %', '100.00 %'],
['Syksy 2013', 1, 0, 0, 0, 0, 0, 1, 0, null, '100.00 %', '0.00 %'],
['Kevät 2013', 0, 0, 0, 0, 0, 0, 0, 0, null, '–', '–'],
['Syksy 2012', 4, 0, 0, 0, 0, 3, 1, 0, null, '100.00 %', '0.00 %'],
['Kevät 2012', 1, 0, 0, 0, 0, 0, 1, 0, null, '100.00 %', '0.00 %'],
@@ -939,7 +963,8 @@ describe('Basic user', () => {
})
})
describe('Attempts tab', () => {
// TODO: Writing these as backend api tests
describe.skip('Attempts tab', () => {
beforeEach(() => {
openAttemptsTab()
})
@@ -947,18 +972,18 @@ describe('Basic user', () => {
it('Show grades off, Separate by semesters off', () => {
const tableContents = [
// [Time, Total attempts, Passed, Failed, Pass rate, Enrollments]
['Total', 288, 241, 25, '90.60 %', 79],
['Total', 270, 237, 9, '96.34 %', 79],
['2023-2024', 6, 2, 0, '33.33 %', 6],
['2022-2023', 28, 28, 0, '100.00 %', 28],
['2021-2022', 45, 27, 0, '60.00 %', 45],
['2020-2021', 34, 34, 0, '100.00 %', null],
['2019-2020', 60, 57, 3, '95.00 %', null],
['2018-2019', 46, 39, 7, '84.78 %', null],
['2017-2018', 35, 26, 9, '74.29 %', null],
['2016-2017', 7, 6, 1, '85.71 %', null],
['2015-2016', 5, 3, 2, '60.00 %', null],
['2014-2015', 7, 6, 1, '85.71 %', null],
['2013-2014', 2, 1, 1, '50.00 %', null],
['2022-2023', 28, 27, 0, '96.43 %', 28],
['2021-2022', 45, 26, 0, '57.78 %', 45],
['2020-2021', 31, 31, 0, '100.00 %', null],
['2019-2020', 56, 56, 0, '100.00 %', null],
['2018-2019', 42, 39, 3, '92.86 %', null],
['2017-2018', 32, 28, 4, '87.50 %', null],
['2016-2017', 6, 5, 1, '83.33 %', null],
['2015-2016', 4, 4, 0, '100.00 %', null],
['2014-2015', 5, 5, 0, '100.00 %', null],
['2013-2014', 2, 2, 0, '100.00 %', null],
['2012-2013', 4, 4, 0, '100.00 %', null],
['2011-2012', 1, 1, 0, '100.00 %', null],
['2010-2011', 1, 1, 0, '100.00 %', null],
@@ -980,55 +1005,55 @@ describe('Basic user', () => {
it('Show grades off, Separate by semesters on', () => {
const tableContents = [
// [Time, Total attempts, Passed, Failed, Pass rate, Enrollments]
['Total', 288, 241, 25, '90.60 %', 79],
['Total', 270, 237, 9, '96.34 %', 79],
['Syksy 2023', 6, 2, 0, '33.33 %', 6],
['Kevät 2023', 13, 10, 0, '76.92 %', 13],
['Syksy 2022', 15, 18, 0, '100.00 %', 15],
['Syksy 2022', 15, 17, 0, '100.00 %', 15],
['Kevät 2022', 30, 11, 0, '36.67 %', 30],
['Syksy 2021', 15, 16, 0, '100.00 %', 15],
['Kevät 2021', 8, 8, 0, '100.00 %', null],
['Syksy 2020', 26, 26, 0, '100.00 %', null],
['Kevät 2020', 26, 26, 0, '100.00 %', null],
['Syksy 2019', 34, 31, 3, '91.18 %', null],
['Kevät 2019', 17, 16, 1, '94.12 %', null],
['Syksy 2018', 29, 23, 6, '79.31 %', null],
['Kevät 2018', 26, 18, 8, '69.23 %', null],
['Syksy 2017', 9, 8, 1, '88.89 %', null],
['Syksy 2021', 15, 15, 0, '100.00 %', 15],
['Kevät 2021', 7, 7, 0, '100.00 %', null],
['Syksy 2020', 24, 24, 0, '100.00 %', null],
['Kevät 2020', 24, 24, 0, '100.00 %', null],
['Syksy 2019', 32, 32, 0, '100.00 %', null],
['Kevät 2019', 15, 14, 1, '93.33 %', null],
['Syksy 2018', 27, 25, 2, '92.59 %', null],
['Kevät 2018', 23, 19, 4, '82.61 %', null],
['Syksy 2017', 9, 9, 0, '100.00 %', null],
['Kevät 2017', 4, 3, 1, '75.00 %', null],
['Syksy 2016', 3, 3, 0, '100.00 %', null],
['Kevät 2016', 4, 2, 2, '50.00 %', null],
['Syksy 2016', 2, 2, 0, '100.00 %', null],
['Kevät 2016', 3, 3, 0, '100.00 %', null],
['Syksy 2015', 1, 1, 0, '100.00 %', null],
['Kevät 2015', 4, 3, 1, '75.00 %', null],
['Syksy 2014', 3, 3, 0, '100.00 %', null],
['Kevät 2015', 3, 3, 0, '100.00 %', null],
['Syksy 2014', 2, 2, 0, '100.00 %', null],
['Kevät 2014', 1, 1, 0, '100.00 %', null],
['Syksy 2013', 1, 0, 1, '0.00 %', null],
['Kevät 2013', 0, 0, 0, '–', 0],
['Syksy 2013', 1, 1, 0, '100.00 %', null],
['Kevät 2013', 0, 0, 0, '–', null],
['Syksy 2012', 4, 4, 0, '100.00 %', null],
['Kevät 2012', 1, 1, 0, '100.00 %', null],
['Syksy 2011', 0, 0, 0, '–', 0],
['Syksy 2011', 0, 0, 0, '–', null],
['Kevät 2011', 1, 1, 0, '100.00 %', null],
['Syksy 2010', 0, 0, 0, '–', 0],
['Kevät 2010', 0, 0, 0, '–', 0],
['Syksy 2009', 0, 0, 0, '–', 0],
['Kevät 2009', 0, 0, 0, '–', 0],
['Syksy 2010', 0, 0, 0, '–', null],
['Kevät 2010', 0, 0, 0, '–', null],
['Syksy 2009', 0, 0, 0, '–', null],
['Kevät 2009', 0, 0, 0, '–', null],
['Syksy 2008', 1, 0, 1, '0.00 %', null],
['Kevät 2008', 1, 1, 0, '100.00 %', null],
['Syksy 2007', 0, 0, 0, '–', 0],
['Syksy 2007', 0, 0, 0, '–', null],
['Kevät 2007', 1, 1, 0, '100.00 %', null],
['Syksy 2006', 0, 0, 0, '–', 0],
['Kevät 2006', 0, 0, 0, '–', 0],
['Syksy 2006', 0, 0, 0, '–', null],
['Kevät 2006', 0, 0, 0, '–', null],
['Syksy 2005', 1, 1, 0, '100.00 %', null],
['Kevät 2005', 0, 0, 0, '–', 0],
['Syksy 2004', 0, 0, 0, '–', 0],
['Kevät 2005', 0, 0, 0, '–', null],
['Syksy 2004', 0, 0, 0, '–', null],
['Kevät 2004', 1, 1, 0, '100.00 %', null],
['Syksy 2003', 0, 0, 0, '–', 0],
['Kevät 2003', 0, 0, 0, '–', 0],
['Syksy 2002', 0, 0, 0, '–', 0],
['Kevät 2002', 0, 0, 0, '–', 0],
['Syksy 2001', 0, 0, 0, '–', 0],
['Kevät 2001', 0, 0, 0, '–', 0],
['Syksy 2000', 0, 0, 0, '–', 0],
['Kevät 2000', 0, 0, 0, '–', 0],
['Syksy 2003', 0, 0, 0, '–', null],
['Kevät 2003', 0, 0, 0, '–', null],
['Syksy 2002', 0, 0, 0, '–', null],
['Kevät 2002', 0, 0, 0, '–', null],
['Syksy 2001', 0, 0, 0, '–', null],
['Kevät 2001', 0, 0, 0, '–', null],
['Syksy 2000', 0, 0, 0, '–', null],
['Kevät 2000', 0, 0, 0, '–', null],
['Syksy 1999', 2, 2, 0, '100.00 %', null],
]
toggleSeparateBySemesters()
@@ -1038,18 +1063,18 @@ describe('Basic user', () => {
it('Show grades on, Separate by semesters off', () => {
const tableContents = [
// [Time, Total attempts, 0, 1, 2, 3, 4, 5, Other passed]
['Total', 288, 25, 6, 15, 9, 37, 169, 5],
['Total', 270, 9, 4, 14, 9, 37, 168, 5],
['2023-2024', 6, 0, 0, 0, 0, 0, 2, 0],
['2022-2023', 28, 0, 0, 0, 0, 6, 22, 0],
['2021-2022', 45, 0, 0, 1, 0, 2, 24, 0],
['2020-2021', 34, 0, 0, 2, 1, 2, 28, 1],
['2019-2020', 60, 3, 2, 5, 3, 9, 38, 0],
['2018-2019', 46, 7, 3, 3, 2, 6, 25, 0],
['2017-2018', 35, 9, 1, 2, 2, 6, 15, 0],
['2016-2017', 7, 1, 0, 0, 1, 1, 4, 0],
['2015-2016', 5, 2, 0, 1, 0, 0, 1, 1],
['2014-2015', 7, 1, 0, 1, 0, 0, 2, 3],
['2013-2014', 2, 1, 0, 0, 0, 0, 1, 0],
['2022-2023', 28, 0, 0, 0, 0, 6, 21, 0],
['2021-2022', 45, 0, 0, 1, 0, 2, 23, 0],
['2020-2021', 31, 0, 0, 1, 1, 2, 26, 1],
['2019-2020', 56, 0, 1, 5, 3, 9, 38, 0],
['2018-2019', 42, 3, 3, 2, 2, 6, 26, 0],
['2017-2018', 32, 4, 0, 3, 2, 6, 17, 0],
['2016-2017', 6, 1, 0, 0, 1, 1, 3, 0],
['2015-2016', 4, 0, 0, 1, 0, 0, 2, 1],
['2014-2015', 5, 0, 0, 1, 0, 0, 1, 3],
['2013-2014', 2, 0, 0, 0, 0, 0, 2, 0],
['2012-2013', 4, 0, 0, 0, 0, 3, 1, 0],
['2011-2012', 1, 0, 0, 0, 0, 0, 1, 0],
['2010-2011', 1, 0, 0, 0, 0, 1, 0, 0],
@@ -1072,28 +1097,28 @@ describe('Basic user', () => {
it('Show grades on, Separate by semesters on', () => {
const tableContents = [
// [Time, Total attempts, 0, 1, 2, 3, 4, 5, Other passed]
['Total', 288, 25, 6, 15, 9, 37, 169, 5],
['Total', 270, 9, 4, 14, 9, 37, 168, 5],
['Syksy 2023', 6, 0, 0, 0, 0, 0, 2, 0],
['Kevät 2023', 13, 0, 0, 0, 0, 3, 7, 0],
['Syksy 2022', 15, 0, 0, 0, 0, 3, 15, 0],
['Syksy 2022', 15, 0, 0, 0, 0, 3, 14, 0],
['Kevät 2022', 30, 0, 0, 0, 0, 2, 9, 0],
['Syksy 2021', 15, 0, 0, 1, 0, 0, 15, 0],
['Kevät 2021', 8, 0, 0, 0, 0, 0, 8, 0],
['Syksy 2020', 26, 0, 0, 2, 1, 2, 20, 1],
['Kevät 2020', 26, 0, 1, 1, 2, 4, 18, 0],
['Syksy 2019', 34, 3, 1, 4, 1, 5, 20, 0],
['Kevät 2019', 17, 1, 1, 1, 1, 2, 11, 0],
['Syksy 2018', 29, 6, 2, 2, 1, 4, 14, 0],
['Kevät 2018', 26, 8, 1, 2, 0, 4, 11, 0],
['Syksy 2017', 9, 1, 0, 0, 2, 2, 4, 0],
['Syksy 2021', 15, 0, 0, 1, 0, 0, 14, 0],
['Kevät 2021', 7, 0, 0, 0, 0, 0, 7, 0],
['Syksy 2020', 24, 0, 0, 1, 1, 2, 19, 1],
['Kevät 2020', 24, 0, 0, 1, 2, 4, 17, 0],
['Syksy 2019', 32, 0, 1, 4, 1, 5, 21, 0],
['Kevät 2019', 15, 1, 1, 1, 1, 2, 9, 0],
['Syksy 2018', 27, 2, 2, 1, 1, 4, 17, 0],
['Kevät 2018', 23, 4, 0, 3, 0, 4, 12, 0],
['Syksy 2017', 9, 0, 0, 0, 2, 2, 5, 0],
['Kevät 2017', 4, 1, 0, 0, 1, 0, 2, 0],
['Syksy 2016', 3, 0, 0, 0, 0, 1, 2, 0],
['Kevät 2016', 4, 2, 0, 1, 0, 0, 1, 0],
['Syksy 2016', 2, 0, 0, 0, 0, 1, 1, 0],
['Kevät 2016', 3, 0, 0, 1, 0, 0, 2, 0],
['Syksy 2015', 1, 0, 0, 0, 0, 0, 0, 1],
['Kevät 2015', 4, 1, 0, 0, 0, 0, 0, 3],
['Syksy 2014', 3, 0, 0, 1, 0, 0, 2, 0],
['Kevät 2015', 3, 0, 0, 0, 0, 0, 0, 3],
['Syksy 2014', 2, 0, 0, 1, 0, 0, 1, 0],
['Kevät 2014', 1, 0, 0, 0, 0, 0, 1, 0],
['Syksy 2013', 1, 1, 0, 0, 0, 0, 0, 0],
['Syksy 2013', 1, 0, 0, 0, 0, 0, 1, 0],
['Kevät 2013', 0, 0, 0, 0, 0, 0, 0, 0],
['Syksy 2012', 4, 0, 0, 0, 0, 3, 1, 0],
['Kevät 2012', 1, 0, 0, 0, 0, 0, 1, 0],
@@ -1156,9 +1181,9 @@ describe('Basic user', () => {
searchByCourseCode('TKT20014')
cy.contains('TKT20014').click()
cy.contains('TKT20014 • Kypsyysnäyte LuK')
cy.contains('50037 • Ruotsinkielinen kypsyysnäyte LuK')
cy.contains('50036 • Suomenkielinen kypsyysnäyte LuK')
cy.contains('TKT20014') // Kypsyysnäyte LuK
cy.contains('50037') // Ruotsinkielinen kypsyysnäyte LuK
cy.contains('50036') // Suomenkielinen kypsyysnäyte LuK
cy.cs('ProviderOrganizationSelect').click()
@@ -1180,14 +1205,13 @@ describe('Basic user', () => {
it('Has right to see all the students, because course rovider is TKT', () => {
cy.visit('coursestatistics?courseCodes=%5B%22TKT10004%22%5D&separate=false')
cy.cs('course-population-for-2021-2022').click()
cy.contains('Students (29)').click()
cy.contains('509781')
cy.contains('529866')
// cy.contains('Students (29)').click()
})
})
})
describe('Only course statistics', () => {
// TODO: Writing these as backend api tests
describe.skip('Only course statistics', () => {
beforeEach(() => {
cy.init('/coursestatistics', 'onlycoursestatistics')
cy.url().should('include', '/coursestatistics')
@@ -1213,15 +1237,15 @@ describe('Only course statistics', () => {
const attemptsTableContents = [
// [Time, Total attempts, Passed, Failed, Pass rate, Enrollments]
['Total *', 255, 217, 20, '91.56 %', 73],
['Total *', 240, 212, 8, '96.36 %', 73],
emptyYear('2023-2024'),
['2022-2023', 28, 28, 0, '100.00 %', 28],
['2021-2022', 45, 27, 0, '60.00 %', 45],
['2020-2021', 34, 34, 0, '100.00 %'],
['2019-2020', 60, 57, 3, '95.00 %'],
['2018-2019', 46, 39, 7, '84.78 %'],
['2017-2018', 35, 26, 9, '74.29 %'],
['2016-2017', 7, 6, 1, '85.71 %'],
['2022-2023', 28, 27, 0, '96.43 %', 28],
['2021-2022', 45, 26, 0, '57.78 %', 45],
['2020-2021', 31, 31, 0, '100.00 %'],
['2019-2020', 56, 56, 0, '100.00 %'],
['2018-2019', 42, 39, 3, '92.86 %'],
['2017-2018', 32, 28, 4, '87.50 %'],
['2016-2017', 6, 5, 1, '83.33 %'],
emptyYear('2015-2016'),
emptyYear('2014-2015'),
emptyYear('2013-2014'),
+1 -1
View File
@@ -12,7 +12,7 @@ const save = () => {
}
const selectSavedPopulation = name => {
cy.cs('history-search').click().type(`${name}{enter}`)
cy.cs('history-search').type(`${name}{enter}`)
}
const deleteAllSearches = () => {
+223 -185
View File
@@ -32,36 +32,21 @@ const deleteTag = tagName => {
describe('Degree programme overview', () => {
describe('Degree programme selector', () => {
beforeEach(() => {
cy.init('/study-programme', 'admin')
})
it('Correct categories', () => {
cy.contains('Bachelor programmes')
cy.contains('Master programmes')
cy.contains('Combined programmes')
cy.contains('Doctoral programmes')
})
it('Degree programme search filter', () => {
cy.init('/study-programme', 'admin')
cy.contains('Tietojenkäsittelytieteen kandiohjelma').should('exist')
cy.contains('Matemaattisten tieteiden kandiohjelma').should('exist')
cy.cs('study-programme-filter').type('Tietojenkäsittelytieteen')
cy.contains('Tietojenkäsittelytieteen kandiohjelma').should('exist')
cy.contains('Matemaattisten tieteiden kandiohjelma').should('not.exist')
})
it('Old and specialized programmes filter', () => {
cy.contains('Biotekniikka (ylempi)').should('not.exist')
cy.cs('filter-old-programmes-toggle').click()
cy.contains('Biotekniikka (ylempi)').should('exist')
})
})
describe('Basic information tab works for basic user', () => {
beforeEach(() => {
cy.init('/study-programme')
cy.contains('a', 'Matemaattisten tieteiden kandiohjelma').click({ force: true })
cy.cs('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
@@ -79,11 +64,11 @@ describe('Degree programme overview', () => {
// [Year, Started studying, Accepted, Graduated, Transferred Away, Transferred to]
...years.map(year => [year, 0, 0, 0, 0, 0]),
[2023, 8, 8, 26, 0, 0],
[2022, 25, 26, 48, 1, 4],
[2021, 29, 32, 47, 0, 2],
[2022, 25, 26, 47, 1, 3],
[2021, 29, 32, 48, 0, 2],
[2020, 26, 27, 12, 1, 3],
[2019, 28, 34, 1, 0, 1],
[2018, 40, 45, 0, 0, 0],
[2018, 40, 45, 0, 0, 1],
[2017, 41, 47, 0, 0, 0],
]
@@ -93,31 +78,14 @@ describe('Degree programme overview', () => {
it('Basic information contains correct credits', () => {
const years = getEmptyYears()
const tableContents = [
...years.map(year => [year, 0, 0, 0, 0, 0]),
[2023, 1519, 1519, 0, 0, 222],
[2022, 3235, 3205, 0, 30, 209],
[2021, 5133, 5108, 0, 25, 428],
[2020, 5801, 5796, 0, 5, 94],
[2019, 5305, 5305, 0, 0, 162],
[2018, 3442, 3432, 0, 10, 21],
[2017, 1211, 1211, 0, 0, 189],
]
cy.checkTableStats(tableContents, 'credits-produced-by-the-study-programme')
})
it("Toggling 'Show special categories' on displays additional information", () => {
cy.cs('special-categories-toggle').click()
const years = getEmptyYears()
const tableContents = [
...years.map(year => [year, 0, 0, 0, 0, 0, 0, 0]),
[2023, 1519, 1519, 0, 0, 0, 0, 222],
[2022, 3235, 3205, 0, 30, 0, 0, 209],
[2021, 5133, 5108, 0, 25, 0, 0, 428],
[2020, 5801, 5796, 0, 5, 0, 0, 94],
[2019, 5305, 5305, 0, 0, 0, 0, 162],
[2018, 3442, 3432, 0, 10, 0, 0, 21],
[2017, 1211, 1211, 0, 0, 0, 0, 189],
...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, 0],
[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],
]
cy.checkTableStats(tableContents, 'credits-produced-by-the-study-programme')
@@ -129,8 +97,8 @@ describe('Degree programme overview', () => {
// [Year, Graduated, Wrote thesis]
...years.map(year => [year, 0, 0]),
[2023, 26, 5],
[2022, 48, 13],
[2021, 47, 76],
[2022, 47, 13],
[2021, 48, 76],
[2020, 12, 23],
[2019, 1, 19],
[2018, 0, 0],
@@ -147,8 +115,8 @@ describe('Degree programme overview', () => {
// [Year, Started studying, Accepted, Graduated]
...years.map(year => [year, 0, 0, 0]),
[2023, 8, 8, 24],
[2022, 25, 26, 44],
[2021, 29, 32, 46],
[2022, 25, 26, 43],
[2021, 29, 32, 47],
[2020, 26, 27, 11],
[2019, 28, 34, 1],
[2018, 40, 45, 0],
@@ -161,8 +129,8 @@ describe('Degree programme overview', () => {
// [Year, Graduated, Wrote thesis]
...years.map(year => [year, 0, 0]),
[2023, 24, 5],
[2022, 44, 12],
[2021, 46, 69],
[2022, 43, 12],
[2021, 47, 69],
[2020, 11, 22],
[2019, 1, 18],
[2018, 0, 0],
@@ -180,24 +148,24 @@ describe('Degree programme overview', () => {
// [Year, Started studying, Accepted, Graduated, Transferred away, Transferred to]
...years.map(year => [year, 0, 0, 0, 0, 0]),
['2023 - 2024', 8, 8, 4, 0, 0],
['2022 - 2023', 25, 26, 36, 0, 0],
['2021 - 2022', 29, 32, 57, 1, 6],
['2022 - 2023', 25, 26, 35, 0, 0],
['2021 - 2022', 29, 32, 58, 1, 5],
['2020 - 2021', 26, 27, 30, 0, 3],
['2019 - 2020', 28, 34, 7, 1, 1],
['2018 - 2019', 40, 45, 0, 0, 0],
['2018 - 2019', 40, 45, 0, 0, 1],
['2017 - 2018', 41, 47, 0, 0, 0],
]
cy.checkTableStats(studentTableContents, 'students-of-the-study-programme')
const creditTableContents = [
...years.map(year => [year, 0, 0, 0, 0, 0]),
['2023 - 2024', 160, 160, 0, 0, 67],
['2022 - 2023', 2725, 2720, 0, 5, 337],
['2021 - 2022', 4092, 4042, 0, 50, 198],
['2020 - 2021', 5420, 5415, 0, 5, 321],
['2019 - 2020', 6043, 6043, 0, 0, 101],
['2018 - 2019', 4856, 4851, 0, 5, 107],
['2017 - 2018', 2350, 2345, 0, 5, 26],
...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, 0],
['2018 - 2019', 4856, 4851, 0, 0, 5, 0, 0, 107, 0],
['2017 - 2018', 2350, 2345, 0, 0, 5, 0, 0, 26, 0],
]
cy.checkTableStats(creditTableContents, 'credits-produced-by-the-study-programme')
@@ -224,18 +192,20 @@ describe('Degree programme overview', () => {
.should('contain', 47)
.should('contain', 76)
cy.cs('bachelor-breakdown-bar-chart-section')
cy.cs('graduation-time-toggle').click()
cy.cs('bachelor-median-time-bar-chart-section').within(() => {
cy.contains('Graduation year')
cy.contains('2022')
cy.contains('48 graduated').trigger('mouseover')
cy.contains('48 students graduated in year 2022')
cy.contains('median study time: 44.5 months')
cy.contains('22 graduated on time')
cy.contains('12 graduated max year overtime')
cy.contains('14 graduated over year late')
cy.cs('graduation-mode-selector').within(() => {
cy.cs('select-median').click()
})
cy.cs('unset-median-bar-chart').within(() => {
cy.contains('47 graduated').should('be.visible')
cy.contains('47 graduated').hover()
})
cy.get('.grad-vals').contains('47 students graduated in year 2022').should('be.visible')
cy.contains('median study time: 6 semesters')
cy.contains('24 graduated on time')
cy.contains('12 graduated max year overtime')
cy.contains('11 graduated over year late')
cy.cs('programmes-before-or-after-stacked-bar-chart-section')
.should('contain', 'Tietojenkäsittelytieteen maisteriohjelma')
@@ -248,37 +218,43 @@ describe('Degree programme overview', () => {
})
describe('Graduation times of master programmes', () => {
it('are split into two graphs', () => {
it('are split into three graphs', () => {
cy.init('/study-programme')
cy.contains('a', 'Matematiikan ja tilastotieteen maisteriohjelma').click({ force: true })
cy.cs('year-toggle').click() // Tests are written for calendar year
cy.cs('bachelor-breakdown-bar-chart-section')
cy.cs('master-breakdown-bar-chart-section')
cy.cs('unset-breakdown-bar-chart')
cy.cs('graduation-time-toggle').click()
cy.cs('master-median-time-bar-chart-section').within(() => {
cy.contains('Master study right')
cy.contains('Graduation year')
cy.contains('2021')
cy.contains('2 graduated').trigger('mouseover')
cy.contains('2 students graduated in year 2021')
cy.contains('median study time: 25 months')
cy.contains('1 graduated on time')
cy.contains('1 graduated max year overtime')
cy.contains('0 graduated over year late')
cy.cs('graduation-mode-selector').within(() => {
cy.cs('select-average').click()
})
cy.cs('unset-graduation-times-section')
.eq(1)
.within(() => {
cy.contains('Master study right')
cy.contains('2 graduated').should('be.visible')
cy.contains('2 graduated').hover()
})
cy.cs('bachelor-median-time-bar-chart-section').within(() => {
cy.contains('Bachelor + master study right')
cy.contains('Graduation year')
cy.contains('2023')
cy.contains('11 graduated').trigger('mouseover')
cy.contains('11 students graduated in year 2023')
cy.contains('median study time: 69 months')
cy.contains('3 graduated on time')
cy.contains('7 graduated max year overtime')
cy.contains('1 graduated over year late')
})
cy.get('.grad-vals').contains('2 students graduated in year 2021').should('be.visible')
cy.contains('average study time: 4.5 semesters')
cy.contains('1 graduated on time')
cy.contains('1 graduated max year overtime')
cy.contains('0 graduated over year late')
cy.cs('unset-graduation-times-section')
.eq(0)
.within(() => {
cy.contains('Bachelor + master study right')
cy.contains('11 graduated').should('be.visible')
cy.contains('11 graduated').hover()
})
cy.get('.grad-vals').contains('11 students graduated in year 2023').should('be.visible')
cy.contains('average study time: 11.18 semesters')
cy.contains('4 graduated on time')
cy.contains('6 graduated max year overtime')
cy.contains('1 graduated over year late')
})
})
@@ -286,7 +262,7 @@ describe('Degree programme overview', () => {
beforeEach(() => {
cy.intercept(
'GET',
'api/v2/studyprogrammes/KH50_001/studytrackstats?graduated=GRADUATED_INCLUDED&special_groups=SPECIAL_INCLUDED&combined_programme='
'api/studyprogrammes/KH50_001/studytrackstats?special_groups=SPECIAL_INCLUDED&combined_programme='
).as('stQuery')
cy.init('/study-programme')
cy.contains('a', 'Matemaattisten tieteiden kandiohjelma').click()
@@ -319,15 +295,15 @@ describe('Degree programme overview', () => {
it('Students of the degree programme are shown correctly', () => {
const tableContents = [
// [Year, All, Started studying, Present, Absent, Inactive, Graduated, Men, Women, Other/Unknown, Finland, Other]
['2023 - 2024', 8, 8, 0, 0, 8, 0, 5, 3, 0, 8, 1],
['2022 - 2023', 26, 25, 0, 0, 24, 2, 19, 7, 0, 25, 1],
['2021 - 2022', 38, 29, 0, 0, 33, 5, 29, 9, 0, 36, 4],
['2020 - 2021', 30, 26, 0, 0, 11, 19, 15, 15, 0, 29, 3],
['2019 - 2020', 35, 28, 0, 0, 8, 27, 22, 13, 0, 35, 2],
['2018 - 2019', 45, 40, 0, 0, 6, 39, 26, 19, 0, 44, 1],
['2017 - 2018', 47, 41, 0, 0, 5, 42, 31, 16, 0, 47, 0],
['Total', 229, 197, 0, 0, 95, 134, 147, 82, 0, 224, 12],
// [Year, All, Started studying, Present, Absent, Passive, Graduated, Has recent attainments, Men, Women, Other/Unknown, Finland, Other]
['2023 - 2024', 8, 8, 0, 0, 8, 0, 0, 5, 3, 0, 8, 1],
['2022 - 2023', 26, 25, 0, 0, 24, 2, 0, 19, 7, 0, 25, 1],
['2021 - 2022', 37, 29, 0, 0, 33, 4, 0, 29, 8, 0, 35, 4],
['2020 - 2021', 30, 26, 0, 0, 11, 19, 0, 15, 15, 0, 29, 3],
['2019 - 2020', 35, 28, 0, 0, 8, 27, 0, 22, 13, 0, 35, 2],
['2018 - 2019', 46, 40, 0, 0, 6, 40, 0, 26, 20, 0, 45, 1],
['2017 - 2018', 47, 41, 0, 0, 5, 42, 0, 31, 16, 0, 47, 0],
['Total', 229, 197, 0, 0, 95, 134, 0, 147, 82, 0, 224, 12],
]
cy.checkTableStats(tableContents, 'study-tracks-and-class-statistics')
@@ -372,14 +348,14 @@ describe('Degree programme overview', () => {
describe('Population link button works for', () => {
it('a single year', () => {
cy.cs('2023-population-link-button').first().click()
cy.contains('Matemaattisten tieteiden kandiohjelma 2023 - 2024')
cy.contains('Class size 8 students')
cy.contains('Matemaattisten tieteiden kandiohjelma')
cy.contains('Class of 2023 - 2024, 8 students')
})
it('total', () => {
cy.cs('total-population-link-button').click()
cy.contains('Matemaattisten tieteiden kandiohjelma')
cy.contains('Class size 227 students')
cy.contains('Class of 2017 - 2026, 227 students')
})
it('Links to class statistics page with study track info included work', () => {
@@ -388,9 +364,9 @@ describe('Degree programme overview', () => {
cy.get('a').click()
})
cy.contains('Matemaattisten tieteiden kandiohjelma 2022 - 2023')
cy.contains('Matemaattisten tieteiden kandiohjelma')
cy.contains('Studytrack MAT-MAT')
cy.contains('Class size 26 students')
cy.contains('Class of 2022 - 2023, 26 students')
cy.contains('Showing 3 out of 26 students')
})
})
@@ -398,55 +374,68 @@ describe('Degree programme overview', () => {
it('Student progress data is shown correctly', () => {
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]
...years.map(year => [year, 0, 0, 0, 0, 0, 0, 0, 0]),
['2023 - 2024', 8, 8, 0, 0, 0, 0, 0, 0],
['2022 - 2023', 26, 9, 9, 4, 3, 0, 1, 0],
['2021 - 2022', 38, 8, 6, 11, 8, 5, 0, 0],
['2020 - 2021', 30, 2, 1, 3, 7, 4, 4, 9],
['2019 - 2020', 35, 1, 0, 2, 1, 0, 4, 27],
['2018 - 2019', 45, 0, 1, 1, 2, 0, 3, 38],
['2017 - 2018', 47, 0, 1, 3, 0, 1, 2, 40],
['Total', 229, 28, 18, 24, 21, 10, 14, 114],
// [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],
]
cy.checkTableStats(tableContents, 'study-programme-progress')
})
it('Progress section', () => {
cy.cs('programme-progress-bar-chart-section').scrollIntoView()
cy.cs('programme-progress-bar-chart-section')
.should('contain', 'Less than 30 credits')
.should('contain', '30–60 credits')
.should('contain', 'At least 180 credits')
.should('contain', '49.8%') // The percentage for total, at least 180 credits, to check that the graph renders
.should('contain', '58.5%') // The percentage for total graduated, to check that the graph renders
cy.cs('programme-progress-bar-chart-section').contains('49.8%').trigger('mouseover', { force: true })
cy.contains('At least 180 credits: 114')
cy.cs('programme-progress-bar-chart-section').contains('58.5%').hover()
cy.contains('Graduated: 134')
})
it('Average graduation times section', () => {
// CI contains different data than locally
it.skip('Average graduation times section', () => {
cy.cs('average-graduation-times-section').scrollIntoView()
cy.cs('average-graduation-times-section').within(() => {
cy.contains('2020 - 2021')
cy.get('[aria-label="2020 - 2021, 15. On time."]').trigger('mouseover')
cy.contains('On time: 15')
cy.contains("Click a bar to view that year's study track level breakdown")
cy.get('[aria-label="2019 - 2020, 22. On time."]').click()
cy.cs('unset-graduation-times-section').within(() => {
cy.contains('2020 - 2021')
})
cy.get('text').contains('16').hover()
cy.contains('On time: 16')
cy.cs('unset-graduation-times-section').within(() => {
cy.contains("Click a bar to view that year's study track level breakdown")
cy.get('text').contains('22').click()
})
cy.contains('Year 2019 - 2020 by start year')
cy.get('[aria-label="MAT-MAT, 14. On time."]').trigger('mouseover')
cy.get('text').contains('13').hover()
cy.contains('Matematiikka')
cy.contains('MAT-MAT')
cy.contains('On time: 14')
cy.contains('On time: 13')
})
cy.cs('graduation-time-toggle').click()
cy.cs('average-graduation-times-section').within(() => {
cy.contains('2020 - 2021')
cy.contains('19 graduated').trigger('mouseover')
cy.contains('19 graduated').hover()
cy.contains('From class of 2020 - 2021, 19/30 students have graduated')
cy.contains('median study time: 33 months')
cy.contains('15 graduated on time')
cy.contains('3 graduated max year overtime')
cy.contains('median study time: 6 semesters')
cy.contains('16 graduated on time')
cy.contains('2 graduated max year overtime')
cy.contains('1 graduated over year late')
})
})
@@ -465,14 +454,14 @@ describe('Degree programme overview', () => {
it('Students of the study track are shown correctly', () => {
cy.cs('study-track-overview-section').contains('Students of the study track MAT-MAT by starting year')
const tableContents = [
// [Year, All, Started studying, Present, Absent, Inactive, Graduated, Men, Women, Other/Unknown, Finland, Other]
['2022 - 2023', 3, 3, 0, 0, 1, 2, 2, 1, 0, 3, 0],
['2021 - 2022', 5, 1, 0, 0, 1, 4, 3, 2, 0, 5, 0],
['2020 - 2021', 13, 10, 0, 0, 1, 12, 7, 6, 0, 12, 3],
['2019 - 2020', 17, 14, 0, 0, 0, 17, 10, 7, 0, 17, 1],
['2018 - 2019', 23, 21, 0, 0, 2, 21, 11, 12, 0, 23, 0],
['2017 - 2018', 28, 24, 0, 0, 1, 27, 15, 13, 0, 28, 0],
['Total', 89, 73, 0, 0, 6, 83, 48, 41, 0, 88, 4],
// [Year, All, Started studying, Present, Absent, Passive, Graduated, Has recent attainment, Men, Women, Other/Unknown, Finland, Other]
['2022 - 2023', 3, 3, 0, 0, 1, 2, 0, 2, 1, 0, 3, 0],
['2021 - 2022', 4, 1, 0, 0, 1, 3, 0, 3, 1, 0, 4, 0],
['2020 - 2021', 13, 10, 0, 0, 1, 12, 0, 7, 6, 0, 12, 3],
['2019 - 2020', 17, 14, 0, 0, 0, 17, 0, 10, 7, 0, 17, 1],
['2018 - 2019', 24, 21, 0, 0, 2, 22, 0, 11, 13, 0, 24, 0],
['2017 - 2018', 28, 24, 0, 0, 1, 27, 0, 15, 13, 0, 28, 0],
['Total', 89, 73, 0, 0, 6, 83, 0, 48, 41, 0, 88, 4],
]
cy.checkTableStats(tableContents, 'study-tracks-and-class-statistics')
})
@@ -480,13 +469,14 @@ describe('Degree programme overview', () => {
it('Links to class statistics page with study track info included work', () => {
cy.cs('2020-population-link-button').first().click()
cy.contains('Matemaattisten tieteiden kandiohjelma 2020 - 2021')
cy.contains('Matemaattisten tieteiden kandiohjelma')
cy.contains('Studytrack MAT-MAT')
cy.contains('Class size 30 students')
cy.contains('Class of 2020 - 2021, 30 students')
cy.contains('Showing 10 out of 30 students')
})
it('Info message about missing progress stats is displayed', () => {
// FIXME: This feature was implemented. The data is no-good, so maybe reimplement this when it's clean.
it.skip('Info message about missing progress stats is displayed', () => {
cy.contains('Progress of students of the study track MAT-MAT by starting year')
cy.contains('Progress data is currently only available for all students of the degree programme.')
})
@@ -496,7 +486,7 @@ describe('Degree programme overview', () => {
cy.get("[data-cy='graduation-times-graph-breakdownBachelor']").within(() => {
cy.contains('Start year')
cy.contains('2020 - 2021')
cy.get('[aria-label="2020 - 2021, 9. On time."]').trigger('mouseover')
cy.get('[aria-label="2020 - 2021, 9. On time."]').hover()
cy.contains('Graduated On time: 9 students')
})
@@ -504,7 +494,7 @@ describe('Degree programme overview', () => {
cy.cs('bachelor-median-time-bar-chart').within(() => {
cy.contains('Start year')
cy.contains('2020 - 2021')
cy.contains('12 graduated').trigger('mouseover')
cy.contains('12 graduated').hover()
cy.contains('From class of 2020 - 2021, 12/13 students have graduated')
cy.contains('median study time: 34 months')
cy.contains('9 graduated on time')
@@ -528,14 +518,23 @@ describe('Degree programme overview', () => {
describe('By credit type tab', () => {
beforeEach(() => {
cy.get('tbody').within(() => {
cy.cs('show-credits-students-toggle').click()
cy.cs('year-toggle').click()
cy.get('thead').within(() => {
cy.get('tr')
.eq(0)
.within(() => {
cy.get('th').eq(3).click()
})
})
cy.get('tbody').within(() => {
cy.get('tr')
.eq(8)
.within(() => {
cy.get('td').eq(0).contains('Course')
cy.get('td').eq(1).contains('MAT21001')
cy.get('td').eq(2).contains('Lineaarialgebra ja matriisilaskenta II')
cy.get('td').eq(3).contains('341')
cy.get('td').eq(1).contains('MAT11004')
cy.get('td').eq(2).contains('Differentiaalilaskenta')
cy.get('td').eq(3).contains('1240')
})
})
})
@@ -548,18 +547,18 @@ describe('Degree programme overview', () => {
cy.get('tbody').within(() => {
cy.get('tr')
.eq(0)
.eq(9)
.within(() => {
cy.get('td').eq(0).contains('Course')
cy.get('td').eq(1).contains('MAT21001')
cy.get('td').eq(2).contains('Lineaarialgebra ja matriisilaskenta II')
cy.get('td').eq(3).contains('121')
cy.get('td').eq(1).contains('MAT21009')
cy.get('td').eq(2).contains('Kandidaatintutkielma')
cy.get('td').eq(3).contains('414')
})
})
})
it('year toggle works', () => {
cy.cs('year-toggle').click()
cy.cs('year-toggle').click() // Default view now Academic years
cy.cs('from-year-select').click()
cy.cs('from-year-select-option-2017').click()
@@ -568,12 +567,12 @@ describe('Degree programme overview', () => {
cy.get('tbody').within(() => {
cy.get('tr')
.eq(0)
.eq(5)
.within(() => {
cy.get('td').eq(0).contains('Course')
cy.get('td').eq(1).contains('MAT21001')
cy.get('td').eq(2).contains('Lineaarialgebra ja matriisilaskenta II')
cy.get('td').eq(3).contains('311')
cy.get('td').eq(1).contains('MAT11001')
cy.get('td').eq(2).contains('Johdatus yliopistomatematiikkaan')
cy.get('td').eq(3).contains('1265')
})
})
})
@@ -632,31 +631,69 @@ describe('Degree programme overview', () => {
})
it("'Show credits/Show students' toggle works", () => {
cy.get('thead tr').within(() => {
cy.get('th').should('have.length', 8)
const headers = ['Type', 'Code', 'Name', 'Total', 'Major', 'Non-major', 'Non-degree', 'Transferred']
headers.forEach((header, index) => {
cy.get('th').eq(index).contains(header)
cy.cs('year-toggle').click()
cy.wait(500) // So that the toggle has time to load
cy.get('thead tr')
.eq(0)
.within(() => {
cy.get('th').should('have.length', 6)
const headers = ['Type', 'Code', 'Name', 'Total credits', 'Credits produced by', 'Transferred credits']
headers.forEach((header, index) => {
cy.get('th').eq(index).contains(header)
})
})
cy.get('thead tr')
.eq(1)
.within(() => {
cy.get('th').should('have.length', 7)
const headers = [
'Degree students',
'Open university (hetu)',
'Open university (no hetu)',
'Exchange students',
'Other universities',
'Separate studies',
'Other',
]
headers.forEach((header, index) => {
cy.get('th').eq(index).contains(header)
})
})
})
cy.cs('show-credits-students-toggle').click()
cy.get('thead tr')
.eq(0)
.within(() => {
cy.get('th').should('have.length', 7)
const headers = [
'Type',
'Code',
'Name',
'Total students',
'Breakdown of total',
'Breakdown of passed',
'Not included in total or passed',
]
headers.forEach((header, index) => {
cy.get('th').eq(index).contains(header)
})
})
cy.get('thead tr')
.eq(1)
.within(() => {
cy.get('th').should('have.length', 10)
const headers = [
'Type',
'Code',
'Name',
'Total',
'Passed',
'Not',
'Major',
'Non-major',
'Non-degree',
'Transferred',
'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',
]
headers.forEach((header, index) => {
cy.get('th').eq(index).contains(header)
@@ -665,12 +702,12 @@ describe('Degree programme overview', () => {
cy.get('tbody').within(() => {
cy.get('tr')
.eq(0)
.eq(5)
.within(() => {
cy.get('td').eq(0).contains('Course')
cy.get('td').eq(1).contains('MAT21001')
cy.get('td').eq(2).contains('Lineaarialgebra ja matriisilaskenta II')
cy.get('td').eq(3).contains('341')
cy.get('td').eq(1).contains('MAT11001')
cy.get('td').eq(2).contains('Johdatus yliopistomatematiikkaan')
cy.get('td').eq(3).contains('272')
})
})
})
@@ -682,7 +719,7 @@ describe('Degree programme overview', () => {
})
// FIXME: Flaky
it('test', () => {
it.skip('test', () => {
// TODO: Implement some real tests for this tab
cy.contains('From')
})
@@ -797,7 +834,8 @@ describe('Degree programme overview', () => {
cy.contains('Students added to tag')
cy.cs('population-link-button').click()
cy.contains('Matemaattisten tieteiden kandiohjelma 2022 - 2023')
cy.contains('Matemaattisten tieteiden kandiohjelma')
cy.contains('Class of 2022 - 2023')
cy.contains(`Tagged with: ${tagName}`)
cy.contains('Students (5)')
.parent()
+5 -5
View File
@@ -110,15 +110,15 @@ describe('Faculty statistics', () => {
})
it('Toggle years works', () => {
cy.cs('credits-produced-by-the-faculty-interactive-data-table').should('contain', '2022')
cy.cs('thesis-writers-of-the-faculty-interactive-data-table').should('contain', '2022')
cy.cs('students-of-the-faculty-interactive-data-table').should('contain', '2022')
cy.cs('graduated-of-the-faculty-interactive-data-table').should('contain', '2022')
cy.cs('year-toggle').click()
cy.cs('credits-produced-by-the-faculty-interactive-data-table').should('contain', '2022 - 2023')
cy.cs('thesis-writers-of-the-faculty-interactive-data-table').should('contain', '2022 - 2023')
cy.cs('students-of-the-faculty-interactive-data-table').should('contain', '2022 - 2023')
cy.cs('graduated-of-the-faculty-interactive-data-table').should('contain', '2022 - 2023')
cy.cs('year-toggle').click()
cy.cs('credits-produced-by-the-faculty-interactive-data-table').should('contain', '2022')
cy.cs('thesis-writers-of-the-faculty-interactive-data-table').should('contain', '2022')
cy.cs('students-of-the-faculty-interactive-data-table').should('contain', '2022')
cy.cs('graduated-of-the-faculty-interactive-data-table').should('contain', '2022')
})
it('Toggle programmes works', () => {
+8 -8
View File
@@ -19,7 +19,7 @@ describe("Population statistics with a master's programme", { testIsolation: fal
it('Study track filter works', () => {
cy.cs('studyTrackFilter-header').click()
cy.selectFromDropdown('studyTrackFilter', 0, true)
cy.selectFromDropdown('studyTrackFilter', 'Matematiikka ja soveltava matematiikka (MAST-MSM)', true)
checkFilteredStudentCount(15)
cy.cs('studyTrackFilter-clear').click()
@@ -184,7 +184,7 @@ describe.skip('Population Statistics', { testIsolation: false }, () => {
cy.selectFromDropdown(`courseFilter-${courses[0].code}`, 1)
checkFilteredStudentCount(7)
cy.selectFromAutocomplete('courseFilter', `${courses[1].code} - ${courses[1].name}`)
checkFilteredStudentCount(5)
checkFilteredStudentCount(6)
cy.selectFromDropdown(`courseFilter-${courses[1].code}`, 1)
checkFilteredStudentCount(4)
@@ -273,12 +273,12 @@ describe('Population Statistics with Bachelor + Master', { testIsolation: false
checkFilteredStudentCount(0)
// Non-Bachelor
cy.get('input[name="studyRightStatusFilter"][value="Inactive Bachelor study right"]').click()
cy.get('input[name="studyRightStatusFilter"][value="Inactive Bachelor study right"]').should('be.checked')
cy.get('input[name="studyRightStatusFilter"][value="Passive Bachelor study right"]').click()
cy.get('input[name="studyRightStatusFilter"][value="Passive Bachelor study right"]').should('be.checked')
checkFilteredStudentCount(5)
// Non-Master (combined)
cy.get('input[name="studyRightStatusFilter"][value="Inactive Master study right"]').click()
cy.get('input[name="studyRightStatusFilter"][value="Inactive Master study right"]').should('be.checked')
cy.get('input[name="studyRightStatusFilter"][value="Passive Master study right"]').click()
cy.get('input[name="studyRightStatusFilter"][value="Passive Master study right"]').should('be.checked')
checkFilteredStudentCount(24)
cy.cs('studyRightStatusFilter-clear').click()
@@ -453,9 +453,9 @@ describe('Custom Population Statistics', { testIsolation: false }, () => {
cy.selectFromDropdown(`courseFilter-${courses[0].code}`, 1)
checkFilteredStudentCount(6)
cy.selectFromAutocomplete('courseFilter', `${courses[1].code} - ${courses[1].name}`)
checkFilteredStudentCount(5)
checkFilteredStudentCount(6)
cy.selectFromDropdown(`courseFilter-${courses[1].code}`, 3)
checkFilteredStudentCount(2)
checkFilteredStudentCount(6)
cy.cs(`courseFilter-${courses[1].code}-clear`).click()
checkFilteredStudentCount(6)
+3 -3
View File
@@ -44,7 +44,7 @@ describe('When language center is opened', () => {
cy.get('table > tbody > tr:first').within(() => {
cy.get('td').should('have.length', 15)
cy.get('td').eq(0).contains('All courses total')
const numbers = [2076, 36, 9, 33, 756, 15, 40, 6, 48, 1, 24, 1059, 49, 0]
const numbers = [2076, 36, 9, 34, 759, 15, 40, 6, 51, 1, 24, 1059, 42, 0]
numbers.forEach((number, index) => {
cy.get('td')
.eq(index + 1)
@@ -55,12 +55,12 @@ describe('When language center is opened', () => {
it('Faculties tab "exceeding" button works', () => {
cy.cs('difference-button').click()
checkNumbers([65, 2, 0, 3, 13, 0, 1, 1, 2, 0, 1, 30, 11, 1], 15, 'faculties')
checkNumbers([66, 3, 0, 4, 18, 0, 1, 1, 4, 0, 0, 31, 3, 1], 15, 'faculties')
})
it('Faculties tab semester selector changes numbers', () => {
chooseSemester('Syksy 2020', 'from')
checkNumbers([1184, 28, 6, 27, 496, 4, 10, 2, 33, 0, 9, 535, 34, 0], 15, 'faculties')
checkNumbers([1184, 28, 6, 28, 499, 4, 10, 2, 36, 0, 9, 535, 27, 0], 15, 'faculties')
})
})
+122
View File
@@ -0,0 +1,122 @@
/// <reference types="cypress" />
const selectCourseStatus = (courseCode, status) => {
cy.cs(`courseFilter-${courseCode}-selector`).click()
cy.get(`[role="listbox"][aria-labelledby=courseFilter-${courseCode}]`).within(() => {
cy.get('li').contains(status).click()
})
}
describe('Numbers should match between', () => {
describe('Programme courses, Course statistics and Course population (no substitutions)', () => {
const test = [
// Code, name, total students, passed, not completed, failed*, enrolled no grade* (not used by Programme courses)
['Course', 'MAT11001', 'Johdatus yliopistomatematiikkaan', '272', '253', (1 + 18).toString(), '1', '18'],
['Course', 'MAT11003', 'Raja-arvot', '270', '249', (1 + 20).toString(), '1', '20'],
['Course', 'MAT11004', 'Differentiaalilaskenta', '262', '248', (1 + 13).toString(), '1', '13'],
['Course', 'MAT21002', 'Sarjat', '256', '250', (0 + 6).toString(), '0', '6'],
['Course', 'MAT11005', 'Integraalilaskenta', '251', '234', (0 + 17).toString(), '0', '17'],
['Course', 'MAT11002', 'Lineaarialgebra ja matriisilaskenta I', '247', '242', (1 + 4).toString(), '1', '4'],
['Course', 'MAT12003', 'Todennäköisyyslaskenta I', '234', '227', (3 + 4).toString(), '3', '4'],
['Course', 'MAT21001', 'Lineaarialgebra ja matriisilaskenta II', '230', '216', (4 + 10).toString(), '4', '10'],
['Course', 'MAT21003', 'Vektorianalyysi I', '228', '202', (3 + 23).toString(), '3', '23'],
['Course', 'MAT20005', 'Akateemiset taidot', '201', '178', (0 + 23).toString(), '0', '23'],
['Module', 'MAT110', 'Matematiikka, perusopinnot', '195', '195', (0 + 0).toString(), '0', '0'],
['Course', 'MAT21014', 'Johdatus logiikkaan I', '188', '170', (0 + 18).toString(), '0', '18'],
]
it('in Programme courses', () => {
// Move to Math bachelor programme's programme courses
cy.init('/study-programme')
cy.contains('a', 'Matemaattisten tieteiden kandiohjelma').click()
cy.cs('ProgrammeCoursesTab').click()
// Sort by Total students
cy.get('thead').within(() => {
cy.get('tr')
.eq(0)
.within(() => {
cy.get('th').eq(3).click()
})
})
cy.get('tbody').within(() => {
test.forEach((course, index) => {
cy.get('tr')
.eq(index)
.within(() => {
// Check a course's fields
course.forEach((field, fieldIndex) => {
if (fieldIndex <= 5) {
// Failed, Enrolled no grade values not used
cy.get('td').eq(fieldIndex).contains(field)
}
})
// Course link button, (name is second entry)
// cy.cs(`${course.at(1).toLocaleLowerCase()}-course-statistics-link-button`).click()
})
})
})
})
it('in Course statistics', () => {
test.forEach(course => {
// Move to Course statistics
cy.init(`/coursestatistics?courseCodes=%5B%22${course.at(1)}%22%5D&combineSubstitutions=false`, 'basic')
cy.url().should('include', `/coursestatistics?courseCodes=%5B%22${course.at(1)}%22%5D`)
cy.contains('Course statistics')
cy.contains(course.at(2)) // course name
cy.cs('FromYearSelector').click()
cy.cs('FromYearSelectorOption2017-2018').click()
cy.get('tbody').within(() => {
cy.get('tr')
.eq(0)
.within(() => {
cy.get('td').eq(1).contains(course.at(3))
cy.get('td').eq(2).contains(course.at(4))
cy.get('td').eq(3).contains(course.at(6)) // jump over combined Not completed
cy.get('td').eq(4).contains(course.at(7))
})
})
})
})
it('in Course population', () => {
test.forEach(course => {
// Academic years between 2017-2023
cy.init(
`/coursepopulation?from=68&to=74&coursecodes=%5B%22${course.at(1)}%22%5D&includeSubstitutions=false`,
'basic'
)
cy.contains(course.at(2)) // Course name
// Select course from Course-filter
cy.cs('courseFilter-filter-card').click()
cy.cs('courseFilter-selector').click()
cy.cs('courseFilter-popper').within(() => {
cy.contains('li', `${course.at(1)} - ${course.at(2)}`).click() // "code - name"
})
selectCourseStatus(course.at(1), 'All')
cy.contains(`Students (${course.at(3)})`)
selectCourseStatus(course.at(1), 'Passed')
cy.contains(`Students (${course.at(4)})`)
selectCourseStatus(course.at(1), 'Failed')
cy.contains(`Students (${course.at(6)})`)
selectCourseStatus(course.at(1), 'Enrolled, No Grade')
cy.contains(`Students (${course.at(7)})`)
})
})
})
describe('Course statistics and Course population', () => {
it.skip('when selecting one year')
it.skip('when selecting the all years')
})
})
-126
View File
@@ -1,126 +0,0 @@
/// <reference types="cypress" />
const deleteAllSearches = () => {
cy.cs('open-uni-search-button').click()
cy.contains('Saved populations').parent().parent().parent().parent().get('.search').as('search')
cy.get('@search').then($el => {
const searchItems = $el.find('div[role=option] > span[class=text]')
for (let i = searchItems.length - 1; 0 <= i; i--) {
if (searchItems[i].textContent.includes('TEST-')) {
cy.get('[data-cy="history-search"]').children().eq(0).type(`${searchItems[i].textContent}{enter}`)
cy.get('button').contains('Delete').click()
}
}
})
}
// Skipping the whole test suite as it is unclear whether the feature has any use or will be
// removed alltogether.
describe.skip('Open uni population tests', () => {
describe('Open uni form can be used', () => {
// When openUniUser is created, change it here
beforeEach(() => {
cy.init('/openunipopulation', 'admin')
})
after(() => {
cy.init('/openunipopulation', 'admin')
cy.contains('Open uni student population')
deleteAllSearches()
})
// TODO: Rewrite the whole component
it.skip('Finds a proper population', () => {
cy.fixture('openUniPopulation').then(({ courseCodesSet1 }) => {
cy.get('[data-cy="open-uni-search-button"]').click()
cy.contains('Insert course code(s)').siblings().get('textarea').as('textarea')
cy.get('@textarea').type(courseCodesSet1.join('\n'))
cy.cs('begin-of-search').click()
cy.cs('begin-of-search').clear()
cy.cs('begin-of-search').type('01.01.2015')
cy.cs('end-of-search').click()
cy.cs('end-of-search').clear()
cy.cs('end-of-search').type('01.01.2020')
cy.get('button').contains('Search population').click()
cy.contains('Beginning of the search for all fields:')
cy.contains('01.01.2015')
cy.contains('End of the search for enrollments:')
cy.contains('01.01.2020')
cy.location('pathname').should('eq', '/openunipopulation')
cy.location('search').then(search => {
const params = new URLSearchParams(search)
const courseCodes = params.getAll('courseCode')
expect(courseCodes).to.include.members(['TKT10001', 'TKT10002'])
expect(params.get('startdate')).to.eq('01-01-2015')
expect(params.get('enddate')).to.eq('01-01-2020')
})
cy.get('[data-cy="open-uni-table-div"]').should('contain.text', courseCodesSet1[0])
cy.get('[data-cy="open-uni-table-div"]').should('contain.text', courseCodesSet1[1])
})
})
it('Textarea can handle different kinds of input', () => {
cy.get('[data-cy="open-uni-search-button"]').click()
cy.contains('Insert course code(s)')
.siblings()
.get('textarea')
.type('tkt10001 tkt10002,tkt10003\n TKT10004,,TKt10005\nTKT-1234')
cy.get('button').contains('Search population').click()
cy.get('[data-cy="open-uni-table-div"]').should('contain.text', 'TKT10001')
cy.get('[data-cy="open-uni-table-div"]').should('contain.text', 'TKT10002')
cy.get('[data-cy="open-uni-table-div"]').should('contain.text', 'TKT10003')
cy.get('[data-cy="open-uni-table-div"]').should('contain.text', 'TKT10004')
cy.get('[data-cy="open-uni-table-div"]').should('contain.text', 'TKT10005')
cy.get('[data-cy="open-uni-table-div"]').should('not.contain', 'TKT-1234')
})
it('Saves a custom population search', () => {
cy.fixture('openUniPopulation').then(({ courseCodesSet1 }) => {
cy.get('[data-cy="open-uni-search-button"]').click()
cy.get('[data-cy="search-name"]').type('TEST-Avoimen-populaatio')
cy.contains('Insert course code(s)').siblings().get('textarea').type(courseCodesSet1.join('\n'))
cy.get('[data-cy="save-search"]').click()
cy.get('[data-cy="history-search"]').children().eq(0).type('TEST-Avoimen-populaatio{enter}')
cy.contains('TEST-Avoimen-populaatio')
cy.get('button').contains('Search population').as('btn')
cy.get('@btn').click()
cy.get('[data-cy="open-uni-table-div"]').should('contain.text', courseCodesSet1[0])
cy.get('[data-cy="open-uni-table-div"]').should('contain.text', courseCodesSet1[1])
})
})
it('Updates a custom population search', () => {
cy.fixture('openUniPopulation').then(({ courseCodesSet1, courseCodesSet2 }) => {
cy.get('[data-cy="open-uni-search-button"]').click()
cy.get('[data-cy="search-name"]').type('TEST-Avoimen-populaatio')
cy.contains('Insert course code(s)').siblings().get('textarea').type(courseCodesSet1.join('\n'))
cy.get('[data-cy="save-search"]').click()
cy.get('button').contains('Search population').click()
cy.get('[data-cy="open-uni-table-div"]').should('contain.text', courseCodesSet1[0])
cy.get('[data-cy="open-uni-table-div"]').should('contain.text', courseCodesSet1[1])
cy.get('[data-cy="open-uni-search-button"]').click()
cy.get('[data-cy="history-search"]').children().eq(0).type('TEST-Avoimen-populaatio{enter}')
cy.contains('Insert course code(s)').siblings().get('textarea').should('contain', courseCodesSet1.join(', '))
cy.contains('Insert course code(s)').siblings().get('textarea').type('\n')
cy.contains('Insert course code(s)').siblings().get('textarea').type(courseCodesSet2.join('\n'))
cy.get('[data-cy="save-search"]').click()
cy.get('button').contains('Search population').click()
cy.get('[data-cy="open-uni-table-div"]').should('contain.text', courseCodesSet1[0])
cy.get('[data-cy="open-uni-table-div"]').should('contain.text', courseCodesSet1[1])
cy.get('[data-cy="open-uni-table-div"]').should('contain.text', courseCodesSet2[0])
cy.get('[data-cy="open-uni-table-div"]').should('contain.text', courseCodesSet2[1])
cy.get('[data-cy="open-uni-table-div"]').should('contain.text', courseCodesSet2[2])
})
})
it('View does not crash with non-existing courses', () => {
cy.get('[data-cy="open-uni-search-button"]').click()
cy.contains('Insert course code(s)').siblings().get('textarea').type('cypress-fun')
cy.get('button').contains('Search population').click()
cy.get('[data-cy="open-uni-table-div"]').should('not.contain', 'cypress-fun')
})
})
})
+14 -14
View File
@@ -27,14 +27,14 @@ describe('Population statistics tests', () => {
describe('When using basic user', () => {
beforeEach(() => {
cy.init('/populations')
cy.intercept('/api/v3/populationstatistics/studyprogrammes').as('studyprogrammes')
cy.intercept('/api/v2/studyprogrammes/**/studytracks').as('studytracks')
cy.intercept('/api/populationstatistics/studyprogrammes').as('studyprogrammes')
cy.intercept('/api/studyprogrammes/**/studytracks').as('studytracks')
})
describe('Population search', () => {
it('Info box works', () => {
cy.cs('PopulationSearch-info-box-content').should('not.exist')
cy.cs('PopulationSearch-info-box-button').trigger('mouseover')
cy.cs('PopulationSearch-info-box-button').hover()
cy.cs('PopulationSearch-info-box-content').should('be.visible')
cy.cs('PopulationSearch-info-box-content').contains('Tässä osiossa voi tarkastella')
cy.cs('PopulationSearch-info-box-button').trigger('mouseout')
@@ -74,8 +74,8 @@ describe('Population statistics tests', () => {
selectStudyProgramme('Matemaattisten tieteiden kandiohjelma')
cy.wait('@studytracks').its('response.statusCode').should('be.oneOf', [200, 304])
cy.contains('See class').click()
cy.contains('Matemaattisten tieteiden kandiohjelma 2017 - 2018')
cy.contains('Class size 47 students')
cy.contains('Matemaattisten tieteiden kandiohjelma')
cy.contains('Class of 2017 - 2018, 47 students')
})
// TODO: Delete MUI
@@ -95,7 +95,7 @@ describe('Population statistics tests', () => {
cy.visit(pathToMathBSc2020)
cy.cs('filtered-students')
cy.contains('Matemaattisten tieteiden kandiohjelma')
cy.contains('Class size 30 students')
cy.contains('Class of 2020 - 2021, 30 students')
cy.contains('Excludes exchange students')
cy.contains('Excludes students with non-degree study right')
cy.contains('Excludes students who have transferred out of this programme')
@@ -251,15 +251,15 @@ describe('Population statistics tests', () => {
it('Is displayed and link to individual course stats page works', () => {
cy.visit(pathToMathBSc2020)
cy.contains('Courses of class').click()
cy.intercept('/api/v3/courseyearlystats**').as('coursePage')
cy.intercept('/api/courseyearlystats**').as('coursePage')
cy.cs('toggle-group-module-MAT110').click()
cy.contains('td', 'MAT11001').siblings().find('[data-testid="NorthEastIcon"]').click()
cy.wait('@coursePage')
cy.url().should('include', '/coursestatistics')
cy.contains('MAT11001 • Johdatus yliopistomatematiikkaan')
cy.contains('AYMAT11001 • Avoin yo: Johdatus yliopistomatematiikkaan')
cy.contains('57033 • Johdatus yliopistomatematiikkaan')
cy.contains('A57033 • Avoin yo: Johdatus yliopistomatematiikkaan')
cy.contains('MAT11001') // Johdatus yliopistomatematiikkaan
cy.contains('AYMAT11001') // Avoin yo: Johdatus yliopistomatematiikkaan
cy.contains('57033') // Johdatus yliopistomatematiikkaan
cy.contains('A57033') // Avoin yo: Johdatus yliopistomatematiikkaan
})
it('Curriculum selection works', () => {
@@ -383,7 +383,7 @@ describe('Population statistics tests', () => {
cy.contains(nonExisting).should('not.exist')
cy.contains('button', 'Check student numbers').click()
cy.contains('Check for student numbers')
cy.cs('check-student-numbers').click().type(`${existing}{enter}${nonExisting}`)
cy.cs('check-student-numbers').type(`${existing}{enter}${nonExisting}`)
cy.contains('button', 'Check students').click()
cy.get('#checkstudentsresults').within(() => {
cy.cs('found-title').click()
@@ -401,8 +401,8 @@ describe('Population statistics tests', () => {
describe('When using IAM user', () => {
beforeEach(() => {
cy.init(pathToMathBSc2020, 'onlyiamrights')
cy.contains('Matemaattisten tieteiden kandiohjelma 2020 - 2021')
cy.contains('Class size 30 students')
cy.contains('Matemaattisten tieteiden kandiohjelma')
cy.contains('Class of 2020 - 2021, 30 students')
})
it('Population statistics is visible', () => {
+85
View File
@@ -0,0 +1,85 @@
import { userHeaders } from '../support/commands.js'
const getBaseRoutes = [
['/', {}],
['/changelog', {}],
['/completedcoursessearch', {}],
['/university', {}],
['/feedback', {}],
['/populations', { roles: ['fullSisuAccess'], programmeRights: [] }],
['/study-programme', { roles: ['fullSisuAccess'], programmeRights: [] }],
['/study-programme/KH50_001', { roles: ['fullSisuAccess'], programmeRights: ['KH50_001'] }],
['/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
describe(`${user.displayname} has access only to views that were intended`, () => {
let userInfo
before(() => {
cy.request({ method: 'GET', url: '/api/login', headers: user }).then(({ body }) => (userInfo = body?.user))
})
beforeEach(() => cy.wrap(userInfo).should('not.be.undefined'))
beforeEach(() => {
cy.intercept('**/api/**', req => {
req.on('response', res => {
expect(res.statusCode, `${req.method} ${req.url}`).to.be.lessThan(500)
})
})
})
for (const [route, requirements] of getBaseRoutes) {
it(`Checking access ${route}`, () => {
cy.init(route, user.uid)
const requiredUserId = !requirements.userId || requirements.userId === userInfo.userId
const requiredRoles = !requirements.roles || requirements.roles?.some(role => userInfo.roles?.includes(role))
const requiredRights =
!requirements.requireProgrammeRights ||
userInfo.roles?.includes('fullSisuAccess') ||
requirements.programmeRights.every(role => userInfo.programmeRights?.includes(role))
// 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
if (shouldHaveAccess) {
cy.contains('Something broke').should('not.exist')
cy.contains('Access denied').should('not.exist')
} else {
cy.contains('Access denied')
}
})
}
})
}
+17 -28
View File
@@ -114,9 +114,9 @@ describe('Students tests', () => {
cy.get('a').click()
})
cy.url().should('include', '/coursestatistics')
cy.contains('MAT12004 • Tilastollinen päättely I')
cy.contains('AYMAT12004 • Avoin yo: Tilastollinen päättely I')
cy.contains('57046 • Johdatus tilastolliseen päättelyyn')
cy.contains('MAT12004') // Tilastollinen päättely I
cy.contains('AYMAT12004') // Avoin yo: Tilastollinen päättely I
cy.contains('57046') // Johdatus tilastolliseen päättelyyn
})
it('Has correct Sisu link', () => {
@@ -162,8 +162,8 @@ describe('Students tests', () => {
.parent()
.within(() => cy.get('a').click())
})
cy.contains('Matemaattisten tieteiden kandiohjelma 2020 - 2021')
cy.contains('Class size 30 students')
cy.contains('Matemaattisten tieteiden kandiohjelma')
cy.contains('Class of 2020 - 2021, 30 students')
})
it('Grade graph works in all three different modes', () => {
@@ -174,18 +174,13 @@ describe('Students tests', () => {
cy.contains('label', 'Group size')
cy.get('input').should('have.value', '5')
})
cy.contains('.highcharts-container text', "Nov '22")
cy.contains('.highcharts-container text', "Jul '22").should('not.exist')
cy.cs('group-size-input').within(() => {
cy.contains('label', 'Group size')
cy.get('input').clear()
cy.get('input').type('10')
cy.get('input').should('have.value', '10')
})
cy.contains('.highcharts-container text', "Jul '22")
cy.contains('.highcharts-container text', "Nov '22").should('not.exist')
cy.contains('button', 'Show semester mean').click()
cy.contains('.highcharts-container text', "Jan '22")
})
describe('Bachelor Honours section', () => {
@@ -264,8 +259,10 @@ describe('Students tests', () => {
it('When a study plan is selected, courses included in the study plan are highlighted with a blue background', () => {
cy.visit('/students/550789')
cy.contains('table tbody tr', 'Kulttuurien tutkimuksen kandiohjelma (01.08.2020–30.06.2023)').within(() => {
cy.get('td').eq(0).click()
cy.contains('Study rights')
cy.get('[data-cy=KH40_005-0]').contains('Kulttuurien tutkimuksen kandiohjelma')
cy.get('[data-cy=KH40_005-0]').within(() => {
cy.get('td').eq(1).click()
})
cy.contains('table tbody tr', 'Kandidaatintutkielma (KUKA-LIS222)').should(
'have.css',
@@ -274,31 +271,23 @@ describe('Students tests', () => {
)
})
it('When a study plan is selected, the time frame of the credit graph is updated', () => {
cy.clock(new Date('2024-08-30').getTime(), ['Date'])
cy.visit('/students/550789')
cy.contains('.highcharts-container text', '1 Aug 2020')
cy.contains('.highcharts-container text', '30 Aug 2024')
cy.contains('table tbody tr', 'Kulttuurien tutkimuksen kandiohjelma (01.08.2020–30.06.2023)').within(() => {
cy.get('td').eq(0).click()
})
cy.contains('.highcharts-container text', '1 Aug 2020')
cy.contains('.highcharts-container text', '19 Aug 2023')
})
it("If there's a study plan corresponding to the degree programme, completed credits are displayed", () => {
cy.visit('/students/550789')
cy.contains('table tbody tr', 'Kulttuurien tutkimuksen kandiohjelma (01.08.2020–30.06.2023)').within(() => {
cy.contains('Study rights')
cy.get('[data-cy=KH40_005-0]').contains('Kulttuurien tutkimuksen kandiohjelma')
cy.get('[data-cy=KH40_005-0]').within(() => {
cy.get('td')
.eq(-1)
.eq(4)
.contains(/^185 cr$/)
})
})
it("If there's a study plan corresponding to the degree programme and the student hasn't graduated, percentage of completion is also displayed", () => {
cy.visit('/students/458723')
cy.contains('table tbody tr', 'Oikeusnotaarin koulutusohjelma (01.08.2020–31.07.2027)').within(() => {
cy.get('td').eq(-1).contains('69% (125 cr)')
cy.contains('Study rights')
cy.get('[data-cy=KH20_001-0]').contains('Oikeusnotaarin koulutusohjelma')
cy.get('[data-cy=KH20_001-0]').within(() => {
cy.get('td').eq(4).contains('69% (125 cr)')
})
})
})
+1 -1
View File
@@ -35,7 +35,7 @@ describe('Teachers page tests', () => {
cy.cs('Search').click()
cy.cs('teacher-search').type(teacher1.split(' ')[0])
cy.get('table tbody tr').should('have.length', 4)
cy.get('table tbody tr').eq(3).contains('td', teacher1)
cy.get('table tbody tr').eq(2).contains('td', teacher1)
})
it('Can check teacher page', () => {
+29 -24
View File
@@ -10,21 +10,18 @@ const checkProgressTables = () => {
progressLevels.forEach(level => cy.cs(`${level}-faculty-progress-table`))
}
const graduationTimesLevels = ['bachelor', 'master', 'doctor'] // ? Bachelor + master seems to be missing in test data
const graduationTimesLevels = ['bachelor', 'bcMsCombo', 'master', 'doctor']
const checkAverageGraduationTimesBreakdownBarCharts = () => {
const checkGraduationCharts = mode => {
graduationTimesLevels.forEach(level => {
cy.cs(`${level}-breakdown-bar-chart`)
cy.cs(`${level}-${mode}-bar-chart`).within(() => {
// Check that bar has loaded
cy.contains('Loading content').should('not.exist')
})
})
}
const checkAverageGraduationTimesMedianBarCharts = () => {
graduationTimesLevels.forEach(level => {
cy.cs(`${level}-median-bar-chart`)
})
}
describe('University view', () => {
describe('University view', { defaultCommandTimeout: 60 * 1000 }, () => {
beforeEach(() => {
cy.init('/university')
cy.contains('University')
@@ -39,12 +36,6 @@ describe('University view', () => {
checkProgressTables()
})
it("'Graduated included / Graduated excluded' toggle works", () => {
cy.cs('graduated-toggle').click()
checkProgressBarCharts()
checkProgressTables()
})
it("'All study rights / Special study rights excluded' toggle works", () => {
cy.cs('study-right-toggle').click()
checkProgressBarCharts()
@@ -55,7 +46,7 @@ describe('University view', () => {
it.skip('years in the tables can be clicked to show faculty level breakdown', () => {
cy.cs('study-right-toggle').click()
cy.cs('bachelors-faculty-progress-table-show-button3').click()
cy.contains('29.5%').trigger('mouseover', { force: true })
cy.contains('29.5%').hover()
cy.contains('Matemaattis-luonnontieteellinen tiedekunta')
cy.contains('H50')
cy.contains('0 Credits: 0')
@@ -79,17 +70,31 @@ describe('University view', () => {
cy.cs('faculty-graduations-tab').click()
})
it('has all the correct median time bar charts', () => {
checkAverageGraduationTimesBreakdownBarCharts()
})
describe('Different modes work', () => {
it('Breakdown', () => {
cy.cs('graduation-mode-selector').within(() => {
cy.cs('select-breakdown').click()
})
checkGraduationCharts('breakdown')
})
it("'Breakdown/Median times' toggle works", () => {
cy.cs('graduation-time-toggle').click()
checkAverageGraduationTimesMedianBarCharts()
it('Median', () => {
cy.cs('graduation-mode-selector').within(() => {
cy.cs('select-median').click()
})
checkGraduationCharts('median')
})
it('Average', () => {
cy.cs('graduation-mode-selector').within(() => {
cy.cs('select-average').click()
})
checkGraduationCharts('average')
})
})
it('info boxes contain correct information', () => {
cy.cs('average-graduation-times-info-box-button').click()
cy.cs('average-graduation-times-info-box-button').hover()
cy.cs('average-graduation-times-info-box-content').contains('Opiskelijoiden keskimääräiset valmistumisajat')
})
})
+1 -1
View File
@@ -72,7 +72,7 @@ describe('Users tests', () => {
})
it("only the mocked user's programmes are visible", () => {
cy.intercept('/api/v3/populationstatistics/studyprogrammes').as('studyprogrammes')
cy.intercept('/api/populationstatistics/studyprogrammes').as('studyprogrammes')
cy.visit('/populations')
cy.wait('@studyprogrammes').its('response.statusCode').should('be.oneOf', [200, 304])
+18 -12
View File
@@ -58,18 +58,7 @@ const onlyStudyGuidanceGroupsUser = {
hypersonsisuid: 'hy-hlo-2222222',
}
// TODO add user into anon-DB (check the need for changing hypersonid)
// REMEMBER TO ADD ROLE openUniSearch
// const openUniUser = {
// uid: 'openuniuser',
// displayname: 'Open Uni User',
// 'shib-session-id': 'mock-cypress-session',
// hygroupcn: 'grp-oodikone-basics-users;hy-ypa-opa-dojo',
// mail: 'grp-toska+mockopenuniuser@helsinki.fi',
// hypersonsisuid: 'hy-hlo-9999999',
// }
const userHeaders = [
export const userHeaders = [
adminUserHeaders,
basicUserHeaders,
onlycoursestatisticsUserHeaders,
@@ -190,3 +179,20 @@ Cypress.Commands.add('checkTableStats', (correctStats, tableName) => {
cy.get('tr').filter(':visible').should('have.length', correctStats.length)
})
})
/**
* Hover on the selected element.
*/
Cypress.Commands.add('hover', { prevSubject: 'element' }, (subject, options = {}) => {
cy.wrap(subject).trigger('mouseover', {
bubbles: true,
cancelable: true,
...options,
})
cy.wrap(subject).trigger('mousemove', {
bubbles: true,
cancelable: true,
...options,
})
})
-1
View File
@@ -13,7 +13,6 @@
// https://on.cypress.io/configuration
// ***********************************************************
require('@cypress/code-coverage/support')
require('./commands')
Cypress.on('uncaught:exception', error => {
+131 -58
View File
@@ -1,15 +1,30 @@
name: oodikone
x-defaults: &defaults
restart: unless-stopped
x-healthcheck-defaults: &healthcheck-defaults
interval: 1s
timeout: 3s
retries: 30
services:
backend:
<<: *defaults
container_name: backend
container_name: oodikone-backend
image: oodikone-backend:staging
build:
context: services
dockerfile: backend/Dockerfile
depends_on:
- kone-db
- redis
- sis-db
- user-db
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
@@ -22,122 +37,152 @@ services:
IMPORTER_DB_API_URL: 'https://api-toska.apps.ocp-prod-0.k8s.it.helsinki.fi/importer'
JAMI_URL: 'http://jami:3003/'
CRYPT_KEY: 'e16730fce263d5270ba8dcf84d068a32556542455e71331880d6c1c5e4db9b4e'
build:
context: services
dockerfile: backend/Dockerfile
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: frontend
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: kone-db
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
image: registry-toska.ext.ocp-prod-0.k8s.it.helsinki.fi/kone-db
ports:
- 4321:5432
healthcheck:
test: ['CMD', 'pg_isready', '-U', 'postgres']
<<: *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']
<<: *healthcheck-defaults
redis:
<<: *defaults
container_name: oodikone-redis
image: redis:7.2.6
command: >
sh -c 'redis-server --appendonly yes && redis-cli flushall'
image: redis:7.2.6
ports:
- 6379:6379
healthcheck:
test: ['CMD', 'redis-cli', 'ping', '|', 'grep', 'PONG']
<<: *healthcheck-defaults
sis-db:
<<: *defaults
container_name: sis-db
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']
<<: *healthcheck-defaults
user-db:
sis-importer-db:
<<: *defaults
container_name: user-db
environment:
PGDATA: /data
POSTGRES_DB: user-db
POSTGRES_HOST_AUTH_METHOD: trust
image: registry-toska.ext.ocp-prod-0.k8s.it.helsinki.fi/user-db
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']
<<: *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=dev
- NODE_ENV=test # previously was development which borks CI
- DATABASE_URL=postgres://postgres:postgres@jami-db:5432/postgres
ports:
- 3003:3003
container_name: jami
jami-db:
<<: *defaults
container_name: oodikone-jami-db
image: postgres:15.10
environment:
- PGDATA=/data
- POSTGRES_PASSWORD=postgres
container_name: jami-db
ports:
- 5433:5432
healthcheck:
test: ['CMD', 'pg_isready', '-U', 'postgres']
<<: *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
container_name: nginx
depends_on:
- frontend
- backend
frontend:
condition: service_started
backend:
condition: service_healthy
ports:
- 1337:80
profiles:
- oodikone
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf
# Following services are used to run sis-updater-worker when anon sis-db needs to be updated. Could probably also use production version, but this certainly works.
sis-importer-db:
<<: *defaults
container_name: sis-importer-db
image: registry-toska.ext.ocp-prod-0.k8s.it.helsinki.fi/sis-importer-db
profiles:
- updater
sis-updater-worker:
<<: *defaults
build:
context: updater/sis-updater-worker
depends_on:
- redis
- sis-db
- sis-importer-db
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
# 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:
<<: *defaults
restart: no
container_name: oodikone-updater-scheduler
build:
context: updater/sis-updater-scheduler
container_name: sis-updater-scheduler
dockerfile: ./Dockerfile
depends_on:
- redis
- sis-importer-db
redis:
condition: service_healthy
sis-importer-db:
condition: service_healthy
environment:
CI: 'true'
REDIS_HOST: redis
@@ -152,3 +197,31 @@ services:
- 8082:8082
profiles:
- updater
healthcheck:
test: ['CMD', 'curl', '-f', 'http://localhost:8082/health']
<<: *healthcheck-defaults
sis-updater-worker:
<<: *defaults
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
-47
View File
@@ -1,47 +0,0 @@
services:
oodikone-backend:
build:
context: ${OODIKONE_REPO:-.}/services
dockerfile: backend/Dockerfile
args:
SENTRY_ENVIRONMENT: ''
SENTRY_RELEASE: ''
SENTRY_DSN: ''
SERVICE_PROVIDER: fd
NODE_ENV: production
image: ${DOCKER_OODIKONE_NAMESPACE:-637423527834.dkr.ecr.eu-north-1.amazonaws.com/oodikone}/oodikone-backend:${DOCKER_IMAGE_TAG:-dev}
oodikone-frontend:
build:
context: ${OODIKONE_REPO:-.}/services
dockerfile: frontend/Dockerfile
args:
SENTRY_ENVIRONMENT: ''
SENTRY_RELEASE: ''
SENTRY_DSN: ''
SERVICE_PROVIDER: fd
LANGUAGE_CENTER_VIEW_ENABLED: 'false' # currently no intention to be enabled
SIS_URL: 'https://sis-qa.funidata.fi'
NODE_ENV: production
image: ${DOCKER_OODIKONE_NAMESPACE:-637423527834.dkr.ecr.eu-north-1.amazonaws.com/oodikone}/oodikone-frontend:${DOCKER_IMAGE_TAG:-dev}
updater-scheduler:
build:
context: ${OODIKONE_REPO:-.}/updater/sis-updater-scheduler
args:
SENTRY_ENVIRONMENT: ''
SENTRY_RELEASE: ''
SENTRY_DSN: ''
NODE_ENV: production
image: ${DOCKER_OODIKONE_NAMESPACE:-637423527834.dkr.ecr.eu-north-1.amazonaws.com/oodikone}/updater-scheduler:${DOCKER_IMAGE_TAG:-dev}
updater-worker:
build:
context: ${OODIKONE_REPO:-.}/updater/sis-updater-worker
args:
SENTRY_ENVIRONMENT: ''
SENTRY_RELEASE: ''
SENTRY_DSN: ''
SERVICE_PROVIDER: fd
NODE_ENV: production
image: ${DOCKER_OODIKONE_NAMESPACE:-637423527834.dkr.ecr.eu-north-1.amazonaws.com/oodikone}/updater-worker:${DOCKER_IMAGE_TAG:-dev}
-190
View File
@@ -1,190 +0,0 @@
x-defaults: &defaults
restart: unless-stopped
x-build_defaults: &build_defaults
dockerfile: dev.Dockerfile
services:
adminer:
<<: *defaults
container_name: adminer
environment:
ADMINER_DESIGN: pepa-linha
image: adminer:4.8.1
ports:
- 5050:8080
volumes:
- ./adminer-plugins:/var/www/html/plugins-enabled
backend:
<<: *defaults
build:
<<: *build_defaults
context: services/backend
depends_on:
- kone-db
- redis
- sis-db
- user-db
env_file:
- .env
environment:
DB_URL_KONE: postgres://postgres@kone-db:5432/kone-db
DB_URL_USER: postgres://postgres@user-db:5432/user-db
FRONT_URL: http://localhost:3000
REDIS: redis
SECRET_TOKEN: dev
SIS_DB_URL: postgres://postgres@sis-db:5432/sis-db
SIS_UPDATER_URL: http://sis-updater-scheduler:8082
IMPORTER_DB_API_URL: 'https://localhost:3005'
JAMI_URL: 'http://jami:3003/'
SERVICE_PROVIDER: fd
SIS_HOST: ${SIS_HOST}
SIS_GRAPHQL_APPLICATION_ACCOUNT: ${SIS_GRAPHQL_APPLICATION_ACCOUNT}
SIS_GRAPHQL_APPLICATION_KEY: ${SIS_GRAPHQL_APPLICATION_KEY}
IMPORTER_DB_API_USER: dev_user
IMPORTER_DB_API_PASSWORD: dev_password
ROOT_ORG_ID: ${ROOT_ORG_ID}
LANGUAGE_CENTER_VIEW_ENABLED: 'false'
ports:
- 8080:8080
volumes:
- backend-nodemod:/opt/app-root/backend/node_modules
- ./environments/testu:/opt/app-root/backend/src/environment
- ./services/backend/:/opt/app-root/backend
- ./services/shared/:/opt/app-root/shared
frontend:
<<: *defaults
build:
<<: *build_defaults
context: services/frontend
container_name: frontend
ports:
- 3000:3000
environment:
REACT_APP_SERVICE_PROVIDER: fd
REACT_APP_LANGUAGE_CENTER_VIEW_ENABLED: 'false'
REACT_APP_SIS_URL: 'https://sis-qa.funidata.fi'
volumes:
- frontend-nodemod:/opt/app-root/frontend/node_modules
- ./services/frontend/:/opt/app-root/frontend
- ./services/shared/:/opt/app-root/shared
kone-db:
<<: *defaults
container_name: kone-db
environment:
PGDATA: /data
POSTGRES_DB: kone-db
POSTGRES_HOST_AUTH_METHOD: trust
image: postgres:15.10
ports:
- 12349:5432
volumes:
- kone-db-data:/data
redis:
<<: *defaults
command: 'redis-server --appendonly yes'
container_name: redis
image: redis:6.2.16
ports:
- '6379:6379'
volumes:
- redis-data:/data
sis-db:
<<: *defaults
container_name: sis-db
environment:
PGDATA: /data
POSTGRES_DB: sis-db
POSTGRES_HOST_AUTH_METHOD: trust
image: postgres:15.10
volumes:
- sis-db-data:/data
shm_size: 2gb
ports:
- 12345:5432
sis-updater-scheduler:
<<: *defaults
build:
<<: *build_defaults
context: updater/sis-updater-scheduler
container_name: sis-updater-scheduler
extra_hosts:
- 'host.docker.internal:host-gateway'
depends_on:
- redis
environment:
REDIS_HOST: redis
REDIS_PORT: 6379
SECRET_TOKEN: dev
SIS_IMPORTER_HOST: ${SIS_IMPORTER_DB_HOST:-host.docker.internal}
SIS_IMPORTER_PORT: ${SIS_IMPORTER_DB_PORT:-15234}
SIS_IMPORTER_DATABASE: importer-db
SIS_IMPORTER_PASSWORD: dev
SIS_IMPORTER_USER: dev
SERVICE_PROVIDER: fd
ports:
- 8082:8082
volumes:
- ./updater/sis-updater-scheduler/:/opt/app-root/src
sis-updater-worker:
<<: *defaults
deploy:
replicas: 2
mode: replicated
build:
<<: *build_defaults
context: updater/sis-updater-worker
depends_on:
- redis
- sis-db
extra_hosts:
- 'host.docker.internal:host-gateway'
environment:
DB_URL: postgres://postgres@sis-db:5432/sis-db
REDIS_HOST: redis
REDIS_PORT: 6379
SIS_IMPORTER_HOST: ${SIS_IMPORTER_DB_HOST:-host.docker.internal}
SIS_IMPORTER_PORT: ${SIS_IMPORTER_DB_PORT:-15234}
SIS_IMPORTER_DATABASE: importer-db
SIS_IMPORTER_PASSWORD: dev
SIS_IMPORTER_USER: dev
SIS_PASSWORD: postgres
ROOT_ORG_ID: ${ROOT_ORG_ID}
SERVICE_PROVIDER: fd
volumes:
- ./updater/sis-updater-worker/:/opt/app-root/src
user-db:
<<: *defaults
container_name: user-db
environment:
PGDATA: /data
POSTGRES_DB: user-db
POSTGRES_HOST_AUTH_METHOD: trust
image: postgres:15.10
volumes:
- user-db-data:/data
ports:
- 12347:5432
volumes:
backend-nodemod:
frontend-nodemod:
kone-db-data:
redis-data:
sis-db-data:
user-db-data:
networks:
default:
name: oodikone
importer:
name: importer_network
external: true
+3
View File
@@ -1,3 +1,5 @@
name: oodikone
services:
backend:
environment:
@@ -20,3 +22,4 @@ services:
volumes:
redis-data-real:
name: oodikone-redis-data-real
+26 -4
View File
@@ -1,5 +1,27 @@
name: oodikone
x-defaults: &defaults
restart: unless-stopped
x-healthcheck-defaults: &healthcheck-defaults
interval: 1s
timeout: 3s
retries: 30
services:
frontend:
volumes:
- frontend-nodemod:/opt/app-root/frontend/node_modules
- ./services/frontend/instrumented:/opt/app-root/frontend/src
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']
<<: *healthcheck-defaults
+138 -70
View File
@@ -1,13 +1,20 @@
name: oodikone
x-defaults: &defaults
restart: unless-stopped
x-build_defaults: &build_defaults
dockerfile: dev.Dockerfile
x-healthcheck-defaults: &healthcheck-defaults
interval: 1s
timeout: 3s
retries: 30
services:
adminer:
<<: *defaults
container_name: adminer
container_name: oodikone-adminer
environment:
ADMINER_DESIGN: pepa-linha
image: adminer:4.8.1
@@ -18,14 +25,19 @@ services:
backend:
<<: *defaults
container_name: oodikone-backend
build:
<<: *build_defaults
context: services/backend
dockerfile: backend/dev.Dockerfile
context: services
depends_on:
- kone-db
- redis
- sis-db
- user-db
kone-db:
condition: service_healthy
redis:
condition: service_healthy
sis-db:
condition: service_healthy
user-db:
condition: service_healthy
env_file:
- .env
tty: true
@@ -44,57 +56,90 @@ services:
profiles:
- oodikone
volumes:
- backend-nodemod:/opt/app-root/backend/node_modules
- ./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
container_name: oodikone-frontend
build:
<<: *build_defaults
context: services/frontend
container_name: frontend
dockerfile: frontend/dev.Dockerfile
context: services
depends_on:
backend:
condition: service_healthy
ports:
- 3000:3000
profiles:
- oodikone
volumes:
- frontend-nodemod:/opt/app-root/frontend/node_modules
- ./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: kone-db
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']
<<: *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']
<<: *healthcheck-defaults
redis:
<<: *defaults
container_name: oodikone-redis
command: 'redis-server --appendonly yes'
container_name: redis
image: redis:7.2.6
ports:
- '6379:6379'
- 6379:6379
volumes:
- redis-data:/data
healthcheck:
test: ['CMD', 'redis-cli', 'ping', '|', 'grep', 'PONG']
<<: *healthcheck-defaults
sis-db:
<<: *defaults
container_name: sis-db
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']
<<: *healthcheck-defaults
sis-importer-db:
<<: *defaults
container_name: sis-importer-db
container_name: oodikone-sis-importer-db
image: registry-toska.ext.ocp-prod-0.k8s.it.helsinki.fi/sis-importer-db
profiles:
- updater
@@ -102,40 +147,55 @@ services:
- sis-importer-db-data:/data
ports:
- 12346:5432
healthcheck:
test: ['CMD', 'pg_isready', '-U', 'postgres']
<<: *healthcheck-defaults
sis-updater-scheduler:
jami:
<<: *defaults
build:
<<: *build_defaults
context: updater/sis-updater-scheduler
container_name: sis-updater-scheduler
container_name: oodikone-jami
image: quay.io/toska/jami:production
depends_on:
- redis
- sis-importer-db
jami-db:
condition: service_healthy
env_file:
- .env
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
- npm_config_cache=/tmp
- PORT=3003
- NODE_ENV=development
- DATABASE_URL=postgres://postgres:postgres@jami-db:5432/jami-db-real
ports:
- 8082:8082
profiles:
- updater
- 3003:3003
jami-db:
<<: *defaults
container_name: oodikone-jami-db
image: postgres:15.10
environment:
- PGDATA=/data
- POSTGRES_PASSWORD=postgres
ports:
- 5433:5432
volumes:
- sis-updater-scheduler-nodemod:/opt/app-root/src/node_modules
- ./updater/sis-updater-scheduler/:/opt/app-root/src
- jami-data:/data
healthcheck:
test: ['CMD', 'pg_isready', '-U', 'postgres']
<<: *healthcheck-defaults
sis-updater-worker:
<<: *defaults
container_name: oodikone-updater-worker
build:
<<: *build_defaults
context: updater/sis-updater-worker
depends_on:
- redis
- sis-db
- sis-importer-db
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
@@ -147,52 +207,60 @@ services:
profiles:
- updater
volumes:
- sis-updater-worker-nodemod:/opt/app-root/src/node_modules
- ./updater/sis-updater-worker/:/opt/app-root/src
- sis-updater-worker-nodemod:/opt/app-root/src/node_modules
user-db:
sis-updater-scheduler:
<<: *defaults
container_name: user-db
image: registry-toska.ext.ocp-prod-0.k8s.it.helsinki.fi/user-db
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:
- oodikone
- updater
volumes:
- user-db-data:/data
jami:
<<: *defaults
image: quay.io/toska/jami:production
env_file:
- .env
environment:
- npm_config_cache=/tmp
- PORT=3003
- NODE_ENV=dev
- DATABASE_URL=postgres://postgres:postgres@jami-db:5432/postgres
ports:
- 3003:3003
container_name: jami
jami-db:
<<: *defaults
image: postgres:15.10
environment:
- PGDATA=/data
- POSTGRES_PASSWORD=postgres
container_name: jami-db
ports:
- 5433:5432
volumes:
- jami-data:/data
- ./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
+1 -4
View File
@@ -8,11 +8,10 @@ Roles are defined [here](../services/backend/src/config/roles.ts). Criteria for
- courseStatistics
- facultyStatistics
- fullSisuAccess
- openUniSearch
- studyGuidanceGroups
- teachers
The roles `facultyStatistics`, `openUniSearch`, and `studyGuidanceGroups` simply enable certain views (_Faculties_, _Open uni student population_, and _Guidance groups_ respectively).
The roles `facultyStatistics` and `studyGuidanceGroups` simply enable certain views (_Faculties_ and _Guidance groups_ respectively).
### courseStatistics
@@ -43,7 +42,6 @@ The roles `facultyStatistics`, `openUniSearch`, and `studyGuidanceGroups` simply
| Custom population | | ✅ | ✅ |
| Completed courses | ✅ | ✅ | ✅ |
| Close to graduation | | ✅ | ✅ |
| Open uni population | | ✅ | |
| Courses | | ✅ | ✅ |
\* = anyone who can log in to Oodikone, check the [IAM groups](#iam-groups) section for more information
@@ -63,7 +61,6 @@ A user must be a member of either the `grp-oodikone-basic-users` or `grp-oodikon
| grp-oodikone-basic-users | courseStatistics |
| grp-oodikone-users | facultyStatistics |
| grp-toska | admin |
| hy-ypa-opa-dojo | openUniSearch |
`grp-oodikone-basic-users`: basically the whole staff (hy-\*-allstaff)
+3 -3
View File
@@ -36,7 +36,7 @@ All things listed below should work automatically, if new combinations are done.
- _Students of the studyprogramme by starting year_
- Graduated field separated for both programmes
- All = currently enrolled + absent + inactive + graduated licentiate
- 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_
@@ -85,12 +85,12 @@ All things listed below should work automatically, if new combinations are done.
- **Graduated from programme** - possibility to filter out students based on graduation either in bachelor's or master's.
- **Studyright status** - active or inactive based on the chosen degree
- **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 `/v3/populationstatisticsbystudentnumbers`. Separation from code 'KHxx_xxx+MHxx_xxx' is done in backend.
- 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.
+3 -3
View File
@@ -8,7 +8,7 @@ These instructions explain how to update the schemas for the following databases
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]
> [!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
@@ -25,7 +25,7 @@ The steps to **add, remove, or modify columns** are mostly the same for each dat
- 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]
> [!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
@@ -73,7 +73,7 @@ If you need to **revert a migration locally**, follow these steps:
## `sis-db`
> [!NOTE]
> [!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:**
+5 -5
View File
@@ -55,11 +55,11 @@ The `semester_enrollments` column is an array of JSON objects. Each object conta
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 | Inactive | neither 1 or 2 has been declared |
| value | meaning | notes |
| ----: | :------ | :------------------------------- |
| 1 | Present | |
| 2 | Absent | |
| 3 | Passive | neither 1 or 2 has been declared |
## Credit type code
+4 -13
View File
@@ -2,26 +2,19 @@
A collection of useful tools, documentation and other resources.
## AI tools
- [ChatGPT](https://chatgpt.com)
- [CurreChat](https://curre.helsinki.fi/chat)
- [GitHub Copilot](https://github.com/features/copilot)
- Free for students!
- [Microsoft Copilot](https://copilot.microsoft.com)
## Code style and maintainability
- [CodeClimate documentation](https://docs.codeclimate.com/docs)
- [Advanced configuration](https://docs.codeclimate.com/docs/advanced-configuration)
- [ESLint documentation](https://eslint.org/docs/v8.x)
- [Prettier documentation](https://prettier.io/docs/en)
- [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
- Install: `$ pip install pgcli`
- 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/)
@@ -29,8 +22,6 @@ A collection of useful tools, documentation and other resources.
- [GitHub documentation](https://docs.github.com/en)
- [GitHub Actions](https://docs.github.com/en/actions)
- [GitHub Education](https://github.com/education/students)
- Free benefits for students!
- [Mermaid.js documentation](https://mermaid.js.org/intro/syntax-reference.html)
## TypeScript
+85 -53
View File
@@ -25,7 +25,7 @@ export default tseslint.config(
'nyc-config.cjs',
'.lintstagedrc.mjs',
'cypress.config.js',
]
],
},
// Global options (excl. e2e)
@@ -44,8 +44,8 @@ export default tseslint.config(
parser: tseslint.parser,
parserOptions: {
projectService: {
allowDefaultProject: ['*.{js,cjs}']
}
allowDefaultProject: ['*.{js,cjs}'],
},
},
globals: {
...globals.browser,
@@ -53,16 +53,16 @@ export default tseslint.config(
},
},
plugins: {
'import-x': eslintPluginImportX
'import-x': eslintPluginImportX,
},
linterOptions: {
reportUnusedDisableDirectives: 'error',
},
rules: {
'camelcase': 'off', // TODO: Enable. old db/backend logic is still using snake_case every now and then and needs a big refactor
camelcase: 'off', // TODO: Enable. old db/backend logic is still using snake_case every now and then and needs a big refactor
'class-methods-use-this': 'off', // Disabled in favor of @typescript-eslint
'consistent-return': 'off', // Disabled in favor of @typescript-eslint
'id-denylist': ['error', 'c', 'd', 'e', 'err', 't'],
'id-denylist': ['error', 'err'],
'no-alert': 'error',
'no-await-in-loop': 'error',
'no-console': 'error',
@@ -75,24 +75,42 @@ export default tseslint.config(
'prefer-const': 'error',
'prefer-destructuring': 'off', // Disabled in favor of @typescript-eslint
'dot-notation': 'off', // Disabled in favor of @typescript-eslint
'no-restricted-imports': ['error', { 'patterns': [{ 'regex': '^@mui/[^/]+$' }] }],
'quotes': ['error', 'single',
{ avoidEscape: true, allowTemplateLiterals: false }
'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).',
},
],
},
],
quotes: ['error', 'single', { avoidEscape: true, allowTemplateLiterals: false }],
'import-x/no-commonjs': 'error',
'import-x/no-default-export': 'error',
'import-x/no-extraneous-dependencies': ['error', { devDependencies: false }],
'import-x/no-unused-modules': ['error',
'import-x/no-unused-modules': [
'error',
{
missingExports: true,
unusedExports: true,
src: ['{services,updater}/**/*.{js,jsx,ts,tsx}']
}],
'import-x/order': ['error', {
alphabetize: { order: 'asc', caseInsensitive: true },
groups: [['builtin', 'external'], ['internal'], ['parent'], ['sibling', 'index']],
}],
src: ['{services,updater}/**/*.{js,jsx,ts,tsx}'],
},
],
'import-x/order': [
'error',
{
alphabetize: { order: 'asc', caseInsensitive: true },
groups: [['builtin', 'external'], ['internal'], ['parent'], ['sibling', 'index']],
},
],
// TypeScript
'@typescript-eslint/no-unused-vars': [
@@ -105,9 +123,7 @@ export default tseslint.config(
'@typescript-eslint/consistent-return': 'error', // NOTE: This is weaker than tsconfig.json noImplicitReturns, and should be replaced once ts migration complete
'@typescript-eslint/consistent-type-definitions': 'off',
'@typescript-eslint/dot-notation': 'error',
'@typescript-eslint/prefer-destructuring': ['error',
{ VariableDeclarator: { object: true } }
],
'@typescript-eslint/prefer-destructuring': ['error', { VariableDeclarator: { object: true } }],
'@typescript-eslint/no-empty-object-type': ['error', { allowInterfaces: 'with-single-extends' }],
'@typescript-eslint/no-explicit-any': 'off', // TODO: enable when feeling brave
'@typescript-eslint/no-implied-eval': 'error',
@@ -127,12 +143,12 @@ export default tseslint.config(
'services/frontend/tsconfig.json',
'services/frontend/tsconfig.node.json',
'services/backend/tsconfig.json',
'services/shared/tsconfig.json'
'services/shared/tsconfig.json',
],
}),
createNodeResolver({
extensions: ['.js', '.jsx', 'cjs', 'mjs', '.ts', '.tsx'],
})
}),
],
},
},
@@ -140,22 +156,22 @@ export default tseslint.config(
{
files: ['services/backend/src/routes/*.{ts,js}'],
rules: {
'@typescript-eslint/no-misused-promises': 'off', // Errors come mainly from Express route handlers that are handled correctly by express-async-errors
'@typescript-eslint/no-misused-promises': 'off', // Errors come mainly from Express route handlers that are handled correctly by express-async-errors
'@typescript-eslint/consistent-return': 'off',
}
},
},
// Disable type-aware linting for javascript files
{
files: ['{updater,services}/**/*.{js,cjs}'],
extends: [tseslint.configs.disableTypeChecked]
extends: [tseslint.configs.disableTypeChecked],
},
// Frontend camelCase and react
{
files: ['services/frontend/**/*.{js,jsx,mjs,cjs,ts,tsx}'],
plugins: {
'react': eslintPluginReact,
react: eslintPluginReact,
},
extends: [
eslintPluginReact.configs.flat.recommended,
@@ -166,14 +182,15 @@ export default tseslint.config(
parserOptions: {
ecmaFeatures: {
jsx: true,
}
}
},
},
},
rules: {
'camelcase': 'warn',
camelcase: 'warn',
'react/prop-types': 'off', // Typescript but worse
'react/no-unescaped-entities': 'off',
'react/function-component-definition': ['error',
'react/function-component-definition': [
'error',
{ namedComponents: 'arrow-function', unnamedComponents: 'arrow-function' },
],
'react/no-array-index-key': 'error',
@@ -181,9 +198,7 @@ export default tseslint.config(
'react/no-this-in-sfc': 'error',
'react/prefer-stateless-function': 'error',
'react/jsx-boolean-value': ['error', 'never'],
'react/jsx-filename-extension': ['error',
{ allow: 'as-needed', extensions: ['.jsx', '.tsx'] }
],
'react/jsx-filename-extension': ['error', { allow: 'as-needed', extensions: ['.jsx', '.tsx'] }],
'react/jsx-no-leaked-render': 'warn', // TODO: enable, tho not that useful after full ts migration
'react/jsx-no-useless-fragment': 'error',
'react/jsx-props-no-spreading': 'warn', // TODO: enable, improves readability
@@ -191,18 +206,25 @@ export default tseslint.config(
'react/jsx-tag-spacing': 'warn',
},
settings: {
'react': {
'version': 'detect'
}
}
react: {
version: 'detect',
},
},
},
// Cypress should only use rules provided by the plugin below
{
files: ['cypress/**/*.js'],
extends: [
eslintPluginCypress.configs.recommended
]
extends: [eslintPluginCypress.configs.recommended],
},
// Override for backend api tests
{
files: ['services/backend/tests/**/*.ts'],
rules: {
'import-x/no-extraneous-dependencies': 'off',
'no-console': 'off',
},
},
// Overrides for CommonJS
@@ -210,7 +232,7 @@ export default tseslint.config(
files: [
'services/backend/**/*.js',
'updater/sis-updater-worker/src/db/migrations/*.cjs',
'updater/sis-updater-scheduler/**/*.js'
'updater/sis-updater-scheduler/**/*.js',
],
languageOptions: {
sourceType: 'commonjs',
@@ -221,20 +243,9 @@ export default tseslint.config(
},
},
// Rapodiff
{
files: ['services/backend/src/rapodiff/**/*'],
rules: {
'no-console': 'off',
},
},
// Migration files
{
files: [
'updater/sis-updater-worker/src/db/migrations/*.cjs',
'services/backend/src/database/**/*.js'
],
files: ['updater/sis-updater-worker/src/db/migrations/*.cjs', 'services/backend/src/database/**/*.js'],
rules: {
'import-x/no-unused-modules': 'off',
'@typescript-eslint/no-empty-function': 'off',
@@ -252,5 +263,26 @@ export default tseslint.config(
'no-promise-executor-return': 'off',
},
},
eslintPluginPrettierRecommended, // This must be the last plugin so it can override other configs
// Backend tests
{
files: ["services/backend/tests/**/*.ts", "services/backend/vitest.config.ts"],
rules: {
'import-x/no-extraneous-dependencies': 'off',
}
},
// Frontend tests
{
files: ['services/frontend/tests/**/*.{ts,tsx}', "services/frontend/playwright/**/*.{ts,tsx}", 'services/frontend/playwright-ct.config.ts'],
rules: {
'import-x/no-extraneous-dependencies': 'off',
'import-x/no-unused-modules': 'off',
'import-x/no-default-export': 'off',
'react/jsx-filename-extension': 'off',
},
},
eslintPluginPrettierRecommended // This must be the last plugin so it can override other configs
)
-6
View File
@@ -1,6 +0,0 @@
const { parserPlugins } = require('@istanbuljs/schema').defaults.nyc
module.exports = {
cache: false,
parserPlugins: parserPlugins.concat('typescript', 'jsx'),
}
+477 -4179
View File
File diff suppressed because it is too large Load Diff
+17 -22
View File
@@ -6,31 +6,32 @@
},
"scripts": {
"cypress": "cypress",
"cypress:open": "cypress open",
"cypress:run": "cypress run --browser chrome",
"instrument": "./scripts/instrument.sh",
"cypress:run": "cypress run --browser firefox",
"eslint": "eslint",
"lint:all": "eslint services/ updater/",
"stylelint": "stylelint",
"prettier": "prettier",
"cli": "./cli.sh",
"oodikone": "./run.sh oodikone anon up --detach",
"logs": "docker compose --file docker-compose.yml logs backend frontend sis-updater-worker sis-updater-scheduler jami -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": "./run.sh oodikone test 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 sis-updater-worker && SCHEDULE_IMMEDIATE=weekly EXIT_AFTER_IMMEDIATES=yes docker-compose --file docker-compose.ci.yml run sis-updater-scheduler npm start",
"flushredis": "docker exec -it redis redis-cli flushall",
"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",
"rapodiff": "docker exec -t oodikone_backend_1 /opt/app-root/backend/node_modules/.bin/tsx /opt/app-root/backend/src/rapodiff/rapodiff.js",
"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",
"install:docker": "docker exec frontend npm i && docker exec oodikone_backend_1 npm i && docker exec sis-updater-scheduler npm i && docker exec oodikone_sis-updater-worker_1 npm i",
"ci:docker": "docker exec frontend npm ci && docker exec oodikone_backend_1 npm ci && docker exec sis-updater-scheduler npm ci && docker exec oodikone_sis-updater-worker_1 npm ci",
"install:docker": "docker exec --user 0 oodikone-frontend sh -lc \"npm i --prefix /opt/app-root/shared && npm i --prefix /opt/app-root/frontend && chown -R 1001:0 /opt/app-root/shared/node_modules /opt/app-root/frontend/node_modules\" && docker exec --user 0 oodikone-backend sh -lc \"npm i --prefix /opt/app-root/backend && chown -R 1001:0 /opt/app-root/backend/node_modules\" && docker exec --user 0 oodikone-updater-scheduler sh -lc \"npm i --prefix /opt/app-root/src && chown -R 1001:0 /opt/app-root/src/node_modules\" && docker exec --user 0 oodikone-updater-worker sh -lc \"npm i --prefix /opt/app-root/src && chown -R 1001:0 /opt/app-root/src/node_modules\"",
"install:both": "npm run install:local && npm run install:docker",
"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 --user 0 oodikone-frontend sh -lc \"npm ci --prefix /opt/app-root/shared && npm ci --prefix /opt/app-root/frontend && chown -R 1001:0 /opt/app-root/shared/node_modules /opt/app-root/frontend/node_modules\" && docker exec --user 0 oodikone-backend sh -lc \"npm ci --prefix /opt/app-root/backend && chown -R 1001:0 /opt/app-root/backend/node_modules\" && docker exec --user 0 oodikone-updater-scheduler sh -lc \"npm ci --prefix /opt/app-root/src && chown -R 1001:0 /opt/app-root/src/node_modules\" && docker exec --user 0 oodikone-updater-worker sh -lc \"npm ci --prefix /opt/app-root/src && chown -R 1001:0 /opt/app-root/src/node_modules\"",
"ci:both": "npm run ci:local && npm run ci:docker"
},
"repository": {
@@ -43,28 +44,22 @@
},
"homepage": "https://github.com/UniversityOfHelsinkiCS/oodikone#readme",
"devDependencies": {
"@cypress/code-coverage": "^3.14.4",
"@eslint/js": "^9.28.0",
"@istanbuljs/schema": "^0.1.3",
"cypress": "^15.0.0",
"cypress": "^15.13.0",
"eslint": "^9.28.0",
"eslint-config-prettier": "^10.1.5",
"eslint-import-resolver-typescript": "^4.4.3",
"eslint-plugin-cypress": "^5.1.1",
"eslint-plugin-cypress": "^6.2.3",
"eslint-plugin-import-x": "^4.16.1",
"eslint-plugin-prettier": "^5.4.1",
"eslint-plugin-react": "^7.37.5",
"eslint-plugin-react-hooks": "^5.2.0",
"globals": "^16.2.0",
"globals": "^17.4.0",
"husky": "^9.1.7",
"lint-staged": "^16.1.0",
"nyc": "^17.1.0",
"prettier": "3.5.3",
"stylelint": "^16.20.0",
"stylelint-config-standard": "^39.0.0",
"stylelint": "^17.6.0",
"stylelint-config-standard": "^40.0.0",
"typescript-eslint": "^8.34.0"
},
"dependencies": {
"lodash": "^4.17.21"
}
}
+7 -7
View File
@@ -23,7 +23,7 @@ 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.
* Command: will be passed to docker compose.
EOF
}
@@ -35,7 +35,7 @@ parse_params() {
option=${args[0]}
# parse arguments
[[ ("$option" != "oodikone" && "$option" != "updater" && "$option" != "both") ]] && \
[[ ("$option" != "oodikone" && "$option" != "updater" && "$option" != "both" && "$option" != "tests") ]] && \
usage && die "Wrong option: $option"
[[ ${#args[@]} -eq 1 ]] && usage && die "Wrong number of arguments"
@@ -47,7 +47,7 @@ parse_params() {
compose_command=${args[*]:1}
else
version=${args[1]}
[[ "$version" != "anon" && "$version" != "real" && "$version" != "test" ]] && usage && \
[[ "$version" != "anon" && "$version" != "real" && "$version" != "api" ]] && usage && \
die "Wrong version $version"
compose_command=${args[*]:2}
fi
@@ -70,8 +70,8 @@ parse_env() {
if [[ "$version" == "real" ]]; then
env="--file docker-compose.yml --file docker-compose.real.yml"
fi
if [[ "$version" == "test" ]]; then
env="--file docker-compose.yml --file docker-compose.test.yml"
if [[ "$version" == "api" ]]; then
env="--file docker-compose.ci.yml"
fi
return 0
}
@@ -85,9 +85,9 @@ parse_params "$@"
parse_profiles
parse_env
if [[ "$compose_command" == "" ]]; then
final_command="docker-compose ${env}"
final_command="docker compose ${env}"
else
final_command="docker-compose ${env} ${profiles} ${compose_command}"
final_command="docker compose ${env} ${profiles} ${compose_command}"
fi
msg "${BLUE}Running: ${final_command}${NOFORMAT}"
+6 -29
View File
@@ -11,9 +11,8 @@ PROJECT_ROOT="$(git rev-parse --show-toplevel)"
DUMP_DIR="$PROJECT_ROOT/.databasedumps"
S3_CONFIG_FILE=~/.s3cfg
S3_BUCKET="s3://psyduck"
DOCKER_COMPOSE=$PROJECT_ROOT/docker-compose.yml
## Following the naming convention in docker-compose, these are names for database
## 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"
@@ -113,11 +112,11 @@ reset_databases() {
infomsg "Restoring PostgreSQL dumps from backups. This might take a while."
npm run docker:down
docker-compose up -d "${databases[@]}"
docker compose up -d "${databases[@]}"
for database in "${databases[@]}"; do
local database_dump="$database_dump_dir/$database.sql.gz"
local database_container="$database"
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"
@@ -139,30 +138,7 @@ reset_databases() {
successmsg "Database setup finished"
}
reset_jami_data() {
local database="jami-db"
local dump_destination="$DUMP_DIR/$database.sql.gz"
local s3_path="jami"
infomsg "Removing old data"
infomsg "Removing database and related volume"
docker volume rm oodikone_jami-data || warningmsg "This is okay, continuing"
docker-compose -f "$DOCKER_COMPOSE" down "$database" || warningmsg "This is okay, continuing"
infomsg "Starting postgres in the background"
docker-compose -f "$DOCKER_COMPOSE" up -d "$database"
retry docker-compose -f "$DOCKER_COMPOSE" exec "$database" pg_isready --dbname="postgres"
infomsg "Populating Jami"
docker exec -i "$database" /bin/bash -c "gunzip | psql -U postgres" < "$dump_destination" 2> /dev/null
docker-compose stop "$database"
msg ""
}
reset_all_real_data() {
reset_all_data() {
infomsg "Downloading real data dumps"
for i in ${!DATABASES[*]}; do
local database="${DATABASES[$i]}"
@@ -239,6 +215,7 @@ set_up_oodikone_from_scratch() {
"$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"
)
@@ -247,7 +224,7 @@ set_up_oodikone_from_scratch() {
"$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"
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"
-30
View File
@@ -1,30 +0,0 @@
#!/usr/bin/env bash
# This script is used for instrumenting the frontend code
# Counters are added for calculating the code coverage
# Instrumented code is used for generating code coverage reports
# Ensure the script exits if any command fails
set -euo pipefail
# Define the directories
SOURCE_DIR="services/frontend/src"
INSTRU_DIR="services/frontend/instrumented"
# Print status
echo -n "Instrumenting frontend code... "
# Create the instrumented directory if it does not exist
mkdir -p $INSTRU_DIR
# Instrument the code
nyc instrument --all --compact=false $SOURCE_DIR $INSTRU_DIR
# Copy remaining files without overwriting instrumented files
#
# Images, CSS and other static files are not instrumended
# but they are still needed to run the application
rsync -av --quiet --ignore-existing $SOURCE_DIR/ $INSTRU_DIR/
# Print status
echo "Done!"
+116
View File
@@ -0,0 +1,116 @@
# 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
-3
View File
@@ -1,3 +0,0 @@
.dockerignore
*Dockerfile
node_modules/
+12
View File
@@ -0,0 +1,12 @@
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'
+3
View File
@@ -0,0 +1,3 @@
ignore-scripts=true
min-release-age=7
save-prefix=
+7 -3
View File
@@ -1,4 +1,4 @@
FROM registry.access.redhat.com/ubi9/nodejs-22-minimal AS build
FROM registry.access.redhat.com/ubi10/nodejs-24-minimal AS build
ENV TZ="Europe/Helsinki"
@@ -29,7 +29,7 @@ RUN npm ci --include=dev --prefix shared && npm ci --include=dev --prefix backen
RUN npm run build --prefix shared && npm run build --prefix backend
FROM registry.access.redhat.com/ubi9/nodejs-22-minimal
FROM registry.access.redhat.com/ubi10/nodejs-24-minimal
ENV TZ="Europe/Helsinki"
@@ -50,9 +50,13 @@ ENV SENTRY_RELEASE=$SENTRY_RELEASE
WORKDIR /opt/app-root/shared
COPY --from=build /opt/app-root/shared/dist .
COPY --from=build /opt/app-root/shared/.npmrc .
COPY --from=build /opt/app-root/shared/package*.json ./
RUN npm ci --omit=dev
WORKDIR /opt/app-root/backend
COPY --from=build /opt/app-root/backend/package*.json .
COPY --from=build /opt/app-root/backend/.npmrc .
COPY --from=build /opt/app-root/backend/package*.json ./
COPY --from=build /opt/app-root/backend/dist ./dist
RUN npm ci --omit=dev
+12 -4
View File
@@ -1,13 +1,21 @@
FROM node:22-alpine
FROM registry.access.redhat.com/ubi10/nodejs-24-minimal
ENV TZ="Europe/Helsinki"
ENV NODE_ENV=development
ENV NODE_OPTIONS=--max-old-space-size=4096
USER 0
WORKDIR /opt/app-root/shared
COPY --chown=1001:0 ./shared .
RUN npm ci && chown -R 1001:0 /opt/app-root/shared
WORKDIR /opt/app-root/backend
COPY . .
RUN npm ci
COPY --chown=1001:0 ./backend .
RUN npm ci && chown -R 1001:0 /opt/app-root/backend
EXPOSE 8080
CMD ["node_modules/.bin/tsx", "watch", "--clear-screen=false", "index.js"]
USER 1001
CMD ["node_modules/.bin/nodemon", "--exec", "node --import tsx index.ts", "--ext", "ts,js,json", "--watch", "src", "--watch", "index.ts", "--legacy-watch"]
+2195 -988
View File
File diff suppressed because it is too large Load Diff
+20 -19
View File
@@ -2,49 +2,50 @@
"name": "backend",
"private": true,
"scripts": {
"build": "tsc"
"build": "tsc",
"test:all": "vitest",
"test:api": "vitest ./tests/api",
"test:services": "vitest ./tests/services",
"test:unit": "vitest ./tests/unit",
"test:silent": "SILENT_TESTING=true vitest"
},
"dependencies": {
"@oodikone/shared": "file:../shared",
"@sentry/node": "^10.5.0",
"@sentry/profiling-node": "^10.5.0",
"@sentry/node": "^10.43.0",
"@sentry/profiling-node": "^10.43.0",
"axios": "^1.9.0",
"bullmq": "^5.53.2",
"compression": "^1.8.0",
"cors": "^2.8.5",
"cron": "^4.3.1",
"dayjs": "^1.11.13",
"express": "^4.21.2",
"express-async-errors": "^3.1.1",
"lodash": "^4.17.21",
"express": "^5.2.1",
"lodash-es": "^4.18.1",
"lru-cache": "^11.1.0",
"morgan": "^1.10.0",
"papaparse": "^5.5.3",
"pg": "^8.16.0",
"pg": "^8.20.0",
"redis": "^4.7.1",
"reflect-metadata": "^0.2.2",
"sequelize": "^6.37.7",
"sequelize": "^6.37.8",
"sequelize-typescript": "^2.1.6",
"umzug": "^3.8.2",
"unfuck-utf8-headers-middleware": "^1.0.1",
"vite": "^7.1.12",
"winston": "^3.17.0",
"winston-gelf-transporter": "^1.0.2",
"winston-loki": "^6.1.3"
},
"devDependencies": {
"@types/compression": "^1.8.1",
"@types/cors": "^2.8.19",
"@types/express": "^4.17.23",
"@types/lodash": "^4.17.17",
"@types/express": "^5.0.6",
"@types/lodash-es": "^4.17.12",
"@types/morgan": "^1.9.10",
"@types/node": "^24.10.1",
"@types/pg": "^8.20.0",
"@types/supertest": "^6.0.3",
"nodemon": "^3.1.11",
"supertest": "^7.2.2",
"tsx": "^4.19.4",
"typescript": "^5.9.2"
},
"overrides": {
"winston-gelf-transporter": {
"gelf-pro": "^1.4.0"
}
"typescript": "^5.9.2",
"vitest": "4.1.9"
}
}
-1
View File
@@ -1,4 +1,3 @@
import 'express-async-errors'
import express from 'express'
import { baseUrl, backendPort } from './config'
+4 -12
View File
@@ -1,14 +1,15 @@
// Node env to use
export const isDev = process.env.NODE_ENV === 'development'
export const isTest = process.env.NODE_ENV === 'test'
export const isStaging = process.env.REACT_APP_STAGING === 'true'
export const isProduction = !isStaging && process.env.NODE_ENV === 'production'
export const runningInCI = process.env.CI === 'true'
export const silentTesting = process.env.SILENT_TESTING === 'true'
// IAM group
export const requiredGroup = isStaging
? ['grp-oodikone-staging-users', 'grp-oodikone-basic-staging-users']
: ['grp-oodikone-users', 'grp-oodikone-basic-users']
export const requiredGroup = ['grp-oodikone-users', 'grp-oodikone-basic-users']
// Pate
export const pateToken = process.env.PATE_API_TOKEN ?? ''
@@ -32,20 +33,11 @@ export const baseUrl = isDev ? '/api' : ''
export const frontUrl = process.env.FRONT_URL
export const backendPort = 8080
// System run in whose environment
export const serviceProvider = process.env.SERVICE_PROVIDER?.toLowerCase() ?? 'toska'
// Optional logout-url configuration
export const configLogoutUrl = serviceProvider === 'toska' ? undefined : process.env.LOGOUT_URL
// Other stuff
export const { DB_URL_KONE, DB_URL_USER, SECRET_TOKEN, SIS_DB_URL, SIS_UPDATER_URL, CRYPT_KEY } = process.env
export const rootOrgId = process.env.ROOT_ORG_ID ?? 'hy-university-root-id'
export const languageCenterViewEnabled =
process.env.LANGUAGE_CENTER_VIEW_ENABLED == null ? true : process.env.LANGUAGE_CENTER_VIEW_ENABLED === 'true'
export const concurrentWorkers = parseInt(process.env.CONCURRENT_WORKERS!, 10) || 2
export const DB_MAX_CONNECTIONS = parseInt(process.env.DB_MAX_CONNECTIONS!, 10) || 5 // sequelize's default
-1
View File
@@ -5,7 +5,6 @@ export const roles = [
'courseStatistics',
'facultyStatistics',
'fullSisuAccess',
'openUniSearch',
'studyGuidanceGroups',
'teachers',
] as const
@@ -24,6 +24,7 @@ import {
TeacherModel,
} from '../models'
import {
BannerModel,
CustomPopulationSearchModel,
ExcludedCourseModel,
OpenUniPopulationSearchModel,
@@ -107,6 +108,7 @@ const sequelizeKone = new Sequelize(conf.DB_URL_KONE!, {
logging: false,
password: conf.KONE_PASSWORD,
models: [
BannerModel,
CustomPopulationSearchModel,
ExcludedCourseModel,
OpenUniPopulationSearchModel,
@@ -0,0 +1,46 @@
const { INTEGER, STRING, DATE } = require('sequelize')
module.exports = {
up: async ({ context: queryInterface }) => {
await queryInterface.createTable('banners', {
id: {
type: INTEGER,
primaryKey: true,
autoIncrement: true,
},
text: {
type: STRING,
allowNull: false,
},
color: {
type: STRING,
allowNull: false,
},
lightness: {
type: STRING,
allowNull: false,
},
start_date: {
type: DATE,
allowNull: false,
},
end_date: {
type: DATE,
allowNull: false,
},
last_modified_by: {
type: STRING,
allowNull: false,
},
created_at: {
type: DATE,
},
updated_at: {
type: DATE,
},
})
},
down: async ({ context: queryInterface }) => {
await queryInterface.dropTable('banners')
},
}
+11 -9
View File
@@ -1,6 +1,6 @@
import { CronJob } from 'cron'
import { isProduction, runningInCI, languageCenterViewEnabled, CRON_SCHEDULE } from './config'
import { isProduction, runningInCI, CRON_SCHEDULE } from './config'
import { getDegreeProgrammesOfFaculty } from './services/faculty/faculty'
import { getFaculties } from './services/faculty/facultyHelpers'
import { updateFacultyOverview, updateFacultyProgressOverview } from './services/faculty/facultyUpdates'
@@ -13,7 +13,7 @@ import { updateBasicView, updateStudyTrackView } from './services/studyProgramme
import { findAndSaveTeachers } from './services/teachers/top'
import { deleteOutdatedUsers } from './services/userService'
import logger from './util/logger'
import { jobMaker, addToFlow } from './worker/queue'
import { jobQueue } from './worker/queue'
const schedule = (cronTime: string, onTick: () => void) => {
const onComplete = null
@@ -41,7 +41,7 @@ export const refreshFaculties = async () => {
logger.info('Adding jobs to refresh all faculties')
const faculties = await getFaculties()
for (const faculty of faculties) {
jobMaker.faculty(faculty.code)
jobQueue.refreshFaculty(faculty.code)
}
}
@@ -53,12 +53,13 @@ export const refreshLanguageCenterData = async () => {
}
const refreshProgrammesAndFaculties = async () => {
logger.info('Refreshing all degree programmes and faculties')
const facultyCodes = (await getFaculties()).map(faculty => faculty.code)
for (const faculty of facultyCodes) {
const programmeCodes = (await getDegreeProgrammesOfFaculty(faculty, true))
.map(programme => programme.code)
.filter(code => isRelevantProgramme(code))
await addToFlow(faculty, programmeCodes)
await jobQueue.addToFlow(faculty, programmeCodes)
}
}
@@ -82,7 +83,7 @@ export const refreshProgrammes = async () => {
}
for (const code of programmeCodes) {
// If combined programme is given, this updates only the bachelor programme
jobMaker.programme(code)
jobQueue.refreshProgramme(code)
}
}
@@ -96,8 +97,9 @@ const dailyJobs = async () => {
try {
await refreshTeacherLeaderboard()
await refreshProgrammesAndFaculties()
if (languageCenterViewEnabled) jobMaker.languagecenter()
jobMaker.closeToGraduation()
jobQueue.refreshLanguagecenter()
jobQueue.refreshCloseToGraduation()
} catch (error) {
logger.error('Daily jobs failed', error)
}
@@ -118,11 +120,11 @@ export const startCron = () => {
})
schedule('0 19 * * 1', () => {
logger.info('Updating students whose studyplans have not been updated recently')
jobMaker.studyplansUpdate(4)
jobQueue.refreshStudyplans(4)
})
schedule('0 10 * * 2', () => {
logger.info('Updating students whose studyplans have not been updated recently')
jobMaker.studyplansUpdate(5)
jobQueue.refreshStudyplans(5)
})
}
}
@@ -1,8 +1,8 @@
import { Request, Response } from 'express'
import { omit } from 'lodash'
import morgan from 'morgan'
import { FormattedUser } from '../types'
import { FormattedUser } from '@oodikone/shared/types'
import { omitKeys } from '@oodikone/shared/util'
import { getFullStudyProgrammeRights } from '../util'
import logger from '../util/logger'
@@ -38,6 +38,11 @@ const accessLogger = morgan((tokens, req: Request, res: Response): undefined =>
meta['req-route'] = req.route.path
}
// Ignore admin route that is frequently pinged for updates.
if (/.*\/updater\/jobs$/.test(meta.url ?? '')) {
return
}
const message = [
`${user.mockedBy ? '(mocking) ' : ''}${user.name}`,
`${(tokens['response-time'](req, res) ?? '0').split('.')[0]} ms`.padEnd(8, ' '),
@@ -52,7 +57,7 @@ const accessLogger = morgan((tokens, req: Request, res: Response): undefined =>
const onlyIamRights = !user.isAdmin && fullStudyProgrammeRights.length === 0
logger.info(message, {
...omit(meta, ['studentsUserCanAccess']), // Don't log student list which might be huge
...omitKeys(meta, ['studentsUserCanAccess']), // Don't log student list which might be huge
isUsageStats: true, // Pass this as a custom field so we can filter by it in Graylog
isIamRights: onlyIamRights || usingIamRights, // Needed for Grafana IAM users panel
})
+10 -37
View File
@@ -1,10 +1,9 @@
import * as Sentry from '@sentry/node'
import { NextFunction, Request, Response } from 'express'
import { intersection } from 'lodash'
import { intersection } from 'lodash-es'
import { configLogoutUrl, isDev, requiredGroup, serviceProvider } from '../config'
import { getMockedUser, getMockedUserFd, getOrganizationAccess, getUserFd, getUserToska } from '../services/userService'
import { FormattedUser } from '../types'
import { requiredGroup, baseUrl } from '../config'
import { getMockedUser, getOrganizationAccess, getUser } from '../services/userService'
import { ApplicationError } from '../util/customErrors'
import logger from '../util/logger'
@@ -25,7 +24,7 @@ type Headers = {
'x-show-as-user'?: string
}
const toskaUserMiddleware = async (req: Request, _: Response, next: NextFunction) => {
export default async (req: Request, _: Response, next: NextFunction) => {
const {
'shib-session-id': sessionId,
'x-show-as-user': showAsUser,
@@ -37,6 +36,11 @@ const toskaUserMiddleware = async (req: Request, _: Response, next: NextFunction
uid: username,
} = req.headers as Headers
if (req.path === `${baseUrl}/health`) {
next()
return
}
if (!username || !sessionId)
throw new ApplicationError(
`${username ?? 'Anonymous user'} requested ${req.url} without authorization headers.`,
@@ -61,7 +65,7 @@ const toskaUserMiddleware = async (req: Request, _: Response, next: NextFunction
const user =
showAsUser && specialGroup?.superAdmin
? await getMockedUser({ userToMock: showAsUser, mockedBy: username })
: await getUserToska({
: await getUser({
username,
name: name!,
email: email!,
@@ -80,34 +84,3 @@ const toskaUserMiddleware = async (req: Request, _: Response, next: NextFunction
next()
}
const fdUserMiddleware = async (req: Request, _res: Response, next: NextFunction) => {
const { remote_user: remoteUser, 'x-show-as-user': showAsUser } = req.headers
if (!remoteUser) {
throw new ApplicationError('Not enough data in request headers, remote_user was missing', 403, { configLogoutUrl })
}
let user: FormattedUser | null
// getMockedUser in production requires the superAdmin role, which is only available
// via iamGroups, so it's now only implemented for the dev environment
if (showAsUser && isDev) {
user = await getMockedUserFd({ userToMock: showAsUser as string, mockedBy: remoteUser as string })
} else {
user = await getUserFd({ username: remoteUser as string })
}
if (!user) {
throw new ApplicationError(`Could not grant access with the eppn ${remoteUser}.`, 403, { configLogoutUrl })
}
req.user = user
req.logoutUrl = configLogoutUrl
next()
}
const currentUserMiddleware = serviceProvider === 'toska' ? toskaUserMiddleware : fdUserMiddleware
export default currentUserMiddleware
+1 -7
View File
@@ -1,10 +1,4 @@
import headersMiddleware from 'unfuck-utf8-headers-middleware'
import { serviceProvider } from '../config'
const headersToska = ['uid', 'displayName', 'shib-session-id', 'hyGroupCn', 'mail', 'hyPersonSisuId']
const headersOther = ['remote_user']
const headers = serviceProvider === 'toska' ? headersToska : headersOther
const headers = ['uid', 'displayName', 'shib-session-id', 'hyGroupCn', 'mail', 'hyPersonSisuId']
export default headersMiddleware(headers)
@@ -71,6 +71,9 @@ export class SISStudyRightModel extends Model implements SISStudyRight {
@Column(DataType.JSONB)
declare semesterEnrollments: SISStudyRight['semesterEnrollments']
@Column(DataType.JSONB)
declare transferInfo: SISStudyRight['transferInfo']
@HasMany(() => CreditModel, { foreignKey: 'studyright_id', sourceKey: 'id' })
declare credits: SISStudyRight['credits']
+3
View File
@@ -73,6 +73,9 @@ export class CourseModel extends Model implements Course {
@Column(DataType.JSONB)
declare substitutions: Course['substitutions']
@Column(DataType.JSONB)
declare substitution_groups: Course['substitution_groups']
@Column(DataType.STRING)
declare course_unit_type: Course['course_unit_type']
@@ -0,0 +1,33 @@
import { Column, DataType, Model, PrimaryKey, Table, AutoIncrement } from 'sequelize-typescript'
import type { Banner } from '@oodikone/shared/models/kone'
@Table({
underscored: true,
modelName: 'banner',
tableName: 'banners',
})
export class BannerModel extends Model implements Banner {
@PrimaryKey
@AutoIncrement
@Column(DataType.INTEGER)
declare id: number
@Column(DataType.DATE)
declare startDate: Date
@Column(DataType.DATE)
declare endDate: Date
@Column(DataType.STRING)
declare text: string
@Column(DataType.STRING)
declare color: string
@Column(DataType.STRING)
declare lightness: 'light' | 'main' | 'dark'
@Column(DataType.STRING)
declare lastModifiedBy: string
}
@@ -1,3 +1,4 @@
export { BannerModel } from './banner'
export { CustomPopulationSearchModel } from './customPopulationSearch'
export { ExcludedCourseModel } from './excludedCourse'
export { OpenUniPopulationSearchModel } from './openUniPopulationSearch'
-2
View File
@@ -1,2 +0,0 @@
*.csv
*.csv#
-22
View File
@@ -1,22 +0,0 @@
# Rapodiff
Use this to check differences between oodikone and rapo.
## How to load the CSV file from Rapo
1. Go to Rapo and open ”Koulutusohjelmien vuosiseuranta”
2. Navigate to ”Opintopisteet ja opetus / Credits and teaching”
3. From the faculty dropdown menu, select all the faculties
4. From the bottom of the page, click ”Vie -> Tiedot -> CSV”
5. Rename the file to `data.csv` and copy it to this directory
## Usage
1. Have real oodikone running. Then run rapodiff **in the root directory**: `npm run rapodiff -- args`
2. place args with a mode. omit args to print possible modes.
## Known differences in APIs
- rapo does not show students that have graduated (it however takes some days for rapo to notice a graduation)
- rapo does not handle the starting year of transfers properly (shows in old staring population)
- rapo show some students incorrectly passive, see https://github.com/UniversityOfHelsinkiCS/oodikone/issues/3521
@@ -1,188 +0,0 @@
const { orderBy } = require('lodash')
// dbConnections must be imported to avoid Sequelize errors even if it's not used
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { dbConnections } = require('../database/connection')
const { computeCreditsProduced } = require('../services/providerCredits')
const { parseCsv, printProgressBar } = require('./helpers')
const diffs = []
let noDiffCounter = 0
// Change weird rapo programmecodes to oodikone format, example: 300-M003 => MH30_003
const transformProgrammeCode = oldCode => `${oldCode[4]}H${oldCode.slice(0, 2)}_${oldCode.slice(5, 8)}`
const formatData = data =>
data.reduce((obj, cur) => {
const parseNum = str => (str === '' ? 0 : parseFloat(str.replace(',', '.')))
const [
year,
facultyCode,
programmeInfo,
basic,
exchange,
otherUni,
openUni,
special,
total,
transferredFromAbroad,
otherTransferred,
] = cur
const programmeCode = transformProgrammeCode(programmeInfo.split(' ')[0])
if (!obj[programmeCode]) obj[programmeCode] = {}
obj[programmeCode][year] = {
year: parseNum(year),
facultyCode,
programmeInfo,
basic: parseNum(basic),
'incoming-exchange': parseNum(exchange),
agreement: parseNum(otherUni),
'open-uni': parseNum(openUni),
separate: parseNum(special),
total: parseNum(total),
transferred: parseNum(transferredFromAbroad) + parseNum(otherTransferred),
}
return obj
}, {})
const diff = (rapoData, okData, code) => {
for (const year of Object.keys(okData)) {
if (year !== '2022' && year !== '2023') continue
const rapo = rapoData[year]
const ok = okData[year]
if (!rapo) {
if (!ok) console.log(`Year ${year} not in rapo for code ${code}, skipping`)
continue
}
for (const field of ['basic', 'incoming-exchange', 'open-uni', 'agreement', 'separate', 'transferred']) {
const okValue = Math.round(ok[field] || 0)
const rapoValue = Math.round(rapo[field] || 0)
const diff = Math.abs(okValue - rapoValue)
if (diff === 0) {
noDiffCounter++
continue
}
const percentage = rapoValue !== 0 ? Number(((diff / rapoValue) * 100).toFixed(2)) : 100
diffs.push({ field, year, code, okValue, rapoValue, diff, 'diff %': percentage })
}
}
}
const processProgrammes = async data => {
let programmeCounter = 0
const rapoProgrammeData = formatData(data.slice(1))
const allProgrammeCodes = [...new Set(Object.keys(rapoProgrammeData))]
for (const programmeCode of allProgrammeCodes) {
const okProgrammeData = await computeCreditsProduced(programmeCode, false, true)
diff(rapoProgrammeData[programmeCode], okProgrammeData.stats, programmeCode)
printProgressBar(allProgrammeCodes.length, ++programmeCounter)
}
const fieldToSortBy = 'diff' // 'diff %'
const orderedDiffs = orderBy(diffs, fieldToSortBy, 'asc')
console.table(orderedDiffs)
console.log(`${diffs.length} diffs found with current settings. Numbers with no diff: ${noDiffCounter}`)
const totalDiff = orderedDiffs.reduce((sum, cur) => cur.diff + sum, 0)
console.log('Total difference: ', totalDiff)
}
const programmeCreditsDiff = async fileName => {
console.log('Running studyprogramme credits diff')
await parseCsv(fileName, processProgrammes)
console.log('Diff completed.')
}
/*
separate
Suoritus on erillisellä opiskeluoikeudella opettajankoulutuksen opintoja suorittavan opiskelijan suorittama
Suoritus on erillisellä opiskeluoikeudella opiskelevan suorittama
agreement
Suoritus on korkeakoulujen välisillä yhteistyösopimuksilla opiskelevan suorittama
open-uni
Suoritus on avoimessa korkeakouluopetuksessa suoritettu
incoming-exchange
Suoritus on tulevan kansainvälisen vaihto-opiskelijan suorittama
basic
perustutkinto-opiskelija = 1
ja
Tuntematon
tai (ehkä):
Suoritus on kotimainen harjoittelujakso
?? Opintosuoritus on täydennyskoulutuksessa suoritettu ??
other
perustutkinto-opiskelija = 0 && tuntematon
*/
const getCategory = (typeName, isBasic, hyvaksiluettu) => {
if (hyvaksiluettu) return 'transferred' // ?? unsure
if (isBasic || ['Suoritus on kotimainen harjoittelujakso', 'Tuntematon'].includes(typeName)) return 'basic'
if (typeName === 'Suoritus on avoimessa korkeakouluopetuksessa suoritettu') return 'open-uni'
if (typeName === 'Suoritus on tulevan kansainvälisen vaihto-opiskelijan suorittama') return 'incoming-exchange'
if (typeName.startsWith('Suoritus on erillisellä opiskeluoikeudella')) return 'separate'
if (typeName === 'Suoritus on korkeakoulujen välisillä yhteistyösopimuksilla opiskelevan suorittama')
return 'agreement'
return 'other'
}
const processIds = async (rawData, code, field) => {
const data = rawData.slice(0, 21628) // 11476 = KTM viimeinen
const rapoData = data
.map(row => ({
id: row[0].slice(11),
type: getCategory(row[1], row[3] === '1', row[6] === '1'),
typeCode: row[8],
isModule: row[9] !== '0',
credits: parseFloat(row[10]),
included: row[4] === '1',
}))
.filter(course => !course.isModule && (field === 'transferred' || course.included))
const rapoIds = rapoData.filter(course => course.credits > 0 && course.type === field).map(row => row.id)
const rapoCredits = rapoData.reduce(
(stats, cur) => {
stats[cur.type] += cur.credits
return stats
},
{
transferred: 0,
total: '_',
basic: 0,
'open-uni': 0,
separate: 0,
'incoming-exchange': 0,
agreement: 0,
other: 0,
}
)
const stats = await computeCreditsProduced(code, false, true, field)
if (!stats.stats.ids) {
console.log({ rapoCredits })
console.log('Edit getCreditsProduced code so that it saves a list of the relevant ids you want to compare.')
return
}
const okIdMap = stats.stats.ids
const rapoIdMap = rapoIds.reduce((obj, cur) => {
obj[cur] = true
return obj
}, {})
const onlyInRapo = Object.keys(rapoIdMap).filter(id => !okIdMap[id])
const onlyInOk = Object.keys(okIdMap).filter(id => !rapoIdMap[id])
console.log('\n onlyInRapo: ', onlyInRapo.length)
console.dir(onlyInRapo.slice(0, 30))
console.log('\n onlyInOk: ', onlyInOk.length)
console.dir(onlyInOk.slice(0, 30))
delete stats.ids
console.log(stats.stats['2022'])
console.log('rapoCredits: ', rapoCredits)
}
const testNewCalc = async (fileName, code, field) => {
await parseCsv(fileName, async data => processIds(data, code, field))
}
module.exports = { programmeCreditsDiff, testNewCalc }
-36
View File
@@ -1,36 +0,0 @@
const fs = require('fs/promises')
const Papa = require('papaparse')
const parseCsv = async (fileName, fun) => {
try {
const file = await fs.readFile(`${__dirname}/${fileName}`, 'utf8')
Papa.parsePromise = file => {
return new Promise((complete, error) => {
Papa.parse(file, { complete, error })
})
}
const data = await Papa.parsePromise(file)
await fun(data?.data)
} catch (error) {
console.log(error)
}
}
const printProgressBar = (total, current) => {
const barLength = 30
const progress = Math.floor((current / total) * barLength)
const empty = barLength - progress
const progressBar = `[${'#'.repeat(progress)}${'-'.repeat(empty)}]`
const percentage = Math.floor((current / total) * 100)
process.stdout.clearLine()
process.stdout.cursorTo(0)
process.stdout.write(`${progressBar} ${percentage} %`)
if (current >= total) {
console.log()
}
}
module.exports = { parseCsv, printProgressBar }
-31
View File
@@ -1,31 +0,0 @@
const { programmeCreditsDiff, testNewCalc } = require('./creditsDiff')
// ** To add new mode, just type key and function here **
const modes = {
// Check credits with current local oodikone data against provided data.csv in rapodiff folder.
credits: () => programmeCreditsDiff('data.csv'),
// Provide programme code as argument. Parses credits from credits.csv in rapodiff folder. This must be obtained from Rapo people. This tells which credit ids are found in oodikone but not rapo, and vice versa.
// notice: check mode requires writing ids in to the stats object in credits calculating function.
// Add something like this there:
// if (category === 'open-uni' && credits > 0 && new Date(attainmentDate).getFullYear() === 2022) stats.ids[id] = true
check: (fileName, code, field) => testNewCalc(fileName, code, field),
}
const main = async () => {
console.log('**RAPODIFF**\n')
const mode = process.argv[2]
const modeFunction = modes[mode]
const arg = process.argv[3]
const arg2 = process.argv[4]
const arg3 = process.argv[5]
if (!modeFunction) {
console.log(
'Unknown or missing mode. Supply the mode as an argument (e.g. `npm run rapodiff -- credits`). Supported modes:\n'
)
console.log(Object.keys(modes).join('\n'))
return
}
await modeFunction(arg, arg2, arg3)
}
main().then(() => process.exit(0))
+12 -18
View File
@@ -3,22 +3,23 @@ import compression from 'compression'
import cors from 'cors'
import { Express, json as jsonExpress } from 'express'
import { frontUrl, languageCenterViewEnabled, serviceProvider } from './config'
import { frontUrl } from './config'
import accessLogger from './middleware/accessLogger'
import * as auth from './middleware/auth'
import currentUserMiddleware from './middleware/currentUser'
import errorHandler from './middleware/errorHandler'
import headersMiddleware from './middleware/headers'
import banners from './routes/banners'
import changelog from './routes/changelog'
import closeToGraduation from './routes/closeToGraduation'
import completedCoursesSearch from './routes/completedCoursesSearch'
import courseExclusions from './routes/courseExclusions'
import courses from './routes/courses'
import curriculumPeriods from './routes/curriculumPeriods'
import customOpenUniSearch from './routes/customOpenUniSearch'
import customPopulationSearch from './routes/customPopulationSearch'
import faculties from './routes/faculties'
import feedback from './routes/feedback'
import health from './routes/health'
import languageCenterData from './routes/languageCenterData'
import login from './routes/login'
import population from './routes/population'
@@ -34,8 +35,7 @@ import tags from './routes/tags'
import teachers from './routes/teachers'
import university from './routes/university'
import updater from './routes/updater'
import usersToska from './routes/users'
import usersFd from './routes/usersFd'
import users from './routes/users'
const routes = (app: Express, url: string) => {
app.use(cors({ credentials: true, origin: frontUrl }))
@@ -43,9 +43,10 @@ const routes = (app: Express, url: string) => {
app.use(compression())
app.use(headersMiddleware)
app.use(currentUserMiddleware) // eslint-disable-line @typescript-eslint/no-misused-promises
app.use(currentUserMiddleware)
app.use(accessLogger)
app.use(`${url}/login`, login)
app.use(`${url}/banners`, banners)
app.use(url, courses)
app.use(`${url}/students`, students)
app.use(url, population)
@@ -53,32 +54,25 @@ const routes = (app: Express, url: string) => {
app.use(`${url}/semesters/codes`, semesters)
app.use(url, tags)
app.use(url, programmeModules)
app.use(`${url}/v3/course-exclusions`, courseExclusions)
app.use(`${url}/v2/studyprogrammes`, studyProgramme)
app.use(`${url}/course-exclusions`, courseExclusions)
app.use(`${url}/studyprogrammes`, studyProgramme)
app.use(`${url}/programme-criteria`, studyProgrammeCriteria)
app.use(`${url}/openunisearch`, auth.roles(['openUniSearch']), customOpenUniSearch)
app.use(`${url}/changelog`, changelog)
app.use(`${url}/completedcoursessearch`, completedCoursesSearch)
if (languageCenterViewEnabled) {
app.use(`${url}/languagecenterdata`, languageCenterData)
}
app.use(`${url}/languagecenterdata`, languageCenterData)
app.use(`${url}/faculties`, faculties)
app.use(`${url}/university`, university)
app.use(`${url}/updater`, auth.roles(['admin']), updater)
app.use(`${url}/teachers`, teachers)
if (serviceProvider === 'toska') {
app.use(`${url}/users`, usersToska)
} else {
app.use(`${url}/users`, usersToska)
app.use(`${url}/users`, usersFd)
}
app.use(`${url}/users`, users)
app.use(`${url}/feedback`, feedback)
app.use(`${url}/custom-population-search`, customPopulationSearch)
app.use(`${url}/studyguidancegroups`, auth.roles(['studyGuidanceGroups']), studyGuidanceGroups)
app.use(`${url}/close-to-graduation`, auth.roles(['fullSisuAccess', 'studyGuidanceGroups']), closeToGraduation)
app.use(`${url}/study-programme-pins`, studyProgrammePins)
app.use(`${url}/curriculum-periods`, curriculumPeriods)
app.get('*', (_, res) => {
app.use(`${url}/health`, health)
app.get('', (_, res) => {
res.status(404).json({ error: 'unknown endpoint' })
})
Sentry.setupExpressErrorHandler(app)
+53
View File
@@ -0,0 +1,53 @@
import { Request, Response, Router } from 'express'
import { Banner } from '@oodikone/shared/models/kone'
import { saveBanner, getActiveBanners, getAllBanners } from '../services/banners'
const router = Router()
router.get<never, Banner[]>('/', async (_req: Request, res: Response) => {
const banners = await getActiveBanners()
res.json(banners).end()
})
router.get<never, Banner[]>('/all', async (req: Request, res: Response) => {
const { user } = req
if (!user.isAdmin || !user.iamGroups.includes('grp-toska')) {
return res.status(403).end()
}
const banners = await getAllBanners()
res.json(banners)
})
router.post('/new', async (req: Request, res: Response) => {
const { user } = req
if (!user.isAdmin || !user.iamGroups.includes('grp-toska')) {
return res.status(403).end()
}
const payload = {
...req.body,
lastModifiedBy: user.username,
}
await saveBanner(payload)
res.status(201).end()
})
router.put('/update', async (req: Request, res: Response) => {
const { user } = req
if (!user.isAdmin || !user.iamGroups.includes('grp-toska')) {
return res.status(403).end()
}
const payload = {
...req.body,
lastModifiedBy: user.username,
}
await saveBanner(payload)
res.status(201).end()
})
export default router
+2 -2
View File
@@ -2,7 +2,7 @@ import axios from 'axios'
import { Router } from 'express'
import { Release } from '@oodikone/shared/types'
import { isDev } from '../config'
import { isDev, isTest } from '../config'
const router = Router()
@@ -13,7 +13,7 @@ router.get<never, Release[]>('/', async (_, res) => {
return res.status(200).send(changelog.data)
}
if (isDev) {
if (isDev || isTest) {
const fakeRelease: Release[] = [
{
description: '**Feature 1**\n- Added a fancy new feature \n\n**Feature 2**\n- Fixed a bug\n- Fixed another bug',
@@ -14,7 +14,7 @@ router.get<never, CloseToGraduationResBody>('/', async (req, res) => {
const studentNumbers = await getAllStudentsUserHasInGroups(user.sisPersonId)
const result = await getCloseToGraduationData(studentNumbers)
res.json(result)
return res.json(result)
}
const result = await getCloseToGraduationData()
@@ -1,14 +1,15 @@
import { Router } from 'express'
import { difference, intersection } from 'lodash'
import { difference, intersection } from 'lodash-es'
import { CanError } from '@oodikone/shared/routes'
import type { CompletedCoursesStudent, CompletedCoursesCourse } from '@oodikone/shared/types'
import { tryCatch } from '@oodikone/shared/util'
import { Courses, FormattedStudent, getCompletedCourses } from '../services/completedCoursesSearch'
import { getCompletedCourses } from '../services/completedCoursesSearch'
import {
getOpenUniSearches,
createNewSearch,
deleteSearch,
updateSearch,
getUserSearches,
createNewUserSearch,
deleteUserSearch,
updateUserSearch,
FoundSearch,
} from '../services/openUni/openUniManageSearches'
import { hasFullAccessToStudentData, safeJSONParse } from '../util'
@@ -19,11 +20,14 @@ const router = Router()
const importerClient = getImporterClient()
export type SearchResBody = CanError<
{ discardedStudentNumbers: string[] } & { students: Omit<FormattedStudent, 'allEnrollments'>[]; courses: Courses }
type SearchResBody = CanError<
{ discardedStudentNumbers: string[] } & {
students: Omit<CompletedCoursesStudent, 'allEnrollments'>[]
courses: CompletedCoursesCourse[]
}
>
export type SearchReqBody = never
export type SearchQuery = {
type SearchReqBody = never
type SearchQuery = {
studentlist: string
courselist: string
}
@@ -72,7 +76,7 @@ export type GetSearchResBody = FoundSearch[]
router.get<never, GetSearchResBody>('/searches', async (req, res) => {
const userId = req.user.id
const foundSearches = await getOpenUniSearches(userId)
const foundSearches = await getUserSearches(userId)
return res.json(foundSearches)
})
@@ -98,7 +102,7 @@ router.post<never, CreateSearchResBody, CreateSearchReqBody>('/searches', async
if (courseCodes && !Array.isArray(courseCodes)) {
return res.status(400).json({ error: 'Courselist must be type of array' })
}
const createdSearch = await createNewSearch(userId, name, courseCodes)
const createdSearch = await createNewUserSearch(userId, name, courseCodes)
if (!createdSearch) {
return res.status(400).json({ error: 'Failed to create courselist' })
}
@@ -130,7 +134,7 @@ router.put<UpdateSearchParams, UpdateSearchResBody, UpdateSearchReqBody>('/searc
return res.status(422).end()
}
const updatedSearch = await updateSearch(userId, id, courseCodes)
const updatedSearch = await updateUserSearch(userId, id, courseCodes)
if (!updatedSearch) {
return res.status(404).json({ error: 'Courselist could not be found' })
}
@@ -155,7 +159,7 @@ router.delete<DeleteSearchParams, DeleteSearchResBody>('/searches/:id', async (r
return res.status(422).end()
}
const deletedSearch = await deleteSearch(userId, id)
const deletedSearch = await deleteUserSearch(userId, id)
if (!deletedSearch) {
return res.status(404).json({ error: 'Courselist could not be found' })
}
+37 -40
View File
@@ -9,71 +9,58 @@ import {
CourseYearlyStatsReqBody,
CourseYearlyStatsQuery,
} from '@oodikone/shared/routes/courses'
import type { CourseWithSubsId } from '@oodikone/shared/types/course'
import { uniq } from '@oodikone/shared/util'
import { getCourseYearlyStats } from '../services/courses'
import { getCoursesByNameAndOrCode, getCoursesByCodes } from '../services/courses/courseFinders'
import { getFullStudyProgrammeRights, hasFullAccessToStudentData, validateParamLength } from '../util'
import type { CourseWithSubsDetails } from '@oodikone/shared/types/course'
import { getCourseDetails, getCourseYearlyStats } from '../services/courses'
import { getCoursesByNameAndOrCode } from '../services/courses/courseFinders'
import {
getFullStudyProgrammeRights,
handleQueryArrays,
hasFullAccessToStudentData,
validateParamLength,
} from '../util'
const router = Router()
router.get<never, CanError<CoursesMultiResBody>, CoursesMultiReqBody, CoursesMultiQuery>(
'/v2/coursesmulti',
'/coursesmulti',
async (req, res) => {
const { name, code, combineSubstitutions, includeSpecial } = req.query
const { name, code, includeSpecial } = req.query
if (!(validateParamLength(name, 5) || validateParamLength(code, 2))) {
return res.status(400).json({ error: 'Query parameter name or code is invalid' })
}
const courses = await getCoursesByNameAndOrCode(name, code, includeSpecial === 'true')
if (combineSubstitutions === 'false') {
const courseCodes = courses.map(course => course.code)
const substitutions = uniq(
courses.flatMap(course => course.substitutions).filter(code => !courseCodes.includes(code))
)
const substitutionDetails = await getCoursesByCodes(substitutions)
for (const substitution of substitutionDetails) {
courses.push(substitution.toJSON())
}
}
const mergedCourses: Record<string, CourseWithSubsId> = {}
const mergedCourses = new Map<string, CourseWithSubsDetails>()
for (const course of courses) {
if (!(course.max_attainment_date && course.min_attainment_date)) continue
const groupId = combineSubstitutions === 'true' ? course.subsId?.toString() : course.code
if (!groupId) continue
if (!mergedCourses.has(course.code)) mergedCourses.set(course.code, structuredClone(course))
const mergedCourse = mergedCourses.get(course.code)!
if (!mergedCourses[groupId]) {
mergedCourses[groupId] = {
...course,
substitutions: combineSubstitutions === 'true' ? course.substitutions : [],
}
} else {
const mergedCourse = mergedCourses[groupId]
if (mergedCourse.max_attainment_date < course.max_attainment_date) {
mergedCourse.max_attainment_date = course.max_attainment_date
}
if (mergedCourse.min_attainment_date > course.min_attainment_date) {
mergedCourse.min_attainment_date = course.min_attainment_date
}
if (mergedCourse.max_attainment_date < course.max_attainment_date) {
mergedCourse.max_attainment_date = course.max_attainment_date
}
if (mergedCourse.min_attainment_date > course.min_attainment_date) {
mergedCourse.min_attainment_date = course.min_attainment_date
}
}
res.json({ courses: Object.values(mergedCourses) })
res.json({ courses: Array.from(mergedCourses.values()) })
}
)
type CourseYearlyStatsResBody = Awaited<ReturnType<typeof getCourseYearlyStats>>
export type CourseYearlyStatsResBody = Awaited<ReturnType<typeof getCourseYearlyStats>>
router.get<never, CanError<CourseYearlyStatsResBody>, CourseYearlyStatsReqBody, CourseYearlyStatsQuery>(
'/v3/courseyearlystats',
'/courseyearlystats',
async (req, res) => {
const { codes, combineSubstitutions, separate } = req.query
if (!codes) {
const courseCodes = handleQueryArrays(codes)
if (!courseCodes?.length) {
return res.status(422).send({ error: 'Missing required query parameters' })
}
@@ -95,9 +82,19 @@ router.get<never, CanError<CourseYearlyStatsResBody>, CourseYearlyStatsReqBody,
const useCombineSubstitutions = combineSubstitutions !== 'false'
const useSeparate = separate === 'true'
const results = await getCourseYearlyStats(codes, useSeparate, anonymizationSalt, useCombineSubstitutions)
res.json(results)
const results = await getCourseYearlyStats(courseCodes, useSeparate, anonymizationSalt, useCombineSubstitutions)
return res.json(results)
}
)
router.get<never, CanError<unknown>, never, { codes: string | string[] }>('/coursedetails', async (req, res) => {
const { codes: coursecodes } = req.query
const codes = handleQueryArrays(coursecodes)
if (!codes?.length) return res.status(500).end()
const details = await getCourseDetails(codes)
return res.json(details)
})
export default router
@@ -1,148 +0,0 @@
import { Router } from 'express'
import { CanError } from '@oodikone/shared/routes'
import {
getOpenUniSearches,
createNewSearch,
deleteSearch,
updateSearch,
} from '../services/openUni/openUniManageSearches'
import { getCustomOpenUniCourses } from '../services/openUni/openUniStats'
import { safeJSONParse } from '../util'
const router = Router()
type SearchResBody = CanError<Awaited<ReturnType<typeof getCustomOpenUniCourses>>>
type SearchReqBody = never
type SearchQuery = {
courselist: string
startdate: string
enddate: string
}
router.get<never, SearchResBody, SearchReqBody, SearchQuery>('/', async (req, res) => {
const { courselist, startdate, enddate } = req.query
const courseCodes = (await safeJSONParse(courselist)) ?? []
if (!Array.isArray(courseCodes)) {
return res.status(400).json({ error: 'Courses must be of type array' })
}
const startDate = startdate !== 'null' ? new Date(startdate) : new Date(2017, 7, 1, 0, 0, 0)
const endDate = enddate !== 'null' ? new Date(enddate) : new Date()
if (enddate === 'null') {
endDate.setHours(23, 59, 59, 999)
}
const customOpenUniSearches = await getCustomOpenUniCourses(courseCodes, startDate, endDate)
return res.json(customOpenUniSearches)
})
type GetSearchResBody = Awaited<ReturnType<typeof getOpenUniSearches>>
router.get<never, GetSearchResBody>('/searches', async (req, res) => {
const { id: userId } = req.user
const foundSearches = await getOpenUniSearches(userId)
return res.json(foundSearches)
})
type CreateSearchResBody = CanError<{
id: string
userId: string
name: string
courseList: string[]
updatedAt: string
}>
type CreateSearchReqBody = {
courselist: string[]
name: string
}
router.post<never, CreateSearchResBody, CreateSearchReqBody>('/searches', async (req, res) => {
const { courselist, name } = req.body
const { id: userId } = req.user
const courseCodes = courselist || []
if (!name) {
return res.status(400).json({ error: 'Name missing' })
}
if (courseCodes && !Array.isArray(courseCodes)) {
return res.status(400).json({ error: 'Course codes must be type of array' })
}
const createdSearch = await createNewSearch(userId, name, courseCodes)
if (!createdSearch) {
return res.status(400).json({ error: 'Failed to create search' })
}
return res.status(201).json({
id: createdSearch.id,
userId: createdSearch.userId,
name: createdSearch.name,
courseList: createdSearch.courseCodes,
updatedAt: createdSearch.updatedAt,
})
})
type UpdateSearchParams = {
id: string
}
type UpdateSearchResBody = CanError<{
id: string
userId: string
name: string
courseList: string[]
updatedAt: string
}>
type UpdateSearchReqBody = {
courselist: string[]
}
router.put<UpdateSearchParams, UpdateSearchResBody, UpdateSearchReqBody>('/searches/:id', async (req, res) => {
const { id } = req.params
const { courselist } = req.body
const { id: userId } = req.user
const courseCodes = courselist || []
if (!id || !userId) {
return res.status(422).end()
}
const updatedSearch = await updateSearch(userId, id, courseCodes)
if (!updatedSearch) {
return res.status(404).json({ error: 'Open uni search could not be found' })
}
return res.json({
id: updatedSearch.id,
userId: updatedSearch.userId,
name: updatedSearch.name,
courseList: updatedSearch.courseCodes,
updatedAt: updatedSearch.updatedAt,
})
})
type DeleteSearchParams = {
id: string
}
type DeleteSearchResBody = CanError<string>
router.delete<DeleteSearchParams, DeleteSearchResBody>('/searches/:id', async (req, res) => {
const { id } = req.params
const { id: userId } = req.user
if (!id || !userId) {
return res.status(422).end()
}
const deletedSearch = await deleteSearch(userId, id)
if (!deletedSearch) {
return res.status(404).json({ error: 'Open uni search could not be found' })
}
return res.json(id)
})
export default router
+237 -197
View File
@@ -1,8 +1,19 @@
import { Request, Response, Router } from 'express'
import { Router } from 'express'
import { Graduated, ProgrammeFilter, SpecialGroups, StatsType, YearType } from '@oodikone/shared/types'
import type { CanError, GenericApplicationError } from '@oodikone/shared/routes'
import type {
FacultyGraduationStatistics,
Graduated,
Name,
NameWithCode,
ProgrammeFilter,
SpecialGroups,
StatsType,
Unarray,
YearType,
} from '@oodikone/shared/types'
import * as auth from '../middleware/auth'
import { getDegreeProgrammesOfFaculty, getFacultyCodeById } from '../services/faculty/faculty'
import { getDegreeProgrammesOfFaculty, getFacultyCodeById, ProgrammesOfOrganization } from '../services/faculty/faculty'
import { combineFacultyBasics } from '../services/faculty/facultyBasics'
import { getFacultyCredits } from '../services/faculty/facultyCredits'
import { countGraduationTimes } from '../services/faculty/facultyGraduationTimes'
@@ -12,248 +23,277 @@ import {
setBasicStats,
getThesisWritersStats,
setThesisWritersStats,
getGraduationStats,
setGraduationStats,
getFacultyStudentStats,
setFacultyStudentStats,
getFacultyProgressStats,
setFacultyProgressStats,
type BasicData,
type ThesisWriterData,
getGraduationStats,
} from '../services/faculty/facultyService'
import { combineFacultyStudentProgress } from '../services/faculty/facultyStudentProgress'
import { combineFacultyStudents } from '../services/faculty/facultyStudents'
import { combineFacultyThesisWriters } from '../services/faculty/facultyThesisWriters'
import { updateFacultyOverview, updateFacultyProgressOverview } from '../services/faculty/facultyUpdates'
import logger from '../util/logger'
import { objectKeyAnnihilator } from '../util'
import { ApplicationError } from '../util/customErrors'
// Faculty uses a lot of tools designed for Degree programme.
// Some of them have been copied here and slightly edited for faculty purpose.
const router = Router()
router.get('/', async (_req: Request, res: Response) => {
const faculties = await getFacultiesForFacultyList()
res.json(faculties)
})
type GetFacultyListResBody = {
id: string
code: string
name: Name
}[]
interface GetStatsRequest extends Request {
query: {
programme_filter: ProgrammeFilter
special_groups: SpecialGroups
year_type: YearType
}
router.get<never, GetFacultyListResBody>('/', async (_req, res) => res.json(await getFacultiesForFacultyList()))
type GetStatsParams = { id: string }
type GetStatsReqBody = never
type GetStatsResBody = BasicData
type GetStatsQuery = {
programme_filter: ProgrammeFilter
special_groups: SpecialGroups
year_type: YearType
}
router.get('/:id/basicstats', auth.roles(['facultyStatistics']), async (req: GetStatsRequest, res: Response) => {
const facultyId = req.params.id
const code = await getFacultyCodeById(facultyId)
if (!code) {
return res.status(422).end()
}
router.get<GetStatsParams, CanError<GetStatsResBody, GenericApplicationError>, GetStatsReqBody, GetStatsQuery>(
'/:id/basicstats',
auth.roles(['facultyStatistics']),
async (req, res) => {
const { id: facultyId } = req.params
const { year_type: yearType, programme_filter: programmeFilter, special_groups: specialGroups } = req.query
const { year_type: yearType, programme_filter: programmeFilter, special_groups: specialGroups } = req.query
const data = await getBasicStats(code, yearType, programmeFilter, specialGroups)
if (data) {
return res.json(data)
}
const faculty = await getFacultyCodeById(facultyId)
if (!faculty) throw new ApplicationError(`The organization with the id ${facultyId} was not found.`, 422)
const programmes = await getDegreeProgrammesOfFaculty(code, programmeFilter === 'NEW_DEGREE_PROGRAMMES')
if (!programmes.length) {
return res.status(422).end()
}
const data = await getBasicStats(faculty.code, yearType, programmeFilter, specialGroups)
if (data) return res.json(data)
// TODO: The types could be handled better. Possible null value is causing issues with TS.
let updatedStats: any = await combineFacultyBasics(code, programmes, yearType, specialGroups)
if (updatedStats) {
updatedStats = await setBasicStats(updatedStats, yearType, programmeFilter, specialGroups)
}
return res.json(updatedStats)
})
const programmes = await getDegreeProgrammesOfFaculty(faculty.code, programmeFilter === 'NEW_DEGREE_PROGRAMMES')
if (!programmes.length) throw new ApplicationError('Unprocessable request', 422)
interface GetCreditStatsRequest extends Request {
query: {
year_type: YearType
const updatedStats = await combineFacultyBasics(faculty.code, programmes, yearType, specialGroups)
if (updatedStats) await setBasicStats(updatedStats, yearType, programmeFilter, specialGroups)
else throw new ApplicationError('No data', 204)
return res.json(updatedStats)
}
)
type GetFacultyStatsResBody = ThesisWriterData
router.get<GetStatsParams, CanError<GetFacultyStatsResBody, GenericApplicationError>, GetStatsReqBody, GetStatsQuery>(
'/:id/thesisstats',
auth.roles(['facultyStatistics']),
async (req, res) => {
const { id: facultyId } = req.params
const { year_type: yearType, programme_filter: programmeFilter, special_groups: specialGroups } = req.query
const faculty = await getFacultyCodeById(facultyId)
if (!faculty) throw new ApplicationError(`The organization with the id ${facultyId} was not found.`, 422)
const data = await getThesisWritersStats(faculty.code, yearType, programmeFilter, specialGroups)
if (data) return res.json(data)
const programmes = await getDegreeProgrammesOfFaculty(faculty.code, programmeFilter === 'NEW_DEGREE_PROGRAMMES')
if (!programmes.length) throw new ApplicationError('Unprocessable request', 422)
const updatedStats = await combineFacultyThesisWriters(faculty.code, programmes, yearType, specialGroups)
if (updatedStats) await setThesisWritersStats(updatedStats, yearType, programmeFilter, specialGroups)
else throw new ApplicationError('No data', 204)
return res.json(updatedStats)
}
)
type GetCreditStatsResBody = {
codes: string[]
ids: string[]
programmeNames: Record<string, NameWithCode>
}
type GetCreditStatsQuery = {
year_type: YearType
}
router.get('/:id/creditstats', auth.roles(['facultyStatistics']), async (req: GetCreditStatsRequest, res: Response) => {
const facultyId = req.params.id
const code = await getFacultyCodeById(facultyId)
router.get<
GetStatsParams,
CanError<GetCreditStatsResBody, GenericApplicationError>,
GetStatsReqBody,
GetCreditStatsQuery
>('/:id/creditstats', auth.roles(['facultyStatistics']), async (req, res) => {
const { id: facultyId } = req.params
const { year_type: yearType } = req.query
const stats = await getFacultyCredits(code, yearType === 'ACADEMIC_YEAR')
return res.json(stats)
const faculty = await getFacultyCodeById(facultyId)
if (!faculty) throw new ApplicationError(`The organization with the id ${facultyId} was not found.`, 422)
return res.json(await getFacultyCredits(faculty.code, yearType === 'ACADEMIC_YEAR'))
})
router.get('/:id/thesisstats', auth.roles(['facultyStatistics']), async (req: GetStatsRequest, res: Response) => {
const facultyId = req.params.id
const code = await getFacultyCodeById(facultyId)
if (!code) {
return res.status(422).end()
}
type GetGraduationStatsQuery = {
programme_filter: ProgrammeFilter
}
const { year_type: yearType, programme_filter: programmeFilter, special_groups: specialGroups } = req.query
const data = await getThesisWritersStats(code, yearType, programmeFilter, specialGroups)
if (data) {
return res.json(data)
}
router.get<
GetStatsParams,
CanError<FacultyGraduationStatistics, GenericApplicationError>,
GetStatsReqBody,
GetGraduationStatsQuery
>('/:id/graduationtimes', auth.roles(['facultyStatistics']), async (req, res) => {
const { id: facultyId } = req.params
const { programme_filter: programmeFilter } = req.query
const programmes = await getDegreeProgrammesOfFaculty(code, programmeFilter === 'NEW_DEGREE_PROGRAMMES')
if (!programmes.length) {
return res.status(422).end()
}
const faculty = await getFacultyCodeById(facultyId)
if (!faculty) throw new ApplicationError(`The organization with the id ${facultyId} was not found.`, 422)
const data = await getGraduationStats(faculty.code, programmeFilter)
// Times were needed to compute medians/averages serverside, but are not needed in frontend.
// They are still kept in redis as university view needs them for the same purpose.
if (data) return res.json(objectKeyAnnihilator(data, 'times', []))
const programmes = await getDegreeProgrammesOfFaculty(faculty.code, programmeFilter === 'NEW_DEGREE_PROGRAMMES')
if (!programmes.length) throw new ApplicationError('Unprocessable request', 422)
const updatedStats = await countGraduationTimes(faculty.code, programmes)
if (updatedStats) await setGraduationStats(updatedStats, programmeFilter)
return res.json(objectKeyAnnihilator(updatedStats, 'times', []))
})
type GetProgressStatsResBody = {
bachelorsProgStats: Record<string, number[][]>
bcMsProgStats: Record<string, number[][]>
creditCounts: Record<string, Record<string, number[]>>
doctoralProgStats: Record<string, number[][]>
id: string
mastersProgStats: Record<string, number[][]>
programmeNames: Record<string, NameWithCode>
yearlyBachelorTitles: string[][]
yearlyBcMsTitles: string[][]
yearlyMasterTitles: string[][]
years: string[]
}
type GetProgressStatsQuery = {
graduated: Graduated
special_groups: SpecialGroups
}
router.get<
GetStatsParams,
CanError<GetProgressStatsResBody, GenericApplicationError>,
GetStatsReqBody,
GetProgressStatsQuery
>('/:id/progressstats', auth.roles(['facultyStatistics']), async (req, res) => {
const { id: facultyId } = req.params
const { special_groups: specialGroups } = req.query
const faculty = await getFacultyCodeById(facultyId)
if (!faculty) throw new ApplicationError(`The organization with the id ${facultyId} was not found.`, 422)
const programmes = await getDegreeProgrammesOfFaculty(faculty.code, true)
if (!programmes.length) throw new ApplicationError('Unprocessable request', 422)
const data = await getFacultyProgressStats(faculty.code, specialGroups)
if (data) return res.json(data)
const updatedStats = await combineFacultyStudentProgress(faculty.code, programmes, specialGroups)
if (updatedStats) await setFacultyProgressStats(updatedStats, specialGroups)
let updatedStats: any = await combineFacultyThesisWriters(code, programmes, yearType, specialGroups)
if (updatedStats) {
updatedStats = await setThesisWritersStats(updatedStats, yearType, programmeFilter, specialGroups)
}
return res.json(updatedStats)
})
interface GetGraduationStatsRequest extends Request {
query: {
programme_filter: ProgrammeFilter
}
export type DegreeProgramme = Pick<
Unarray<ProgrammesOfOrganization>,
'code' | 'name' | 'degreeProgrammeType' | 'progId'
>
type GetStudentStatsResBody = {
id: string
years: string[]
facultyTableStats: Record<string, (number | string)[]>
facultyTableStatsExtra: Record<string, Record<string, Record<string, number>>>
/**
* NOTE: programmeStats order is random (not by the year). Make sure to index properly.
*
* (Order is based on the order in which studyrights were processed in the backend)
*/
programmeStats: Record<string, Record<string, (string | number)[]>>
titles: string[]
programmeNames: Record<string, DegreeProgramme>
}
router.get(
'/:id/graduationtimes',
auth.roles(['facultyStatistics']),
async (req: GetGraduationStatsRequest, res: Response) => {
const facultyId = req.params.id
const code = await getFacultyCodeById(facultyId)
if (!code) {
return res.status(422).end()
}
router.get<
GetStatsParams,
CanError<GetStudentStatsResBody, GenericApplicationError>,
GetStatsReqBody,
GetProgressStatsQuery
>('/:id/studentstats', auth.roles(['facultyStatistics']), async (req, res) => {
const { id: facultyId } = req.params
const { special_groups: specialGroups } = req.query
const { programme_filter: programmeFilter } = req.query
const data = await getGraduationStats(code, programmeFilter)
if (data) {
return res.json(data)
}
const faculty = await getFacultyCodeById(facultyId)
if (!faculty) throw new ApplicationError(`The organization with the id ${facultyId} was not found.`, 422)
const programmes = await getDegreeProgrammesOfFaculty(code, programmeFilter === 'NEW_DEGREE_PROGRAMMES')
if (!programmes) {
return res.status(422).end()
}
const data = await getFacultyStudentStats(faculty.code, specialGroups)
if (data) return res.json(data)
let updatedStats: any = await countGraduationTimes(code, programmes)
if (updatedStats) {
updatedStats = await setGraduationStats(updatedStats, programmeFilter)
}
return res.json(updatedStats)
}
)
const programmes = await getDegreeProgrammesOfFaculty(faculty.code, true)
if (!programmes.length) throw new ApplicationError('Unprocessable request', 422)
interface GetProgressStatsRequest extends Request {
query: {
graduated: Graduated
special_groups: SpecialGroups
}
const updatedStats = await combineFacultyStudents(faculty.code, programmes, specialGroups)
if (updatedStats) await setFacultyStudentStats(updatedStats, specialGroups)
return res.json(updatedStats)
})
type GetUpdateBasicViewResBody = never
type GetUpdateBasicViewQuery = {
stats_type: StatsType
}
router.get(
'/:id/progressstats',
auth.roles(['facultyStatistics']),
async (req: GetProgressStatsRequest, res: Response) => {
const facultyId = req.params.id
const code = await getFacultyCodeById(facultyId)
if (!code) {
return res.status(422).end()
}
router.get<
GetStatsParams,
CanError<GetUpdateBasicViewResBody, GenericApplicationError>,
GetStatsReqBody,
GetUpdateBasicViewQuery
>('/:id/update_basicview', auth.roles(['facultyStatistics']), async (req, res) => {
const { id: facultyId } = req.params
const { stats_type: statsType } = req.query
const programmes = await getDegreeProgrammesOfFaculty(code, true)
if (!programmes) {
return res.status(422).end()
}
const faculty = await getFacultyCodeById(facultyId)
if (!faculty) throw new ApplicationError(`The organization with the id ${facultyId} was not found.`, 422)
const { graduated, special_groups: specialGroups } = req.query
const data = await getFacultyProgressStats(code, specialGroups, graduated)
if (data) {
return res.json(data)
}
let updatedStats: any = await combineFacultyStudentProgress(code, programmes, specialGroups, graduated)
if (updatedStats) {
updatedStats = await setFacultyProgressStats(updatedStats, specialGroups, graduated)
}
return res.json(updatedStats)
}
)
interface GetStudentStatsRequest extends GetProgressStatsRequest {}
router.get(
'/:id/studentstats',
auth.roles(['facultyStatistics']),
async (req: GetStudentStatsRequest, res: Response) => {
const facultyId = req.params.id
const code = await getFacultyCodeById(facultyId)
if (!code) {
return res.status(422).end()
}
const { graduated, special_groups: specialGroups } = req.query
/* Fetch cached data if exists */
const data = await getFacultyStudentStats(code, specialGroups, graduated)
if (data) {
return res.json(data)
}
const newProgrammes = await getDegreeProgrammesOfFaculty(code, true)
if (!newProgrammes.length) {
return res.status(422).end()
}
const updatedStats = await combineFacultyStudents(code, newProgrammes, specialGroups, graduated)
if (updatedStats) {
void setFacultyStudentStats(updatedStats, specialGroups, graduated)
}
return res.json(updatedStats)
}
)
interface GetUpdateBasicViewRequest extends Request {
query: {
stats_type: StatsType
}
}
router.get(
'/:id/update_basicview',
auth.roles(['facultyStatistics']),
async (req: GetUpdateBasicViewRequest, res: Response) => {
const facultyId = req.params.id
const code = await getFacultyCodeById(facultyId)
if (!code) {
return res.status(422).end()
}
const { stats_type: statsType } = req.query
try {
const result = await updateFacultyOverview(code, statsType)
return res.json(result)
} catch (error) {
const message = `Failed to update basic stats for faculty ${code}`
logger.error(message, { error })
return res.status(500).json({ error: message })
}
}
)
router.get('/:id/update_progressview', auth.roles(['facultyStatistics']), async (req: Request, res: Response) => {
const facultyId = req.params.id
const code = await getFacultyCodeById(facultyId)
if (!code) {
return res.status(422).end()
}
try {
const result = await updateFacultyProgressOverview(code)
return res.json(result)
await updateFacultyOverview(faculty.code, statsType)
return res.json()
} catch (error) {
const message = `Failed to update progress tab stats for faculty ${code}`
logger.error(message, { error })
return res.status(500).json({ error: message })
throw new ApplicationError(`Failed to update basic stats for faculty ${faculty.code}`, 500)
}
})
router.get<GetStatsParams, CanError<never, GenericApplicationError>, GetStatsReqBody, GetStatsQuery>(
'/:id/update_progressview',
auth.roles(['facultyStatistics']),
async (req, res) => {
const { id: facultyId } = req.params
const faculty = await getFacultyCodeById(facultyId)
if (!faculty) throw new ApplicationError(`The organization with the id ${facultyId} was not found.`, 422)
try {
await updateFacultyProgressOverview(faculty.code)
return res.json()
} catch (error) {
throw new ApplicationError(`Failed to update progress tab stats for faculty ${faculty.code}`, 500)
}
}
)
export default router
+2 -2
View File
@@ -1,6 +1,6 @@
import { Request, Response, Router } from 'express'
import { sendFeedbackToToska } from '../services/mailService'
import { sendFeedback } from '../services/mailService'
import { ApplicationError } from '../util/customErrors'
import logger from '../util/logger'
@@ -20,7 +20,7 @@ router.post('/email', async (req: EmailRequest, res: Response) => {
throw new ApplicationError('User not found', 404)
}
await sendFeedbackToToska({
await sendFeedback({
feedbackContent: content,
user,
})
+10
View File
@@ -0,0 +1,10 @@
import { Router } from 'express'
const router = Router()
/** Used for checking if backend is up eg. Docker healthchecks */
router.get<never, { status: string }>('/', (_, res) => {
return res.status(200).json({ status: 'healthy' })
})
export default router
+1 -1
View File
@@ -1,8 +1,8 @@
import { Router } from 'express'
import { CanError } from '@oodikone/shared/routes'
import { FormattedUser } from '@oodikone/shared/types'
import { omitKeys } from '@oodikone/shared/util'
import { FormattedUser } from '../types/user'
import { ApplicationError } from '../util/customErrors'
const router = Router()
+53 -37
View File
@@ -1,5 +1,5 @@
import { Router } from 'express'
import { difference, intersection, uniq } from 'lodash'
import { difference, intersection, uniq } from 'lodash-es'
import type { CanError } from '@oodikone/shared/routes'
import {
@@ -15,18 +15,22 @@ import {
PopulationstatisticsMaxYearsToCreatePopulationFormQuery,
PopulationstatisticsMaxYearsToCreatePopulationFormResBody,
} from '@oodikone/shared/routes/populations'
import { Unification, Unarray } from '@oodikone/shared/types'
import { mapToProviders } from '@oodikone/shared/util'
import { Unification } from '@oodikone/shared/types'
import { keyBy, mapToProviders } from '@oodikone/shared/util'
import { rootOrgId } from '../config'
import { getCourseProvidersForCourses, maxYearsToCreatePopulationFrom } from '../services/courses'
import {
getCourseProvidersForCourses,
maxYearsToCreatePopulationFrom,
searchAndCombineSubstitutionGroupsToCodes,
} from '../services/courses'
import { encrypt } from '../services/encrypt'
import { getDegreeProgrammesOfOrganization, ProgrammesOfOrganization } from '../services/faculty/faculty'
import { getDegreeProgrammesOfOrganization } from '../services/faculty/faculty'
import { getStudentTagMap } from '../services/populations/getStudentData'
import { parseDateRangeFromParams } from '../services/populations/shared'
import { statisticsOf } from '../services/populations/statisticsOf'
import { getStudentNumbersWithStudyRights } from '../services/populations/studentNumbersWithStudyRights'
import { findByCourseAndSemesters } from '../services/students'
import { getFullStudyProgrammeRights, hasFullAccessToStudentData } from '../util'
import { getFullStudyProgrammeRights, handleQueryArrays, hasFullAccessToStudentData } from '../util'
const router = Router()
@@ -98,13 +102,17 @@ const obfuscateStuff = ({
}
router.get<never, CanError<PopulationstatisticsResBody>, PopulationstatisticsReqBody, PopulationstatisticsQuery>(
'/v3/populationstatistics',
'/populationstatistics',
async (req, res) => {
const { id: userId, roles: userRoles, programmeRights: userProgrammeRights, studentsUserCanAccess } = req.user
const { years, semesters, programme, combinedProgramme, studentStatuses } = req.query
const { programme, combinedProgramme } = req.query
const years = handleQueryArrays(req.query.years)
const semesters = handleQueryArrays(req.query.semesters)
const studentStatuses = handleQueryArrays(req.query.studentStatuses) // NOTE: TRANSFERRED means transferred out of the program
const requiredFields = [years, semesters, programme]
if (requiredFields.some(field => !field)) {
if (requiredFields.some(field => !field.length)) {
return res.status(400).json({ error: 'The query should have years, semesters and a programme defined' })
}
@@ -112,9 +120,11 @@ router.get<never, CanError<PopulationstatisticsResBody>, PopulationstatisticsReq
return res.status(400).json({ error: 'Semester should be either SPRING OR FALL' })
}
const hasCorrectStatus = (studentStatuses: string[]) =>
studentStatuses.every(status => ['EXCHANGE', 'NONDEGREE', 'TRANSFERRED'].includes(status))
if (studentStatuses && !hasCorrectStatus(studentStatuses)) {
const hasCorrectStatus = (studentStatuses: (string | undefined)[]) =>
studentStatuses.every(status => status && ['EXCHANGE', 'NONDEGREE', 'TRANSFERRED'].includes(status))
// NOTE: This has always the length of at least 1, [undefined]
if (studentStatuses[0] && !hasCorrectStatus(studentStatuses)) {
return res.status(400).json({ error: 'Student status should be either EXCHANGE or NONDEGREE or TRANSFERRED' })
}
@@ -128,10 +138,8 @@ router.get<never, CanError<PopulationstatisticsResBody>, PopulationstatisticsReq
return res.status(403).json({ error: 'Trying to request unauthorized students data' })
}
const { startDate, endDate } = parseDateRangeFromParams({
...req.query,
years: req.query.years,
})
// NOTE: Years are academic years
const { startDate, endDate } = parseDateRangeFromParams({ semesters, years })
const studentNumbers = await getStudentNumbersWithStudyRights({
programmeCodes: [programme],
@@ -164,14 +172,15 @@ router.get<
CanError<PopulationstatisticsbycourseResBody>,
PopulationstatisticsbycourseReqBody,
PopulationstatisticsbycourseParams
>('/v3/populationstatisticsbycourse', async (req, res) => {
>('/populationstatisticsbycourse', async (req, res) => {
const {
id: userId,
roles: userRoles,
programmeRights: userProgrammeRights,
studentsUserCanAccess: allStudentsUserCanAccess,
} = req.user
const { coursecodes: coursecodeJSON, from, to, separate, unifyCourses } = req.query
const { coursecodes: coursecodeJSON, from, to, separate, unifyCourses, ...restParams } = req.query
const includeSubstitutions = restParams.includeSubstitutions === 'true'
if (!coursecodeJSON || !from || !to) {
return res.status(400).json({ error: 'The body should have a yearcode and coursecode defined' })
@@ -179,16 +188,17 @@ router.get<
const isSeparate = separate === 'true'
const coursecodes = JSON.parse(coursecodeJSON)
const codes = includeSubstitutions ? await searchAndCombineSubstitutionGroupsToCodes(coursecodes) : coursecodes
const toFromDiff = Math.abs(Number(to) - Number(from) + 1)
const requestedYearsToCreatePopulationFrom = Math.ceil(isSeparate ? toFromDiff / 2 : toFromDiff) // 2 semesters = 1 year
const maxYearsForPopulation = await maxYearsToCreatePopulationFrom(coursecodes, Unification.REGULAR)
const maxYearsForPopulation = await maxYearsToCreatePopulationFrom(codes, Unification.REGULAR)
if (requestedYearsToCreatePopulationFrom > maxYearsForPopulation) {
return res.status(400).json({ error: `Max years to create population from is ${maxYearsForPopulation}` })
}
const studentNumbers = await findByCourseAndSemesters(coursecodes, Number(from), Number(to), isSeparate, unifyCourses)
const studentNumbers = await findByCourseAndSemesters(codes, Number(from), Number(to), isSeparate, unifyCourses)
const courseProviders: string[] = await getCourseProvidersForCourses(coursecodes)
const fullStudyProgrammeRights = getFullStudyProgrammeRights(userProgrammeRights)
@@ -209,18 +219,25 @@ router.get<
studentsUserCanAccess,
})
return res.json(processed)
return res.json({ ...processed, mainCourseCodes: coursecodes, allCourseCodes: codes })
})
// Used in custom population and single study guidance groups
router.post<never, GetCustomPopulationResBody, CustomPopulationQuery>(
'/v3/populationstatisticsbystudentnumbers',
router.post<never, CanError<GetCustomPopulationResBody>, CustomPopulationQuery>(
'/populationstatisticsbystudentnumbers',
async (req, res) => {
const { studentNumbers, tags } = req.body
const { studentNumbers, tags } = req.body ?? { studentNumbers: [] }
const { id: userId, roles, studentsUserCanAccess } = req.user
const students = handleQueryArrays(studentNumbers)
if (!students.length) {
return res.status(422).json({ error: 'Body should include student numbers' })
}
const filteredStudentNumbers = hasFullAccessToStudentData(roles)
? studentNumbers
: intersection(studentNumbers, studentsUserCanAccess)
? students
: intersection(students, studentsUserCanAccess)
const studyProgrammeCode = tags?.studyProgramme?.split('+')[0]
@@ -234,14 +251,14 @@ router.post<never, GetCustomPopulationResBody, CustomPopulationQuery>(
const result = await statisticsOf(filteredStudentNumbers, studyRights, tagMap, startDate)
const resultWithStudyProgramme = { ...result, studyProgramme: tags?.studyProgramme }
const discardedStudentNumbers = difference(studentNumbers, filteredStudentNumbers)
const discardedStudentNumbers = difference(students, filteredStudentNumbers)
res.status(200).json({ ...resultWithStudyProgramme, discardedStudentNumbers })
}
)
router.get<never, PopulationstatisticsStudyprogrammesResBody>(
'/v3/populationstatistics/studyprogrammes',
'/populationstatistics/studyprogrammes',
async (req, res) => {
const { roles, programmeRights } = req.user
const programmes = await getDegreeProgrammesOfOrganization(rootOrgId, false)
@@ -252,14 +269,13 @@ router.get<never, PopulationstatisticsStudyprogrammesResBody>(
const filteredProgrammes = hasFullAccessToStudentData(roles)
? programmes
: programmes.filter(programme => allRights.includes(programme.code))
const formattedProgrammes = filteredProgrammes.reduce<Record<string, Unarray<ProgrammesOfOrganization>>>(
(acc, curr) => {
acc[curr.code] = curr
return acc
},
{}
)
res.json(formattedProgrammes)
const formattedFilteredProgrammes = keyBy(filteredProgrammes, 'code')
const formattedAllProgrammes = keyBy(programmes, 'code')
res.json({
allProgrammes: formattedAllProgrammes,
filteredProgrammes: formattedFilteredProgrammes,
})
}
)
@@ -268,7 +284,7 @@ router.get<
PopulationstatisticsMaxYearsToCreatePopulationFormResBody,
never,
PopulationstatisticsMaxYearsToCreatePopulationFormQuery
>('/v3/populationstatistics/maxYearsToCreatePopulationFrom', async (req, res) => {
>('/populationstatistics/maxYearsToCreatePopulationFrom', async (req, res) => {
const courseCodes = JSON.parse(req.query.courseCodes) as string[]
const maxYearsToCreatePopulationFromOpen = await maxYearsToCreatePopulationFrom(courseCodes, Unification.OPEN)
const maxYearsToCreatePopulationFromUni = await maxYearsToCreatePopulationFrom(courseCodes, Unification.REGULAR)
@@ -1,16 +1,21 @@
import { Request, Response, Router } from 'express'
import { Router } from 'express'
import { CurriculumOption } from '@oodikone/shared/types'
import { getCoursesAndModules, getCurriculumOptions } from '../services/programmeModules'
const router = Router()
router.get('/v3/curriculum-options/:code', async (req: Request, res: Response) => {
type CurriculumOptionParams = { code: string }
router.get<CurriculumOptionParams, CurriculumOption[] | null>('/curriculum-options/:code', async (req, res) => {
const { code } = req.params
const result = await getCurriculumOptions(code)
res.json(result)
})
router.get('/v3/curriculum/:code/:periodIds', async (req: Request, res: Response) => {
type CurriculumPeriodParams = { code: string; periodIds: string }
router.get<CurriculumPeriodParams>('/curriculum/:code/:periodIds', async (req, res) => {
const { code, periodIds } = req.params
const { defaultProgrammeCourses, secondProgrammeCourses } = await getCoursesAndModules(code, periodIds)
+71 -76
View File
@@ -1,6 +1,7 @@
import { Request, Response, Router } from 'express'
import { Router } from 'express'
import { Graduated, SpecialGroups, YearType } from '@oodikone/shared/types'
import { CreditStatsPayload } from '@oodikone/shared/types/studyProgramme'
import {
getBasicStats,
setBasicStats,
@@ -11,10 +12,8 @@ import {
} from '../services/analyticsService'
import { getCreditsProduced } from '../services/providerCredits'
import { getBasicStatsForStudytrack } from '../services/studyProgramme/studyProgrammeBasics'
import {
getStudyProgrammeCoursesForStudyTrack,
getStudyProgrammeStatsForColorizedCoursesTable,
} from '../services/studyProgramme/studyProgrammeCourses'
import { getStudyProgrammeCoursesForStudyTrack } from '../services/studyProgramme/studyProgrammeCourses'
import { getStudyProgrammeStatsForColorizedCoursesTable } from '../services/studyProgramme/studyProgrammeCoursesBySemester'
import { getGraduationStatsForStudyTrack } from '../services/studyProgramme/studyProgrammeGraduations'
import { updateBasicView, updateStudyTrackView } from '../services/studyProgramme/studyProgrammeUpdates'
import { getStudyRightsInProgramme, getStudyTracksForProgramme } from '../services/studyProgramme/studyRightFinders'
@@ -24,32 +23,30 @@ import { logInfoForGrafana } from '../util/logInfoForGrafana'
const router = Router()
interface GetCreditStatsRequest extends Request {
query: {
codes: string
specialGroups: SpecialGroups
yearType: YearType
}
type StudyProgrammeParams = { id: string }
type UpdateStudyProgrammeQuery = { combined_programme: string }
type CreditStatsQuery = {
codes: string
specialGroups: SpecialGroups
yearType: YearType
}
router.get('/creditstats', async (req: GetCreditStatsRequest, res: Response) => {
router.get<StudyProgrammeParams, unknown, never, CreditStatsQuery>('/creditstats', async (req, res) => {
const { codes, specialGroups, yearType } = req.query
const stats = {}
const stats: CreditStatsPayload = {}
for (const code of JSON.parse(codes)) {
stats[code] = await getCreditsProduced(code, yearType === 'ACADEMIC_YEAR', specialGroups === 'SPECIAL_INCLUDED')
}
return res.json({ stats })
return res.json(stats)
})
interface GetStatsRequest extends Request {
query: {
year_type: YearType
special_groups: SpecialGroups
combined_programme: string
}
type StatsRequestQuery = {
year_type: YearType
special_groups: SpecialGroups
combined_programme: string
}
router.get('/:id/basicstats', async (req: GetStatsRequest, res: Response) => {
router.get<StudyProgrammeParams, unknown, never, StatsRequestQuery>('/:id/basicstats', async (req, res) => {
const code = req.params.id
const { year_type: yearType, special_groups: specialGroups, combined_programme: combinedProgramme } = req.query
if (!code) {
@@ -76,7 +73,7 @@ router.get('/:id/basicstats', async (req: GetStatsRequest, res: Response) => {
return res.json(updatedStats)
})
router.get('/:id/graduationstats', async (req: GetStatsRequest, res: Response) => {
router.get<StudyProgrammeParams, unknown, never, StatsRequestQuery>('/:id/graduationstats', async (req, res) => {
const code = req.params.id
const { year_type: yearType, special_groups: specialGroups, combined_programme: combinedProgramme } = req.query
if (!code) {
@@ -103,14 +100,12 @@ router.get('/:id/graduationstats', async (req: GetStatsRequest, res: Response) =
return res.json(updatedStats)
})
interface GetCourseStatsRequest extends Request {
query: {
combinedProgramme: string
yearType: YearType
}
type CourseStatsQuery = {
combinedProgramme: string
yearType: YearType
}
router.get('/:id/coursestats', async (req: GetCourseStatsRequest, res: Response) => {
router.get<StudyProgrammeParams, unknown, never, CourseStatsQuery>('/:id/coursestats', async (req, res) => {
const code = req.params.id
const { combinedProgramme, yearType } = req.query
const date = new Date()
@@ -124,23 +119,21 @@ router.get('/:id/coursestats', async (req: GetCourseStatsRequest, res: Response)
}
})
interface GetStudyTrackStatsRequest extends Request {
query: {
graduated: Graduated
special_groups: SpecialGroups
combined_programme: string
}
type StudyTrackStatsQuery = {
graduated: Graduated
special_groups: SpecialGroups
combined_programme: string
}
router.get('/:id/studytrackstats', async (req: GetStudyTrackStatsRequest, res: Response) => {
router.get<StudyProgrammeParams, unknown, never, StudyTrackStatsQuery>('/:id/studytrackstats', async (req, res) => {
const code = req.params.id
const { graduated, special_groups: specialGroups, combined_programme: combinedProgramme } = req.query
const { special_groups: specialGroups, combined_programme: combinedProgramme } = req.query
if (!code) {
return res.status(422).end()
}
void logInfoForGrafana(code, combinedProgramme)
const data = await getStudyTrackStats(code, combinedProgramme, graduated, specialGroups)
const data = await getStudyTrackStats(code, combinedProgramme, specialGroups)
if (data) {
return res.json(data)
}
@@ -150,18 +143,20 @@ router.get('/:id/studytrackstats', async (req: GetStudyTrackStatsRequest, res: R
studyProgramme: code,
combinedProgramme,
settings: {
graduated: graduated === 'GRADUATED_INCLUDED',
specialGroups: specialGroups === 'SPECIAL_INCLUDED',
},
studyRightsOfProgramme,
})
if (updated) {
await setStudyTrackStats(updated, graduated, specialGroups)
await setStudyTrackStats(updated, specialGroups)
return res.json(updated)
}
return res.json(updated)
return res.status(500).end()
})
router.get('/:id/colorizedtablecoursestats', async (req: Request, res: Response) => {
router.get<StudyProgrammeParams>('/:id/colorizedtablecoursestats', async (req, res) => {
const code = req.params.id
try {
const data = await getStudyProgrammeStatsForColorizedCoursesTable(code)
@@ -171,7 +166,7 @@ router.get('/:id/colorizedtablecoursestats', async (req: Request, res: Response)
}
})
router.get('/:id/studytracks', async (req: Request, res: Response) => {
router.get<StudyProgrammeParams>('/:id/studytracks', async (req, res) => {
const code = req.params.id
if (!code) {
return res.status(422).end()
@@ -184,42 +179,42 @@ router.get('/:id/studytracks', async (req: Request, res: Response) => {
}
})
interface GetUpdateViewRequest extends Request {
query: {
combined_programme: string
router.get<StudyProgrammeParams, unknown, never, UpdateStudyProgrammeQuery>(
'/:id/update_basicview',
async (req, res) => {
const code = req.params.id
const combinedProgramme = req.query?.combined_programme
if (!code) {
return res.status(400).json({ error: 'Missing code' })
}
try {
const result = await updateBasicView(code, combinedProgramme)
return res.json(result)
} catch (error) {
const message = `Failed to update basic stats for programme ${code}${combinedProgramme ? `+${combinedProgramme}` : ''}`
logger.error(message, { error })
return res.status(500).json({ error: message })
}
}
}
)
router.get('/:id/update_basicview', async (req: GetUpdateViewRequest, res: Response) => {
const code = req.params.id
const combinedProgramme = req.query?.combined_programme
if (!code) {
return res.status(400).json({ error: 'Missing code' })
router.get<StudyProgrammeParams, unknown, never, UpdateStudyProgrammeQuery>(
'/:id/update_studytrackview',
async (req, res) => {
const code = req.params.id
const combinedProgramme = req.query?.combined_programme
if (!code) {
return res.status(400).json({ error: 'Missing code' })
}
try {
const result = await updateStudyTrackView(code, combinedProgramme)
return res.json(result)
} catch (error) {
const message = `Failed to update study track stats for programme ${code}${combinedProgramme ? `+${combinedProgramme}` : ''}`
logger.error(message, { error })
return res.status(500).json({ error: message })
}
}
try {
const result = await updateBasicView(code, combinedProgramme)
return res.json(result)
} catch (error) {
const message = `Failed to update basic stats for programme ${code}${combinedProgramme ? `+${combinedProgramme}` : ''}`
logger.error(message, { error })
return res.status(500).json({ error: message })
}
})
router.get('/:id/update_studytrackview', async (req: GetUpdateViewRequest, res: Response) => {
const code = req.params.id
const combinedProgramme = req.query?.combined_programme
if (!code) {
return res.status(400).json({ error: 'Missing code' })
}
try {
const result = await updateStudyTrackView(code, combinedProgramme)
return res.json(result)
} catch (error) {
const message = `Failed to update study track stats for programme ${code}${combinedProgramme ? `+${combinedProgramme}` : ''}`
logger.error(message, { error })
return res.status(500).json({ error: message })
}
})
)
export default router
+1 -1
View File
@@ -1,5 +1,5 @@
import { Router } from 'express'
import { difference } from 'lodash'
import { difference } from 'lodash-es'
import { CanError } from '@oodikone/shared/routes'
import { NewTag, Role, StudentTag, Tag } from '@oodikone/shared/types'

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