Compare commits

...
425 Commits
Author SHA1 Message Date
Riku RauhalaandGitHub 218f774f5b Merge pull request #4690 from UniversityOfHelsinkiCS/remove-evaluation-overview
Remove evaluation overview
2024-11-27 10:29:42 +02:00
Riku Rauhala 98926ae7c2 [Routes] Fix exact routing for protected routes
For some reason the exact prop does not work when given directly to the wrapped Route component
2024-11-27 10:15:40 +02:00
Riku Rauhala c2583e5bc9 [Evaluation overview] Remove everything 2024-11-27 10:14:29 +02:00
Riku Rauhala 6bf083cc89 [Course statistics] Add retries to a flaky test 2024-11-27 09:09:47 +02:00
Riku Rauhala c88d5984ef [Navigation bar] Turn the user button orange when mocking 2024-11-27 08:49:33 +02:00
Riku Rauhala e328258071 [Shared] Extract type ProgrammeMedians 2024-11-26 21:10:58 +02:00
Riku Rauhala 37a16b54e1 [University] Type AllProgressStats response and request 2024-11-26 21:06:04 +02:00
Riku Rauhala 4729fa457c [Frontend] Move graduation time colors to theme 2024-11-26 20:16:14 +02:00
Riku RauhalaandGitHub 4b37fd4536 Merge pull request #4688 from UniversityOfHelsinkiCS/material-ui-university
Material UI: University
2024-11-26 19:54:22 +02:00
Riku Rauhala ec5cfcc6c5 [University] Update tests 2024-11-26 19:45:18 +02:00
Riku Rauhala b41846a51e [TypeScript] Fix issues 2024-11-25 22:06:06 +02:00
Riku Rauhala 5b866377a9 [University] Disable toggles while errors or loading 2024-11-25 20:44:02 +02:00
Riku Rauhala febba2752c [Evaluation Overview] Remove UniversityPage 2024-11-25 20:42:51 +02:00
Riku Rauhala 71f733817a [Section] Add a loading spinner on top of the skeleton 2024-11-25 20:41:41 +02:00
Riku Rauhala 82d2885d99 [University] Improve faculty graduations 2024-11-25 19:44:36 +02:00
dad7ddc1f2 [CourseStatistics, StudyProgramme, Teachers] Added serviceProvider checks to hide some tables, tabs and routes when not running in toska enviroment (#4685)
* [CourseStatistics, StudyProgramme, Teachers] Added serviceProvider checks to hide some tables and tabs when not running in toska enviroment

* [StudentsTable] Use cloneDeep for toskaColumns to prevent mutation

* [Frontend] Replace serviceProvider checks with isDefaultServiceProvider function for consistency

---------

Co-authored-by: Valtteri Kantanen <valtteri.kantanen@iki.fi>
2024-11-25 17:04:05 +02:00
Riku Rauhala ee9c2c8eb8 [University] Improve Faculty graduations 2024-11-25 15:31:40 +02:00
Riku Rauhala 6c9b11a105 [Language] Type useLanguage 2024-11-25 11:48:30 +02:00
Riku Rauhala afa52f7e54 [University] Rewrite Faculty graduations tab 2024-11-25 10:51:03 +02:00
Riku Rauhala f2865b9145 [Faculty stats] Type allgraduationstats query 2024-11-25 10:39:14 +02:00
Riku Rauhala 0514f4fdfa [Shared] Extract more types 2024-11-25 10:37:21 +02:00
Riku Rauhala 170a2e40e1 [University] Extract tabs and improve query param handling 2024-11-22 21:47:18 +02:00
Riku Rauhala 5849e89ac6 [University] Type ProgrammeProgressChart 2024-11-22 21:35:27 +02:00
Riku Rauhala bc6cccfe03 [University] Move ba+ma progress message to info box 2024-11-22 20:48:42 +02:00
Riku Rauhala 53d1890a4a [University] Improve Faculty progress tab 2024-11-22 20:39:32 +02:00
Riku Rauhala 4a93c4dce4 [University] Implement FacultyProgressTable in Material UI 2024-11-22 20:18:51 +02:00
Riku Rauhala be79d8bb3f [University] Type FacultyBarChart 2024-11-22 18:00:55 +02:00
Riku Rauhala 97df3bee41 [Frontend] Install types for Highcharts 2024-11-22 14:08:54 +02:00
Riku Rauhala e1c4e792f1 [University] Add loading and error handling for Faculty progress tab 2024-11-22 14:00:22 +02:00
Riku Rauhala 230d07cbde [University] Separate the main sections into tabs 2024-11-22 13:45:29 +02:00
Riku Rauhala 92f848bf21 [University] Begin converting to Material UI 2024-11-21 19:07:06 +02:00
Riku Rauhala ae8aeede1b [Toggle] Create a new version with MUI 2024-11-21 11:46:23 +02:00
Riku Rauhala 2db12dcaac [InfoBox] Add a mini variant with smaller button 2024-11-21 11:40:08 +02:00
Riku Rauhala 9672b542b8 [Section] Create new Section wrapper
Contains a title of the section and an optional InfoBox
2024-11-21 11:27:52 +02:00
Riku Rauhala e6a8946b65 [InfoBox] Create a new version with MUI 2024-11-21 11:24:01 +02:00
Valtteri Kantanen 3b0ccb5424 [Changelog] Fix time of release
Previously the date of the latest commit in the release was used instead of when the release was published
2024-11-21 11:23:11 +02:00
Valtteri Kantanen 0c54ef578f [Custom population search] Fix unselecting a saved population 2024-11-19 13:07:47 +02:00
Valtteri Kantanen 4ee0f74361 Add ESLint rule for 'prefer-const'
https://eslint.org/docs/latest/rules/prefer-const
2024-11-19 13:07:05 +02:00
Valtteri Kantanen 362850c2de [Class statistics] Display additional info in Credit accumulation graph if 'show full study path' is chosen
Now all credits that have been attained during the selected education will be shown. Also all graduations in the correct study right will be shown in the graph.
2024-11-19 13:06:13 +02:00
Valtteri Kantanen 42c41345a0 Upgrade Node.js version to 22 in all packages 2024-11-19 10:45:27 +02:00
Riku Rauhala 301208cf99 [Front page] Only display feedback link for HY 2024-11-19 10:40:42 +02:00
Riku Rauhala b173e27f70 [Footer] Update timestamp format and add release version 2024-11-19 10:14:01 +02:00
Riku Rauhala 1bdbec963b [Front page] Center notification icon vertically 2024-11-18 22:00:32 +02:00
Riku Rauhala 80cf4acf01 [AccessDeniedMessage] Use the Alert component instead 2024-11-18 21:59:14 +02:00
Riku Rauhala fdd5cbd2e5 [Front page] Add a temporary notification about new UI 2024-11-18 21:52:05 +02:00
Riku RauhalaandGitHub 38e398598f Merge pull request #4680 from UniversityOfHelsinkiCS/material-ui-footer
Material UI: Footer
2024-11-18 17:49:38 +02:00
Riku Rauhala 0b01d223ae [Frontend] Delete ProtectedRoute.jsx
Replaced by .tsx
2024-11-18 17:39:17 +02:00
Riku Rauhala c4fd257b42 [Frontend] Restore theme 2024-11-18 17:37:52 +02:00
Riku Rauhala e7ee8749c8 [Footer] Update JSDoc 2024-11-18 17:33:33 +02:00
Riku Rauhala fb21d171ff [Footer] Fix alignment 2024-11-18 17:24:07 +02:00
Riku Rauhala a6905313c3 [Changelog] Remove redundant elevation prop 2024-11-18 17:24:07 +02:00
Riku Rauhala 7e8e5bc2e5 [Access denied] Use the previous icon 2024-11-18 17:24:07 +02:00
Riku Rauhala c84259e67a [Footer] Extract logo component and make responsive 2024-11-18 17:24:07 +02:00
Riku Rauhala 63ccd5adc1 [Footer] Implement initial version 2024-11-18 17:24:07 +02:00
Riku Rauhala 2d7bc42a9e [Frontend] Update AccessDenied message to Material UI 2024-11-18 17:24:07 +02:00
Riku Rauhala 0c6077d287 [Documentation] Add Material icons to resources.md 2024-11-18 17:20:13 +02:00
Riku Rauhala cf24936bc9 [Backend] Move type Role to shared 2024-11-18 17:20:13 +02:00
Valtteri Kantanen d0134192c6 [Filters] Fix tests 2024-11-18 17:14:51 +02:00
Valtteri Kantanen f7609dca3d [Teachers] Grant access to more users 2024-11-18 16:16:42 +02:00
Riku RauhalaandGitHub 4add4409eb Merge pull request #4679 from UniversityOfHelsinkiCS/material-ui-frontpage
Material UI: Front page
2024-11-15 17:16:30 +02:00
Riku Rauhala 2b5bf09514 [PageTitle] Tweak subtitle 2024-11-15 17:08:39 +02:00
Riku Rauhala 8ea1308c77 [Frontend] Remove build at
Each release has an associated time stamp now. This could still be used in a possible footer?
2024-11-15 16:59:51 +02:00
Riku Rauhala faba796303 [FrontPage] Remove old Semantic UI implementation 2024-11-15 16:56:21 +02:00
Riku Rauhala 04715e4503 [Changelog] Create a new page for changelog 2024-11-15 16:49:06 +02:00
Riku Rauhala 39b77e43d7 [Frontend] Update FrontPage route 2024-11-15 16:49:06 +02:00
Riku Rauhala b6a99dda83 [Backend] Update fake changelog data 2024-11-15 16:49:06 +02:00
Riku Rauhala ff25524d32 [Frontpage] Rewrite with Material UI 2024-11-15 16:49:06 +02:00
Riku Rauhala d37228e68e [Changelog] Rename ChangelogData to Release 2024-11-15 16:49:06 +02:00
Riku Rauhala 779c1a9627 [Changelog] Extract ChangelogData and type changelogApi 2024-11-15 16:49:06 +02:00
Riku Rauhala f6d8ca87f1 [Redux] Convert changelogApi to TypeScript 2024-11-15 16:49:06 +02:00
Riku Rauhala 1903f74764 [PageTitle] Center by default and add optional subtitle 2024-11-15 16:49:06 +02:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
181d39a9f9 Update all non-major dependencies (#4672)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-15 13:05:40 +02:00
Valtteri Kantanen f16b211a88 [Updater] Add error event listeners for Redis 2024-11-15 12:21:07 +02:00
Valtteri Kantanen 71f3abcf3e [Close to graduation] Add earliest HOPS attainment and number of absent semesters 2024-11-14 16:56:49 +02:00
Valtteri Kantanen 32197643ef [Study programme overview] Fix tests 2024-11-14 15:56:54 +02:00
Valtteri Kantanen d84a02d659 Fix a couple of ESLint errors 2024-11-14 15:42:29 +02:00
Valtteri Kantanen 5d2b52cfaf [Curriculum picker] Fetch curriculum periods from db instead of hardcoding them 2024-11-14 15:41:46 +02:00
Valtteri Kantanen 3d09abb518 [Faculties] Add retries to a flaky test 2024-11-14 14:14:24 +02:00
Valtteri Kantanen a69c0b6d00 Always run 'Check services run correctly' job in GitHub Actions 2024-11-14 14:00:06 +02:00
Valtteri Kantanen c7185bf3d7 [Updater] Check that the teacher exists before trying to create a new creditTeacher 2024-11-14 13:58:40 +02:00
Riku Rauhala 920895c583 [StatusNotification] Increase autoHideDuration to 30 seconds 2024-11-14 13:26:20 +02:00
Valtteri Kantanen f277ca758d [Filters] Add retries for a flaky age filter test 2024-11-14 13:24:13 +02:00
Valtteri Kantanen afffd17485 [Frontend] Take type for StatusNotification severity from MUI 2024-11-14 13:22:56 +02:00
Valtteri Kantanen 3b83a59b1a Ensure tests always run regardless of previous job outcomes 2024-11-14 12:14:04 +02:00
Valtteri Kantanen f92dab550d Re-enable ESLint rule 2024-11-14 10:24:33 +02:00
Riku Rauhala 7b8a160cc9 [Documentation] Add link to MUI docs in resources.md 2024-11-13 12:45:37 +02:00
Riku Rauhala 44f40373cc [Feedback] Acually make the success message work 2024-11-12 21:13:59 +02:00
Riku RauhalaandGitHub 2ab3dbf861 Merge pull request #4678 from UniversityOfHelsinkiCS/renovate/migrate-config
Migrate renovate config
2024-11-12 12:51:49 +02:00
renovate[bot]andGitHub be10cefba7 Migrate config renovate.json 2024-11-12 10:31:25 +00:00
Riku RauhalaandGitHub 3d0b6cf864 Merge pull request #4677 from UniversityOfHelsinkiCS/material-ui-feedback
Material UI: Feedback
2024-11-12 11:27:46 +02:00
Riku Rauhala af9ceda816 [ESLint] Update rules 2024-11-12 11:16:04 +02:00
Riku Rauhala ec58964032 Merge branch 'master' into material-ui-feedback 2024-11-12 11:10:16 +02:00
Riku Rauhala b3f7cccae4 [Feedback] Update modal button disabling logic 2024-11-12 10:21:17 +02:00
Riku Rauhala 73ff9f585b [Feedback] Remove old Semantic UI component 2024-11-12 10:20:49 +02:00
Riku Rauhala 1c81efd4b6 [Feedback] Rewrite with Material UI components 2024-11-12 10:16:09 +02:00
Valtteri KantanenandGitHub bdda5dcf01 Migrate ESLint to v9 (#4676) 2024-11-11 16:08:16 +02:00
Valtteri Kantanen cd2cd18eea Ignore node minor and patch updates on Renovate 2024-11-11 13:22:17 +02:00
Riku Rauhala a7810cf774 [Documentation] Add Code Climate screenshots 2024-11-11 12:35:54 +02:00
Riku RauhalaandGitHub b17b119a59 Merge pull request #4675 from UniversityOfHelsinkiCS/material-ui
Material UI: Navigation bar
2024-11-11 12:17:26 +02:00
Riku Rauhala eb60d1bc5d [Frontend] Delete old NavigationBar component 2024-11-11 11:59:34 +02:00
Riku Rauhala 3c09f2186d [Users] Update tests 2024-11-11 11:57:40 +02:00
Riku Rauhala 9ef0d8b59b [Navigation bar] Mark current paths as active 2024-11-11 09:54:53 +02:00
Riku Rauhala 9aa4150b9a [Navigation bar] Add arrows for items with sub-items 2024-11-08 23:37:38 +02:00
Riku Rauhala 0c5cac1c8c [Navigation bar] Rename logout to log out
Logout is a noun, log out is a verb
2024-11-08 23:20:21 +02:00
Riku Rauhala a52997fbd4 [NavigationBar] Hide admin button from non-admins 2024-11-08 23:16:20 +02:00
Riku Rauhala b6d68f6b3d [NavigationBar] Implement mocking 2024-11-08 22:55:46 +02:00
Riku Rauhala 5e750a4f40 [NavigationBar] Make the buttons functional 2024-11-08 22:17:47 +02:00
Riku Rauhala 0b81a0dbf5 [Navigation bar] Implement language selection 2024-11-08 21:01:08 +02:00
Riku Rauhala 12996de95a [Navigation bar] Implement initial MUI version 2024-11-08 18:45:27 +02:00
Riku Rauhala 7995635034 [Frontend] Install Material UI and other dependencies 2024-11-08 17:11:23 +02:00
Valtteri Kantanen b4e176a1a6 [Updater] Replace unmaintained winston-sentry-log with winston-transport-sentry-node 2024-11-08 12:10:23 +02:00
Valtteri Kantanen ce982e38d0 Add 'dependencies' label for Renovate PRs 2024-11-08 12:09:11 +02:00
Valtteri Kantanen 6c1f49838b [Updater] Include Sentry env variables in GHA workflow 2024-11-08 12:08:51 +02:00
Valtteri Kantanen 3739a17b78 Refactor GHA workflow to check for new migration files before downloading and building images 2024-11-07 14:41:59 +02:00
Valtteri Kantanen a1ecc12042 [Faculties] Fix tests 2024-11-07 13:59:33 +02:00
Riku Rauhala e78f2d64a4 [Course statistics] Update tests 2024-11-07 13:47:23 +02:00
Riku Rauhala 6991713112 [AutoSubmitSearchInput] Convert to TypeScript and remove unnecessary props 2024-11-07 13:47:23 +02:00
Valtteri Kantanen 60fc1d135d [Class statistics] Display 'Show full study path' toggle for admins 2024-11-07 13:41:11 +02:00
Valtteri Kantanen 89cc24fb81 Update patch versions in package.json files 2024-11-07 13:34:49 +02:00
Valtteri Kantanen 1243e09dfe [Backend] Upgrade Sentry from v5 to v8 2024-11-07 13:09:11 +02:00
Riku Rauhala 0dff5d89c0 [Course statistics] Remove new feature message
This feature is already 9 months old
2024-11-07 13:00:07 +02:00
Riku Rauhala b3cc8e3054 [Filters] Fix tests 2024-11-07 12:40:36 +02:00
Riku Rauhala b89f8a33f1 [Frotend] Use more consistent spelling of study right 2024-11-07 12:16:17 +02:00
Valtteri Kantanen 5279a1f6b7 Automatically merge non-major dependency updates after the tests have passed 2024-11-07 12:13:28 +02:00
Valtteri Kantanen 6f853d0248 [Updater] Replace uuid with node:crypto 2024-11-07 11:58:57 +02:00
Riku Rauhala bf8aec914e [Class statistics] Test study track selection 2024-11-07 10:56:09 +02:00
Valtteri Kantanen e190a66f6e Fix loading the updater images in GHA workflow 2024-11-07 10:50:36 +02:00
Valtteri Kantanen dec58cba25 [Updater] Debug loading the Docker image in GHA workflow 2024-11-07 10:44:02 +02:00
Valtteri Kantanen a7743cfe06 Upgrade actions/upload-artifact and actions/download-artifact to v4 for the rest of the files 2024-11-07 10:36:47 +02:00
Riku Rauhala 7e2f6b212f [Study guidance groups] Remove graduation filter
Graduated students are removed from study guidance groups so there is no need for this filter here
2024-11-07 10:22:16 +02:00
Valtteri Kantanen eb2665b034 Fix loading the backend image in GHA workflow 2024-11-07 10:14:19 +02:00
Valtteri Kantanen e5eb58aee6 Fix downloading the backend image in GHA workflow 2024-11-07 10:09:49 +02:00
Valtteri Kantanen cb820fd2e9 Try upgrading actions/upload-artifact to v4 in test.yaml 2024-11-07 10:03:31 +02:00
Valtteri Kantanen d3bc063e39 Try upgrading actions/upload-artifact to v4 2024-11-06 16:58:24 +02:00
Valtteri Kantanen fe25985eec [Class statistics] Capture the full stack trace of errors instead of creating a new one 2024-11-06 15:58:29 +02:00
Valtteri Kantanen 361d5da6a4 [Class statistics] Fix syntax error in SQL query caused by casing inconsistencies 2024-11-06 15:57:37 +02:00
Valtteri Kantanen 7fc06a5534 [Faculties] Refactor programme sorting in interactive data tables 2024-11-05 17:31:17 +02:00
Riku RauhalaandGitHub 98bcf78fc7 Merge pull request #4671 from UniversityOfHelsinkiCS/graduation-times-percentages
Hide percentages for bachelor + master and master study rights
2024-11-05 13:30:27 +02:00
Riku Rauhala fc42613625 [Faculties] Update tests 2024-11-05 13:23:37 +02:00
Riku Rauhala 62419f0fe4 [Faculties] Type new function calculateStats 2024-11-05 13:13:46 +02:00
Riku Rauhala ad6bd96af6 [TSConfig] Update ES2020 lib to ES2023 2024-11-05 13:06:24 +02:00
Riku Rauhala cad1fe590c Merge branch 'master' into graduation-times-percentages 2024-11-05 13:01:12 +02:00
Riku Rauhala f911ae9d1c [Graduation times] Display percentages only for bachelor study rights 2024-11-05 12:58:02 +02:00
Riku Rauhala 8c05c1c2f3 [StudyTrackOverview] Convert most components to TypeScript 2024-11-05 12:58:02 +02:00
Riku Rauhala b2a92d9003 [ProgressOfStudents] Convert to TypeScript 2024-11-05 12:58:02 +02:00
Riku Rauhala 44d15b5ed8 [facultyHelpers] Convert to TypeScript 2024-11-05 12:58:02 +02:00
Riku Rauhala 6468aaff87 [SegmentDimmer] Convert to TypeScript 2024-11-05 12:58:02 +02:00
Riku Rauhala cbf8663997 [Graduation times] Display percentages only for bachelor study rights 2024-11-05 12:55:33 +02:00
Valtteri Kantanen 388a605937 [Faculties] Move calculateStats into facultyHelpers to enable hot reloading 2024-11-05 12:36:33 +02:00
Riku Rauhala 80cffc5fbf [StudyTrackOverview] Convert most components to TypeScript 2024-11-05 11:32:15 +02:00
Valtteri Kantanen 0e66802c32 [Close to graduation] Use degree programme types to determine programme type 2024-11-05 10:54:44 +02:00
Riku Rauhala 49da696a0b [ProgressOfStudents] Convert to TypeScript 2024-11-05 10:26:52 +02:00
Riku Rauhala 517638a44e [facultyHelpers] Convert to TypeScript 2024-11-05 10:01:47 +02:00
Riku Rauhala abcd00cbad [SegmentDimmer] Convert to TypeScript 2024-11-05 10:00:41 +02:00
Valtteri Kantanen 3a697b94b1 [Students] Use an HTTP status code instead of throwing an error if student is not found or user does not have permission to view student's data 2024-11-05 09:57:35 +02:00
Riku Rauhala 807e5fb2f6 [YearFilter] Convert to TypeScript 2024-11-05 09:29:48 +02:00
Riku Rauhala 4e74a157d7 [Study programme] Update tests 2024-11-04 18:51:56 +02:00
Riku Rauhala 4ffde19997 [Frontend] Fix broken infobox contents 2024-11-04 16:57:45 +02:00
Riku Rauhala 40f3713130 [HoverableHelpPopup] Convert to TypeScript 2024-11-04 16:46:53 +02:00
Riku Rauhala 81d4921bf0 [Toggle] Convert to TypeScript 2024-11-04 16:45:58 +02:00
Riku RauhalaandGitHub 5a508636a0 Merge pull request #4670 from UniversityOfHelsinkiCS/ts-low-hanging
[Frontend] Low hanging TypeScript
2024-11-04 15:14:52 +02:00
Riku Rauhala 77b7fd960d Merge branch 'master' into ts-low-hanging 2024-11-04 14:59:15 +02:00
Riku Rauhala c2a1f25d8d [ProgrammeDropdown] Refactor and convert to TypeScript 2024-11-04 14:56:52 +02:00
Riku Rauhala 1ec3e78ebe [CompletedCoursesSearch] Convert to TypeScript 2024-11-04 14:52:03 +02:00
Riku Rauhala 27c071dca4 [SisuLinkItem] Convert to TypeScript 2024-11-04 14:51:07 +02:00
Valtteri Kantanen 7432f59a81 [Updater] Fix is_study_module column in course table 2024-11-04 13:48:22 +02:00
Valtteri Kantanen 4da174c5de [Close to graduation] Display when the data was last updated 2024-11-04 13:46:45 +02:00
Valtteri Kantanen 78f894660b [Close to graduation] Include timestamp of last update in data 2024-11-04 13:44:07 +02:00
Valtteri Kantanen 3ed1b9aaaf Add name for lint_backend_and_frontend job 2024-11-04 13:43:06 +02:00
Valtteri Kantanen df6175fb75 [Close to graduation] Use study right instead of course provider to identify thesis attainments 2024-11-04 13:42:46 +02:00
Riku Rauhala dbf176ff2c [Cypress] Update tests 2024-11-04 13:05:08 +02:00
Riku Rauhala f9398f8707 [ESLint] Disable no-unused-imports for js/jsx files
This is causing issues when importing from JavaScript into TypeScript files. This can be enabled again when there is a sufficient enough TypeScript coverage for the frontend
2024-11-04 12:49:17 +02:00
Riku RauhalaandGitHub fa05ca2bf8 Merge branch 'master' into ts-low-hanging 2024-11-04 11:48:52 +02:00
Riku Rauhala 25dcd4f009 [Shared] Update tsconfig 2024-11-04 11:40:59 +02:00
Riku Rauhala 1ac1f01c41 [CourseFilterToggle] Convert to TypeScript 2024-11-04 11:40:24 +02:00
Riku RauhalaandGitHub 85a64bdd2e Merge pull request #4668 from UniversityOfHelsinkiCS/renovate/all-minor-patch
Update all non-major dependencies
2024-11-04 10:06:15 +02:00
renovate[bot]andGitHub 911f1c6156 Update all non-major dependencies 2024-11-04 07:28:06 +00:00
Riku Rauhala 70f937dc74 [Shared] Move type Name from backend to shared 2024-11-01 17:36:33 +02:00
Riku Rauhala 191dd7c148 [FilterOldProgrammesToggle] Convert into TypeScript 2024-11-01 17:19:59 +02:00
Riku Rauhala caf62699f1 [FilterToggleIcon] Replace css with inline styling 2024-11-01 16:47:57 +02:00
Riku Rauhala 344dafdc88 [FilterToggleIcon] Convert to TypeScript 2024-11-01 16:41:29 +02:00
Riku Rauhala 4b79059809 [Frontend] Convert constants into TypeScript 2024-11-01 16:30:56 +02:00
Riku Rauhala 24d8a157b3 [Frontend] Extract common function formatContent 2024-11-01 16:15:52 +02:00
Riku Rauhala 05551f1f14 [InfoToolTips] Rename with camelCase for consistency 2024-11-01 16:03:55 +02:00
Valtteri Kantanen d075a1c24b [Study programme overview] Generalize doCombo 2024-11-01 14:54:53 +02:00
Riku Rauhala 82a9fac4cb [InfoToolTips] Convert to TypeScript 2024-11-01 10:03:46 +02:00
Riku Rauhala 1cb4a94772 [Population search] Test Study track selector 2024-10-31 20:52:52 +02:00
Riku RauhalaandGitHub da84c544c4 Merge pull request #4667 from UniversityOfHelsinkiCS/study-track-selector
Population search: Sort study tracks
2024-10-31 20:34:50 +02:00
Riku Rauhala 0c1959dfb8 [Population search] Sort study tracks 2024-10-31 20:26:09 +02:00
Valtteri Kantanen 5240a4bbe9 [Population search] Skip study tracks query if programme is null 2024-10-31 13:22:31 +02:00
Riku Rauhala 5277f71dd9 [InfoBox] Convert to TypeScript 2024-10-31 11:46:21 +02:00
Riku Rauhala a1610d15a9 [ESLint] Disable require-default-props rules for TypeScript 2024-10-31 11:44:03 +02:00
Riku RauhalaandGitHub 6a9d92e1f3 Merge pull request #4666 from UniversityOfHelsinkiCS/study-track-selector
[Class statistics] Add selector for study tracks in population search
2024-10-30 20:05:03 +02:00
Riku Rauhala 6808bfddc7 [Population search] Fix logic for clearing the selections 2024-10-30 19:56:22 +02:00
Riku Rauhala 062b81d537 [Population search] Update info box text
Add mention of the new study track selector
2024-10-30 19:31:22 +02:00
Riku Rauhala 48f5c58a3f [Population search] Replace LoadingIcons with disabling inputs
Prevents layout shift and disallows selecting a study track if there are no available study tracks
2024-10-30 19:15:39 +02:00
Riku Rauhala 943b78def4 [Population search] Extract LoadingIcon 2024-10-30 18:27:36 +02:00
Riku Rauhala 2a5f47da36 [Search history] Update styling 2024-10-30 18:11:58 +02:00
Valtteri KantanenandGitHub 0bdd010b2b [Class statistics] First version of 'Show full study path' feature 2024-10-30 16:55:26 +02:00
Riku Rauhala 576051dae3 [Class statistics] Remove redundant date from search history 2024-10-30 15:44:12 +02:00
Riku Rauhala fc70a24b41 [Class statistics] Remove error message from the search from 2024-10-30 15:28:08 +02:00
Riku Rauhala 84504ab0d2 [Class statistics] Add initial form for selecting study tracks 2024-10-30 15:22:17 +02:00
Riku Rauhala f2a8672c22 [Study programme] Add route for study tracks of a programmec 2024-10-30 14:23:56 +02:00
Valtteri Kantanen 65c6319e87 Revert "[Class statistics] First version of 'Show full study path' feature"
This reverts commit 3244499fc6.
2024-10-30 10:33:26 +02:00
Riku Rauhala 78e3957541 [Frontend] Rename useGetStudyTrackStatsQuery 2024-10-29 21:05:15 +02:00
Riku Rauhala 37fcd15f06 [Study programme] Rename a bunch of stuff 2024-10-29 20:08:44 +02:00
Valtteri Kantanen 3244499fc6 [Class statistics] First version of 'Show full study path' feature 2024-10-29 15:54:42 +02:00
Riku Rauhala 07c25cb9af [Backend] Rename a bunch of things for consistency 2024-10-29 15:26:38 +02:00
Riku Rauhala c9592ba954 [Study programmes] Fix crashing when excluding graduated 2024-10-29 14:15:44 +02:00
Riku RauhalaandGitHub 34a479c1f6 Merge pull request #4662 from UniversityOfHelsinkiCS/misc-updates
Misc updates
2024-10-29 09:03:22 +02:00
Riku Rauhala f34cc2cd44 [Study programme] Update Cypress attributes
[Study programme] Update tests

[Study programme] Update data-cy

[Evaluation overview] Update tests

[Evaluation overview] Update tests
2024-10-29 08:47:34 +02:00
Riku Rauhala 90beeda177 [Study programme] Update tests 2024-10-28 16:16:12 +02:00
Valtteri Kantanen 2dc74af290 [Close to graduation] Fix semester enrollments in Excel export 2024-10-28 14:57:49 +02:00
Riku Rauhala f488019790 [Update statistics] Make the loader match other status icons 2024-10-28 13:06:18 +02:00
Riku Rauhala 7fe7557f7c [Study programme] Simplify Update buttons
The buttons contain only the text 'Update' in the Update statistics tab for Faculties, let's make it consistent with it
2024-10-28 13:02:36 +02:00
Riku Rauhala feca2bbe0a [UpdateView] Extract common component UpdateStatusIcon 2024-10-28 12:55:25 +02:00
Riku Rauhala 179f9737be [Study programme] Clarify button text in Update statistics tab
It was not clear which tab/view this button was referring to
2024-10-28 12:28:02 +02:00
Riku Rauhala c2653c96a7 [Study programme] Fix infobox texts 2024-10-28 12:27:00 +02:00
Valtteri Kantanen d83a36206d [Class statistics] Refactor population search form 2024-10-28 12:13:30 +02:00
Riku Rauhala 6cdcbb5f5f [Frontend] Make spelling of study track / right more consistent 2024-10-28 11:44:57 +02:00
Riku RauhalaandGitHub 8337e9ac94 Merge pull request #4661 from UniversityOfHelsinkiCS/renovate/all-minor-patch
Update all non-major dependencies
2024-10-28 09:09:56 +02:00
renovate[bot]andGitHub 18fd991175 Update all non-major dependencies 2024-10-28 06:57:27 +00:00
Riku Rauhala cb3e953be3 [Population search] Extract common getMonths function 2024-10-24 14:00:52 +03:00
Riku Rauhala d45d2c5922 [Population search] Upgrade from connect to useSelector 2024-10-24 14:00:52 +03:00
Riku Rauhala 3f0e1d947a [PopulationSearchForm] Upgrade to useDispatch and useSelector 2024-10-24 14:00:52 +03:00
Riku Rauhala 55a475a322 [Population search] Upgrade from connect to useSelector 2024-10-24 14:00:52 +03:00
Riku Rauhala e4c703b877 [Population search] Update error message 2024-10-24 14:00:52 +03:00
Valtteri Kantanen f8496edcc4 [Class statistics] Fix a bug where change in filtered students didn't trigger a new request for course stats
Also added two tests for this scenario
2024-10-24 13:28:56 +03:00
Valtteri Kantanen 59cb4638ce [Updater] Remove startdate, enddate and latest_instance_date from Course model 2024-10-24 09:55:35 +03:00
Riku Rauhala 8a7426ae02 [Shared] Move formatQueryParamsToArrays to shared/util 2024-10-24 09:41:58 +03:00
Riku Rauhala 3d5ced89c6 [Class statistics] Fix pass rate 2024-10-22 19:49:00 +03:00
Riku Rauhala 6334fe5696 [TypeScript] Replace some type assertions with type annotations 2024-10-22 19:39:28 +03:00
Riku Rauhala cf280b0a9d [Class statistics] Fix broken totals for multi year stats 2024-10-22 19:04:52 +03:00
Riku Rauhala 7f3f4f5dfe [Backend] Convert statMerger service to TypeScript 2024-10-22 19:04:52 +03:00
Riku Rauhala f8e13c9fea [Class statistics] Rename passed-% to pass rate
This is called pass rate elsewhere in Oodikone
2024-10-22 19:04:52 +03:00
Riku RauhalaandGitHub e23b843375 Merge pull request #4659 from UniversityOfHelsinkiCS/OOD-66
OOD-66: added debugging logging to pin point courses stats problem
2024-10-22 17:25:31 +03:00
Jan Rundt 922f65cf81 OOD-66: similar message to make filtering logs easier 2024-10-22 13:53:48 +03:00
Jan Rundt cee1d0f6cb OOD-66: some more details 2024-10-22 13:45:08 +03:00
Valtteri KantanenandGitHub 0a3124e90f Merge pull request #4656 from UniversityOfHelsinkiCS/OOD-46 2024-10-22 13:19:15 +03:00
Jan Rundt 6065329c58 OOD-66: added debugging logging to pin point courses stats problem in demo environment 2024-10-22 13:19:06 +03:00
Valtteri KantanenandGitHub d521b8a99f Merge pull request #4658 from UniversityOfHelsinkiCS/OOD-68
OOD-68: [backend] continue if study right does not have a student
2024-10-22 12:56:37 +03:00
Valtteri Kantanen bb67309a7c Fix evaluation overview 2024-10-22 12:53:38 +03:00
Matti Räsänen fe190a83ea Merge branch 'master' into OOD-46 2024-10-22 07:22:45 +03:00
Valtteri Kantanen ed0bcd7fee Remove unused constant 2024-10-21 18:42:30 +03:00
Valtteri Kantanen 18df30c09a [Students] Fix tests 2024-10-21 18:32:31 +03:00
Valtteri Kantanen 17ef03d05d [Students] Improve appearance of credit graph tooltip 2024-10-21 18:19:51 +03:00
Valtteri Kantanen 2bf804d7c7 [Students] Fix credit target line 2024-10-21 18:16:43 +03:00
Valtteri Kantanen 04da6d92d3 [Class statistics] Fix course filter tooltip not showing 2024-10-21 18:13:09 +03:00
Jan Rundt 2e3d22baba OOD-68: prettier code 2024-10-21 17:04:19 +03:00
Jan Rundt 758d35c298 OOD-68: continue if study right does not have a student 2024-10-21 16:53:41 +03:00
Riku Rauhala f6323229f9 [Course statistics] Test each toggle case in Attempts tab with combine substitutions 2024-10-21 14:19:55 +03:00
Riku Rauhala 28a04770bc [Course statistics] Test each toggle case in Students tab with combine substitutions 2024-10-21 13:51:49 +03:00
Riku Rauhala 0a417bec49 [Course statistics] Test each toggle case in Attempts tab 2024-10-21 13:31:41 +03:00
Riku Rauhala 2423bd45da [Course statistics] Test each toggle case in Students tab 2024-10-21 13:08:02 +03:00
Riku Rauhala 0155624c86 [Course statistics] Refactor tests 2024-10-21 12:19:50 +03:00
Riku Rauhala 1d310b2d9a [Course statistics] Update selected courses title based on substitutions 2024-10-21 12:06:07 +03:00
Riku Rauhala 231cf8e836 [Course statistics] Improve test coverage 2024-10-21 10:08:46 +03:00
Riku Rauhala f55c30b21e [Course statistics] Remove unused test 2024-10-21 10:08:46 +03:00
Riku RauhalaandGitHub e9f0490ed1 Merge pull request #4657 from UniversityOfHelsinkiCS/renovate/all-minor-patch
Update all non-major dependencies
2024-10-21 09:04:23 +03:00
renovate[bot]andGitHub f9ab98159d Update all non-major dependencies 2024-10-21 05:57:29 +00:00
Matti Räsänen ec9746097b Code cleaning 2024-10-21 08:46:50 +03:00
Matti Räsänen 067e47a99e [Backend][FacultyStatistics] Changed to use id instead of code in frontend and backend faculties router. Implemented getFacultyCodeById to backend to find code by id in faculties router 2024-10-21 08:25:03 +03:00
Riku Rauhala 2e760c7473 [Class statistics] Remove 'Passed after retry' and 'Failed multiple times' 2024-10-18 15:09:04 +03:00
Riku RauhalaandGitHub b3449fcf2a Merge pull request #4655 from UniversityOfHelsinkiCS/OOD-63-v2
OOD-63: [updater] require logger from correct file
2024-10-18 13:36:29 +03:00
Jan Rundt 009f3c13ae ATK is complex and hard 2024-10-18 13:29:18 +03:00
Jan Rundt 06c31b3922 logger from correct place 2024-10-18 13:24:21 +03:00
Valtteri Kantanen e9657fec00 [Class statistics] Remove Students tab from Courses of class 2024-10-18 13:12:42 +03:00
Riku Rauhala 64f2c8f9f6 [Course statistics] Hide subheaders if there are no substitutions 2024-10-18 11:06:13 +03:00
Riku Rauhala db374b6d84 [Course statistics] Limit pass rate to 100 percent 2024-10-18 10:55:09 +03:00
Riku Rauhala 9056a91857 [Course statistics] Reduce copy-paste in tests 2024-10-18 10:41:24 +03:00
Riku Rauhala a8d94c3707 [Course statistics] Refactor tests to reduce copy-paste 2024-10-18 10:08:35 +03:00
Riku Rauhala 02243fdf4b [Course statistics] Test Students table, grades shown 2024-10-18 09:50:49 +03:00
Valtteri KantanenandGitHub 810bf65030 Merge pull request #4649 from UniversityOfHelsinkiCS/OOD-63
OOD-63: [updater] some added error handling and a null check
2024-10-17 16:38:45 +03:00
Valtteri Kantanen 207711b980 [Updater] Use Array.isArray instead of isArray function in lodash 2024-10-17 16:29:26 +03:00
Valtteri Kantanen dce0748b43 [Updater] Use optional chaining instead of checking the object first 2024-10-17 16:28:25 +03:00
Riku Rauhala bd299a0b84 [Course statistics] Test Students table, grades hidden 2024-10-17 15:56:12 +03:00
Riku Rauhala 454bb610af [Course statistics] Test all data on Attempts table 2024-10-17 15:14:54 +03:00
Riku Rauhala 134752f94c [Course statistics] Improve tests 2024-10-17 15:01:52 +03:00
Riku Rauhala 92c01d1eaa [Course statistics] Rename yearcode 2024-10-17 14:35:32 +03:00
Valtteri Kantanen 4333d6a44f [Filters] Fix age filter test 2024-10-17 14:22:10 +03:00
Riku Rauhala 34440de0d5 [Backend] Remove duplicate code 2024-10-17 13:45:02 +03:00
Riku Rauhala b3e352d51d [Backend] Remove student pass categories 2024-10-17 13:36:31 +03:00
Riku Rauhala 3ca2235ca1 [Course statistics] Remove student numbers from total 2024-10-17 10:36:25 +03:00
Jan Rundt 453586a634 null checking effectiveValidityPeriod 2024-10-16 14:56:37 +03:00
Jan Rundt 4313ba2c94 adding array check for programme module children var 2024-10-16 13:23:37 +03:00
Jan Rundt 295683ced1 some added error handling 2024-10-16 13:07:36 +03:00
Valtteri Kantanen b05b11aa06 Don't fetch enrollments with state other than ENROLLED or REJECTED (#4292)
Rejected enrollments are only used in Programme courses / By semester and Language center view
2024-10-14 17:38:44 +03:00
Valtteri Kantanen ab7e5d01f5 [Class statistics] Credit distribution development: Fix edge case on categories 2024-10-14 14:16:09 +03:00
Riku Rauhala e206e2b956 [Course statistics] Use same colors in both pass rate charts 2024-10-14 13:47:30 +03:00
Riku Rauhala af6cc538da [Course statistics] Update info boxes 2024-10-14 13:05:27 +03:00
Valtteri Kantanen 8e83da9138 [Class statistics] Fix Credits gained for master's programmes 2024-10-14 12:16:24 +03:00
Riku Rauhala 7811bdd509 [Course statistics] Fix tests 2024-10-14 11:46:48 +03:00
Riku Rauhala 63dc848857 [Course statistics] Visually separate main code and substitutions 2024-10-14 10:41:02 +03:00
Valtteri Kantanen 2d6a9afc80 Add no-unnecessary-type-assertion rule for ESLint and fix errors 2024-10-14 10:15:00 +03:00
Riku Rauhala ffb12bc89d [Course statistics] Summary tab: replace connect with hooks 2024-10-14 09:54:35 +03:00
Riku Rauhala c08db04472 [Course statistics] Replace Redux connect with hooks 2024-10-14 09:33:30 +03:00
Riku RauhalaandGitHub 63dec8a96d Merge pull request #4646 from UniversityOfHelsinkiCS/renovate/all-minor-patch
Update all non-major dependencies
2024-10-14 09:07:25 +03:00
renovate[bot]andGitHub 92b01943a0 Update all non-major dependencies 2024-10-14 06:00:36 +00:00
Riku RauhalaandGitHub 00196be0e7 Merge pull request #4645 from UniversityOfHelsinkiCS/fix-course-stats
Course statistics: Fix numbers in Students table
2024-10-11 17:13:59 +03:00
Riku Rauhala 106fa5ed0f Merge branch 'master' into fix-course-stats 2024-10-11 17:06:27 +03:00
Riku Rauhala 42074567e8 [Course statistics] Simplify fail rate calculation 2024-10-11 17:02:32 +03:00
Riku Rauhala eed4a316da [Course statistics] Remove withEnrollments 2024-10-11 17:02:32 +03:00
Riku Rauhala feb80d4203 [Course statistics] Update keys for consistency 2024-10-11 17:02:32 +03:00
Riku Rauhala 89c2e95dd0 [Course statistics] Update prop type
This was giving an annoying console warning every time the page is loaded
2024-10-11 17:02:32 +03:00
Riku Rauhala 142896bc0e [Course statistics] Fix Total students and Failed in Students table 2024-10-11 17:02:32 +03:00
Valtteri Kantanen a5f4ab9ad2 Fix tests after update in anon sis-db 2024-10-11 16:34:42 +03:00
Valtteri Kantanen a99fbbb6c4 [Study programme overview] Fix tests after update in anon sis-db 2024-10-11 15:47:44 +03:00
Riku Rauhala 24f5018861 [Course statistics] Remove categories from frontend and fix pass/fail rates 2024-10-11 15:45:13 +03:00
Riku Rauhala 90f6c52469 [Course statistics] Fix total failed 2024-10-11 15:28:05 +03:00
Valtteri Kantanen 66f603e2ca Add minimumCredits to ProgrammeModule model for degree programmes 2024-10-11 15:16:05 +03:00
Riku Rauhala 937bb70b08 [Course statistics] Fix pass rate chart 2024-10-11 15:11:55 +03:00
Riku Rauhala ae509ae8ea [Course statistics] Refactor 2024-10-11 15:06:13 +03:00
Riku Rauhala 8beddf655a [Course statistics] Fix total passed 2024-10-11 15:04:55 +03:00
Riku Rauhala 3486112d1c [Course statistics] Simplify fail rate calculation 2024-10-11 09:50:04 +03:00
Riku Rauhala 7cadd05c5a [Course statistics] Remove withEnrollments 2024-10-11 09:27:24 +03:00
Valtteri Kantanen 3b19485d44 [Class statistics] Don't include secondary attainments in Credits gained tab 2024-10-10 17:34:21 +03:00
Valtteri Kantanen 37c000b8dc [Course statistics] Fix tests 2024-10-10 17:25:54 +03:00
Valtteri Kantanen 8835d38fd3 [Population statistics] Mark improved grades as passed 2024-10-10 16:12:30 +03:00
Valtteri Kantanen 4bc5ed6558 [Updater] Ignore enrollments with state other than ENROLLED or REJECTED 2024-10-10 14:08:42 +03:00
Riku Rauhala b96d409a00 [Course statistics] Update keys for consistency 2024-10-10 14:03:18 +03:00
Riku Rauhala da2e24f4e6 [Course statistics] Update prop type
This was giving an annoying console warning every time the page is loaded
2024-10-10 14:00:01 +03:00
Riku Rauhala 80ca4185ed [Course statistics] Fix Total students and Failed in Students table 2024-10-10 11:43:19 +03:00
Valtteri Kantanen c1982596d5 [Backend] Improve logging 2024-10-09 17:03:55 +03:00
Valtteri Kantanen f6d3728bd1 [Teachers] Calculate leaderboard data if it's not present 2024-10-09 16:04:30 +03:00
Valtteri Kantanen b0590afc29 [Class statistics] Remove unused parameters and props 2024-10-09 16:02:45 +03:00
Valtteri Kantanen 703c1be7c3 [Study programme overview] Improve infobox texts 2024-10-09 16:02:00 +03:00
Valtteri Kantanen 416c160667 [Class statistics] Fix old courses data being used when the study programme changes (#4632) 2024-10-09 16:01:24 +03:00
Valtteri KantanenandGitHub d9e1943c51 Merge pull request #4644 from UniversityOfHelsinkiCS/OOD-59
OOD-59: [updater] [backend] try-catch-clauses for updates and if-clauses for using HY-specific programmeCode-map
2024-10-08 15:07:54 +03:00
Jan Rundt 52a8cd2e75 OOD-59: try-catch-clauses for updates and if-clauses for using HY-specific programmeCode-map 2024-10-08 12:23:31 +03:00
Valtteri Kantanen 4d712a308c [Frontend] Upgrade Sentry from v6 to v8 2024-10-07 13:02:46 +03:00
Valtteri Kantanen b81d179f27 [Study programme overview] Update tests 2024-10-07 12:23:50 +03:00
Valtteri Kantanen 083aa87d0a [Study programme overview] Add study track level breakdown for graduation times (#4468) 2024-10-07 11:58:50 +03:00
Valtteri Kantanen 420db2dd88 [Class statistics] Make progress bar label always have the same width 2024-10-07 11:25:38 +03:00
Valtteri Kantanen 5a03c2b426 [Class statistics] Fix ambiguous chart legend: Previously "150–180", now "150 ≤ credits < 180" 2024-10-07 11:24:01 +03:00
Valtteri Kantanen a227d65aa4 [Class statistics] Fix misleading infobox text 2024-10-07 11:21:52 +03:00
Valtteri Kantanen 5236638322 [Class statistics] Fix filters on Credits gained tab not working properly
Previously the filters used the Credits earned filter, but that's not correct as it uses number of all credits, no matter when they were attained. Previously the Personal study plan filter didn't have an effect on the table either even though the table header suggested it did. Now HOPS credits are used in the table if Personal study plan filter is active.
2024-10-07 11:21:01 +03:00
Riku Rauhala 16fd293290 [Course statistics] Fix missing enrollments 2024-10-07 10:10:53 +03:00
Riku RauhalaandGitHub 2d98a776c4 Merge pull request #4643 from UniversityOfHelsinkiCS/renovate/all-minor-patch
Update all non-major dependencies
2024-10-07 09:36:23 +03:00
renovate[bot]andGitHub be784074ee Update all non-major dependencies 2024-10-07 06:27:50 +00:00
Riku Rauhala 7aedb82dcf [Course statistics] Update tests 2024-10-04 17:28:43 +03:00
Riku Rauhala e93031427a [Course statistics] Remove duplicate primary course info 2024-10-04 17:01:42 +03:00
Riku Rauhala 5f98df491d [Backend] Update substitution courses logic 2024-10-04 16:58:29 +03:00
Riku Rauhala 8970d62413 [Course statistics] Update tests 2024-10-04 16:04:10 +03:00
Riku Rauhala ba5224a3b7 [Population statistics] Update tests 2024-10-04 15:43:45 +03:00
Riku Rauhala 934f5afa2a [Course statistics] Update tests 2024-10-04 15:41:18 +03:00
Riku Rauhala 8f331e6078 [Backend] Include main course in alternatives 2024-10-04 15:31:41 +03:00
Riku Rauhala e8b51cc5e3 [Course statistics] Fix broken links 2024-10-04 15:02:15 +03:00
Riku Rauhala 1bcf1baa3e [Students] Update tests 2024-10-04 14:47:26 +03:00
Riku Rauhala 9c04ca7bf1 [Backend] Fix invalid date 2024-10-04 14:28:53 +03:00
Riku Rauhala 517e67aeab [Course statistics] Extract CourseLabel component and Selected courses text 2024-10-04 14:28:53 +03:00
Riku Rauhala 13d8db6ed2 [Course statistics] Include name of alternative courses and improve styling 2024-10-04 14:28:53 +03:00
Valtteri Kantanen 2cf2d240f6 [Students] Fix grade symbol and name going on different rows in the courses table 2024-10-03 11:13:39 +03:00
Riku Rauhala 71c297d345 [Course statistics] Update enrolled, no grade calculation 2024-09-30 15:34:15 +03:00
Riku Rauhala 4b718b3cf6 [Course statistics] Update enrollments column title and helper text 2024-09-30 14:37:56 +03:00
Riku Rauhala 6375c816ac [Backend] Remove enrollment state from course statistics 2024-09-30 14:35:17 +03:00
Riku Rauhala df081ae3a4 [Course statistics] Fix missing enrollments for groups other than All 2024-09-30 14:02:36 +03:00
Riku Rauhala dba194c52e [Course statistics] Refactor 2024-09-30 09:29:51 +03:00
Riku RauhalaandGitHub 493f56c28a Merge pull request #4634 from UniversityOfHelsinkiCS/renovate/all-minor-patch
Update all non-major dependencies
2024-09-30 08:42:39 +03:00
renovate[bot]andGitHub 668c37c00c Update all non-major dependencies 2024-09-30 05:35:17 +00:00
Valtteri Kantanen 7897ef596a Remove composite column from credit_teachers and course_providers 2024-09-27 18:05:29 +03:00
Riku Rauhala 09b8f75a59 [Course statistics] Update tests 2024-09-27 17:43:52 +03:00
Valtteri Kantanen ef4f1518cb [Backend] Fix SISStudyRight model definition 2024-09-27 17:08:02 +03:00
Valtteri Kantanen 71a3686846 Fix some model definitions 2024-09-27 17:00:20 +03:00
Valtteri Kantanen c761949d68 [Updater] Improve logging of Sequelize errors 2024-09-27 16:54:46 +03:00
Valtteri Kantanen b9bee01681 [Updater] Add unique constraint to course_providers 2024-09-27 16:54:17 +03:00
Valtteri Kantanen 586985c45f [Updater] Add unique constraint to credit_teachers 2024-09-27 16:53:46 +03:00
Riku Rauhala da4e495a33 [Course statistics] Update total attempts text
Let's use multiline for show grades as well for consistency
2024-09-27 14:55:11 +03:00
Riku Rauhala 88652020ca [Course statistics] Remove enrollments by state 2024-09-27 14:50:23 +03:00
Riku Rauhala 5bd9e7ff9f [Course statistics] Remove enrollment types from Attempts tab 2024-09-27 13:33:17 +03:00
Riku Rauhala c87ea60b37 [Course statistics] Change never passed text to failed in pass rate chart 2024-09-27 13:27:45 +03:00
Valtteri Kantanen 60f5c23de4 [Frontend] Upgrade vulnerable version of rollup 2024-09-27 13:00:23 +03:00
Valtteri Kantanen 57083d03fa Add hasPersonalIdentityCode field to Student model 2024-09-27 12:59:00 +03:00
Riku Rauhala 18f5666daf [Course statistics] Add enrolled, no grade to pass rate chart in Attempts tab 2024-09-27 11:19:54 +03:00
Riku Rauhala 43e746de6f [Course statistics] Refactor countTotalStats 2024-09-27 10:47:39 +03:00
Riku Rauhala 3cc0f89dbd [Course statistics] Convert countTotalStats to TypeScript 2024-09-27 09:35:58 +03:00
Valtteri Kantanen a9013204d6 [Course statistics] Fix tests 2024-09-26 14:40:21 +03:00
Valtteri Kantanen eb1a124292 [Study programmes] Sort study tracks alphabetically 2024-09-26 14:34:06 +03:00
Valtteri Kantanen 4db0ef4897 [Study programmes] Fix combined programme stats not getting saved in Redis 2024-09-26 14:18:33 +03:00
Riku Rauhala 21cbfb18d4 [Course statistics] Simplify pass rate calculation 2024-09-26 14:08:13 +03:00
Riku Rauhala cfe27ed599 [Course statistics] Calculate pass rate with enrollments 2024-09-26 13:57:43 +03:00
Riku Rauhala 7dafd5d9c2 [Course statistics] Update tests 2024-09-23 15:08:22 +03:00
Valtteri Kantanen 76c037dd72 [Study programmes] Try to optimize Programme courses 2024-09-23 14:58:05 +03:00
Riku Rauhala 053a20f285 [Course statistics] Attempts tab: count total enrollments in total attempts
Enrollments are also considered attempts
2024-09-23 14:40:34 +03:00
Riku Rauhala ae33741452 [Course statistics] Improve the pass rate chart
For the relative passed/failed chart, let's display the bars on top of each other
2024-09-23 13:04:14 +03:00
Riku Rauhala 5c55b58278 [Study programme overview] Add retries to a flaky test 2024-09-23 10:29:55 +03:00
Valtteri Kantanen a692a2dc66 [Sortable table] Fix issue with column filtering and sorting not working in certain cases 2024-09-23 10:21:24 +03:00
Riku RauhalaandGitHub 4e97210422 Merge pull request #4628 from UniversityOfHelsinkiCS/renovate/all-minor-patch
Update all non-major dependencies
2024-09-23 09:16:30 +03:00
renovate[bot]andGitHub 4816db4791 Update all non-major dependencies 2024-09-23 06:09:59 +00:00
Riku Rauhala 66af7cfe07 [Language center view] Update tests 2024-09-20 19:10:11 +03:00
Riku Rauhala 56f3eb98a4 [Language center view] Update tests 2024-09-20 18:37:08 +03:00
Valtteri Kantanen 3ebff5d4f9 [Class statistics] Fix credit accumulation chart 2024-09-20 15:40:11 +03:00
Valtteri Kantanen f842364c1f Remove .only from a test 2024-09-20 15:39:58 +03:00
Riku Rauhala 3d5bc801ca [Language center] Update tests 2024-09-20 11:44:44 +03:00
Riku Rauhala d04476d874 [Language center] Update tests 2024-09-20 10:46:51 +03:00
Riku Rauhala 34444db544 [Programme courses] Export course name 2024-09-20 09:40:32 +03:00
Valtteri Kantanen 1c6e87ac5f [Updater] Add index for course_id column in enrollment table 2024-09-19 18:24:52 +03:00
Valtteri KantanenandGitHub a7707358d6 Merge pull request #4631 from UniversityOfHelsinkiCS/renovate/npm-vite-vulnerability
Update dependency vite to v5.4.6 [SECURITY]
2024-09-19 14:00:59 +03:00
Riku RauhalaandGitHub 949b85776a Merge pull request #4629 from UniversityOfHelsinkiCS/delete-user-in-fd-environment-merge-with-master
[Frontend][Backend] Delete user in fd environment, can not add if user already exists in db in fd environment
2024-09-19 11:08:22 +03:00
Valtteri Kantanen 41551182a4 [Close to graduation] Update new credit limits to infobox 2024-09-19 08:44:44 +03:00
Valtteri Kantanen 447142c7fc [Close to graduation] Adjust lower limits 2024-09-19 08:39:00 +03:00
renovate[bot]andGitHub 085e102bd6 Update dependency vite to v5.4.6 [SECURITY] 2024-09-17 21:16:31 +00:00
Jan Rundt 4de0da3b7a using delete as method in delete request 2024-09-17 10:26:43 +03:00
Valtteri Kantanen c0fb7208d8 Fix updating teacher leaderboards via frontend 2024-09-16 15:09:28 +03:00
Jan Rundt 47cb76d78b small fixes 2024-09-16 15:01:38 +03:00
Jan Rundt 1d03e59691 Merge branch 'master' into delete-user-in-fd-environment-merge-with-master 2024-09-16 12:55:12 +03:00
Jan Rundt 07d1db6d36 not overwriting existing user 2024-09-16 12:50:21 +03:00
Valtteri Kantanen a914b46c1c Don't try to get Sisu roles in staging 2024-09-16 12:46:18 +03:00
Valtteri Kantanen 52cca4dd01 Upgrade redis to v4 and redis-lock to v1 2024-09-16 12:19:58 +03:00
Valtteri Kantanen 2360d05f25 [Teachers] Sort leaderboard rows by number of credits 2024-09-16 11:56:15 +03:00
Riku Rauhala f5829c0c18 [Course statistics] Simplify pass rate chart 2024-09-16 10:03:51 +03:00
Jan Rundt 587ec153eb delete shown only for fd 2024-09-16 09:14:43 +03:00
Riku RauhalaandGitHub eebabf0ba1 Merge pull request #4627 from UniversityOfHelsinkiCS/renovate/all-minor-patch
Update all non-major dependencies
2024-09-16 09:08:59 +03:00
Jan Rundt fc93028e57 deleting a user, backend and frontend 2024-09-16 09:07:45 +03:00
renovate[bot]andGitHub 42bbce2f57 Update all non-major dependencies 2024-09-16 05:57:35 +00:00
Valtteri Kantanen 86430e12e1 [Study programmes] Fix irrelevant programmes data being saved to Redis 2024-09-13 13:05:35 +03:00
Valtteri Kantanen c4daf60e8c [Study programmes] Make types more specific 2024-09-13 13:00:47 +03:00
Valtteri Kantanen 66341a1bc4 [Faculties] Replace getProgrammes with getDegreeProgrammesOfFaculty 2024-09-13 10:57:39 +03:00
Riku Rauhala 2ae0d23271 Fix tests 2024-09-13 10:26:23 +03:00
Riku Rauhala 029491fb84 [Frontend] Fix grammar issues 2024-09-13 10:12:00 +03:00
Riku Rauhala 9a91b5fba7 [Frontend] Type PassRateChart.tsx 2024-09-13 10:03:48 +03:00
Riku Rauhala b16ad3d28b [Frontend] Convert PassRateChart to TypeScript 2024-09-13 09:20:14 +03:00
Riku Rauhala 2c7d9bce74 [Course statistics] Remove details from the Students tab
Fail-% is always visible, passed first/eventually is removed
2024-09-12 21:33:10 +03:00
Riku Rauhala 2f7343bdc0 [Course statistics] Improve table formatting 2024-09-12 21:33:10 +03:00
Riku Rauhala 7e474b7599 [Backend] Remove type PassedCategory 2024-09-12 21:33:10 +03:00
Valtteri Kantanen 03d9ccb8aa [Updater] Handle custom attainments in HY the way they used to be handled 2024-09-12 15:46:42 +03:00
448 changed files with 15429 additions and 10468 deletions
+4 -117
View File
@@ -1,119 +1,6 @@
// ! If we don't have this file, running ESLint will fail because of the import/no-unused-modules rule
// ! See: https://github.com/import-js/eslint-plugin-import/issues/3079
// ! When this issue is resolved, this file can be removed
{
"env": {
"commonjs": true,
"es2021": true,
"node": true
},
"parserOptions": {
"ecmaVersion": "latest",
"sourceType": "commonjs"
},
"extends": [
"eslint:recommended",
"airbnb",
"plugin:cypress/recommended",
"plugin:import/recommended",
"plugin:prettier/recommended"
],
"overrides": [
{
"files": ["**/*.ts"],
"env": {
"es2021": true,
"node": true
},
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"airbnb-typescript",
"plugin:prettier/recommended"
],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": 2021,
"sourceType": "module",
"project": true
},
"rules": {
"import/prefer-default-export": "off",
"@typescript-eslint/lines-between-class-members": "off",
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-shadow": "off",
"@typescript-eslint/return-await": "off"
}
},
{
"files": ["services/backend/src/rapodiff/**/*"],
"rules": {
"no-console": "off"
}
}
],
"rules": {
"camelcase": "off",
"consistent-return": "off",
"cypress/no-assigning-return-values": "off",
"cypress/unsafe-to-chain-command": "off",
"default-param-last": "off",
"id-denylist": ["error", "c", "d", "e", "err", "t"],
"import/extensions": ["error", "never"],
"import/order": [
"error",
{
"alphabetize": { "order": "asc", "caseInsensitive": true },
"groups": [["builtin", "external"], ["internal"], ["parent"], ["sibling", "index"]],
"pathGroups": [
{
"pattern": "../../../**",
"group": "internal",
"position": "before"
},
{
"pattern": "../../**",
"group": "internal",
"position": "after"
},
{
"pattern": "../**",
"group": "parent",
"position": "before"
},
{
"pattern": "./**",
"group": "sibling",
"position": "before"
}
],
"pathGroupsExcludedImportTypes": ["builtin", "external"]
}
],
"no-async-promise-executor": "off",
"no-await-in-loop": "off",
"no-console": "error",
"no-continue": "off",
"no-param-reassign": "off",
"no-plusplus": "off",
"no-promise-executor-return": "off",
"no-restricted-syntax": [
"error",
{
"selector": "ForInStatement",
"message": "for..in loops iterate over the entire prototype chain, which is virtually never what you want. Use Object.{keys,values,entries}, and iterate over the resulting array."
},
"LabeledStatement",
"WithStatement"
],
"no-return-await": "off",
"no-shadow": "off",
"prettier/prettier": "error",
"quotes": ["error", "single", { "avoidEscape": true }]
},
"settings": {
"import/resolver": {
"node": {
"extensions": [".js", ".jsx", ".ts", ".tsx"]
}
}
},
"ignorePatterns": ["build/", "dist/", "instrumented/", "node_modules/", "**/*migrations*/**"]
"ignorePatterns": ["**/node_modules/", "**/dist/", "**/build/"]
}
+1 -1
View File
@@ -18,4 +18,4 @@ runs:
run: npm run stylelint "services/frontend/src/**/*.css"
- name: 'Run ESLint for JS and TS files'
shell: bash
run: npm run eslint -- --ext .js,.jsx,.ts,.tsx services --report-unused-disable-directives
run: npm run eslint -- services
+5 -2
View File
@@ -11,6 +11,9 @@ inputs:
spec:
required: true
description: 'Path to Cypress file'
test_name:
required: true
description: 'Name of the test'
runs:
using: composite
@@ -35,10 +38,10 @@ runs:
CYPRESS_baseUrl: http://localhost:1337
- name: 'Upload video if tests fail'
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
if: failure()
with:
name: Cypress videos
name: 'Cypress videos - ${{ inputs.test_name }}'
path: ./cypress/videos/*
- name: 'Print logs on failure'
+2 -2
View File
@@ -14,6 +14,7 @@ on:
jobs:
lint_backend_and_frontend:
name: 'Lint backend and frontend'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
@@ -54,8 +55,6 @@ jobs:
spec: cypress/e2e/Language_center.js
- name: 'Test study guidance groups'
spec: cypress/e2e/Study_guidance_groups.js
- name: 'Test evaluation overview'
spec: cypress/e2e/Evaluation_overview.js
- name: 'Test university'
spec: cypress/e2e/University.js
name: ${{ matrix.name }}
@@ -67,3 +66,4 @@ jobs:
username: ${{ secrets.TOSKAREGISTRY_USERNAME }}
password: ${{ secrets.TOSKAREGISTRY_PASSWORD }}
spec: ${{ matrix.spec }}
test_name: ${{ matrix.name }}
+45 -27
View File
@@ -38,9 +38,12 @@ jobs:
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@v3
uses: actions/upload-artifact@v4
with:
name: backend-image
path: /tmp/backend.tar
build_frontend:
@@ -65,18 +68,37 @@ jobs:
BASE_PATH=/oodikone
STAGING=true
build_and_push_anon_sis_db:
name: 'Build anon sis-db and push to toska hub if new migrations have been added'
check_migration_files:
name: Check if new migrations have been added
runs-on: ubuntu-latest
outputs:
kone_db_migrations: ${{ steps.check_migrations.outputs.kone_db_migrations }}
user_db_migrations: ${{ steps.check_migrations.outputs.user_db_migrations }}
sis_db_migrations: ${{ steps.check_migrations.outputs.sis_db_migrations }}
steps:
- uses: actions/checkout@v4
- uses: dorny/paths-filter@v3
- name: Check if new migrations have been added
id: check_migrations
uses: dorny/paths-filter@v3
with:
filters: |
migrations:
kone_db_migrations:
- 'services/backend/src/database/migrations_kone/**'
- '.github/workflows/test.yaml'
user_db_migrations:
- 'services/backend/src/database/migrations_user/**'
- '.github/workflows/test.yaml'
sis_db_migrations:
- 'updater/sis-updater-worker/src/db/migrations/**'
- '.github/workflows/test.yaml'
build_and_push_anon_sis_db:
name: 'Build anon sis-db and push to toska hub if new migrations have been added'
needs: check_migration_files
runs-on: ubuntu-latest
if: needs.check_migration_files.outputs.sis_db_migrations == 'true'
steps:
- uses: actions/checkout@v4
- name: 'Login to toska docker registry'
uses: docker/login-action@v3
with:
@@ -84,7 +106,6 @@ jobs:
username: ${{ secrets.username }}
password: ${{ secrets.password }}
- name: 'Let sis-updater-worker run migrations, then push new version of image to toska hub'
if: steps.check_migrations.outputs.migrations == 'true'
run: |
docker compose --file docker-compose.ci.yml up --detach sis-updater-worker
sleep 30 # wait for sis-updater-worker to start and run migrations
@@ -95,22 +116,17 @@ jobs:
needs:
- build_backend
- build_frontend
- check_migration_files
runs-on: ubuntu-latest
if: needs.check_migration_files.outputs.kone_db_migrations == 'true'
steps:
- uses: actions/checkout@v4
- uses: dorny/paths-filter@v3
id: check_migrations
- name: 'Download backend image'
uses: actions/download-artifact@v4
with:
filters: |
migrations:
- 'services/backend/src/database/migrations_kone/**'
- '.github/workflows/test.yaml'
- name: 'Download images'
uses: actions/download-artifact@v3
with:
path: /tmp/
name: backend-image
- name: 'Load built backend image'
run: docker load --input /tmp/artifact/backend.tar
run: docker load --input backend.tar
- name: 'Login to toska docker registry'
uses: docker/login-action@v3
with:
@@ -118,10 +134,9 @@ jobs:
username: ${{ secrets.username }}
password: ${{ secrets.password }}
- name: 'Let backend run migrations, then push new version of image to toska hub'
if: steps.check_migrations.outputs.migrations == 'true'
run: |
docker compose --file docker-compose.ci.yml up --detach backend
sleep 30 # wait for sis-updater-worker to start and run migrations
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:
@@ -129,7 +144,9 @@ jobs:
needs:
- build_backend
- build_frontend
- check_migration_files
runs-on: ubuntu-latest
if: needs.check_migration_files.outputs.user_db_migrations == 'true'
steps:
- uses: actions/checkout@v4
- uses: dorny/paths-filter@v3
@@ -139,12 +156,12 @@ jobs:
migrations:
- 'services/backend/src/database/migrations_user/**'
- '.github/workflows/test.yaml'
- name: 'Download images'
uses: actions/download-artifact@v3
- name: 'Download backend image'
uses: actions/download-artifact@v4
with:
path: /tmp/
name: backend-image
- name: 'Load built backend image'
run: docker load --input /tmp/artifact/backend.tar
run: docker load --input backend.tar
- name: 'Login to toska docker registry'
uses: docker/login-action@v3
with:
@@ -152,10 +169,9 @@ jobs:
username: ${{ secrets.username }}
password: ${{ secrets.password }}
- name: 'Let backend run migrations, then push new version of image to toska hub'
if: steps.check_migrations.outputs.migrations == 'true'
run: |
docker compose --file docker-compose.ci.yml up --detach backend
sleep 30 # wait for sis-updater-worker to start and run migrations
sleep 30 # wait for backend to start and run migrations
./anonyymioodi/commit_and_push_to_toska_hub.sh user-db
# === End-to-end tests ===
@@ -165,6 +181,8 @@ jobs:
- 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()
strategy:
fail-fast: false
matrix:
@@ -195,8 +213,6 @@ jobs:
spec: cypress/e2e/Language_center.js
- name: 'Test study guidance groups'
spec: cypress/e2e/Study_guidance_groups.js
- name: 'Test evaluation overview'
spec: cypress/e2e/Evaluation_overview.js
- name: 'Test university'
spec: cypress/e2e/University.js
name: ${{ matrix.name }}
@@ -208,6 +224,7 @@ jobs:
username: ${{ secrets.username }}
password: ${{ secrets.password }}
spec: ${{ matrix.spec }}
test_name: ${{ matrix.name }}
# === Other tests ===
test_service_exit_statuses:
@@ -216,6 +233,7 @@ jobs:
- build_and_push_anon_sis_db
- build_and_push_anon_kone_db
- build_and_push_anon_user_db
if: always()
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
+22 -9
View File
@@ -27,7 +27,7 @@ jobs:
- name: 'Install dependencies'
run: npm ci && cd updater/sis-updater-scheduler && npm ci && cd ../sis-updater-worker && npm ci
- name: 'Run linter'
run: npm run eslint updater -- --report-unused-disable-directives
run: npm run eslint -- updater
build_worker:
name: 'Build Updater worker'
@@ -42,9 +42,12 @@ jobs:
push: false
outputs: type=docker,dest=/tmp/worker.tar
tags: toska/sis-updater-worker:latest
- name: 'Store built image'
uses: actions/upload-artifact@v3
env:
DOCKER_BUILD_SUMMARY: false
- name: 'Upload image for other jobs'
uses: actions/upload-artifact@v4
with:
name: updater-worker-image
path: /tmp/worker.tar
build_scheduler:
@@ -60,9 +63,12 @@ jobs:
push: false
outputs: type=docker,dest=/tmp/scheduler.tar
tags: toska/sis-updater-scheduler:latest
- name: 'Store built image'
uses: actions/upload-artifact@v3
env:
DOCKER_BUILD_SUMMARY: false
- name: 'Upload image for other jobs'
uses: actions/upload-artifact@v4
with:
name: updater-scheduler-image
path: /tmp/scheduler.tar
test_updater:
@@ -81,13 +87,14 @@ jobs:
username: ${{ secrets.TOSKAREGISTRY_USERNAME }}
password: ${{ secrets.TOSKAREGISTRY_PASSWORD }}
- name: 'Download built images'
uses: actions/download-artifact@v3
uses: actions/download-artifact@v4
with:
path: /tmp/
pattern: 'updater-{worker,scheduler}-image'
merge-multiple: true
- name: 'Load worker image'
run: docker load --input /tmp/artifact/worker.tar
run: docker load --input worker.tar
- name: 'Load scheduler image'
run: docker load --input /tmp/artifact/scheduler.tar
run: docker load --input scheduler.tar
- name: 'Start worker'
run: docker compose --file docker-compose.ci.yml up --detach --no-recreate sis-updater-worker
- name: 'Run hourly jobs'
@@ -128,6 +135,9 @@ jobs:
context: updater/sis-updater-worker
containerfiles: |
updater/sis-updater-worker/Dockerfile
build-args: |
SENTRY_RELEASE=${{ github.sha }}
SENTRY_ENVIRONMENT=production
- name: Push to quay.io
uses: redhat-actions/push-to-registry@v2
@@ -155,6 +165,9 @@ jobs:
context: updater/sis-updater-scheduler
containerfiles: |
updater/sis-updater-scheduler/Dockerfile
build-args: |
SENTRY_RELEASE=${{ github.sha }}
SENTRY_ENVIRONMENT=production
- name: Push to quay.io
uses: redhat-actions/push-to-registry@v2
+3 -3
View File
@@ -1,7 +1,7 @@
/* eslint-disable global-require */
/* eslint-disable import/extensions */
/* eslint-disable @typescript-eslint/no-require-imports */
/* eslint-disable import/no-commonjs */
/* eslint-disable import/no-extraneous-dependencies */
/* eslint-disable import/no-unused-modules */
const { defineConfig } = require('cypress')
module.exports = defineConfig({
File diff suppressed because it is too large Load Diff
-514
View File
@@ -1,514 +0,0 @@
/// <reference types="cypress" />
const { getEmptyYears } = require('../support/commands')
describe('Evaluation overview', () => {
describe('Programme view', () => {
beforeEach(() => {
cy.init('/evaluationoverview/programme/KH50_001', 'admin')
})
it('Page opens correctly', () => {
cy.contains('Matemaattisten tieteiden kandiohjelma')
cy.contains('KH50_001')
cy.contains("This view is an abridged version of Oodikone's Studyprogramme Overview")
})
it('Progress data is shown correctly with graduated included', () => {
cy.get('[data-cy="Graph-StudytrackProgress"]').within(() => {
const totalStats = ['12.9%', '6.9%', '9.7%', '9.2%', '3.7%', '6.0%', '51.6%']
for (const stat of totalStats) {
cy.contains(stat)
}
})
const years = getEmptyYears(true)
const tableContents = [
...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', 32, 8, 3, 8, 8, 5, 0, 0],
['2020 - 2021', 27, 2, 1, 3, 6, 2, 4, 9],
['2019 - 2020', 33, 1, 0, 2, 1, 0, 3, 26],
['2018 - 2019', 44, 0, 1, 1, 2, 0, 3, 37],
['2017 - 2018', 47, 0, 1, 3, 0, 1, 2, 40],
['Total', 217, 28, 15, 21, 20, 8, 13, 112],
]
cy.checkTableStats(tableContents, 'StudytrackProgress')
})
it('Progress data is shown correctly with graduated excluded', () => {
cy.get('[data-cy=GraduatedToggle]').click()
cy.get('[data-cy="Graph-StudytrackProgress"]').within(() => {
const totalStats = ['30.8%', '15.4%', '22.0%', '20.9%', '4.4%', '5.5%']
for (const stat of totalStats) {
cy.contains(stat)
}
})
const years = getEmptyYears(true)
const tableContents = [
...years.map(year => [year, 0, 0, 0, 0, 0, 0, 0, 0]),
['2023 - 2024', 8, 8, 0, 0, 0, 0, 0, 0],
['2022 - 2023', 24, 9, 8, 4, 3, 0, 0, 0],
['2021 - 2022', 31, 8, 3, 8, 8, 4, 0, 0],
['2020 - 2021', 11, 2, 1, 2, 5, 0, 0, 1],
['2019 - 2020', 7, 1, 0, 2, 1, 0, 0, 3],
['2018 - 2019', 5, 0, 1, 1, 2, 0, 0, 1],
['2017 - 2018', 5, 0, 1, 3, 0, 0, 1, 0],
['Total', 91, 28, 14, 20, 19, 4, 1, 5],
]
cy.checkTableStats(tableContents, 'StudytrackProgress')
})
it('Graduation times breakdown data is shown correctly', () => {
cy.get('[data-cy=graduation-times-graph-breakdown]').within(() => {
cy.contains('Graduation year')
cy.contains('2021')
cy.get('[aria-label="2021, 8. Overtime."]')
cy.get('[aria-label="2021, 24. Max. year overtime."]')
cy.get('[aria-label="2021, 14. On time."]').trigger('mouseover')
cy.contains('Graduated On time: 14 students')
})
cy.get('[data-cy=YearToggle]').click()
cy.get('[data-cy=graduation-times-graph-breakdown]').within(() => {
cy.contains('Graduation year')
cy.contains('2021 - 2022')
cy.get('[aria-label="2021 - 2022, 13. Overtime."]')
cy.get('[aria-label="2021 - 2022, 21. Max. year overtime."]')
cy.get('[aria-label="2021 - 2022, 21. On time."]').trigger('mouseover')
cy.contains('Graduated On time: 21 students')
})
})
it('Graduation times median time data is shown correctly', () => {
cy.get('[data-cy=GraduationTimeToggle]').click()
cy.get('[data-cy=graduation-times-graph]').within(() => {
cy.contains('Graduation year')
cy.contains('2021')
cy.contains('46 graduated').trigger('mouseover')
cy.contains('46 students graduated in year 2021')
cy.contains('median study time: 41 months')
cy.contains('14 graduated on time')
cy.contains('24 graduated max year overtime')
cy.contains('8 graduated over year late')
})
cy.get('[data-cy=YearToggle]').click()
cy.get('[data-cy=graduation-times-graph]').within(() => {
cy.contains('Graduation year')
cy.contains('2021 - 2022')
cy.contains('55 graduated').trigger('mouseover')
cy.contains('55 students graduated in year 2021 - 2022')
cy.contains('median study time: 40 months')
cy.contains('21 graduated on time')
cy.contains('21 graduated max year overtime')
cy.contains('13 graduated over year late')
})
})
it("'Programmes before or after' data is shown correctly", () => {
cy.get('[data-cy=Graph-ProgrammesBeforeOrAfter]').within(() => {
cy.contains('Matematiikan ja tilastotieteen maisteriohjelma')
cy.contains('Tietojenkäsittelytieteen maisteriohjelma')
cy.contains('Datatieteen maisteriohjelma')
cy.contains('2021')
cy.contains('32')
cy.contains('19')
})
let emptyYears = new Array(getEmptyYears(false).length).fill(0)
let tableContents = [
['MH50_001', 'MAST', 'Matematiikan ja tilastotieteen maisteriohjelma', 0, 0, 1, 9, 32, 19, 15, ...emptyYears],
['MH50_002', 'LSI', 'Life Science Informatics -maisteriohjelma', 0, 0, 0, 1, 2, 1, 1, ...emptyYears],
[
'MH50_003',
'TCM',
'Teoreettisten ja laskennallisten menetelmien maisteriohjelma',
0,
0,
0,
0,
1,
2,
1,
...emptyYears,
],
['MH50_009', 'CSM', 'Tietojenkäsittelytieteen maisteriohjelma', 0, 0, 0, 0, 2, 3, 2, ...emptyYears],
['MH50_010', 'DATA', 'Datatieteen maisteriohjelma', 0, 0, 0, 1, 5, 10, 1, ...emptyYears],
['MH70_009', 'ECON', 'Taloustieteen maisteriohjelma', 0, 0, 0, 0, 4, 7, 4, ...emptyYears],
]
cy.checkTableStats(tableContents, 'undefined')
cy.get('[data-cy=YearToggle]').click()
cy.get('[data-cy=Graph-ProgrammesBeforeOrAfter]').within(() => {
cy.contains('Matematiikan ja tilastotieteen maisteriohjelma')
cy.contains('Tietojenkäsittelytieteen maisteriohjelma')
cy.contains('Datatieteen maisteriohjelma')
cy.contains('2021 - 2022')
cy.contains('28')
cy.contains('16')
})
emptyYears = new Array(getEmptyYears(true).length).fill(0)
tableContents = [
['MH50_001', 'MAST', 'Matematiikan ja tilastotieteen maisteriohjelma', 0, 0, 4, 24, 28, 16, 4, ...emptyYears],
['MH50_002', 'LSI', 'Life Science Informatics -maisteriohjelma', 0, 0, 1, 0, 3, 1, 0, ...emptyYears],
[
'MH50_003',
'TCM',
'Teoreettisten ja laskennallisten menetelmien maisteriohjelma',
0,
0,
0,
1,
1,
2,
0,
...emptyYears,
],
['MH50_009', 'CSM', 'Tietojenkäsittelytieteen maisteriohjelma', 0, 0, 0, 0, 4, 3, 0, ...emptyYears],
['MH50_010', 'DATA', 'Datatieteen maisteriohjelma', 0, 0, 1, 0, 12, 4, 0, ...emptyYears],
['MH70_009', 'ECON', 'Taloustieteen maisteriohjelma', 0, 0, 0, 4, 6, 5, 0, ...emptyYears],
]
cy.checkTableStats(tableContents, 'undefined')
})
})
describe('Faculty view', () => {
beforeEach(() => {
cy.init('/evaluationoverview/faculty/H50', 'admin')
})
it('Page opens correctly', () => {
cy.contains('Matemaattis-luonnontieteellinen tiedekunta')
cy.contains('H50')
cy.contains("This view is an abridged version of Oodikone's Faculty Overview")
})
describe('Progress stats', () => {
describe('Bachelor stats', () => {
it('graph with graduated included is shown correctly', () => {
cy.get('[data-cy="Graph-FacultyBachelorsProgress"]').within(() => {
const totalStats = ['12.2%', '5.3%', '7.5%', '8.2%', '5.3%', '6.3%', '55.2%']
for (const stat of totalStats) {
cy.contains(stat)
}
})
})
it('graph with graduated excluded is shown correctly', () => {
cy.get('[data-cy="GraduatedToggle"]').click()
cy.get('[data-cy="Graph-FacultyBachelorsProgress"]').within(() => {
const totalStats = ['30.5%', '12.5%', '18.0%', '19.5%', '7.0%', '3.1%', '9.4%']
for (const stat of totalStats) {
cy.contains(stat)
}
})
})
it('table with graduated included is shown correctly', () => {
cy.get('[data-cy="Table-FacultyBachelorsProgress"]')
const years = getEmptyYears(true)
const tableContents = [
...years.map(year => [year, 0, 0, 0, 0, 0, 0, 0, 0]),
['2023 - 2024', 14, 13, 0, 0, 0, 0, 0, 1],
['2022 - 2023', 32, 11, 10, 6, 4, 0, 1, 0],
['2021 - 2022', 44, 10, 4, 9, 11, 7, 1, 2],
['2020 - 2021', 38, 3, 1, 3, 7, 3, 8, 13],
['2019 - 2020', 50, 2, 0, 2, 1, 4, 5, 36],
['2018 - 2019', 75, 0, 1, 1, 2, 2, 3, 66],
['2017 - 2018', 66, 0, 1, 3, 1, 1, 2, 58],
['Total', 319, 39, 17, 24, 26, 17, 20, 176],
]
cy.checkTableStats(tableContents, 'FacultyBachelorsProgress')
})
it('table with graduated excluded is shown correctly', () => {
cy.get('[data-cy="GraduatedToggle"]').click()
cy.get('[data-cy="Table-FacultyBachelorsProgress"]')
const years = getEmptyYears(true)
const tableContents = [
...years.map(year => [year, 0, 0, 0, 0, 0, 0, 0, 0]),
['2023 - 2024', 14, 13, 0, 0, 0, 0, 0, 1],
['2022 - 2023', 30, 11, 9, 6, 4, 0, 0, 0],
['2021 - 2022', 41, 10, 4, 9, 11, 6, 0, 1],
['2020 - 2021', 14, 3, 1, 2, 6, 0, 1, 1],
['2019 - 2020', 14, 2, 0, 2, 1, 3, 2, 4],
['2018 - 2019', 6, 0, 1, 1, 2, 0, 0, 2],
['2017 - 2018', 9, 0, 1, 3, 1, 0, 1, 3],
['Total', 128, 39, 16, 23, 25, 9, 4, 12],
]
cy.checkTableStats(tableContents, 'FacultyBachelorsProgress')
})
})
describe('Bachelor + master stats', () => {
it('graph with graduated included is shown correctly', () => {
cy.get('[data-cy=Graph-FacultyBachelorMastersProgress]').within(() => {
const totalStats = ['21.3%', '8.4%', '10.1%', '14.0%', '12.4%', '6.7%', '27.0%']
for (const stat of totalStats) {
cy.contains(stat)
}
})
})
it('graph with graduated excluded is shown correctly', () => {
cy.get('[data-cy="GraduatedToggle"]').click()
cy.get('[data-cy=Graph-FacultyBachelorMastersProgress]').within(() => {
const totalStats = ['28.7%', '11.6%', '14.0%', '18.6%', '15.5%', '3.1%', '8.5%']
for (const stat of totalStats) {
cy.contains(stat)
}
})
})
it('table with graduated included is shown correctly', () => {
cy.get('[data-cy="Table-FacultyBachelorMastersProgress"]')
const years = getEmptyYears(true)
const tableContents = [
...years.map(year => [year, 0, 0, 0, 0, 0, 0, 0, 0]),
['2023 - 2024', 0, 0, 0, 0, 0, 0, 0, 0],
['2022 - 2023', 2, 2, 0, 0, 0, 0, 0, 0],
['2021 - 2022', 4, 2, 0, 0, 0, 1, 0, 1],
['2020 - 2021', 23, 13, 2, 4, 0, 4, 0, 0],
['2019 - 2020', 32, 7, 7, 4, 7, 3, 3, 1],
['2018 - 2019', 63, 10, 2, 5, 13, 7, 4, 22],
['2017 - 2018', 54, 4, 4, 5, 5, 7, 5, 24],
['Total', 178, 38, 15, 18, 25, 22, 12, 48],
]
cy.checkTableStats(tableContents, 'FacultyBachelorMastersProgress')
})
it('table with graduated excluded is shown correctly', () => {
cy.get('[data-cy="GraduatedToggle"]').click()
cy.get('[data-cy="Table-FacultyBachelorMastersProgress"]')
const years = getEmptyYears(true)
const tableContents = [
...years.map(year => [year, 0, 0, 0, 0, 0, 0, 0, 0]),
['2023 - 2024', 0, 0, 0, 0, 0, 0, 0, 0],
['2022 - 2023', 2, 2, 0, 0, 0, 0, 0, 0],
['2021 - 2022', 4, 2, 0, 0, 0, 1, 0, 1],
['2020 - 2021', 21, 13, 2, 4, 0, 2, 0, 0],
['2019 - 2020', 31, 7, 7, 4, 7, 3, 2, 1],
['2018 - 2019', 41, 9, 2, 5, 12, 7, 1, 5],
['2017 - 2018', 30, 4, 4, 5, 5, 7, 1, 4],
['Total', 129, 37, 15, 18, 24, 20, 4, 11],
]
cy.checkTableStats(tableContents, 'FacultyBachelorMastersProgress')
})
})
describe('Master stats', () => {
it('graph with graduated included is shown correctly', () => {
cy.get('[data-cy=Graph-FacultyMastersProgress]').within(() => {
const totalStats = ['16.1%', '12.9%', '6.5%', '12.9%', '9.7%', '16.1%', '25.8%']
for (const stat of totalStats) {
cy.contains(stat)
}
})
})
it('graph with graduated excluded is shown correctly', () => {
cy.get('[data-cy="GraduatedToggle"]').click()
cy.get('[data-cy=Graph-FacultyMastersProgress]').within(() => {
const totalStats = ['23.8%', '19.0%', '9.5%', '9.5%', '9.5%', '14.3%', '14.3%']
for (const stat of totalStats) {
cy.contains(stat)
}
})
})
it('table with graduated included is shown correctly', () => {
cy.get('[data-cy="Table-FacultyMastersProgress"]')
const years = getEmptyYears(true)
const tableContents = [
...years.map(year => [year, 0, 0, 0, 0, 0, 0, 0, 0]),
['2023 - 2024', 4, 4, 0, 0, 0, 0, 0, 0],
['2022 - 2023', 5, 0, 1, 1, 2, 0, 0, 1],
['2021 - 2022', 8, 1, 2, 0, 2, 1, 1, 1],
['2020 - 2021', 3, 0, 0, 0, 0, 0, 2, 1],
['2019 - 2020', 7, 0, 0, 1, 0, 1, 1, 4],
['2018 - 2019', 2, 0, 0, 0, 0, 1, 0, 1],
['2017 - 2018', 2, 0, 1, 0, 0, 0, 1, 0],
['Total', 31, 5, 4, 2, 4, 3, 5, 8],
]
cy.checkTableStats(tableContents, 'FacultyMastersProgress')
})
it('table with graduated excluded is shown correctly', () => {
cy.get('[data-cy="GraduatedToggle"]').click()
cy.get('[data-cy="Table-FacultyMastersProgress"]')
const years = getEmptyYears(true)
const tableContents = [
...years.map(year => [year, 0, 0, 0, 0, 0, 0, 0, 0]),
['2023 - 2024', 4, 4, 0, 0, 0, 0, 0, 0],
['2022 - 2023', 5, 0, 1, 1, 2, 0, 0, 1],
['2021 - 2022', 5, 1, 2, 0, 0, 0, 1, 1],
['2020 - 2021', 0, 0, 0, 0, 0, 0, 0, 0],
['2019 - 2020', 3, 0, 0, 1, 0, 1, 1, 0],
['2018 - 2019', 2, 0, 0, 0, 0, 1, 0, 1],
['2017 - 2018', 2, 0, 1, 0, 0, 0, 1, 0],
['Total', 21, 5, 4, 2, 2, 2, 3, 3],
]
cy.checkTableStats(tableContents, 'FacultyMastersProgress')
})
})
describe('Doctor stats', () => {
it('graph with graduated included is shown correctly', () => {
cy.get('[data-cy=Graph-FacultyDoctoralProgress]').within(() => {
const totalStats = ['6.0%', '18.0%', '16.0%', '14.0%', '46.0%']
for (const stat of totalStats) {
cy.contains(stat)
}
})
})
it('graph with graduated excluded is shown correctly', () => {
cy.get('[data-cy="GraduatedToggle"]').click()
cy.get('[data-cy=Graph-FacultyDoctoralProgress]').within(() => {
const totalStats = ['9.1%', '24.2%', '12.1%', '15.2%', '39.4%']
for (const stat of totalStats) {
cy.contains(stat)
}
})
})
it('table with graduated included is shown correctly', () => {
cy.get('[data-cy="Table-FacultyDoctoralProgress"]')
const years = getEmptyYears(true)
const tableContents = [
...years.map(year => [year, 0, 0, 0, 0, 0, 0]),
['2023 - 2024', 1, 1, 0, 0, 0, 0],
['2022 - 2023', 9, 1, 7, 1, 0, 0],
['2021 - 2022', 3, 1, 1, 0, 0, 1],
['2020 - 2021', 5, 0, 0, 2, 1, 2],
['2019 - 2020', 11, 0, 0, 1, 4, 6],
['2018 - 2019', 14, 0, 1, 4, 2, 7],
['2017 - 2018', 7, 0, 0, 0, 0, 7],
['Total', 50, 3, 9, 8, 7, 23],
]
cy.checkTableStats(tableContents, 'FacultyDoctoralProgress')
})
it('table with graduated excluded is shown correctly', () => {
cy.get('[data-cy="GraduatedToggle"]').click()
cy.get('[data-cy="Table-FacultyDoctoralProgress"]')
const years = getEmptyYears(true)
const tableContents = [
...years.map(year => [year, 0, 0, 0, 0, 0, 0]),
['2023 - 2024', 1, 1, 0, 0, 0, 0],
['2022 - 2023', 9, 1, 7, 1, 0, 0],
['2021 - 2022', 3, 1, 1, 0, 0, 1],
['2020 - 2021', 4, 0, 0, 2, 0, 2],
['2019 - 2020', 9, 0, 0, 1, 4, 4],
['2018 - 2019', 5, 0, 0, 0, 1, 4],
['2017 - 2018', 2, 0, 0, 0, 0, 2],
['Total', 33, 3, 8, 4, 5, 13],
]
cy.checkTableStats(tableContents, 'FacultyDoctoralProgress')
})
})
it('Years in the tables can be clicked to show programme level breakdown', () => {
cy.get('[data-cy="Table-FacultyBachelorsProgress"]').within(() => {
cy.contains('2021 - 2022').click()
cy.contains('31.3%').trigger('mouseover', { force: true })
cy.contains('Matemaattisten tieteiden kandiohjelma')
cy.contains('MAT - KH50_001')
cy.contains('0 Credits: 0')
cy.contains('1 ≤ Credits < 45: 10')
cy.contains('45 ≤ Credits < 90: 9')
cy.contains('90 ≤ Credits < 135: 11')
cy.contains('135 ≤ Credits < 180: 2')
cy.contains('180 ≤ Credits: 0')
})
})
})
describe('Graduation times stats', () => {
it('All the correct sections are displayed', () => {
const checkSections = breakdown => {
const sections = ['bachelor', 'bcMsCombo', 'master', 'doctor']
for (const section of sections) {
cy.get(`[data-cy=Section-${section}]`).within(() => {
cy.get('div.graduations-chart-container').within(() => {
cy.get(`div.faculty-${breakdown ? 'breakdown-' : ''}graph`)
cy.get('div.graduations-message').contains("Click a bar to view that year's programme level breakdown")
})
})
}
}
checkSections(true)
cy.get('[data-cy=GraduationTimeToggle]').click()
checkSections(false)
})
it('Graduation times breakdown data is shown correctly', () => {
cy.get('[data-cy=Section-bachelor]').within(() => {
cy.contains('Graduation year')
cy.contains('2022')
cy.get('[aria-label="2022, 29. On time."]').trigger('mouseover')
cy.contains('On time: 29')
cy.get('[aria-label="2022, 29. On time."]').trigger('click')
cy.contains('Year 2022 by graduation year')
cy.get('[aria-label="MAT, 22. On time."]').trigger('mouseover')
cy.contains('Matemaattisten tieteiden kandiohjelma')
cy.contains('KH50_001')
cy.contains('On time: 22')
cy.get('[aria-label="MFKK, 7. On time."]').trigger('mouseover')
cy.contains('Matematiikan, fysiikan ja kemian opettajan kandiohjelma')
cy.contains('KH50_004')
cy.contains('On time: 7')
})
})
it('Graduation times median time data is shown correctly', () => {
cy.get('[data-cy=GraduationTimeToggle]').click()
cy.get('[data-cy=Section-bachelor]').within(() => {
cy.contains('Graduation year')
cy.contains('2021')
cy.contains('70 graduated').trigger('mouseover')
cy.contains('70 students graduated in year 2021')
cy.contains('median study time: 39.5 months')
cy.contains('28 graduated on time')
cy.contains('32 graduated max year overtime')
cy.contains('10 graduated over year late')
cy.contains('70 graduated').trigger('click')
cy.contains('Year 2021 by graduation year')
cy.get('[aria-label="MAT, 41."]').trigger('mouseover')
cy.contains('Matemaattisten tieteiden kandiohjelma')
cy.contains('KH50_001')
cy.contains('47 students graduated in year 2021')
cy.contains('median study time: 41 months')
cy.contains('14 graduated on time')
cy.contains('25 graduated max year overtime')
cy.contains('8 graduated over year late')
})
})
})
})
})
+9 -9
View File
@@ -23,7 +23,7 @@ describe('Faculty overview', () => {
})
it('Credits produced by faculty are shown', () => {
cy.get('[data-cy="Section-CreditsProducedByTheFaculty"]').should('be.visible')
cy.get('[data-cy="Section-creditsProducedByTheFaculty"]').should('be.visible')
cy.get('[data-cy="Graph-CreditsProducedByTheFaculty"]').should('be.visible')
})
@@ -75,14 +75,14 @@ describe('Faculty overview', () => {
})
it('Students of the faculty infobox works', () => {
cy.get('[data-cy="StudentsOfTheFaculty-info-content"]').should('not.exist')
cy.get('[data-cy="StudentsOfTheFaculty-open-info"]').click()
cy.get('[data-cy="StudentsOfTheFaculty-info-content"]').should('be.visible')
cy.get('[data-cy="StudentsOfTheFaculty-close-info"]').click()
cy.get('[data-cy="StudentsOfTheFaculty-info-content"]').should('not.exist')
cy.get('[data-cy="studentsOfTheFaculty-info-content"]').should('not.exist')
cy.get('[data-cy="studentsOfTheFaculty-open-info"]').click()
cy.get('[data-cy="studentsOfTheFaculty-info-content"]').should('be.visible')
cy.get('[data-cy="studentsOfTheFaculty-close-info"]').click()
cy.get('[data-cy="studentsOfTheFaculty-info-content"]').should('not.exist')
})
it('Data can be exported to Excel files', () => {
it('Data can be exported to Excel files', { retries: 3 }, () => {
const sections = [
'StudentsOfTheFaculty',
'GraduatedOfTheFaculty',
@@ -91,7 +91,7 @@ describe('Faculty overview', () => {
]
sections.forEach(section => {
cy.get(`[data-cy="Graph-${section}"]`).within(() => {
cy.get('[aria-label="View chart menu, Chart"]').click()
cy.get('[aria-label="View chart menu, Chart"]').click({ force: true })
cy.contains('li.highcharts-menu-item', 'Download XLS').click()
const downloadedFile = `oodikone_${section}_H50_${timestamp}.xls`
cy.readFile(path.join(downloadsFolder, downloadedFile))
@@ -234,7 +234,7 @@ describe('Faculty overview', () => {
cy.get('[data-cy="Section-bachelor"]').within(() => {
cy.get('div[class="faculty-graph"]')
cy.contains('24 graduated (64.9 % of class)').trigger('mouseover')
cy.contains('24 graduated').trigger('mouseover')
cy.contains('From class of 2020 - 2021, 24/37 students have graduated')
})
+26 -18
View File
@@ -18,7 +18,6 @@ const clearSingleDropdownSelection = dataCyAttribute => {
}
const testRangeFilter = (parentEl, min, max, expected) => {
cy.clock(MOCKED_DATE, ['Date'])
cy.cs(parentEl)
.cs('range-selector-min')
.find('input')
@@ -29,13 +28,17 @@ const testRangeFilter = (parentEl, min, max, expected) => {
.find('input')
.invoke('val')
.then(initialMax => {
cy.cs(parentEl).cs('range-selector-min').find('input').type(`{selectall}{backspace}${min}`).blur()
cy.cs(parentEl).cs('range-selector-max').find('input').type(`{selectall}{backspace}${max}`).blur()
cy.cs(parentEl).cs('range-selector-min').find('input').clear()
cy.cs(parentEl).cs('range-selector-min').find('input').type(min)
cy.cs(parentEl).cs('range-selector-max').find('input').clear()
cy.cs(parentEl).cs('range-selector-max').find('input').type(max)
checkFilteringResult(expected)
cy.cs(parentEl).cs('range-selector-min').find('input').type(`{selectall}{backspace}${initialMin}`).blur()
cy.cs(parentEl).cs('range-selector-max').find('input').type(`{selectall}{backspace}${initialMax}`).blur()
cy.cs(parentEl).cs('range-selector-min').find('input').clear()
cy.cs(parentEl).cs('range-selector-min').find('input').type(initialMin)
cy.cs(parentEl).cs('range-selector-max').find('input').clear()
cy.cs(parentEl).cs('range-selector-max').find('input').type(initialMax)
})
})
}
@@ -83,27 +86,27 @@ describe("Population statistics with a master's programme", () => {
})
})
describe('Studyright type filter', () => {
describe('Study right type filter', () => {
it('is visible', () => {
cy.contains('Studyright type')
cy.contains('Study right type')
})
it('is set to all by default', () => {
runTestStepWithPreAndPostParts('studyright-type', () => {
const card = cy.cs('studyright-type-filter-card')
runTestStepWithPreAndPostParts('studyRightTypeFilter', () => {
const card = cy.cs('studyRightTypeFilter-filter-card')
card.get('[data-cy="all"] input').should('be.checked')
})
})
it('works', () => {
runTestStepWithPreAndPostParts('studyright-type', () => {
runTestStepWithPreAndPostParts('studyRightTypeFilter', () => {
const expectedBaMaStudents = 24
const expectedMasterOnlyStudents = 2
cy.cs('studyright-type-filter-card').cs('bama').click()
cy.cs('studyRightTypeFilter-filter-card').cs('bama').click()
checkFilteringResult(expectedBaMaStudents)
cy.cs('studyright-type-filter-card').cs('master').click()
cy.cs('studyRightTypeFilter-filter-card').cs('master').click()
checkFilteringResult(expectedMasterOnlyStudents)
cy.cs('studyright-type-filter-card').cs('all').click()
cy.cs('studyRightTypeFilter-filter-card').cs('all').click()
checkFilteringResult(defaultAmountOfStudents)
})
})
@@ -169,6 +172,7 @@ describe('Population Statistics', () => {
})
it('Age filter works', { retries: 3 }, () => {
cy.clock(MOCKED_DATE, ['Date'])
runTestStepWithPreAndPostParts('Age', () => {
testRangeFilter('Age-filter-card', 23, 30, 21)
})
@@ -228,6 +232,7 @@ describe('Population Statistics', () => {
})
it('Filter combinations work', () => {
cy.clock(MOCKED_DATE, ['Date'])
runTestStepWithPreAndPostParts('GraduatedFromProgramme', () => {
runTestStepWithPreAndPostParts('Age', () => {
const getCard = () => cy.cs('GraduatedFromProgramme-filter-card')
@@ -249,8 +254,8 @@ describe('Population Statistics', () => {
checkFilteringResult(defaultAmountOfStudents)
})
it('Studyright type filter is not visible', () => {
cy.contains('Studyright type').should('not.exist')
it('Study right type filter is not visible', () => {
cy.contains('Study right type').should('not.exist')
})
})
@@ -284,13 +289,14 @@ describe('Course Statistics', () => {
it('Programme filter works', () => {
runTestStepWithPreAndPostParts('Programme', () => {
const card = cy.cs('Programme-filter-card')
const programmeDropdown = card.cs('Programme-filter-dropdown').selectFromDropdown(2)
checkFilteringResult(30)
const programmeDropdown = card.cs('Programme-filter-dropdown').selectFromDropdown(1)
checkFilteringResult(33)
programmeDropdown.get('i.delete').click()
})
})
it('Age filter works', () => {
it('Age filter works', { retries: 3 }, () => {
cy.clock(MOCKED_DATE, ['Date'])
runTestStepWithPreAndPostParts('Age', () => {
testRangeFilter('Age-filter-card', 21, 30, 36)
})
@@ -322,6 +328,7 @@ describe('Course Statistics', () => {
})
it('Filter combinations work', () => {
cy.clock(MOCKED_DATE, ['Date'])
runTestStepWithPreAndPostParts('Grade', () => {
runTestStepWithPreAndPostParts('Age', () => {
cy.cs('gradeFilter-5').click()
@@ -348,6 +355,7 @@ describe('Custom Population Statistics', () => {
})
it('Age filter works', () => {
cy.clock(MOCKED_DATE, ['Date'])
runTestStepWithPreAndPostParts('Age', () => {
testRangeFilter('Age-filter-card', 24, 28, 5)
})
+10 -9
View File
@@ -16,13 +16,14 @@ const chooseSemester = (semester, fromOrTo) => {
})
}
const checkNumbers = (numbers, numberOfColumns) => {
const checkNumbers = (numbers, numberOfColumns, mode) => {
const offset = mode === 'semesters' ? 2 : 1
cy.get('table > tbody > tr:first').within(() => {
cy.get('td').should('have.length', numberOfColumns)
cy.get('td').eq(0).contains('All courses total')
numbers.forEach((number, index) => {
cy.get('td')
.eq(index + 1)
.eq(index + offset)
.contains(number)
})
})
@@ -33,7 +34,7 @@ const checkStyle = (styles, numberOfColumns) => {
cy.get('td').should('have.length', numberOfColumns)
styles.forEach((style, index) => {
cy.get('td')
.eq(index + 1)
.eq(index + 2)
.should('have.attr', 'style')
.and('include', style)
})
@@ -64,7 +65,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 = [36, 9, 33, 751, 15, 40, 6, 49, 1, 24, 1059, 53, 0, 2076]
const numbers = [36, 9, 34, 755, 15, 40, 6, 48, 1, 24, 1059, 49, 0, 2076]
numbers.forEach((number, index) => {
cy.get('td')
.eq(index + 1)
@@ -75,12 +76,12 @@ describe('When language center is opened', () => {
it('Faculties tab "exceeding" button works', () => {
cy.get('[data-cy="difference-button"]').click()
checkNumbers([...new Array(12).fill(0), 1, 218], 15)
checkNumbers([...new Array(12).fill(0), 1, 227], 15)
})
it('Faculties tab semester selector changes numbers', () => {
chooseSemester('Syksy 2020', 'from')
checkNumbers([28, 6, 27, 491, 4, 10, 2, 34, 0, 9, 535, 38, 0, 1184], 15)
checkNumbers([28, 6, 28, 495, 4, 10, 2, 33, 0, 9, 535, 34, 0, 1184], 15)
})
})
@@ -92,7 +93,7 @@ describe('When language center is opened', () => {
})
it('Semester tab shows numbers', () => {
checkNumbers([69, 26, 298, 58, 343, 98, 438, 138, 310, 90, 123, 74, 10, 1, 2076], 16)
checkNumbers([69, 26, 298, 58, 343, 98, 438, 138, 310, 90, 123, 74, 10, 1, 2076], 17, 'semesters')
})
it('Coloring mode works on semester tab', () => {
@@ -101,10 +102,10 @@ describe('When language center is opened', () => {
checkStyle(
expectedAlphas.map(alpha => `background-color: rgba(0, 170, 0, ${alpha})`),
16
17
)
cy.get('table > tbody > tr:first').within(() => {
cy.get('td').eq(15).should('have.attr', 'style').and('not.include', 'background-color')
cy.get('td').eq(16).should('have.attr', 'style').and('not.include', 'background-color')
})
})
})
+116 -56
View File
@@ -1,65 +1,82 @@
/// <reference types="cypress" />
// Now "Class statistics" in UI
describe('Population Statistics tests', () => {
const pathToMathBSc2020 =
'/populations?months=49&semesters=FALL&semesters=SPRING&studyRights=%7B"programme"%3A"KH50_001"%7D&year=2020'
const pathToMathMSc2020 =
'/populations?months=49&semesters=FALL&semesters=SPRING&studyRights=%7B"programme"%3A"MH50_001"%7D&year=2020'
describe('when using basic user', () => {
const selectStudyProgramme = programme => {
cy.contains('Select study programme')
cy.get('[data-cy=select-study-programme]').click().children().contains(programme).click()
}
const selectStudyTrack = studyTrack => {
cy.contains('Select study track')
cy.get('[data-cy=select-study-track]').click().children().contains(studyTrack).click()
}
describe('Population statistics tests', () => {
const getPath = programme => {
return `/populations?months=49&semesters=FALL&semesters=SPRING&studyRights=%7B"programme"%3A"${programme}"%7D&year=2020`
}
const pathToMathBSc2020 = getPath('KH50_001')
const pathToMathMSc2020 = getPath('MH50_001')
describe('When using basic user', () => {
beforeEach(() => {
cy.init('/populations')
})
it('Population statistics search infobox works', () => {
cy.get('[data-cy="PopulationSearch-info-content"]').should('not.exist')
cy.get('[data-cy="PopulationSearch-open-info"]').click()
cy.get('[data-cy="PopulationSearch-info-content"]').should('be.visible')
cy.get('[data-cy="PopulationSearch-info-content"]').should(
'contain',
'lukuvuosi, jolloin opiskelija on ilmoittautunut'
)
cy.get('[data-cy="PopulationSearch-close-info"]').click()
cy.get('[data-cy="PopulationSearch-info-content"]').should('not.exist')
})
describe('Population search', () => {
it('Info box works', () => {
cy.get('[data-cy="PopulationSearch-info-content"]').should('not.exist')
cy.get('[data-cy="PopulationSearch-open-info"]').click()
cy.get('[data-cy="PopulationSearch-info-content"]').should('be.visible')
cy.get('[data-cy="PopulationSearch-info-content"]').should('contain', 'Tässä osiossa voi tarkastella')
cy.get('[data-cy="PopulationSearch-close-info"]').click()
cy.get('[data-cy="PopulationSearch-info-content"]').should('not.exist')
})
it('Population statistics search form is usable', () => {
cy.contains('See class').should('be.disabled')
cy.contains('Search for class')
cy.contains('Class of')
.parent()
.within(() => {
cy.get('.form-control').as('enrollmentSelect')
it('Form is usable', () => {
cy.contains('Search for class')
cy.contains('See class').should('be.disabled')
cy.contains('Class of')
.parent()
.within(() => {
cy.get('.form-control').as('enrollmentSelect')
})
cy.get('@enrollmentSelect')
.its(`${[0]}.value`)
.then(beforeVal => {
cy.get('@enrollmentSelect').click()
cy.get('.yearSelectInput .rdtPrev').click({ force: true })
cy.get('.yearSelectInput table').contains('2018-2019').click({ force: true })
cy.get('@enrollmentSelect').should('not.have.value', beforeVal)
})
cy.contains('No study tracks available for this programme')
selectStudyProgramme('Matematiikan ja tilastotieteen maisteriohjelma')
cy.contains('See class').should('be.enabled')
selectStudyTrack('Matematiikka ja soveltava matematiikka')
cy.contains('See class').should('be.enabled')
})
describe('Correct population is shown for programme', () => {
it('without study tracks', () => {
cy.contains('Select study programme')
selectStudyProgramme('Matemaattisten tieteiden kandiohjelma')
cy.contains('See class').click()
cy.contains('Matemaattisten tieteiden kandiohjelma 2017 - 2018')
cy.contains('class size 47 students')
})
cy.get('@enrollmentSelect')
.its(`${[0]}.value`)
.then(beforeVal => {
cy.get('@enrollmentSelect').click()
cy.get('.yearSelectInput .rdtPrev').click({ force: true })
cy.get('.yearSelectInput table').contains('2018-2019').click({ force: true })
cy.get('@enrollmentSelect').should('not.have.value', beforeVal)
it('with study tracks', () => {
selectStudyProgramme('Matematiikan ja tilastotieteen maisteriohjelma')
selectStudyTrack('Matematiikka ja soveltava matematiikka')
cy.contains('See class').click()
cy.contains('Matematiikan ja tilastotieteen maisteriohjelma 2017 - 2018')
cy.contains('studytrack MAST-MSM')
cy.contains('class size 1 students')
})
cy.contains('Select study programme')
cy.get('[data-cy=select-study-programme]')
.click()
.children()
.contains('Matematiikan ja tilastotieteen maisteriohjelma')
.click()
cy.contains('See class').should('be.enabled')
})
it('Searching for population really shows population', () => {
cy.contains('Select study programme')
cy.get('[data-cy=select-study-programme]')
.click()
.children()
.contains('Matemaattisten tieteiden kandiohjelma')
.click()
cy.contains('See class').click()
cy.contains('Matemaattisten tieteiden kandiohjelma 2017 - 2018')
cy.contains('class size 47 students')
})
})
it('Population statistics is usable on general level', () => {
@@ -81,18 +98,61 @@ describe('Population Statistics tests', () => {
cy.contains('td', 'MAT11001').siblings().find('i.level.up').click()
cy.wait('@coursePage')
cy.url().should('include', '/coursestatistics')
cy.contains('MAT11001, 57033, AYMAT11001, A57033 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('Courses of class curriculum selection works', () => {
cy.visit(pathToMathBSc2020)
cy.contains('Courses of class').click()
cy.get('[data-cy=curriculum-picker]').contains('2020 - 2023')
cy.get('[data-cy=curriculum-picker]').contains('2020–2023')
cy.get('[data-cy=toggle-group-module-MAT-tyo]')
cy.get('[data-cy=curriculum-picker]').click().contains('2023 - 2026').click()
cy.get('[data-cy=curriculum-picker]').click().contains('2023–2026').click()
cy.get('[data-cy=toggle-group-module-MAT-tyo]').should('not.exist')
})
it('New fetch of courses data is done when curriculum is changed', () => {
cy.visit(pathToMathBSc2020)
cy.contains('Courses of class').click()
cy.intercept('/api/v2/populationstatistics/courses').as('courseData')
cy.get('[data-cy=curriculum-picker]').click().contains('2020–2023').click()
cy.wait('@courseData').then(({ response }) => {
expect(response.body).to.have.property('allStudents')
expect(response.body).to.have.property('coursestatistics')
expect(response.body.allStudents).to.equal(27)
expect(response.body.coursestatistics.some(stat => stat.course.code === 'DIGI-100')).to.equal(true)
})
cy.get('[data-cy=curriculum-picker]').click().contains('2023–2026').click()
cy.wait('@courseData').then(({ response }) => {
expect(response.body).to.have.property('allStudents')
expect(response.body).to.have.property('coursestatistics')
expect(response.body.allStudents).to.equal(27)
expect(response.body.coursestatistics.some(stat => stat.course.code === 'DIGI-100')).to.equal(false)
})
})
it('New fetch of courses data is done when filtered students change', () => {
cy.visit(pathToMathBSc2020)
cy.contains('Courses of class').click()
cy.intercept('/api/v2/populationstatistics/courses').as('courseData')
cy.wait('@courseData').then(({ response }) => {
expect(response.body).to.have.property('allStudents')
expect(response.body).to.have.property('coursestatistics')
expect(response.body.allStudents).to.equal(27)
})
cy.get('[data-cy=GraduatedFromProgramme-filter-card]').within(() => {
cy.get('[data-cy=GraduatedFromProgramme-header]').click()
cy.get('[data-cy=option-graduated-true]').click()
cy.wait('@courseData').then(({ response }) => {
expect(response.body).to.have.property('allStudents')
expect(response.body).to.have.property('coursestatistics')
expect(response.body.allStudents).to.equal(16)
})
})
})
it("Empty 'tags' tab has a link to the page where tags can be created", { retries: 2 }, () => {
cy.visit(pathToMathBSc2020)
cy.contains('Students (27)')
@@ -254,7 +314,7 @@ describe('Population Statistics tests', () => {
})
})
describe('when using admin', () => {
describe('When using admin', () => {
it('Student list checking works as intended', () => {
cy.init(pathToMathBSc2020, 'admin')
const existing = '433237'
@@ -283,7 +343,7 @@ describe('Population Statistics tests', () => {
})
})
describe('when using IAM user', () => {
describe('When using IAM user', () => {
beforeEach(() => {
cy.init(pathToMathBSc2020, 'onlyiamrights')
cy.contains('Matemaattisten tieteiden kandiohjelma 2020 - 2021')
+4 -2
View File
@@ -91,7 +91,9 @@ describe('Students tests', () => {
cy.get('.level').click()
})
cy.url().should('include', '/coursestatistics')
cy.contains('MAT12004, 57046, AYMAT12004 Tilastollinen päättely I')
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', () => {
@@ -247,7 +249,7 @@ 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', '2 Aug 2020')
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()
+1 -1
View File
@@ -132,7 +132,7 @@ describe('Study guidance group tests', () => {
const firstRowHeadings = [
'Student number',
'Credits',
'Start of studyright',
'Start of study right',
'Started in programme',
'Semesters present',
'Graduation date',
+71 -61
View File
@@ -9,21 +9,21 @@ const deleteTag = name => {
cy.contains('td', name).should('not.exist')
}
describe('Studyprogramme overview', () => {
describe('Study programme overview', () => {
/* Basic information overview -tests */
describe('Basic information -view works for basic user', () => {
describe('Basic information view works for basic user', () => {
beforeEach(() => {
cy.init('/study-programme')
cy.contains('a', 'Matemaattisten tieteiden kandiohjelma').click({ force: true })
})
// If the backend breaks for one of the sections, the section header is not rendered and this will fail
it('Basic information -tab loads', () => {
cy.get('[data-cy=Section-StudentsOfTheStudyprogramme]')
cy.get('[data-cy=Section-CreditsProducedByTheStudyprogramme]')
cy.get('[data-cy=Section-GraduatedAndThesisWritersOfTheProgramme]')
cy.get('[data-cy=Section-ProgrammesBeforeOrAfter]')
cy.get('[data-cy=Section-AverageGraduationTimes]')
it('Basic information tab loads', () => {
cy.get('[data-cy=Section-studentsOfTheStudyProgramme]')
cy.get('[data-cy=Section-creditsProducedByTheStudyProgramme]')
cy.get('[data-cy=Section-graduatedAndThesisWritersOfTheProgramme]')
cy.get('[data-cy=Section-programmesBeforeOrAfter]')
cy.get('[data-cy=Section-averageGraduationTimes]')
})
it('Basic information contains correct students', () => {
@@ -40,7 +40,7 @@ describe('Studyprogramme overview', () => {
[2017, 41, 47, 0, 0, 0],
]
cy.checkTableStats(tableContents, 'StudentsOfTheStudyprogramme')
cy.checkTableStats(tableContents, 'StudentsOfTheStudyProgramme')
})
it('Basic information contains correct credits', () => {
@@ -51,12 +51,12 @@ describe('Studyprogramme overview', () => {
[2022, 3235, 3205, 0, 30, 209],
[2021, 5133, 5108, 0, 25, 428],
[2020, 5801, 5796, 0, 5, 94],
[2019, 5295, 5295, 0, 0, 162],
[2019, 5305, 5305, 0, 0, 162],
[2018, 3442, 3432, 0, 10, 21],
[2017, 1211, 1211, 0, 0, 189],
]
cy.checkTableStats(tableContents, 'CreditsProducedByTheStudyprogramme')
cy.checkTableStats(tableContents, 'CreditsProducedByTheStudyProgramme')
})
it("Toggling 'Show special categories' on displays additional information", () => {
@@ -68,12 +68,12 @@ describe('Studyprogramme overview', () => {
[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, 5295, 5295, 0, 0, 0, 0, 162],
[2019, 5305, 5305, 0, 0, 0, 0, 162],
[2018, 3442, 3432, 0, 10, 0, 0, 21],
[2017, 1211, 1211, 0, 0, 0, 0, 189],
]
cy.checkTableStats(tableContents, 'CreditsProducedByTheStudyprogramme')
cy.checkTableStats(tableContents, 'CreditsProducedByTheStudyProgramme')
})
it('Basic information contains correct thesis writers and graduates', () => {
@@ -93,7 +93,7 @@ describe('Studyprogramme overview', () => {
cy.checkTableStats(tableContents, 'GraduatedAndThesisWritersOfTheProgramme')
})
it('Special studyrights can be excluded and basic data changes accordingly', () => {
it('Special study rights can be excluded and basic data changes accordingly', () => {
cy.get('[data-cy=StudentToggle]').click()
const years = getEmptyYears()
const studentTableContents = [
@@ -108,7 +108,7 @@ describe('Studyprogramme overview', () => {
[2017, 41, 47, 0],
]
cy.checkTableStats(studentTableContents, 'StudentsOfTheStudyprogramme')
cy.checkTableStats(studentTableContents, 'StudentsOfTheStudyProgramme')
const graduatedTableContents = [
// [Year, Graduated, Wrote thesis]
@@ -140,7 +140,7 @@ describe('Studyprogramme overview', () => {
['2018 - 2019', 40, 45, 0, 0, 0],
['2017 - 2018', 41, 47, 0, 0, 0],
]
cy.checkTableStats(studentTableContents, 'StudentsOfTheStudyprogramme')
cy.checkTableStats(studentTableContents, 'StudentsOfTheStudyProgramme')
const creditTableContents = [
...years.map(year => [year, 0, 0, 0, 0, 0]),
@@ -149,23 +149,23 @@ describe('Studyprogramme overview', () => {
['2021 - 2022', 4092, 4042, 0, 50, 198],
['2020 - 2021', 5420, 5415, 0, 5, 321],
['2019 - 2020', 6043, 6043, 0, 0, 101],
['2018 - 2019', 4846, 4841, 0, 5, 107],
['2018 - 2019', 4856, 4851, 0, 5, 107],
['2017 - 2018', 2350, 2345, 0, 5, 26],
]
cy.checkTableStats(creditTableContents, 'CreditsProducedByTheStudyprogramme')
cy.checkTableStats(creditTableContents, 'CreditsProducedByTheStudyProgramme')
cy.get('[data-cy=YearToggle]').click()
})
it('Basic information graphs render', () => {
cy.get('[data-cy=Graph-StudentsOfTheStudyprogramme')
cy.get('[data-cy=Graph-StudentsOfTheStudyProgramme')
.should('contain', 'Started studying')
.should('contain', 'Accepted')
.should('contain', 'Graduated')
.should('contain', 'Transferred away')
.should('contain', 'Transferred to')
cy.get('[data-cy=Graph-CreditsProducedByTheStudyprogramme')
cy.get('[data-cy=Graph-CreditsProducedByTheStudyProgramme')
.should('contain', 'Degree students')
.should('contain', 'Transferred')
.should('contain', 5796)
@@ -210,7 +210,7 @@ describe('Studyprogramme overview', () => {
cy.get('[data-cy=GraduationTimeToggle]').click()
cy.get('[data-cy=graduation-times-graphMaster]').within(() => {
cy.contains('Master studyright')
cy.contains('Master study right')
cy.contains('Graduation year')
cy.contains('2021')
cy.contains('2 graduated').trigger('mouseover')
@@ -222,7 +222,7 @@ describe('Studyprogramme overview', () => {
})
cy.get('[data-cy=graduation-times-graphBachelor]').within(() => {
cy.contains('Bachelor + master studyright')
cy.contains('Bachelor + master study right')
cy.contains('Graduation year')
cy.contains('2023')
cy.contains('11 graduated').trigger('mouseover')
@@ -235,22 +235,22 @@ describe('Studyprogramme overview', () => {
})
})
/* Studytrack overview -tests */
describe('Studytrack overview works for basic user', () => {
/* Study track overview tests */
describe('Study track overview works for basic user', () => {
beforeEach(() => {
cy.init('/study-programme')
cy.contains('a', 'Matemaattisten tieteiden kandiohjelma').click()
cy.get('.attached').contains('Studytracks and class statistics').click()
cy.get('.attached').contains('Study tracks and class statistics').click()
})
// If the backend breaks for one of the sections, the section header is not rendered and this will fail
it('Studytracks and class statistics -tab loads', () => {
cy.get('[data-cy=Section-StudytrackOverview]')
cy.get('[data-cy=Section-StudytrackProgress]')
cy.get('[data-cy=Section-AverageGraduationTimesStudytracks]')
it('Study tracks and class statistics -tab loads', () => {
cy.get('[data-cy=Section-studyTrackOverview]')
cy.get('[data-cy=Section-studyTrackProgress]')
cy.get('[data-cy=Section-averageGraduationTimesStudyTracks]')
})
it('Students of the studyprogramme are shown correctly', () => {
it('Students of the study 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, 0],
@@ -263,11 +263,11 @@ describe('Studyprogramme overview', () => {
['Total', 229, 197, 0, 0, 95, 134, 147, 82, 0, 222, 7],
]
cy.checkTableStats(tableContents, 'StudytrackOverview')
cy.checkTableStats(tableContents, 'StudyTrackOverview')
})
it('Years in the students table can be expanded and study track data will be shown', () => {
cy.get('[data-cy=Table-StudytrackOverview]').within(() => {
cy.get('[data-cy=Table-StudyTrackOverview]').within(() => {
cy.get('tbody tr.header-row')
.eq(3)
.within(() => {
@@ -302,7 +302,7 @@ describe('Studyprogramme overview', () => {
})
it('Links to class statistics page work', () => {
cy.get('[data-cy=Table-StudytrackOverview]').within(() => {
cy.get('[data-cy=Table-StudyTrackOverview]').within(() => {
cy.get('tbody tr.header-row')
.eq(1)
.within(() => {
@@ -314,8 +314,8 @@ describe('Studyprogramme overview', () => {
cy.contains('class size 26 students')
})
it('Links to class statistics page with all years combined work', () => {
cy.get('[data-cy=Table-StudytrackOverview]').within(() => {
it('Links to class statistics page with all years combined work', { retries: 2 }, () => {
cy.get('[data-cy=Table-StudyTrackOverview]').within(() => {
cy.get('td.total-row-cell a').click()
})
@@ -324,7 +324,7 @@ describe('Studyprogramme overview', () => {
})
it('Links to class statistics page with study track info included work', () => {
cy.get('[data-cy=Table-StudytrackOverview]').within(() => {
cy.get('[data-cy=Table-StudyTrackOverview]').within(() => {
cy.get('tbody tr.header-row')
.eq(3)
.within(() => {
@@ -357,29 +357,37 @@ describe('Studyprogramme overview', () => {
['Total', 229, 28, 18, 24, 21, 10, 14, 114],
]
cy.checkTableStats(tableContents, 'StudytrackProgress')
cy.checkTableStats(tableContents, 'StudyTrackProgress')
})
it('Studytrack overview graphs render', () => {
cy.get('[data-cy=Graph-StudytrackProgress]')
it('Study track overview graphs render', () => {
cy.get('[data-cy=Graph-StudyTrackProgress]')
.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
cy.get('[data-cy=Graph-StudytrackProgress]').contains('49.8%').trigger('mouseover', { force: true })
cy.get('[data-cy=Graph-StudyTrackProgress]').contains('49.8%').trigger('mouseover', { force: true })
cy.contains('At least 180 credits: 114')
cy.get("[data-cy='graduation-times-graph-breakdownBachelor']").within(() => {
cy.get("[data-cy='Section-KH50_001']").within(() => {
cy.contains('Start year')
cy.contains('2020 - 2021')
cy.get('[aria-label="2020 - 2021, 15. On time."]').trigger('mouseover')
cy.contains('Graduated On time: 15 students')
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.contains("Click a bar to view that year's study track level breakdown").should('not.exist')
cy.contains('Year 2019 - 2020 by start year')
cy.get('[aria-label="MAT-MAT, 14. On time."]').trigger('mouseover')
cy.contains('Matematiikka')
cy.contains('MAT-MAT')
cy.contains('On time: 14')
})
cy.get('[data-cy=GraduationTimeToggle]').click()
cy.get('[data-cy=graduation-times-graphBachelor]').within(() => {
cy.get("[data-cy='Section-KH50_001']").within(() => {
cy.contains('Start year')
cy.contains('2020 - 2021')
cy.contains('19 graduated').trigger('mouseover')
@@ -391,14 +399,16 @@ describe('Studyprogramme overview', () => {
})
})
describe('Studytrack can be changed', () => {
describe('Study track can be changed', () => {
beforeEach(() => {
cy.get('.studytrack-selector').contains('All students of the programme, KH50_001').click()
cy.get('.studytrack-selector .visible.menu').contains('Matematiikka, MAT-MAT').click()
})
it('Students of the study track are shown correctly', () => {
cy.get("[data-cy='Section-StudytrackOverview']").contains('Students of the studytrack MAT-MAT by starting year')
cy.get("[data-cy='Section-studyTrackOverview']").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],
@@ -409,11 +419,11 @@ describe('Studyprogramme overview', () => {
['2017 - 2018', 28, 24, 0, 0, 1, 27, 15, 13, 0, 28, 0],
['Total', 89, 73, 0, 0, 6, 83, 48, 41, 0, 87, 2],
]
cy.checkTableStats(tableContents, 'StudytrackOverview')
cy.checkTableStats(tableContents, 'StudyTrackOverview')
})
it('Links to class statistics page with study track info included work', () => {
cy.get('[data-cy=Table-StudytrackOverview]').within(() => {
cy.get('[data-cy=Table-StudyTrackOverview]').within(() => {
cy.get('tbody tr.regular-row')
.eq(2)
.within(() => {
@@ -428,7 +438,7 @@ describe('Studyprogramme overview', () => {
})
it('Info message about missing progress stats is displayed', () => {
cy.contains('.divider', 'Progress of students of the studytrack MAT-MAT by starting year')
cy.contains('.divider', 'Progress of students of the study track MAT-MAT by starting year')
cy.contains(
'.message',
'Currently progress data is only available for all students of the study programme. Please select ”All students of the programme” to view the progress data.'
@@ -436,7 +446,7 @@ describe('Studyprogramme overview', () => {
})
it('Average graduation times are displayed correctly', () => {
cy.get("[data-cy='Section-AverageGraduationTimesStudytracks']")
cy.get("[data-cy='Section-averageGraduationTimesStudyTracks']")
cy.get("[data-cy='graduation-times-graph-breakdownBachelor']").within(() => {
cy.contains('Start year')
cy.contains('2020 - 2021')
@@ -656,7 +666,7 @@ describe('Studyprogramme overview', () => {
it('content loads', () => {
cy.contains('h3', 'Select curriculum to edit:')
cy.get('[data-cy=curriculum-picker]').contains('2023 - 2026')
cy.get('[data-cy=curriculum-picker]').contains('2023–2026')
cy.contains('Change visibility of degree courses and select criteria for academic years')
cy.contains('form', 'First year (12 months) last set: 0')
cy.get('table').within(() => {
@@ -756,7 +766,7 @@ describe('Studyprogramme overview', () => {
it('can access programme and correct tabs are visible', () => {
cy.contains('Basic information')
cy.contains('Studytracks and class statistics')
cy.contains('Study tracks and class statistics')
cy.contains('Update statistics').should('not.exist')
cy.contains('Degree courses').should('not.exist')
@@ -765,19 +775,19 @@ describe('Studyprogramme overview', () => {
it('can access basic information', () => {
cy.contains('Basic information').click()
cy.get('[data-cy=Section-StudentsOfTheStudyprogramme]')
cy.get('[data-cy=Section-CreditsProducedByTheStudyprogramme]')
cy.get('[data-cy=Section-GraduatedAndThesisWritersOfTheProgramme]')
cy.get('[data-cy=Section-ProgrammesBeforeOrAfter]')
cy.get('[data-cy=Section-AverageGraduationTimes]')
cy.get('[data-cy=Section-studentsOfTheStudyProgramme]')
cy.get('[data-cy=Section-creditsProducedByTheStudyProgramme]')
cy.get('[data-cy=Section-graduatedAndThesisWritersOfTheProgramme]')
cy.get('[data-cy=Section-programmesBeforeOrAfter]')
cy.get('[data-cy=Section-averageGraduationTimes]')
})
it('can access studytracks', () => {
cy.get('.attached').contains('Studytracks and class statistics').click()
it('can access study tracks', () => {
cy.get('.attached').contains('Study tracks and class statistics').click()
cy.get('[data-cy=Section-StudytrackOverview]')
cy.get('[data-cy=Section-StudytrackProgress]')
cy.get('[data-cy=Section-AverageGraduationTimesStudytracks]')
cy.get('[data-cy=Section-studyTrackOverview]')
cy.get('[data-cy=Section-studyTrackProgress]')
cy.get('[data-cy=Section-averageGraduationTimesStudyTracks]')
})
it("doesn't see other tabs", () => {
+51 -114
View File
@@ -1,159 +1,96 @@
/// <reference types="cypress" />
const levels = ['Bachelors', 'BachelorMasters', 'Masters', 'Doctoral']
const progressLevels = ['Bachelors', 'BachelorMasters', 'Masters', 'Doctoral']
const checkProgressGraphs = () => {
levels.forEach(level => cy.get(`[data-cy=Graph-Faculty${level}Progress]`))
const checkProgressBarCharts = () => {
progressLevels.forEach(level => cy.get(`[data-cy=Faculty${level}ProgressBarChart]`))
}
const checkProgressTables = () => {
levels.forEach(level => cy.get(`[data-cy=Table-Faculty${level}Progress]`))
progressLevels.forEach(level => cy.get(`[data-cy=Faculty${level}ProgressTable]`))
}
const checkAverageGraduationTimesGraphs = breakdown => {
const levels = ['bachelor', 'bcMsCombo', 'master', 'doctor']
levels.forEach(level => {
cy.get(`[data-cy=Section-${level}] .graduations-chart-container .faculty-${breakdown ? 'breakdown-' : ''}graph`)
const graduationTimesLevels = ['bachelor', 'master', 'doctor'] // ? Bachelor + master seems to be missing in test data
const checkAverageGraduationTimesBreakdownBarCharts = () => {
graduationTimesLevels.forEach(level => {
cy.get(`[data-cy=${level}BreakdownBarChart]`)
})
}
const checkProgressStatsFacultyLevelBreakdown = () => {
cy.get('[data-cy=Table-FacultyBachelorsProgress]').within(() => {
cy.contains('2021 - 2022').click()
cy.contains('29.5%').trigger('mouseover', { force: true })
cy.contains('Matemaattis-luonnontieteellinen tiedekunta')
cy.contains('H50 - H50')
cy.contains('0 Credits: 0')
cy.contains('1 ≤ Credits < 45: 13')
cy.contains('45 ≤ Credits < 90: 10')
cy.contains('90 ≤ Credits < 135: 16')
cy.contains('135 ≤ Credits < 180: 3')
cy.contains('180 ≤ Credits: 2')
})
}
const checkAverageGraduationTimesFacultyLevelBreakdown = breakdown => {
cy.get('[data-cy=Section-bachelor] .graduations-chart-container').within(() => {
cy.contains('.graduations-message', "Click a bar to view that year's faculty level breakdown")
cy.contains(breakdown ? '51' : '141 graduated').click()
cy.contains('.graduations-message', "Click a bar to view that year's faculty level breakdown").should('not.exist')
cy.contains(`.programmes-${breakdown ? 'breakdown-' : ''}graph`, 'Year 2021 by graduation year')
const checkAverageGraduationTimesMedianBarCharts = () => {
graduationTimesLevels.forEach(level => {
cy.get(`[data-cy=${level}MedianBarChart]`)
})
}
describe('University view', () => {
beforeEach(() => {
cy.init('/university')
cy.contains('University-level view')
cy.contains('University')
cy.contains('Programme MH90_001 (Veterinary medicine bachelor + licentiate) is currently excluded.')
})
describe('Progress stats section', () => {
it('has all the correct progress graphs', () => {
checkProgressGraphs()
describe('Faculty progress tab', () => {
it('contains all the correct progress bar charts', () => {
checkProgressBarCharts()
})
it('has all the correct progress tables', () => {
it('contains all the correct progress tables', () => {
checkProgressTables()
})
it("'Graduated included/excluded' toggle works", () => {
it("'Graduated included / Graduated excluded' toggle works", () => {
cy.get('[data-cy=GraduatedToggle]').click()
checkProgressGraphs()
checkProgressBarCharts()
checkProgressTables()
})
it("'All studyrights/Special studyrights excluded' toggle works", () => {
cy.get('[data-cy=StudentToggle]').click()
checkProgressGraphs()
it("'All study rights / Special study rights excluded' toggle works", () => {
cy.get('[data-cy=StudyRightToggle]').click()
checkProgressBarCharts()
checkProgressTables()
})
it('Years in the tables can be clicked to show faculty level breakdown', () => {
// Filtering out special study rights to make the data match with the Evaluation overview page
cy.get('[data-cy=StudentToggle]').click()
checkProgressStatsFacultyLevelBreakdown()
it('years in the tables can be clicked to show faculty level breakdown', () => {
cy.get('[data-cy=StudyRightToggle]').click()
cy.get('[data-cy=FacultyBachelorsProgressTableShowButton3]').click()
cy.contains('29.5%').trigger('mouseover', { force: true })
cy.contains('Matemaattis-luonnontieteellinen tiedekunta')
cy.contains('H50')
cy.contains('0 Credits: 0')
cy.contains('1 ≤ Credits < 45: 13')
cy.contains('45 ≤ Credits < 90: 10')
cy.contains('90 ≤ Credits < 135: 16')
cy.contains('135 ≤ Credits < 180: 3')
cy.contains('180 ≤ Credits: 2')
})
it('info boxes contain correct information', () => {
cy.get('[data-cy=FacultyProgressInfoBoxButton]').click()
cy.get('[data-cy=FacultyProgressInfoBoxContent]').contains('Kuvaa tiedekuntaan kuuluvien')
cy.get('[data-cy=FacultyBachelorMastersProgressInfoBoxButton]').click()
cy.get('[data-cy=FacultyBachelorMastersProgressInfoBoxContent]').contains('The starting year is the')
})
})
describe('Average graduation times section', () => {
it('graphs exist', () => {
checkAverageGraduationTimesGraphs(true)
describe('Faculty graduations tab', () => {
beforeEach(() => {
cy.get('[data-cy=FacultyGraduationsTab]').click()
})
it('Faculty level breakdown can be seen by clicking a bar in the breakdown graph', () => {
checkAverageGraduationTimesFacultyLevelBreakdown(true)
it('has all the correct median time bar charts', () => {
checkAverageGraduationTimesBreakdownBarCharts()
})
it("'Breakdown/Median times' toggle works", () => {
cy.get('[data-cy=GraduationTimeToggle]').click()
checkAverageGraduationTimesGraphs(false)
checkAverageGraduationTimesMedianBarCharts()
})
it('Faculty level breakdown can be seen by clicking a bar in the median times graph', () => {
cy.get('[data-cy=GraduationTimeToggle]').click()
checkAverageGraduationTimesFacultyLevelBreakdown(false)
})
})
})
// It's better to have these tests defined here than in the 'Evaluation overview' tests as they use the same data as the 'University view' tests
describe("'Evaluation overview' university page", () => {
beforeEach(() => {
cy.init('/evaluationoverview/university')
cy.contains('University-level view')
})
it('page opens', () => {
cy.contains("This view is a combined version of Oodikone's Faculty Evaluation Overview")
})
it('has links to faculty evaluation overview pages', () => {
cy.get('.facultyLinkBox span').should('have.length', 12)
cy.contains('.facultyLinkBox span a', 'H50 Matemaattis-luonnontieteellinen tiedekunta').click()
cy.url().should('include', '/evaluationoverview/faculty/H50')
})
describe('Progress stats section', () => {
it('has all the correct progress graphs', () => {
checkProgressGraphs()
})
it('has all the correct progress tables', () => {
checkProgressTables()
})
it("'Graduated included/excluded' toggle works", () => {
cy.get('[data-cy=GraduatedToggle]').click()
checkProgressGraphs()
checkProgressTables()
})
it("'All studyrights/Special studyrights excluded' toggle doesn't exist", () => {
cy.get('.toggle-container [data-cy=StudentToggle]').should('not.exist')
})
it('Years in the tables can be clicked to show faculty level breakdown', () => {
checkProgressStatsFacultyLevelBreakdown()
})
})
describe('Average graduation times section', () => {
it('graphs exist', () => {
checkAverageGraduationTimesGraphs(true)
})
it('Faculty level breakdown can be seen by clicking a bar in the breakdown graph', () => {
checkAverageGraduationTimesFacultyLevelBreakdown(true)
})
it("'Breakdown/Median times' toggle works", () => {
cy.get('[data-cy=GraduationTimeToggle]').click()
checkAverageGraduationTimesGraphs(false)
})
it('Faculty level breakdown can be seen by clicking a bar in the median times graph', () => {
cy.get('[data-cy=GraduationTimeToggle]').click()
checkAverageGraduationTimesFacultyLevelBreakdown(false)
it('info boxes contain correct information', () => {
cy.get('[data-cy=AverageGraduationTimesInfoBoxButton]').click()
cy.get('[data-cy=AverageGraduationTimesInfoBoxContent]').contains('Opiskelijoiden keskimääräiset valmistumisajat')
})
})
})
+59 -41
View File
@@ -1,47 +1,60 @@
/// <reference types="cypress" />
const visibleLinks = {
norights: ['University', 'Faculties', 'Special populations', 'Give feedback', 'Logout'],
onlycoursestatistics: ['University', 'Courses', 'Special populations', 'Give feedback', 'Logout'],
norights: ['University', 'Faculties', 'Special populations', 'Feedback'],
onlycoursestatistics: ['University', 'Courses', 'Special populations', 'Feedback'],
}
visibleLinks.basic = [...visibleLinks.onlycoursestatistics, 'Faculties', 'Programmes', 'Students']
visibleLinks.admin = [...visibleLinks.basic, 'Teachers', 'Users', 'Updater']
visibleLinks.admin = [...visibleLinks.basic, 'Teachers', 'Admin']
const hasVisibleItems = visibleItems => {
cy.get('[data-cy=nav-bar]').children().should('have.length', visibleItems)
}
const containsLinks = links => {
cy.get('[data-cy=nav-bar]').within(() => {
for (const link of links) {
cy.contains(link)
}
})
}
const userButtonWorks = (username, mocking = false) => {
cy.get('[data-cy=nav-bar-user-button]').click()
cy.contains(mocking ? `Mocking as ${username}` : `Logged in as ${username}`)
cy.contains('Language')
cy.contains('suomi')
cy.contains('English')
cy.contains('svenska')
cy.contains(mocking ? 'Stop mocking' : 'Log out')
}
describe('Users tests', () => {
describe('Using user with just grp-oodikone-user, no other rights', () => {
describe('Using as user with just grp-oodikone-user, no other rights', () => {
it('shows correct tabs', () => {
cy.init('', 'norights')
cy.get('[data-cy=navBar]').children().should('have.length', 7)
cy.get('[data-cy=navBar]').within(() => {
for (const link of visibleLinks.norights) {
cy.contains(link)
}
})
hasVisibleItems(7)
containsLinks(visibleLinks.norights)
userButtonWorks('norights')
})
})
describe('Using as coursestatistics user', () => {
it('shows correct tabs', () => {
cy.init('', 'onlycoursestatistics')
cy.get('[data-cy=navBar]').children().should('have.length', 7)
cy.get('[data-cy=navBar]').within(() => {
for (const link of visibleLinks.onlycoursestatistics) {
cy.contains(link)
}
})
hasVisibleItems(7)
containsLinks(visibleLinks.onlycoursestatistics)
userButtonWorks('onlycoursestatistics')
})
})
describe('Using as basic user', () => {
it('shows correct tabs', () => {
cy.init('')
cy.get('[data-cy=navBar]').children().should('have.length', 10)
cy.get('[data-cy=navBar]').within(() => {
for (const link of visibleLinks.basic) {
cy.contains(link)
}
})
hasVisibleItems(10)
containsLinks(visibleLinks.basic)
userButtonWorks('basic')
})
})
@@ -51,27 +64,32 @@ describe('Users tests', () => {
})
it('should see more stuff than others', () => {
cy.get('[data-cy=navBar]').children().should('have.length', 13)
cy.get('[data-cy=navBar]').within(() => {
for (const link of visibleLinks.admin) {
cy.contains(link)
}
})
hasVisibleItems(13)
containsLinks(visibleLinks.admin)
userButtonWorks('admin')
})
it("mocking normal user shows only the mocked user's programmes", () => {
cy.contains('mocking').should('not.exist')
cy.cs('user-edit-button-basic').click()
cy.get('i.spy').click()
cy.contains('mocking as basic')
cy.contains('Programmes').click().siblings().contains('Class statistics').click()
cy.contains('label', 'Study programme')
.siblings()
.within(() => {
cy.get("div[role='option']").should('have.length', 2)
cy.get("div[role='option']").eq(0).contains('Matemaattisten tieteiden kandiohjelma')
cy.get("div[role='option']").eq(1).contains('Matematiikan ja tilastotieteen maisteriohjelma')
})
describe('can mock as other users', () => {
beforeEach(() => {
cy.cs('user-edit-button-basic').click()
cy.get('i.spy').click()
})
it('user button shows mocked user', () => {
userButtonWorks('basic', true)
})
it("only the mocked user's programmes are visible", () => {
cy.get('[data-cy=nav-bar-button-studyProgramme]').click()
cy.get('[data-cy=nav-bar-button-class]').click()
cy.contains('label', 'Study programme')
.siblings()
.within(() => {
cy.get("div[role='option']").should('have.length', 2)
cy.get("div[role='option']").eq(0).contains('Matemaattisten tieteiden kandiohjelma')
cy.get("div[role='option']").eq(1).contains('Matematiikan ja tilastotieteen maisteriohjelma')
})
})
})
})
})
+1 -1
View File
@@ -15,7 +15,7 @@
/**
* @type {Cypress.PluginConfig}
*/
// eslint-disable-next-line no-unused-vars
// eslint-disable-next-line @typescript-eslint/no-unused-vars
module.exports = (on, config) => {
// `on` is used to hook into various events Cypress emits
// `config` is the resolved Cypress config
+3 -7
View File
@@ -17,12 +17,8 @@
require('@cypress/code-coverage/support')
require('./commands')
// This error is benign and seems to be only present in Cypress
const resizeObserverLoopErrRe = /^[^(ResizeObserver loop limit exceeded)]/
Cypress.on('uncaught:exception', err => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
Cypress.on('uncaught:exception', error => {
/* returning false here prevents Cypress from failing the test */
if (resizeObserverLoopErrRe.test(err.message)) {
return false
}
return true
})
+1 -1
View File
@@ -46,7 +46,7 @@ services:
<<: *defaults
command: >
sh -c 'redis-server --appendonly yes && redis-cli flushall'
image: redis:6.2.14
image: redis:6.2.16
ports:
- 6379:6379
+1 -1
View File
@@ -85,7 +85,7 @@ services:
<<: *defaults
command: 'redis-server --appendonly yes'
container_name: redis
image: redis:6.2.14
image: redis:6.2.16
ports:
- '6379:6379'
volumes:
+1 -1
View File
@@ -76,7 +76,7 @@ services:
<<: *defaults
command: 'redis-server --appendonly yes'
container_name: redis
image: redis:6.2.14
image: redis:6.2.16
ports:
- '6379:6379'
volumes:
+9 -4
View File
@@ -13,7 +13,7 @@ Roles are defined [here](../services/backend/src/config/roles.ts). Criteria for
- studyGuidanceGroups
- teachers
The roles `openUniSearch`, `teachers` and `studyGuidanceGroups` simply enable certain views (_Open uni student population_, _Teachers_ and _Guidance groups_ respectively).
The roles `openUniSearch` and `studyGuidanceGroups` simply enable certain views (_Open uni student population_ and _Guidance groups_ respectively).
### courseStatistics
@@ -23,9 +23,15 @@ The roles `openUniSearch`, `teachers` and `studyGuidanceGroups` simply enable ce
- Can see statistics of every course and year
### katselemusViewer
### katselmusViewer
- Enables the _Evaluation overview_ view
- Enables the _Evaluation overview_ view (deprecated)
### teachers
- By default, the role only allows seeing the statistics of course providers (study programmes) they have full rights to (see [Programmes](#programmes))
- This role always needs to be given and removed manually
- If the user is a member of the `hy-dekaanit` or `hy-varadekaanit-opetus` IAM group (or is an admin), they can see leaderboards, information about individual teachers, and statistics of all course providers. They don't need to be given the `teachers` role manually.
### Other roles
@@ -67,7 +73,6 @@ A user must be a member of either the `grp-oodikone-basic-users` or `grp-oodikon
| grp-oodikone-users | facultyStatistics |
| grp-toska | admin |
| grp-katselmus-\* | katselmusViewer |
| hy-one | teachers |
| hy-ypa-opa-dojo | openUniSearch |
`grp-oodikone-basic-users`: basically the whole staff (hy-\*-allstaff)
Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

+2
View File
@@ -42,6 +42,8 @@ A collection of useful tools, documentation and other resources.
## User interface
- [Material UI documentation](https://mui.com/material-ui/getting-started/)
- [Material Icons](https://mui.com/material-ui/material-icons/)
- [Semantic UI React documentation](https://react.semantic-ui.com)
<!--
+177
View File
@@ -0,0 +1,177 @@
import pluginJs from '@eslint/js'
import globals from 'globals'
import pluginCypress from 'eslint-plugin-cypress/flat'
import pluginImport from 'eslint-plugin-import'
import pluginPrettierRecommended from 'eslint-plugin-prettier/recommended'
import pluginReact from 'eslint-plugin-react'
import pluginReactHooks from 'eslint-plugin-react-hooks'
import tsEslint from 'typescript-eslint'
/** @type {import('eslint').Linter.Config[]} */
export default [
{
files: ['**/*.{js,ts,jsx,tsx}'],
},
{
ignores: ['eslint.config.mjs', '**/build/', '**/dist/', '**/node_modules/'],
},
{
languageOptions: {
globals: {
...globals.browser,
...globals.node,
},
},
},
{
linterOptions: {
reportUnusedDisableDirectives: 'error',
},
},
{
files: ['**/*.{ts,tsx}'],
languageOptions: {
parserOptions: {
project: [
'services/frontend/tsconfig.json',
'services/frontend/tsconfig.node.json',
'services/backend/tsconfig.json',
'services/shared/tsconfig.json',
],
},
},
},
pluginJs.configs.recommended,
...tsEslint.configs.recommendedTypeChecked,
...tsEslint.configs.stylisticTypeChecked,
pluginCypress.configs.recommended,
pluginReact.configs.flat.recommended,
pluginReact.configs.flat['jsx-runtime'],
pluginImport.flatConfigs.recommended,
pluginImport.flatConfigs.typescript,
pluginPrettierRecommended, // This must be the last plugin so it can override other configs
{
plugins: {
'react-hooks': pluginReactHooks,
},
},
{
rules: {
camelcase: 'off', // TODO: Enable this rule (at least for frontend)
'class-methods-use-this': 'error',
'consistent-return': 'error',
'id-denylist': ['error', 'c', 'd', 'e', 'err', 't'],
'import/no-commonjs': 'error',
'import/no-default-export': 'error',
'import/no-extraneous-dependencies': ['error', { devDependencies: false }],
'import/no-unused-modules': ['error', { missingExports: true, unusedExports: true }],
'import/order': [
'error',
{
alphabetize: { order: 'asc', caseInsensitive: true },
groups: [['builtin', 'external'], ['internal'], ['parent'], ['sibling', 'index']],
},
],
'no-alert': 'error',
'no-async-promise-executor': 'error',
'no-await-in-loop': 'error',
'no-console': 'error',
'no-implied-eval': 'error',
'no-param-reassign': ['error', { props: false }],
'no-promise-executor-return': 'error',
'no-return-assign': 'error',
'no-return-await': 'error',
'object-shorthand': ['error', 'always'],
'prefer-const': 'error',
'prefer-destructuring': ['error', { VariableDeclarator: { object: true } }],
quotes: ['error', 'single', { avoidEscape: true, allowTemplateLiterals: false }],
'react/display-name': 'off', // TODO: Delete this override
'react/function-component-definition': [
'error',
{ namedComponents: 'arrow-function', unnamedComponents: 'arrow-function' },
],
'react/jsx-filename-extension': ['error', { allow: 'as-needed', extensions: ['.jsx', '.tsx'] }],
'react/jsx-sort-props': ['error', { reservedFirst: false }],
'react/no-array-index-key': 'error',
'react/no-this-in-sfc': 'error',
'react/no-unescaped-entities': 'off',
'react/no-unknown-property': 'off', // TODO: Delete this override
'react/prefer-stateless-function': 'error',
'react/prop-types': 'off',
...pluginReactHooks.configs.recommended.rules,
'react-hooks/exhaustive-deps': 'off', // TODO: Delete this override
'@typescript-eslint/array-type': 'off',
'@typescript-eslint/consistent-indexed-object-style': 'off',
'@typescript-eslint/consistent-type-definitions': 'off',
'@typescript-eslint/no-empty-function': 'off',
'@typescript-eslint/no-empty-object-type': ['error', { allowInterfaces: 'with-single-extends' }],
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-misused-promises': 'off', // Most of these errors come from Express route handlers that are handled correctly by express-async-errors
'@typescript-eslint/no-unsafe-argument': 'off',
'@typescript-eslint/no-unsafe-assignment': 'off',
'@typescript-eslint/no-unsafe-call': 'off',
'@typescript-eslint/no-unsafe-member-access': 'off',
'@typescript-eslint/no-unsafe-return': 'off',
'@typescript-eslint/no-unused-vars': [
'error',
{ argsIgnorePattern: '^_', caughtErrors: 'none', ignoreRestSiblings: true, varsIgnorePattern: '^_' },
],
'@typescript-eslint/restrict-template-expressions': 'off',
},
},
{
files: ['**/*.{js,jsx}'],
rules: {
...tsEslint.configs.disableTypeChecked.rules,
'dot-notation': 'error',
},
},
{
files: ['services/backend/src/rapodiff/**/*'],
rules: {
'no-console': 'off',
},
},
{
files: ['cypress/**/*.js', 'services/backend/**/*.js', 'updater/**/*.js'],
languageOptions: {
sourceType: 'commonjs',
},
rules: {
'@typescript-eslint/no-require-imports': 'off',
'import/no-commonjs': 'off',
'import/no-unused-modules': 'off',
},
},
{
files: ['services/backend/**/*.{js,ts}', 'updater/**/*.js'],
rules: {
// TODO: Most of these overrides should probably be removed
'consistent-return': 'off',
'import/no-default-export': 'off',
'import/no-unused-modules': 'off',
'no-await-in-loop': 'off',
'no-param-reassign': 'off',
'no-promise-executor-return': 'off',
'no-return-await': 'off',
},
},
{
settings: {
'import/internal-regex': '^@/shared/',
'import/resolver': {
typescript: {
project: [
'services/frontend/tsconfig.json',
'services/frontend/tsconfig.node.json',
'services/backend/tsconfig.json',
'services/shared/tsconfig.json',
],
},
},
react: {
version: '18.3',
},
},
},
]
+878 -849
View File
File diff suppressed because it is too large Load Diff
+17 -19
View File
@@ -7,7 +7,7 @@
"cypress:run": "cypress run --browser chrome",
"instrument": "./scripts/instrument.sh",
"eslint": "eslint",
"lint:all": "eslint --ext .js,.jsx,.ts,.tsx services/ updater/ --report-unused-disable-directives",
"lint:all": "eslint services/ updater/",
"stylelint": "stylelint",
"actionlint": "node-actionlint",
"prettier": "prettier",
@@ -38,29 +38,27 @@
},
"homepage": "https://github.com/UniversityOfHelsinkiCS/oodikone#readme",
"devDependencies": {
"@cypress/code-coverage": "^3.12.39",
"@cypress/code-coverage": "^3.13.6",
"@eslint/js": "^9.14.0",
"@istanbuljs/schema": "^0.1.3",
"@typescript-eslint/eslint-plugin": "^7.16.1",
"@typescript-eslint/parser": "^7.16.1",
"cypress": "^13.2.0",
"eslint": "^8.56.0",
"eslint-config-airbnb": "^19.0.4",
"eslint-config-airbnb-typescript": "^18.0.0",
"cypress": "^13.15.2",
"eslint": "^9.14.0",
"eslint-config-prettier": "^9.1.0",
"eslint-import-resolver-custom-alias": "^1.3.2",
"eslint-plugin-cypress": "^3.0.0",
"eslint-plugin-import": "^2.29.1",
"eslint-plugin-jsx-a11y": "^6.8.0",
"eslint-import-resolver-typescript": "^3.6.3",
"eslint-plugin-cypress": "^4.1.0",
"eslint-plugin-import": "^2.31.0",
"eslint-plugin-prettier": "^5.2.1",
"eslint-plugin-react": "^7.33.2",
"eslint-plugin-react-hooks": "^4.6.0",
"husky": "^9.0.0",
"lint-staged": "^15.0.0",
"eslint-plugin-react": "^7.37.2",
"eslint-plugin-react-hooks": "^5.0.0",
"globals": "^15.12.0",
"husky": "^9.1.6",
"lint-staged": "^15.2.10",
"node-actionlint": "^1.2.2",
"nyc": "^17.0.0",
"nyc": "^17.1.0",
"prettier": "3.3.3",
"stylelint": "^16.2.1",
"stylelint-config-standard": "^36.0.0"
"stylelint": "^16.10.0",
"stylelint-config-standard": "^36.0.1",
"typescript-eslint": "^8.13.0"
},
"dependencies": {
"lodash": "^4.17.21",
+15 -3
View File
@@ -5,18 +5,30 @@
{
"groupName": "all non-major dependencies",
"groupSlug": "all-minor-patch",
"matchPackagePatterns": ["*"],
"matchUpdateTypes": ["minor", "patch"]
"matchUpdateTypes": ["minor", "patch"],
"matchCurrentVersion": "!/^0/",
"automerge": true,
"matchPackageNames": ["*"]
},
{
"matchDatasources": ["docker"],
"matchPackageNames": ["node"],
"versionCompatibility": "^(?<version>[^-]+)(?<compatibility>-.*)?$",
"versioning": "node"
},
{
"matchDatasources": ["docker"],
"matchPackageNames": ["node"],
"matchUpdateTypes": ["minor", "patch"],
"enabled": false
}
],
"rebaseWhen": "never",
"vulnerabilityAlerts": {
"labels": ["security"]
}
},
"addLabels": ["dependencies"],
"rangeStrategy": "bump",
"schedule": ["before 5:00am"],
"timezone": "Europe/Helsinki"
}
+2 -2
View File
@@ -1,4 +1,4 @@
FROM registry.access.redhat.com/ubi9/nodejs-20-minimal AS build
FROM registry.access.redhat.com/ubi9/nodejs-22-minimal AS build
ENV TZ="Europe/Helsinki"
@@ -28,7 +28,7 @@ RUN npm run build
FROM registry.access.redhat.com/ubi9/nodejs-20-minimal
FROM registry.access.redhat.com/ubi9/nodejs-22-minimal
ENV TZ="Europe/Helsinki"
+1 -1
View File
@@ -1,4 +1,4 @@
FROM node:20-alpine
FROM node:22-alpine
ENV TZ="Europe/Helsinki"
+1
View File
@@ -1,3 +1,4 @@
require('./src/util/sentry') // Sentry must be the first import
require('./src/app')
const logger = require('./src/util/logger')
+1490 -362
View File
File diff suppressed because it is too large Load Diff
+19 -20
View File
@@ -5,47 +5,46 @@
"build": "tsc"
},
"dependencies": {
"@sentry/node": "^5.30.0",
"@sentry/tracing": "^6.18.2",
"@sentry/node": "^8.37.1",
"@sentry/profiling-node": "^8.37.1",
"axios": "^0.28.1",
"bluebird": "^3.7.2",
"bullmq": "^5.0.0",
"compression": "^1.7.4",
"bullmq": "^5.25.4",
"compression": "^1.7.5",
"cors": "^2.8.5",
"cron": "^3.0.0",
"express": "^4.19.2",
"cron": "^3.1.9",
"express": "^4.21.1",
"express-async-errors": "^3.1.1",
"lodash": "^4.17.21",
"lru-cache": "^11.0.0",
"lru-cache": "^11.0.2",
"moment": "^2.30.1",
"moment-timezone": "^0.5.37",
"moment-timezone": "^0.5.46",
"morgan": "^1.10.0",
"oodikone-shared": "file:../oodikone-shared",
"papaparse": "^5.4.1",
"pg": "^8.11.5",
"redis": "^3.1.2",
"pg": "^8.13.1",
"redis": "^4.7.0",
"reflect-metadata": "^0.2.2",
"sequelize": "^6.37.3",
"sequelize": "^6.37.5",
"sequelize-typescript": "^2.1.6",
"umzug": "^3.8.1",
"umzug": "^3.8.2",
"unfuck-utf8-headers-middleware": "^1.0.1",
"winston": "^3.8.2",
"winston": "^3.17.0",
"winston-gelf-transporter": "^1.0.2"
},
"devDependencies": {
"@types/compression": "^1.7.5",
"@types/cors": "^2.8.17",
"@types/express": "^4.17.21",
"@types/lodash": "^4.17.6",
"@types/lodash": "^4.17.13",
"@types/morgan": "^1.9.9",
"@types/node": "^20.14.2",
"@types/validator": "^13.12.0",
"tsx": "^4.16.2",
"typescript": "^5.5.3"
"@types/node": "^22.9.0",
"@types/validator": "^13.12.2",
"tsx": "^4.19.2",
"typescript": "^5.6.3"
},
"overrides": {
"winston-gelf-transporter": {
"gelf-pro": "^1.3.4"
"gelf-pro": "^1.3.12"
}
}
}
-7
View File
@@ -3,10 +3,6 @@ const isDev = process.env.NODE_ENV === 'development'
const isStaging = process.env.REACT_APP_STAGING === 'true'
const isProduction = !isStaging && process.env.NODE_ENV === 'production'
// Sentry
const sentryRelease = process.env.SENTRY_RELEASE || ''
const sentryEnvironment = process.env.SENTRY_ENVIRONMENT || ''
const sentryDSN = process.env.SENTRY_DSN || ''
const runningInCI = process.env.CI === 'true'
// IAM group
@@ -84,9 +80,6 @@ module.exports = {
SIS_DB_URL,
SIS_UPDATER_URL,
runningInCI,
sentryRelease,
sentryEnvironment,
sentryDSN,
isProduction,
pateToken,
jamiUrl,
@@ -1,4 +1,4 @@
// Linter was not having any of this so neeeded to add resolveJsonModule:true to tsconfig
// Linter was not having any of this so needed to add resolveJsonModule:true to tsconfig
// export const { facultyCodes, ignoredFacultyCodes, magicFacultyCode } = require('../environment/organizationConfig.json')
export { facultyCodes, ignoredFacultyCodes, magicFacultyCode } from '../environment/organizationConfig.json'
+1 -1
View File
@@ -118,7 +118,7 @@ const sequelizeKone = new Sequelize(conf.DB_URL_KONE!, {
],
})
sequelizeKone.query(`SET SESSION search_path to ${conf.DB_SCHEMA_KONE}`)
void sequelizeKone.query(`SET SESSION search_path to ${conf.DB_SCHEMA_KONE}`)
const sequelizeUser = new Sequelize(conf.DB_URL_USER!, {
logging: false,
+11 -8
View File
@@ -8,8 +8,8 @@ import { computeLanguageCenterData, LANGUAGE_CENTER_REDIS_KEY } from './services
import { findStudentsCloseToGraduation, CLOSE_TO_GRADUATION_REDIS_KEY } from './services/populations/closeToGraduation'
import { redisClient } from './services/redis'
import { getCurrentSemester } from './services/semesters'
import { combinedStudyprogrammes, isRelevantProgramme } from './services/studyProgramme/studyProgrammeHelpers'
import { updateBasicView, updateStudytrackView } from './services/studyProgramme/studyProgrammeUpdates'
import { combinedStudyProgrammes, isRelevantProgramme } from './services/studyProgramme/studyProgrammeHelpers'
import { updateBasicView, updateStudyTrackView } from './services/studyProgramme/studyProgrammeUpdates'
import { findAndSaveTeachers } from './services/teachers/top'
import { deleteOutdatedUsers } from './services/userService'
import logger from './util/logger'
@@ -25,7 +25,10 @@ const schedule = (cronTime: string, onTick: () => void) => {
export const refreshCloseToGraduating = async () => {
logger.info('Refreshing students close to graduating')
const updatedData = await findStudentsCloseToGraduation()
await redisClient.setAsync(CLOSE_TO_GRADUATION_REDIS_KEY, JSON.stringify(updatedData))
await redisClient.set(
CLOSE_TO_GRADUATION_REDIS_KEY,
JSON.stringify({ ...updatedData, lastUpdated: new Date().toISOString() })
)
logger.info('Students close to graduating updated!')
}
@@ -45,7 +48,7 @@ export const refreshFaculties = async () => {
export const refreshLanguageCenterData = async () => {
logger.info('Refreshing language center data')
const freshData = await computeLanguageCenterData()
await redisClient.setAsync(LANGUAGE_CENTER_REDIS_KEY, JSON.stringify(freshData))
await redisClient.set(LANGUAGE_CENTER_REDIS_KEY, JSON.stringify(freshData))
logger.info('Language center data refreshed!')
}
@@ -61,10 +64,10 @@ const refreshProgrammesAndFaculties = async () => {
export const refreshProgramme = async (code: string) => {
await updateBasicView(code, '')
await updateStudytrackView(code, '')
const combinedProgramme = combinedStudyprogrammes[code] || ''
await updateStudyTrackView(code, '')
const combinedProgramme = combinedStudyProgrammes[code] || ''
await updateBasicView(code, combinedProgramme)
await updateStudytrackView(code, combinedProgramme)
await updateStudyTrackView(code, combinedProgramme)
}
export const refreshProgrammes = async () => {
@@ -83,7 +86,7 @@ export const refreshProgrammes = async () => {
}
}
const refreshTeacherLeaderboard = async () => {
export const refreshTeacherLeaderboard = async () => {
logger.info('Refreshing statistics for teacher leaderboard')
const currentSemestersYearCode = (await getCurrentSemester()).getDataValue('yearcode')
await findAndSaveTeachers(currentSemestersYearCode, currentSemestersYearCode - 1)
@@ -40,15 +40,15 @@ const accessLogger = morgan((tokens, req: Request, res: Response): undefined =>
const message = [
`${user.mockedBy ? '(mocking) ' : ''}${user.name}`,
`${(tokens['response-time'](req, res) || '0').split('.')[0]} ms`.padEnd(8, ' '),
`${(tokens['response-time'](req, res) ?? '0').split('.')[0]} ms`.padEnd(8, ' '),
tokens.status(req, res),
tokens.method(req, res),
decodeURIComponent(tokens.url(req, res) as string),
decodeURIComponent(tokens.url(req, res)!),
].join(' ')
const fullStudyProgrammeRights = getFullStudyProgrammeRights(user.programmeRights)
const iamBasedRights = user.programmeRights.filter(right => right.isIamBased)
const usingIamRights = iamBasedRights.some(right => (tokens.url(req, res) as string).includes(right.code))
const usingIamRights = iamBasedRights.some(right => tokens.url(req, res)!.includes(right.code))
const onlyIamRights = !user.isAdmin && fullStudyProgrammeRights.length === 0
logger.info(message, {
+13 -8
View File
@@ -1,12 +1,17 @@
import { NextFunction, Request, Response } from 'express'
import { Role } from '../types'
import { Role } from '../shared/types'
export const roles = (requiredRoles: Role[]) => async (req: Request, res: Response, next: NextFunction) => {
const { roles } = req.user
if (requiredRoles.some(role => roles.includes(role)) || roles.includes('admin')) {
return next()
export const roles =
(requiredRoles: Role[], requiredIamGroups?: string[]) => (req: Request, res: Response, next: NextFunction) => {
const { roles, iamGroups } = req.user
if (
requiredRoles.some(role => roles.includes(role)) ||
requiredIamGroups?.some(group => iamGroups.includes(group)) ||
roles.includes('admin')
) {
return next()
}
res.status(403).json({ error: 'Missing required roles or IAM groups' })
}
res.status(403).json({ error: 'Missing required roles' })
}
+8 -22
View File
@@ -25,42 +25,28 @@ const getUser = async (
iamAccess: IamAccess
) => {
if (showAsUser && specialGroup?.superAdmin) {
return await getMockedUser({ userToMock: showAsUser as string, mockedBy: username as string })
return await getMockedUser({ userToMock: showAsUser, mockedBy: username })
}
return await getUserToska({
username: username as string,
name: name as string,
email: email as string,
username,
name,
email,
iamGroups,
specialGroup,
sisId: sisId as string,
sisId,
iamAccess,
})
}
type Headers = {
host?: string
'user-agent'?: string
accept?: string
'accept-language'?: string
'accept-encoding'?: string
referer?: string
displayname?: string
hygroupcn?: string
hypersonsisuid?: string
mail?: string
remote_user?: string
'shib-session-id'?: string
shib_logout_url?: string
uid?: string
'x-show-as-user'?: string
connection?: string
cookie?: string
'sec-fetch-dest'?: string
'sec-fetch-mode'?: string
'sec-fetch-site'?: string
'if-none-match'?: string
priority?: string
}
const toskaUserMiddleware = async (req: Request, _res: Response, next: NextFunction) => {
@@ -87,8 +73,8 @@ const toskaUserMiddleware = async (req: Request, _res: Response, next: NextFunct
)
}
const iamGroups = parseIamGroups(hygroupcn as string)
const { iamAccess = {}, specialGroup = {} } = await getOrganizationAccess(sisId as string, iamGroups)
const iamGroups = parseIamGroups(hygroupcn!)
const { iamAccess = {}, specialGroup = {} } = await getOrganizationAccess(sisId!, iamGroups)
const iamRights = Object.keys(iamAccess)
if (!hasRequiredIamGroup(iamGroups, iamRights)) {
@@ -107,7 +93,7 @@ const toskaUserMiddleware = async (req: Request, _res: Response, next: NextFunct
Sentry.setUser({ username: user.mockedBy ?? username! })
req.user = user
req.logoutUrl = logoutUrl as string
req.logoutUrl = logoutUrl!
next()
}
+1 -3
View File
@@ -1,4 +1,3 @@
/* eslint-disable import/no-cycle */
import { InferAttributes } from 'sequelize'
import {
BelongsTo,
@@ -50,11 +49,10 @@ export class SISStudyRight extends Model<InferAttributes<SISStudyRight>> {
@Column(DataType.BOOLEAN)
cancelled!: boolean
@ForeignKey(() => Student)
@Column(DataType.STRING)
studentNumber!: string
@BelongsTo(() => Student)
@BelongsTo(() => Student, { foreignKey: 'studentNumber', targetKey: 'studentnumber' })
student!: Student
@ForeignKey(() => StudyrightExtent)
@@ -1,4 +1,3 @@
/* eslint-disable import/no-cycle */
import { InferAttributes } from 'sequelize'
import {
BelongsTo,
@@ -12,7 +11,8 @@ import {
UpdatedAt,
} from 'sequelize-typescript'
import { DegreeProgrammeType, Name, Phase, StudyTrack } from '../types'
import { Name } from '../shared/types'
import { DegreeProgrammeType, Phase, StudyTrack } from '../types'
import { SISStudyRight } from './SISStudyRight'
@Table({
+1 -11
View File
@@ -1,4 +1,3 @@
/* eslint-disable import/no-cycle */
import { InferAttributes } from 'sequelize'
import {
BelongsTo,
@@ -14,7 +13,7 @@ import {
UpdatedAt,
} from 'sequelize-typescript'
import { Name } from '../types'
import { Name } from '../shared/types'
import { CourseProvider } from './courseProvider'
import { CourseType } from './courseType'
import { Credit } from './credit'
@@ -57,15 +56,6 @@ export class Course extends Model<InferAttributes<Course>> {
@Column(DataType.JSONB)
name!: Name
@Column(DataType.DATE)
latest_instance_date!: Date
@Column(DataType.DATE)
startdate!: Date
@Column(DataType.DATE)
enddate!: Date
@Column(DataType.DATE)
max_attainment_date!: Date
+1 -12
View File
@@ -1,6 +1,5 @@
/* eslint-disable import/no-cycle */
import { InferAttributes } from 'sequelize'
import { Column, CreatedAt, DataType, ForeignKey, Model, PrimaryKey, Table, UpdatedAt } from 'sequelize-typescript'
import { Column, CreatedAt, DataType, ForeignKey, Model, Table, UpdatedAt } from 'sequelize-typescript'
import { Course } from './course'
import { Organization } from './organization'
@@ -9,18 +8,8 @@ import { Organization } from './organization'
underscored: true,
modelName: 'course_provider',
tableName: 'course_providers',
indexes: [
{
unique: true,
fields: ['coursecode', 'organizationcode'],
},
],
})
export class CourseProvider extends Model<InferAttributes<CourseProvider>> {
@PrimaryKey
@Column(DataType.STRING)
composite!: string
@ForeignKey(() => Course)
@Column(DataType.STRING)
coursecode!: string
+1 -2
View File
@@ -1,8 +1,7 @@
/* eslint-disable import/no-cycle */
import { InferAttributes } from 'sequelize'
import { Column, CreatedAt, DataType, HasMany, Model, PrimaryKey, Table, UpdatedAt } from 'sequelize-typescript'
import { Name } from '../types'
import { Name } from '../shared/types'
import { Course } from './course'
@Table({
-1
View File
@@ -1,4 +1,3 @@
/* eslint-disable import/no-cycle */
import { InferAttributes } from 'sequelize'
import {
BelongsTo,
+1 -17
View File
@@ -1,16 +1,5 @@
/* eslint-disable import/no-cycle */
import { InferAttributes } from 'sequelize'
import {
Column,
CreatedAt,
DataType,
ForeignKey,
Model,
PrimaryKey,
Table,
Unique,
UpdatedAt,
} from 'sequelize-typescript'
import { Column, CreatedAt, DataType, ForeignKey, Model, Table, UpdatedAt } from 'sequelize-typescript'
import { Credit } from './credit'
import { Teacher } from './teacher'
@@ -21,11 +10,6 @@ import { Teacher } from './teacher'
tableName: 'credit_teachers',
})
export class CreditTeacher extends Model<InferAttributes<CreditTeacher>> {
@PrimaryKey
@Unique
@Column(DataType.STRING)
composite!: string
@ForeignKey(() => Credit)
@Column(DataType.STRING)
credit_id!: string
+2 -2
View File
@@ -1,8 +1,8 @@
/* eslint-disable import/no-cycle */
import { InferAttributes } from 'sequelize'
import { Column, CreatedAt, DataType, Model, PrimaryKey, Table, UpdatedAt } from 'sequelize-typescript'
import { CreditTypeCode, Name } from '../types'
import { Name } from '../shared/types'
import { CreditTypeCode } from '../types'
@Table({
underscored: true,
@@ -1,8 +1,7 @@
/* eslint-disable import/no-cycle */
import { InferAttributes } from 'sequelize'
import { Column, CreatedAt, DataType, Model, Table, UpdatedAt } from 'sequelize-typescript'
import { Name } from '../types'
import { Name } from '../shared/types'
@Table({
underscored: true,
@@ -1,4 +1,3 @@
/* eslint-disable import/no-cycle */
import { InferAttributes } from 'sequelize'
import {
BelongsTo,
-1
View File
@@ -1,4 +1,3 @@
/* eslint-disable import/no-cycle */
import { CreditTypeCode } from '../types'
import { Course } from './course'
import { CourseProvider } from './courseProvider'
-1
View File
@@ -1,4 +1,3 @@
/* eslint-disable import/no-cycle */
import { CreationOptional, InferAttributes, InferCreationAttributes } from 'sequelize'
import { AutoIncrement, BelongsTo, Column, DataType, ForeignKey, Model, PrimaryKey, Table } from 'sequelize-typescript'
@@ -1,4 +1,3 @@
/* eslint-disable import/no-cycle */
import { CreationOptional, InferAttributes, InferCreationAttributes } from 'sequelize'
import { AutoIncrement, BelongsTo, Column, DataType, ForeignKey, Model, PrimaryKey, Table } from 'sequelize-typescript'
+1 -2
View File
@@ -1,4 +1,3 @@
/* eslint-disable import/no-cycle */
import { InferAttributes } from 'sequelize'
import {
BelongsToMany,
@@ -12,7 +11,7 @@ import {
UpdatedAt,
} from 'sequelize-typescript'
import { Name } from '../types'
import { Name } from '../shared/types'
import { Course, CourseProvider, ProgrammeModule, SISStudyRight } from '.'
@Table({
@@ -1,4 +1,3 @@
/* eslint-disable import/no-cycle */
import { InferAttributes } from 'sequelize'
import {
BelongsTo,
@@ -13,7 +12,8 @@ import {
UpdatedAt,
} from 'sequelize-typescript'
import { DegreeProgrammeType, Name } from '../types'
import { Name } from '../shared/types'
import { DegreeProgrammeType } from '../types'
import { Organization } from './organization'
import { ProgrammeModuleChild } from './programmeModuleChild'
@@ -70,6 +70,9 @@ export class ProgrammeModule extends Model<InferAttributes<ProgrammeModule>> {
@Column(DataType.STRING)
degreeProgrammeType!: DegreeProgrammeType | null
@Column(DataType.INTEGER)
minimumCredits!: number | null
@CreatedAt
@Column(DataType.DATE)
createdAt!: Date
@@ -1,4 +1,3 @@
/* eslint-disable import/no-cycle */
import { InferAttributes } from 'sequelize'
import { Column, CreatedAt, DataType, ForeignKey, Model, PrimaryKey, Table, UpdatedAt } from 'sequelize-typescript'
+1 -2
View File
@@ -1,8 +1,7 @@
/* eslint-disable import/no-cycle */
import { InferAttributes } from 'sequelize'
import { Column, CreatedAt, DataType, Model, PrimaryKey, Table, UpdatedAt } from 'sequelize-typescript'
import { Name } from '../types'
import { Name } from '../shared/types'
@Table({
underscored: true,
+4 -2
View File
@@ -1,4 +1,3 @@
/* eslint-disable import/no-cycle */
import { InferAttributes } from 'sequelize'
import { Column, CreatedAt, DataType, HasMany, Model, PrimaryKey, Table, UpdatedAt } from 'sequelize-typescript'
@@ -27,7 +26,7 @@ export class Student extends Model<InferAttributes<Student>> {
@HasMany(() => Enrollment, { foreignKey: 'studentnumber', sourceKey: 'studentnumber' })
enrollments!: Enrollment[]
@HasMany(() => SISStudyRight)
@HasMany(() => SISStudyRight, { foreignKey: 'studentNumber', sourceKey: 'studentnumber' })
studyRights!: SISStudyRight[]
@HasMany(() => Studyplan, { foreignKey: 'studentnumber', sourceKey: 'studentnumber' })
@@ -81,6 +80,9 @@ export class Student extends Model<InferAttributes<Student>> {
@Column(DataType.STRING)
sis_person_id!: string
@Column(DataType.BOOLEAN)
hasPersonalIdentityCode!: boolean
@CreatedAt
@Column(DataType.DATE)
createdAt!: Date
-1
View File
@@ -1,4 +1,3 @@
/* eslint-disable import/no-cycle */
import { InferAttributes } from 'sequelize'
import {
BelongsTo,
@@ -1,4 +1,3 @@
/* eslint-disable import/no-cycle */
import { InferAttributes } from 'sequelize'
import { Column, CreatedAt, DataType, Model, PrimaryKey, Table, UpdatedAt } from 'sequelize-typescript'
-1
View File
@@ -1,4 +1,3 @@
/* eslint-disable import/no-cycle */
import { InferAttributes } from 'sequelize'
import { BelongsToMany, Column, CreatedAt, DataType, Model, PrimaryKey, Table, UpdatedAt } from 'sequelize-typescript'
+1 -2
View File
@@ -1,8 +1,7 @@
/* eslint-disable import/no-cycle */
import { CreationOptional, InferAttributes, InferCreationAttributes } from 'sequelize'
import { AutoIncrement, Column, DataType, Default, Model, PrimaryKey, Table, Unique } from 'sequelize-typescript'
import { Language, Role } from '../types'
import { Language, Role } from '../shared/types'
@Table({
underscored: true,
+4 -3
View File
@@ -1,7 +1,8 @@
const { orderBy } = require('lodash')
// dbConnections must be imported to avoid Sequelize errors even if it's not used
// eslint-disable-next-line no-unused-vars
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { dbConnections } = require('../database/connection')
const { computeCreditsProduced } = require('../services/providerCredits')
const { parseCsv, printProgressBar } = require('./helpers')
@@ -68,7 +69,7 @@ const diff = (rapoData, okData, code) => {
}
}
const process = async data => {
const processProgrammes = async data => {
let programmeCounter = 0
const rapoProgrammeData = formatData(data.slice(1))
const allProgrammeCodes = [...new Set(Object.keys(rapoProgrammeData))]
@@ -87,7 +88,7 @@ const process = async data => {
const programmeCreditsDiff = async fileName => {
console.log('Running studyprogramme credits diff')
await parseCsv(fileName, process)
await parseCsv(fileName, processProgrammes)
console.log('Diff completed.')
}
+4 -11
View File
@@ -36,14 +36,8 @@ import university from './routes/university'
import updater from './routes/updater'
import usersToska from './routes/users'
import usersFd from './routes/usersFd'
import initializeSentry from './util/sentry'
const routes = (app: Express, url: string) => {
initializeSentry(app)
app.use(Sentry.Handlers.requestHandler())
app.use(Sentry.Handlers.tracingHandler())
app.use(cors({ credentials: true, origin: frontUrl }))
app.use(express.json())
app.use(compression())
@@ -71,7 +65,7 @@ const routes = (app: Express, url: string) => {
app.use(`${url}/faculties`, faculties)
app.use(`${url}/university`, university)
app.use(`${url}/updater`, auth.roles(['admin']), updater)
app.use(`${url}/teachers`, auth.roles(['teachers']), teachers)
app.use(`${url}/teachers`, teachers)
if (serviceProvider === 'toska') {
app.use(`${url}/users`, usersToska)
} else {
@@ -84,11 +78,10 @@ const routes = (app: Express, url: string) => {
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('*', async (_, res) => {
const results = { error: 'unknown endpoint' }
res.status(404).json(results)
app.get('*', (_, res) => {
res.status(404).json({ error: 'unknown endpoint' })
})
app.use(Sentry.Handlers.errorHandler())
Sentry.setupExpressErrorHandler(app)
app.use(errorHandler)
}
+21 -16
View File
@@ -2,39 +2,44 @@ import axios from 'axios'
import { Request, Response, Router } from 'express'
import { isDev } from '../config'
import { Release } from '../shared/types'
const router = Router()
type ChangeLogData = {
description: string
time: Date
title: string
}
const changelog: { data?: ChangeLogData[] } = {}
const changelog: { data?: Release[] } = {}
router.get('/', async (_req: Request, res: Response) => {
if (changelog.data) {
return res.status(200).send(changelog.data)
}
if (isDev) {
const fakeChangeLogData: ChangeLogData[] = [
const fakeRelease: Release[] = [
{
description: "### Fake release\nLet's not spam the GitHub API in development!",
title: 'Fake title for fake release',
time: new Date(),
description: '**Feature 1**\n- Added a fancy new feature \n\n**Feature 2**\n- Fixed a bug\n- Fixed another bug',
title: 'Release 1',
time: new Date().toISOString(),
},
{
description: "Let's not spam the GitHub API in development!",
title: 'Release 2',
time: new Date().toISOString(),
},
{
description: 'This release should not be visible on the frontpage',
title: 'Release 3',
time: new Date().toISOString(),
},
]
return res.status(200).json(fakeChangeLogData)
return res.status(200).json(fakeRelease)
}
const response = await axios.get('https://api.github.com/repos/UniversityOfHelsinkiCS/oodikone/releases')
const newChangelogData = response.data.map((release: Record<string, any>) => ({
const releasesFromAPI: Release[] = response.data.map((release: Record<string, any>) => ({
description: release.body,
time: release.created_at,
time: release.published_at,
title: release.name,
}))
changelog.data = newChangelogData
res.status(200).send(newChangelogData)
changelog.data = releasesFromAPI
res.status(200).json(releasesFromAPI)
})
export default router
+8 -1
View File
@@ -1,10 +1,12 @@
import crypto from 'crypto'
import { Request, Response, Router } from 'express'
import { serviceProvider } from '../config'
import { getCourseYearlyStats } from '../services/courses'
import { getCoursesByNameAndOrCode, getCoursesByCodes } from '../services/courses/courseFinders'
import { CourseWithSubsId } from '../types'
import { getFullStudyProgrammeRights, hasFullAccessToStudentData, validateParamLength } from '../util'
import logger from '../util/logger'
const router = Router()
@@ -54,7 +56,7 @@ router.get('/v3/courseyearlystats', async (req: GetCourseYearlyStatsRequest, res
const { roles, programmeRights } = req.user
const userHasFullAccessToStudentData = hasFullAccessToStudentData(roles)
const userHasCorrectRole = userHasFullAccessToStudentData || roles.includes('courseStatistics')
const userHasCorrectRole = userHasFullAccessToStudentData === true || roles.includes('courseStatistics')
const fullStudyProgrammeRights = getFullStudyProgrammeRights(programmeRights)
// If user has rights to see at least one programme, then they are allowed to see all of them
@@ -70,11 +72,16 @@ router.get('/v3/courseyearlystats', async (req: GetCourseYearlyStatsRequest, res
const combineSubstitutions = req.query.combineSubstitutions !== 'false'
const separate = req.query.separate === 'true'
if (serviceProvider === 'fd')
logger.info('Debugging c.y.s.: got through authorization and collected req query params.')
// Student numbers should be obfuscated to all other users except admins,
// fullSisuAccess users, and users with rights to any specific study programmes
const anonymize = !userHasFullAccessToStudentData && fullStudyProgrammeRights.length === 0
const anonymizationSalt = anonymize ? crypto.randomBytes(12).toString('hex') : null
const results = await getCourseYearlyStats(codes, separate, anonymizationSalt, combineSubstitutions)
if (serviceProvider === 'fd')
logger.info('Debugging c.y.s.: getCourseYearlyStats returned and route should return result.')
res.json(results)
})
@@ -19,7 +19,7 @@ interface GetSearchRequest extends Request {
}
router.get('/', async (req: GetSearchRequest, res: Response) => {
const courseCodes = JSON.parse(req.query?.courselist as string) || []
const courseCodes = JSON.parse(req.query?.courselist) || []
const startDate = req.query?.startdate !== 'null' ? new Date(req.query.startdate) : new Date(2017, 7, 1, 0, 0, 0)
const endDate = req.query?.enddate !== 'null' ? new Date(req.query.enddate) : new Date()
if (req.query?.enddate === 'null') {
+17 -9
View File
@@ -1,7 +1,7 @@
import { Request, Response, Router } from 'express'
import * as auth from '../middleware/auth'
import { getDegreeProgrammesOfFaculty } from '../services/faculty/faculty'
import { getDegreeProgrammesOfFaculty, getFacultyCodeById } from '../services/faculty/faculty'
import { combineFacultyBasics } from '../services/faculty/facultyBasics'
import { getFacultyCredits } from '../services/faculty/facultyCredits'
import { countGraduationTimes } from '../services/faculty/facultyGraduationTimes'
@@ -47,7 +47,8 @@ router.get(
'/:id/basicstats',
auth.roles(['facultyStatistics', 'katselmusViewer']),
async (req: GetStatsRequest, res: Response) => {
const code = req.params.id
const facultyId = req.params.id
const code = await getFacultyCodeById(facultyId)
if (!code) {
return res.status(422).end()
}
@@ -82,7 +83,8 @@ router.get(
'/:id/creditstats',
auth.roles(['facultyStatistics', 'katselmusViewer']),
async (req: GetCreditStatsRequest, res: Response) => {
const code = req.params.id
const facultyId = req.params.id
const code = await getFacultyCodeById(facultyId)
const { year_type: yearType } = req.query
const stats = await getFacultyCredits(code, yearType === 'ACADEMIC_YEAR')
return res.json(stats)
@@ -93,7 +95,8 @@ router.get(
'/:id/thesisstats',
auth.roles(['facultyStatistics', 'katselmusViewer']),
async (req: GetStatsRequest, res: Response) => {
const code = req.params.id
const facultyId = req.params.id
const code = await getFacultyCodeById(facultyId)
if (!code) {
return res.status(422).end()
}
@@ -127,7 +130,8 @@ router.get(
'/:id/graduationtimes',
auth.roles(['facultyStatistics', 'katselmusViewer']),
async (req: GetGraduationStatsRequest, res: Response) => {
const code = req.params.id
const facultyId = req.params.id
const code = await getFacultyCodeById(facultyId)
if (!code) {
return res.status(422).end()
}
@@ -162,7 +166,8 @@ router.get(
'/:id/progressstats',
auth.roles(['facultyStatistics', 'katselmusViewer']),
async (req: GetProgressStatsRequest, res: Response) => {
const code = req.params.id
const facultyId = req.params.id
const code = await getFacultyCodeById(facultyId)
if (!code) {
return res.status(422).end()
}
@@ -192,7 +197,8 @@ router.get(
'/:id/studentstats',
auth.roles(['facultyStatistics', 'katselmusViewer']),
async (req: GetStudentStatsRequest, res: Response) => {
const code = req.params.id
const facultyId = req.params.id
const code = await getFacultyCodeById(facultyId)
if (!code) {
return res.status(422).end()
}
@@ -227,7 +233,8 @@ router.get(
'/:id/update_basicview',
auth.roles(['facultyStatistics', 'katselmusViewer']),
async (req: GetUpdateBasicViewRequest, res: Response) => {
const code = req.params.id
const facultyId = req.params.id
const code = await getFacultyCodeById(facultyId)
if (!code) {
return res.status(422).end()
}
@@ -247,7 +254,8 @@ router.get(
'/:id/update_progressview',
auth.roles(['facultyStatistics', 'katselmusViewer']),
async (req: Request, res: Response) => {
const code = req.params.id
const facultyId = req.params.id
const code = await getFacultyCodeById(facultyId)
if (!code) {
return res.status(422).end()
}
+3 -2
View File
@@ -1,4 +1,4 @@
import { Request, Router } from 'express'
import { Request, Response, Router } from 'express'
import { sendFeedbackToToska } from '../services/mailService'
import { ApplicationError } from '../util/customErrors'
@@ -12,7 +12,7 @@ interface EmailRequest extends Request {
}
}
router.post('/email', async (req: EmailRequest) => {
router.post('/email', async (req: EmailRequest, res: Response) => {
const { content } = req.body
const { user } = req
@@ -26,6 +26,7 @@ router.post('/email', async (req: EmailRequest) => {
})
logger.info(`${user.userId} succesfully sent feedback to Toska`)
return res.status(200).end()
})
export default router
+1 -1
View File
@@ -5,7 +5,7 @@ import { ApplicationError } from '../util/customErrors'
const router = Router()
router.get('/', async (req: Request, res: Response) => {
router.get('/', (req: Request, res: Response) => {
const { user, logoutUrl } = req
if (!user) {
+22 -18
View File
@@ -12,7 +12,7 @@ import { bottlenecksOf } from '../services/populations/bottlenecksOf'
import { optimizedStatisticsOf } from '../services/populations/optimizedStatisticsOf'
import { populationStudentsMerger, populationCourseStatsMerger } from '../services/statMerger'
import { findByTag, findByCourseAndSemesters } from '../services/students'
import { mapToProviders } from '../shared/util/mapToProviders'
import { mapToProviders } from '../shared/util'
import { GenderCode, ParsedCourse, Unarray, Unification, UnifyStatus } from '../types'
import { getFullStudyProgrammeRights, hasFullAccessToStudentData } from '../util'
import { ApplicationError } from '../util/customErrors'
@@ -88,7 +88,7 @@ router.post('/v2/populationstatistics/courses', async (req: PostPopulationStatis
if (req.body.years) {
const upperYearBound = new Date().getFullYear() + 1
const multicoursestatPromises = Promise.all(
const multiCourseStatPromises = Promise.all(
req.body.years.map(year => {
if (req.body.selectedStudentsByYear) {
req.body.selectedStudents = encrypted
@@ -101,8 +101,8 @@ router.post('/v2/populationstatistics/courses', async (req: PostPopulationStatis
return coursestatistics
})
)
const multicoursestats = await multicoursestatPromises
const result = populationCourseStatsMerger(multicoursestats)
const multiCourseStats = await multiCourseStatPromises
const result = populationCourseStatsMerger(multiCourseStats)
if (result.error) {
return res.status(400).json(result)
}
@@ -118,8 +118,10 @@ router.post('/v2/populationstatistics/courses', async (req: PostPopulationStatis
try {
const result = await bottlenecksOf(query, null, encrypted)
return res.json(result)
} catch (error: any) {
Sentry.captureException(new Error(error.message))
} catch (error: unknown) {
if (error instanceof Error) {
Sentry.captureException(error)
}
return res.status(400).end()
}
})
@@ -156,8 +158,10 @@ router.post(
studentNumberList
)
return res.json(result)
} catch (error: any) {
Sentry.captureException(new Error(error.message))
} catch (error: unknown) {
if (error instanceof Error) {
Sentry.captureException(error)
}
return res.status(400).end()
}
}
@@ -200,8 +204,10 @@ router.post(
studentNumbers
)
return res.json(result)
} catch (error: any) {
Sentry.captureException(new Error(error.message))
} catch (error: unknown) {
if (error instanceof Error) {
Sentry.captureException(error)
}
return res.status(400).end()
}
}
@@ -248,7 +254,7 @@ router.get('/v3/populationstatistics', async (req: GetPopulationStatisticsReques
if (req.query.years) {
const upperYearBound = new Date().getFullYear() + 1
const multipopulationstudentPromises = Promise.all(
const multiPopulationStudentPromises = Promise.all(
req.query.years.map(year => {
const newMonths = (upperYearBound - Number(year)) * 12
const populationStudents = optimizedStatisticsOf({
@@ -260,10 +266,9 @@ router.get('/v3/populationstatistics', async (req: GetPopulationStatisticsReques
return populationStudents
})
)
const multipopulationstudents = await multipopulationstudentPromises
const result = populationStudentsMerger(multipopulationstudents)
const multiPopulationStudents = await multiPopulationStudentPromises
const result = populationStudentsMerger(multiPopulationStudents)
res.json(filterPersonalTags(result, userId))
} else {
const result: any = await optimizedStatisticsOf({
@@ -427,10 +432,9 @@ router.post('/v3/populationstatisticsbystudentnumbers', async (req: PostByStuden
const filteredStudentNumbers = hasFullAccessToStudentData(roles)
? studentnumberlist
: intersection(studentnumberlist, studentsUserCanAccess)
const studyProgrammeCode =
tags?.studyProgramme && tags?.studyProgramme.includes('+')
? tags?.studyProgramme.split('+')[0]
: tags?.studyProgramme
const studyProgrammeCode = tags?.studyProgramme?.includes('+')
? tags?.studyProgramme.split('+')[0]
: tags?.studyProgramme
const result = await optimizedStatisticsOf(
{
+8 -12
View File
@@ -6,14 +6,6 @@ import { ApplicationError } from '../util/customErrors'
const router = Router()
const filterStudentTags = (student: Awaited<ReturnType<typeof withStudentNumber>>, userId: string) => {
if (!student) return null
return {
...student,
tags: (student.tags ?? []).filter(({ tag }) => !tag.personal_user_id || tag.personal_user_id === userId),
}
}
interface GetStudentsRequest extends Request {
query: {
searchTerm: string
@@ -59,11 +51,15 @@ router.get('/:studentNumber', async (req: GetStudentRequest, res: Response) => {
} = req
if (!hasFullAccessToStudentData(roles) && !studentsUserCanAccess.includes(studentNumber)) {
throw new ApplicationError('Error finding student', 400)
return res
.status(403)
.json({ error: `User does not have permission to view data for student number ${studentNumber}.` })
}
const student = await withStudentNumber(studentNumber)
const filteredTags = filterStudentTags(student, id)
res.json(filteredTags)
const student = await withStudentNumber(studentNumber, id)
if (!student) {
return res.status(404).json({ error: `Student not found with student number ${studentNumber}.` })
}
res.json(student)
})
export default router
+30 -86
View File
@@ -5,20 +5,20 @@ import {
setBasicStats,
getGraduationStats,
setGraduationStats,
getStudytrackStats,
setStudytrackStats,
getStudyTrackStats,
setStudyTrackStats,
} from '../services/analyticsService'
import { getCreditsProduced } from '../services/providerCredits'
import { getProgrammeName } from '../services/studyProgramme'
import { getBasicStatsForStudytrack } from '../services/studyProgramme/studyProgrammeBasics'
import {
getStudyProgrammeCoursesForStudyTrack,
getStudyProgrammeStatsForColorizedCoursesTable,
} from '../services/studyProgramme/studyProgrammeCourses'
import { getGraduationStatsForStudytrack } from '../services/studyProgramme/studyProgrammeGraduations'
import { updateBasicView, updateStudytrackView } from '../services/studyProgramme/studyProgrammeUpdates'
import { getStudyRightsInProgramme } from '../services/studyProgramme/studyRightFinders'
import { getGraduationStatsForStudyTrack } from '../services/studyProgramme/studyProgrammeGraduations'
import { updateBasicView, updateStudyTrackView } from '../services/studyProgramme/studyProgrammeUpdates'
import { getStudyRightsInProgramme, getStudyTracksForProgramme } from '../services/studyProgramme/studyRightFinders'
import { getStudyTrackStatsForStudyProgramme } from '../services/studyProgramme/studyTrackStats'
import { Graduated, SpecialGroups, YearType } from '../types'
import logger from '../util/logger'
import { logInfoForGrafana } from '../util/logInfoForGrafana'
@@ -43,8 +43,8 @@ router.get('/creditstats', async (req: GetCreditStatsRequest, res: Response) =>
interface GetStatsRequest extends Request {
query: {
year_type: string
special_groups: string
year_type: YearType
special_groups: SpecialGroups
combined_programme: string
}
}
@@ -56,7 +56,7 @@ router.get('/:id/basicstats', async (req: GetStatsRequest, res: Response) => {
return res.status(422).end()
}
logInfoForGrafana(code, combinedProgramme)
void logInfoForGrafana(code, combinedProgramme)
const data = await getBasicStats(code, combinedProgramme, yearType, specialGroups)
if (data) {
return res.json(data)
@@ -88,7 +88,7 @@ router.get('/:id/graduationstats', async (req: GetStatsRequest, res: Response) =
return res.json(data)
}
const updatedStats = await getGraduationStatsForStudytrack({
const updatedStats = await getGraduationStatsForStudyTrack({
studyProgramme: code,
combinedProgramme,
settings: {
@@ -116,7 +116,7 @@ router.get('/:id/coursestats', async (req: GetCourseStatsRequest, res: Response)
const combinedProgramme = req.query?.combined_programme
const date = new Date()
date.setHours(23, 59, 59, 999)
logInfoForGrafana(code, combinedProgramme)
void logInfoForGrafana(code, combinedProgramme)
try {
const data = await getStudyProgrammeCoursesForStudyTrack(date.getTime(), code, showByYear, combinedProgramme)
return res.json(data)
@@ -127,8 +127,8 @@ router.get('/:id/coursestats', async (req: GetCourseStatsRequest, res: Response)
interface GetStudyTrackStatsRequest extends Request {
query: {
graduated: string
special_groups: string
graduated: Graduated
special_groups: SpecialGroups
combined_programme: string
}
}
@@ -140,8 +140,8 @@ router.get('/:id/studytrackstats', async (req: GetStudyTrackStatsRequest, res: R
return res.status(422).end()
}
logInfoForGrafana(code, combinedProgramme)
const data = await getStudytrackStats(code, combinedProgramme, graduated, specialGroups)
void logInfoForGrafana(code, combinedProgramme)
const data = await getStudyTrackStats(code, combinedProgramme, graduated, specialGroups)
if (data) {
return res.json(data)
}
@@ -157,7 +157,7 @@ router.get('/:id/studytrackstats', async (req: GetStudyTrackStatsRequest, res: R
studyRightsOfProgramme,
})
if (updated) {
await setStudytrackStats(updated, graduated, specialGroups)
await setStudyTrackStats(updated, graduated, specialGroups)
}
return res.json(updated)
})
@@ -172,6 +172,19 @@ router.get('/:id/colorizedtablecoursestats', async (req: Request, res: Response)
}
})
router.get('/:id/studytracks', async (req: Request, res: Response) => {
const code = req.params.id
if (!code) {
return res.status(422).end()
}
try {
const data = await getStudyTracksForProgramme(code)
return res.json(data)
} catch (error) {
logger.error({ message: `Failed to get study tracks for study programme ${code}`, meta: `${error}` })
}
})
interface GetUpdateViewRequest extends Request {
query: {
combined_programme: string
@@ -201,7 +214,7 @@ router.get('/:id/update_studytrackview', async (req: GetUpdateViewRequest, res:
return res.status(400).json({ error: 'Missing code' })
}
try {
const result = await updateStudytrackView(code, combinedProgramme)
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}` : ''}`
@@ -210,73 +223,4 @@ router.get('/:id/update_studytrackview', async (req: GetUpdateViewRequest, res:
}
})
interface GetEvaluationStatsRequest extends Request {
query: {
graduated: string
year_type: string
special_groups: string
}
}
router.get('/:id/evaluationstats', async (req: GetEvaluationStatsRequest, res: Response) => {
const code = req.params.id
const { graduated, year_type: yearType, special_groups: specialGroups } = req.query
if (!code) {
return res.status(422).end()
}
// Statistics for Tilannekuvalomake view
const combinedProgramme = ''
let gradData = await getGraduationStats(code, combinedProgramme, yearType, specialGroups)
if (!gradData) {
const updatedStats = await getGraduationStatsForStudytrack({
studyProgramme: code,
combinedProgramme,
settings: {
isAcademicYear: yearType === 'ACADEMIC_YEAR',
includeAllSpecials: specialGroups === 'SPECIAL_INCLUDED',
},
})
if (updatedStats) {
await setGraduationStats(updatedStats, yearType, specialGroups)
gradData = updatedStats
}
}
let progressData = await getStudytrackStats(code, combinedProgramme, graduated, specialGroups)
if (!progressData) {
const studyRightsOfProgramme = await getStudyRightsInProgramme(code, false, true)
const updated = await getStudyTrackStatsForStudyProgramme({
studyProgramme: code,
combinedProgramme,
settings: {
graduated: graduated === 'GRADUATED_INCLUDED',
specialGroups: specialGroups === 'SPECIAL_INCLUDED',
},
studyRightsOfProgramme,
})
if (updated) {
await setStudytrackStats(updated, graduated, specialGroups)
progressData = updated
}
}
const programmeName = await getProgrammeName(code)
delete gradData.tableStats
delete gradData.graphStats
delete gradData.titles
const data = {
id: code,
programmeName,
status: gradData?.status,
lastUpdated: gradData.lastUpdated,
graduations: gradData,
creditCounts: progressData?.creditCounts,
creditCountsCombo: progressData?.creditCountsCombo,
years: progressData?.years,
}
return res.json(data)
})
export default router
@@ -19,7 +19,7 @@ router.get('/', async (req: StudyProgrammeCriteriaRequest, res: Response) => {
if (studyProgramme !== '' && !studyProgramme) {
return res.status(422).end()
}
const studyProgrammeCriteria = await getCriteria(studyProgramme as string)
const studyProgrammeCriteria = await getCriteria(studyProgramme)
return res.json(studyProgrammeCriteria)
})
+1 -2
View File
@@ -14,7 +14,7 @@ import {
TagFromFrontend,
StudentTagFromFrontend,
} from '../services/tags'
import { Role } from '../types'
import { Role } from '../shared/types'
import { getFullStudyProgrammeRights, hasFullAccessToStudentData } from '../util'
const router = Router()
@@ -69,7 +69,6 @@ interface PostTagRequest extends Request {
router.post('/tags', async (req: PostTagRequest, res: Response) => {
const {
// eslint-disable-next-line @typescript-eslint/naming-convention
tag: { studytrack, tagname, year, personal_user_id },
} = req.body
const { roles, id, programmeRights } = req.user
+32 -12
View File
@@ -1,20 +1,27 @@
import { Request, Response, Router } from 'express'
import { serviceProvider } from '../config'
import * as auth from '../middleware/auth'
import { getProvidersOfFaculty, isFaculty } from '../services/organizations'
import { getTeachersBySearchTerm, getTeacherStatistics, getYearlyStatistics } from '../services/teachers'
import { CategoryID, getTeacherStats, findAndSaveTeachers, getCategoriesAndYears } from '../services/teachers/top'
import { mapToProviders } from '../shared/util/mapToProviders'
import { mapToProviders } from '../shared/util'
import { getFullStudyProgrammeRights, splitByEmptySpace } from '../util'
const router = Router()
const iamGroupsGivingFullAccess = ['hy-dekaanit', 'hy-varadekaanit-opetus']
const fullAccessAuth = () =>
serviceProvider === 'toska' ? auth.roles([], iamGroupsGivingFullAccess) : auth.roles(['teachers'])
interface GetTeachersRequest extends Request {
query: {
searchTerm: string
}
}
router.get('/', async (req: GetTeachersRequest, res: Response) => {
router.get('/', fullAccessAuth(), async (req: GetTeachersRequest, res: Response) => {
const { searchTerm } = req.query
if (!searchTerm) {
return res.status(400).json({ error: 'Search term missing' })
@@ -38,30 +45,37 @@ interface GetTopTeachersRequest extends Request {
}
}
router.get('/top', async (req: GetTopTeachersRequest, res: Response) => {
router.get('/top', fullAccessAuth(), async (req: GetTopTeachersRequest, res: Response) => {
const { yearcode, category = CategoryID.ALL } = req.query
if (!yearcode) {
return res.status(422).send('Missing required yearcode query param')
}
const result = await getTeacherStats(category, Number(yearcode))
res.json(result)
if (result) {
return res.json(result)
}
await findAndSaveTeachers(Number(yearcode), Number(yearcode))
const updatedStats = await getTeacherStats(category, Number(yearcode))
res.json(updatedStats)
})
interface PostTopTeachersRequest extends Request {
body: {
startyearcode: string
endyearcode: string
startyearcode?: string
endyearcode?: string
}
}
router.post('/top', async (req: PostTopTeachersRequest, res: Response) => {
router.post('/top', auth.roles(['admin']), async (req: PostTopTeachersRequest, res: Response) => {
const { startyearcode, endyearcode } = req.body
const endYear = endyearcode ? Number(endyearcode) : undefined
const startYear = startyearcode ? Number(startyearcode) : undefined
res.status(200).end()
await findAndSaveTeachers(Number(endyearcode), Number(startyearcode))
await findAndSaveTeachers(endYear, startYear)
})
router.get('/top/categories', async (_req: Request, res: Response) => {
router.get('/top/categories', fullAccessAuth(), async (_req: Request, res: Response) => {
const result = await getCategoriesAndYears()
res.json(result)
})
@@ -75,7 +89,7 @@ interface GetTeacherStatsRequest extends Request {
}
router.get('/stats', async (req: GetTeacherStatsRequest, res: Response) => {
const { roles, programmeRights } = req.user
const { roles, programmeRights, iamGroups } = req.user
const fullStudyProgrammeRights = getFullStudyProgrammeRights(programmeRights)
const { providers, semesterStart, semesterEnd } = req.query
@@ -84,7 +98,13 @@ router.get('/stats', async (req: GetTeacherStatsRequest, res: Response) => {
}
const providerRights = mapToProviders(fullStudyProgrammeRights)
if (!(providers.every(provider => providerRights.includes(provider)) || roles.includes('admin'))) {
if (
!(
(roles.includes('teachers') && providers.every(provider => providerRights.includes(provider))) ||
roles.includes('admin') ||
iamGroups.some(group => iamGroupsGivingFullAccess.includes(group))
)
) {
return res.status(403).send('You do not have permission to see this data')
}
@@ -112,7 +132,7 @@ interface GetTeacherByIdRequest extends Request {
}
}
router.get('/:id', async (req: GetTeacherByIdRequest, res: Response) => {
router.get('/:id', fullAccessAuth(), async (req: GetTeacherByIdRequest, res: Response) => {
const { id } = req.params
const result = await getTeacherStatistics(id)
if (!result) {
+2 -2
View File
@@ -1,4 +1,3 @@
// const router = require('express').Router()
import { Request, Response, Router } from 'express'
import { cloneDeep } from 'lodash'
@@ -16,6 +15,7 @@ import {
} from '../services/faculty/facultyService'
import { combineFacultyStudentProgress, FacultyProgressData } from '../services/faculty/facultyStudentProgress'
import { getMedian } from '../services/studyProgramme/studyProgrammeHelpers'
import { NameWithCode } from '../shared/types'
import { Graduated } from '../types'
import logger from '../util/logger'
@@ -24,7 +24,7 @@ const router = Router()
const degreeNames = ['bachelor', 'bachelorMaster', 'master', 'doctor'] as const
const getProgrammeNames = (faculties: Organization[]) => {
return faculties.reduce<Record<string, { code: string; en?: string; fi?: string; sv?: string }>>((obj, faculty) => {
return faculties.reduce<Record<string, NameWithCode>>((obj, faculty) => {
const { name, code } = faculty.dataValues
obj[faculty.code] = { code, ...name }
return obj
+10 -15
View File
@@ -65,13 +65,6 @@ router.post('/update/v2/customlist/:type', async (req: PostCustomListRequest, re
}
})
router.post('/update/v2/courses', async (req: Request, res: Response) => {
const response = await updateSISCoursesByCourseCode(req.body)
if (response) {
res.status(200).json('Update SIS courses scheduled')
}
})
router.get('/update/v2/programmes', async (_req: Request, res: Response) => {
const response = await updateSISProgrammes()
if (response) {
@@ -87,25 +80,27 @@ router.get('/refresh_redis_cache', async (req: Request, res: Response) => {
}
})
router.post('/refresh_statistic_v2', async (req: Request, res: Response) => {
logger.info(`${req.user.username} requested refresh of statistics`)
jobMaker.statistics()
res.status(200).json('Teacher and study programme statistics refreshed')
router.post('/refresh-teacher-leaderboard', (req: Request, res: Response) => {
logger.info(
`${req.user.username} requested refresh of teacher leaderboard for the current and previous academic year`
)
jobMaker.teacherLeaderboard()
res.status(200).json('Teacher leaderboard for the current and previous academic year refreshed')
})
router.post('/refresh_study_programmes_v2', async (req: Request, res: Response) => {
logger.info(`${req.user.username} requested refresh of study programmes`)
refreshProgrammes()
await refreshProgrammes()
res.status(200).json('Added job for refreshing study programme overviews')
})
router.post('/refresh_faculties_v2', async (req: Request, res: Response) => {
logger.info(`${req.user.username} requested refresh of faculties`)
refreshFaculties()
await refreshFaculties()
res.status(200).json('Added job for refreshing faculties')
})
router.post('/refresh_language_center_data', async (req: Request, res: Response) => {
router.post('/refresh_language_center_data', (req: Request, res: Response) => {
logger.info(`${req.user.username} requested refresh of language center data`)
if (!languageCenterViewEnabled)
res.status(418).json({ error: 'The language center functionality is not activated in your environment.' })
@@ -113,7 +108,7 @@ router.post('/refresh_language_center_data', async (req: Request, res: Response)
res.status(200).json('Added job for refreshing language center data')
})
router.post('/refresh-close-to-graduation', async (req: Request, res: Response) => {
router.post('/refresh-close-to-graduation', (req: Request, res: Response) => {
logger.info(`${req.user.username} requested refresh of close to graduation data`)
jobMaker.closeToGraduation()
res.status(200).json('Added job for refreshing close to graduation data')
+2 -2
View File
@@ -5,7 +5,7 @@ import * as auth from '../middleware/auth'
import { sendNotificationAboutAccessToUser, previewNotificationAboutAccessToUser } from '../services/mailService'
import * as userService from '../services/userService'
import { LANGUAGE_CODES } from '../shared/language'
import { Language } from '../types'
import { Language } from '../shared/types'
import logger from '../util/logger'
const router = Router()
@@ -15,7 +15,7 @@ router.get('/', auth.roles(['admin']), async (_req: Request, res: Response) => {
res.json(results)
})
router.get('/access_groups', auth.roles(['admin']), async (_req: Request, res: Response) => {
router.get('/access_groups', auth.roles(['admin']), (_req: Request, res: Response) => {
res.json(roles)
})
+33 -3
View File
@@ -23,16 +23,46 @@ router.get('/from-sisu-by-eppn/:newUserEppn', auth.roles(['admin']), async (req:
res.json(person)
})
interface User {
id: string
first_name: string
last_name: string
call_name: string
eppn: string
email_address: string
student_number: string
phone_number: string
document_state: string
}
interface AddUserRequest extends Request {
body: {
user: any // TODO: Funidata, what is the type of user?
user: User
}
}
interface DeleteUserRequest extends Request {
body: {
userId: string
}
}
router.post('/add', auth.roles(['admin']), async (req: AddUserRequest, res: Response) => {
const { user } = req.body
const person = await userService.addNewUser(user)
res.json(person)
const oldUser = await userService.findByUsername(user.eppn)
if (oldUser) return res.status(400).json({ error: 'Trying to add a user already present in the user database.' })
const success = await userService.addNewUser(user)
res.json(success)
})
router.delete('/delete', auth.roles(['admin']), async (req: DeleteUserRequest, res: Response) => {
const { userId } = req.body
try {
await userService.deleteUserById(userId)
res.json({ deleted: userId })
} catch (error) {
return res.status(400).json({ error: 'Deletion was unsuccessful with the user id included in the request' })
}
})
export default router
@@ -1,44 +1,49 @@
import moment from 'moment'
import { facultyCodes, ignoredFacultyCodes } from '../config/organizationConstants'
import { redisClient } from './redis'
// Only new bachelor, masters and doctoral programmes get their data updated in redis every night, use redis for them
const isUpdatedNewProgramme = (code: string) => code.includes('KH') || code.includes('MH') || /^(T)[0-9]{6}$/.test(code)
import { facultyCodes, ignoredFacultyCodes } from '../config/organizationConstants'
import { Graduated, SpecialGroups, YearType } from '../types'
import { redisClient } from './redis'
import { isRelevantProgramme } from './studyProgramme/studyProgrammeHelpers'
const filteredFacultyCodes = facultyCodes.filter(item => !ignoredFacultyCodes.includes(item))
const isFaculty = (code: string) => filteredFacultyCodes.includes(code)
const createRedisKeyForBasicStats = (id: string, yearType: string, specialGroups: string) => {
const createRedisKeyForBasicStats = (id: string, yearType: YearType, specialGroups: SpecialGroups) => {
return `BASIC_STATS_${id}_${yearType}_${specialGroups}`
}
const createRedisKeyForCreditStats = (id: string, yearType: string, specialGroups: string) => {
const createRedisKeyForCreditStats = (id: string, yearType: YearType, specialGroups: SpecialGroups) => {
return `CREDIT_STATS_${id}_${yearType}_${specialGroups}`
}
const createRedisKeyForGraduationStats = (id: string, yearType: string, specialGroups: string) => {
const createRedisKeyForGraduationStats = (id: string, yearType: YearType, specialGroups: SpecialGroups) => {
return `GRADUATION_STATS_${id}_${yearType}_${specialGroups}`
}
const createRedisKeyForStudytrackStats = (id: string, graduated: string, specialGroups: string) => {
const createRedisKeyForStudyTrackStats = (id: string, graduated: Graduated, specialGroups: SpecialGroups) => {
return `STUDYTRACK_STATS_${id}_${graduated}_${specialGroups}`
}
const shouldSaveToRedis = (id: unknown) => {
if (typeof id !== 'string') return false
if (isRelevantProgramme(id)) return true
const splitId = id.split('-')
return splitId.length === 2 && isRelevantProgramme(splitId[0]) && isRelevantProgramme(splitId[1])
}
export const getBasicStats = async (
id: string,
combinedProgramme: string | null,
yearType: string,
specialGroups: string
yearType: YearType,
specialGroups: SpecialGroups
) => {
if (!isUpdatedNewProgramme(id)) {
return null
}
const searchkey = combinedProgramme ? `${id}-${combinedProgramme}` : id
const redisKey = createRedisKeyForBasicStats(searchkey, yearType, specialGroups)
const dataFromRedis = await redisClient.getAsync(redisKey)
const searchKey = combinedProgramme ? `${id}-${combinedProgramme}` : id
const redisKey = createRedisKeyForBasicStats(searchKey, yearType, specialGroups)
const dataFromRedis = await redisClient.get(redisKey)
if (!dataFromRedis) {
return null
}
return JSON.parse(dataFromRedis)
}
export const setBasicStats = async (data, yearType: string, specialGroups: string) => {
export const setBasicStats = async (data, yearType: YearType, specialGroups: SpecialGroups) => {
const { id } = data
const redisKey = createRedisKeyForBasicStats(id, yearType, specialGroups)
const dataToRedis = {
@@ -46,10 +51,10 @@ export const setBasicStats = async (data, yearType: string, specialGroups: strin
status: 'DONE',
lastUpdated: moment().format(),
}
if (!isUpdatedNewProgramme(id)) {
if (!shouldSaveToRedis(id)) {
return dataToRedis
}
const setOperationStatus = await redisClient.setAsync(redisKey, JSON.stringify(dataToRedis))
const setOperationStatus = await redisClient.set(redisKey, JSON.stringify(dataToRedis))
if (setOperationStatus !== 'OK') {
return null
}
@@ -62,7 +67,7 @@ export const getCreditStats = async (providerCode: string, isAcademicYear: boole
isAcademicYear ? 'ACADEMIC_YEAR' : 'CALENDAR_YEAR',
specialGroups ? 'SPECIAL_INCLUDED' : 'SPECIAL_EXCLUDED'
)
const dataFromRedis = await redisClient.getAsync(redisKey)
const dataFromRedis = await redisClient.get(redisKey)
if (!dataFromRedis) {
return null
}
@@ -81,10 +86,10 @@ export const setCreditStats = async (data, isAcademicYear: boolean, specialGroup
status: 'DONE',
lastUpdated: moment().format(),
}
if (!isUpdatedNewProgramme(data.id) && !isFaculty(data.id)) {
if (!isRelevantProgramme(id) && !isFaculty(id)) {
return dataToRedis
}
const setOperationStatus = await redisClient.setAsync(redisKey, JSON.stringify(dataToRedis))
const setOperationStatus = await redisClient.set(redisKey, JSON.stringify(dataToRedis))
if (setOperationStatus !== 'OK') {
return null
}
@@ -94,22 +99,19 @@ export const setCreditStats = async (data, isAcademicYear: boolean, specialGroup
export const getGraduationStats = async (
id: string,
combinedProgramme: string | null,
yearType: string,
specialGroups: string
yearType: YearType,
specialGroups: SpecialGroups
) => {
if (!isUpdatedNewProgramme(id)) {
return null
}
const searchkey = combinedProgramme ? `${id}-${combinedProgramme}` : id
const redisKey = createRedisKeyForGraduationStats(searchkey, yearType, specialGroups)
const dataFromRedis = await redisClient.getAsync(redisKey)
const searchKey = combinedProgramme ? `${id}-${combinedProgramme}` : id
const redisKey = createRedisKeyForGraduationStats(searchKey, yearType, specialGroups)
const dataFromRedis = await redisClient.get(redisKey)
if (!dataFromRedis) {
return null
}
return JSON.parse(dataFromRedis)
}
export const setGraduationStats = async (data, yearType: string, specialGroups: string) => {
export const setGraduationStats = async (data, yearType: YearType, specialGroups: SpecialGroups) => {
const { id } = data
const redisKey = createRedisKeyForGraduationStats(id, yearType, specialGroups)
const dataToRedis = {
@@ -117,46 +119,43 @@ export const setGraduationStats = async (data, yearType: string, specialGroups:
status: 'DONE',
lastUpdated: moment().format(),
}
if (!isUpdatedNewProgramme(id)) {
if (!shouldSaveToRedis(id)) {
return dataToRedis
}
const setOperationStatus = await redisClient.setAsync(redisKey, JSON.stringify(dataToRedis))
const setOperationStatus = await redisClient.set(redisKey, JSON.stringify(dataToRedis))
if (setOperationStatus !== 'OK') {
return null
}
return dataToRedis
}
export const getStudytrackStats = async (
export const getStudyTrackStats = async (
id: string,
combinedProgramme: string | null,
graduated: string,
specialGroups: string
graduated: Graduated,
specialGroups: SpecialGroups
) => {
if (!isUpdatedNewProgramme(id)) {
return null
}
const searchkey = combinedProgramme ? `${id}-${combinedProgramme}` : id
const redisKey = createRedisKeyForStudytrackStats(searchkey, graduated, specialGroups)
const dataFromRedis = await redisClient.getAsync(redisKey)
const searchKey = combinedProgramme ? `${id}-${combinedProgramme}` : id
const redisKey = createRedisKeyForStudyTrackStats(searchKey, graduated, specialGroups)
const dataFromRedis = await redisClient.get(redisKey)
if (!dataFromRedis) {
return null
}
return JSON.parse(dataFromRedis)
}
export const setStudytrackStats = async (data, graduated: string, specialGroups: string) => {
export const setStudyTrackStats = async (data, graduated: Graduated, specialGroups: SpecialGroups) => {
const { id } = data
const redisKey = createRedisKeyForStudytrackStats(id, graduated, specialGroups)
const redisKey = createRedisKeyForStudyTrackStats(id, graduated, specialGroups)
const dataToRedis = {
...data,
status: 'DONE',
lastUpdated: moment().format(),
}
if (!isUpdatedNewProgramme(id)) {
if (!shouldSaveToRedis(id)) {
return dataToRedis
}
const setOperationStatus = await redisClient.setAsync(redisKey, JSON.stringify(dataToRedis))
const setOperationStatus = await redisClient.set(redisKey, JSON.stringify(dataToRedis))
if (setOperationStatus !== 'OK') {
return null
}
@@ -15,7 +15,6 @@ interface StudentCredit {
interface StudentEnrollment {
date: Date
courseCode: string
state: EnrollmentState
substitution: string | null
}
@@ -84,7 +83,7 @@ const getCredits = async (
? null
: courses.find(course => course.substitutions.includes(credit.course_code))?.code
return {
courseCode: originalCode || credit.course_code,
courseCode: originalCode ?? credit.course_code,
substitution: originalCode ? credit.course_code : null,
studentNumber: credit.student_studentnumber,
creditType: credit.credittypecode,
@@ -95,9 +94,9 @@ const getCredits = async (
}
const getEnrollments = async (courses: Courses, fullCourseCodes: string[], studentNumbers: string[]) => {
const enrollments: Array<Pick<Enrollment, 'course_code' | 'enrollment_date_time' | 'studentnumber' | 'state'>> =
const enrollments: Array<Pick<Enrollment, 'course_code' | 'enrollment_date_time' | 'studentnumber'>> =
await Enrollment.findAll({
attributes: ['course_code', 'enrollment_date_time', 'studentnumber', 'state'],
attributes: ['course_code', 'enrollment_date_time', 'studentnumber'],
where: {
course_code: {
[Op.in]: fullCourseCodes,
@@ -113,11 +112,10 @@ const getEnrollments = async (courses: Courses, fullCourseCodes: string[], stude
const formattedEnrollments = enrollments.map(enrollment => {
const originalCode = courses.find(course => course.substitutions.includes(enrollment.course_code))?.code
return {
courseCode: originalCode || enrollment.course_code,
courseCode: originalCode ?? enrollment.course_code,
substitution: originalCode ? enrollment.course_code : null,
studentNumber: enrollment.studentnumber,
date: enrollment.enrollment_date_time,
state: enrollment.state,
}
})
return formattedEnrollments
@@ -201,7 +199,6 @@ export const getCompletedCourses = async (studentNumbers: string[], courseCodes:
studentCredits[enrollment.studentNumber].enrollments.push({
date: enrollment.date,
courseCode: enrollment.courseCode,
state: enrollment.state,
substitution: enrollment.substitution,
})
})
@@ -1,4 +1,5 @@
import { EnrollmentState, Name } from '../../types'
import { Name } from '../../shared/types'
import { EnrollmentState } from '../../types'
import { lengthOf, percentageOf } from '../../util'
const fall: string[] = []
@@ -21,26 +22,16 @@ const initializePassingSemesters = () => {
return passingSemesters
}
const getEnrolledStudents = (enrollments: DynamicEnrollments) => {
if (enrollments) {
return Object.keys(enrollments)
.filter(key => key !== EnrollmentState.ENROLLED && key !== 'semesters')
.reduce((acc, key) => [...acc, ...[...enrollments[key]].map(studentNumber => studentNumber)], [] as string[])
const getFilteredEnrolledNoGrade = (enrollments: DynamicEnrollments, allStudents: string[]) => {
const enrolledStudents = [...(enrollments[EnrollmentState.ENROLLED] ?? [])]
const result: Record<string, boolean> = {}
for (const student of enrolledStudents) {
if (allStudents.includes(student)) {
continue
}
result[student] = true
}
return []
}
const getFilteredEnrolledNoGrade = (
enrollments: DynamicEnrollments,
enrolledStudents: string[],
allStudents: string[]
) => {
if (enrollments[EnrollmentState.ENROLLED]) {
return [...enrollments[EnrollmentState.ENROLLED]]
.filter(student => !enrolledStudents.includes(student) && !allStudents.includes(student))
.reduce((acc, student) => ({ ...acc, [student]: true }), {} as Record<string, boolean>)
}
return {} as Record<string, boolean>
return result
}
type DynamicEnrollments = {
@@ -57,8 +48,6 @@ type Stats = {
students: number
passed: number
failed: number
failedMany: number
retryPassed: number
attempts: number
improvedPassedGrade: number
percentage: number | undefined
@@ -76,8 +65,6 @@ type Students = {
all: Record<string, boolean>
passed: Record<string, boolean>
failed: Record<string, boolean>
retryPassed: Record<string, boolean>
failedMany: Record<string, boolean>
improvedPassedGrade: Record<string, boolean>
markedToSemester: Record<string, boolean>
enrolledNoGrade: Record<string, boolean>
@@ -126,8 +113,6 @@ export class CourseStatsCounter {
all: {},
passed: {},
failed: {},
retryPassed: {},
failedMany: {},
improvedPassedGrade: {},
markedToSemester: {},
enrolledNoGrade: {},
@@ -136,8 +121,6 @@ export class CourseStatsCounter {
students: 0,
passed: 0,
failed: 0,
failedMany: 0,
retryPassed: 0,
attempts: 0,
improvedPassedGrade: 0,
percentage: undefined,
@@ -216,13 +199,11 @@ export class CourseStatsCounter {
private removeFromFailed(studentNumber: string) {
delete this.students.failed[studentNumber]
delete this.students.failedMany[studentNumber]
}
private markPassingGrade(studentNumber: string) {
this.students.passed[studentNumber] = true
if (this.failedBefore(studentNumber)) {
this.students.retryPassed[studentNumber] = true
this.removeFromFailed(studentNumber)
}
}
@@ -235,12 +216,8 @@ export class CourseStatsCounter {
private markFailedGrade(studentNumber: string) {
if (this.passedBefore(studentNumber)) {
this.students.retryPassed[studentNumber] = true
this.removeFromFailed(studentNumber)
} else {
if (this.failedBefore(studentNumber)) {
this.students.failedMany[studentNumber] = true
}
this.students.failed[studentNumber] = true
}
}
@@ -275,17 +252,14 @@ export class CourseStatsCounter {
public getFinalStats() {
const { stats, students } = this
const enrolledStudents = getEnrolledStudents(this.enrollments)
const allStudents = Object.keys(students.all).map(student => student)
const filteredEnrolledNoGrade = getFilteredEnrolledNoGrade(this.enrollments, enrolledStudents, allStudents)
const allStudents = Object.keys(students.all)
const filteredEnrolledNoGrade = getFilteredEnrolledNoGrade(this.enrollments, allStudents)
students.all = { ...students.all, ...filteredEnrolledNoGrade }
students.enrolledNoGrade = filteredEnrolledNoGrade
stats.students = lengthOf(students.all)
stats.passed = lengthOf(students.passed)
stats.failed = lengthOf(students.failed)
stats.failedMany = lengthOf(students.failedMany)
stats.retryPassed = lengthOf(students.retryPassed)
stats.improvedPassedGrade = lengthOf(students.improvedPassedGrade)
stats.percentage = percentageOf(stats.passed, stats.students)
stats.passedOfPopulation = percentageOf(stats.passed, this.studentsInTotal)
@@ -1,4 +1,4 @@
import { EnrollmentState, Name, PassedCategory } from '../../types'
import { Name } from '../../shared/types'
import { FormattedProgramme, OrganizationDetails } from './helpers'
type Programme = {
@@ -34,13 +34,8 @@ type GroupAttempts = {
}
type GroupStudents = {
categories: {
passedFirst: string[]
passedEventually: string[]
neverPassed: string[]
}
grades: { [studentNumber: string]: { grade: string; passed: boolean } }
studentnumbers: string[]
studentNumbers: string[]
}
type Group = {
@@ -49,14 +44,13 @@ type Group = {
coursecode: string
attempts: GroupAttempts
students: GroupStudents
enrollments: { studentNumber: string; state: string; enrollmentDateTime: Date }[]
allEnrollments: { studentNumber: string; state: string; enrollmentDateTime: Date }[]
yearcode: number
enrollments: { studentNumber: string; enrollmentDateTime: Date }[]
allEnrollments: { studentNumber: string; enrollmentDateTime: Date }[]
yearCode: number
}
type Student = {
earliestAttainment: Date
category: PassedCategory
code: number
}
@@ -117,17 +111,12 @@ export class CourseYearlyStatsCounter {
},
},
students: {
categories: {
passedFirst: [],
passedEventually: [],
neverPassed: [],
},
grades: {},
studentnumbers: [],
studentNumbers: [],
},
enrollments: [],
allEnrollments: [],
yearcode: yearCode,
yearCode,
}
}
@@ -147,7 +136,6 @@ export class CourseYearlyStatsCounter {
if (!this.facultyStats[yearCode]) {
this.initFacultyYear(yearCode)
}
if (
facultyCode &&
!this.facultyStats[yearCode].faculties[facultyCode] &&
@@ -163,7 +151,6 @@ export class CourseYearlyStatsCounter {
this.programmes[code].passed[yearCode] = []
this.programmes[code].credits[yearCode] = 0
}
if (facultyCode && !this.facultyStats[yearCode].allStudents.includes(studentNumber)) {
this.facultyStats[yearCode].allStudents.push(studentNumber)
this.facultyStats[yearCode].faculties[facultyCode].students.push(studentNumber)
@@ -172,7 +159,6 @@ export class CourseYearlyStatsCounter {
this.programmes[code].passed[yearCode].push(studentNumber)
this.programmes[code].credits[yearCode] += credits
}
if (facultyCode && passed && !this.facultyStats[yearCode].allPassed.includes(studentNumber)) {
this.facultyStats[yearCode].allPassed.push(studentNumber)
this.facultyStats[yearCode].faculties[facultyCode].passed.push(studentNumber)
@@ -211,7 +197,6 @@ export class CourseYearlyStatsCounter {
public markEnrollmentToGroup(
studentNumber: string,
state: string,
enrollmentDateTime: Date,
groupCode: number,
groupName: string | Name,
@@ -221,22 +206,13 @@ export class CourseYearlyStatsCounter {
if (!this.groups[groupCode]) {
this.initGroup(groupCode, groupName, courseCode, yearCode)
}
const enrollment = { studentNumber, state, enrollmentDateTime }
const enrollment = { studentNumber, enrollmentDateTime }
this.groups[groupCode].allEnrollments.push(enrollment)
const oldEnrollment = this.groups[groupCode].enrollments.find(
enrollment => enrollment.studentNumber === studentNumber
)
if (!oldEnrollment) {
return this.groups[groupCode].enrollments.push({ studentNumber, state, enrollmentDateTime })
}
if (oldEnrollment.state === EnrollmentState.ENROLLED || oldEnrollment.state === EnrollmentState.CONFIRMED) {
return
}
if (state !== EnrollmentState.ENROLLED && state !== EnrollmentState.CONFIRMED) {
return
}
if (![EnrollmentState.ENROLLED, EnrollmentState.CONFIRMED].includes(state as EnrollmentState)) {
return
return this.groups[groupCode].enrollments.push({ studentNumber, enrollmentDateTime })
}
this.groups[groupCode].enrollments = this.groups[groupCode].enrollments
.filter(enrollment => enrollment.studentNumber !== studentNumber)
@@ -272,56 +248,25 @@ export class CourseYearlyStatsCounter {
this.groups[groupCode].students.grades[studentNumber] = { grade, passed }
}
public markCreditToStudentCategories(
studentNumber: string,
passed: boolean,
attainmentDate: Date,
groupCode: number
) {
public markCreditToStudentCategories(studentNumber: string, attainmentDate: Date, groupCode: number) {
if (!this.students.has(studentNumber)) {
this.addNewStudent(studentNumber, passed, attainmentDate, groupCode)
this.addOrUpdateStudent(studentNumber, attainmentDate, groupCode)
} else {
this.updateExistingStudent(studentNumber, passed, attainmentDate, groupCode)
this.updateExistingStudent(studentNumber, attainmentDate, groupCode)
}
}
private addNewStudent(studentNumber: string, passed: boolean, attainmentDate: Date, groupCode: number) {
const category = passed ? 'passedFirst' : 'neverPassed'
private addOrUpdateStudent(studentNumber: string, attainmentDate: Date, groupCode: number) {
this.students.set(studentNumber, {
earliestAttainment: attainmentDate,
category,
code: groupCode,
})
}
private updateExistingStudent(studentNumber: string, passed: boolean, attainmentDate: Date, groupCode: number) {
private updateExistingStudent(studentNumber: string, attainmentDate: Date, groupCode: number) {
const student = this.students.get(studentNumber)
if (attainmentDate < student!.earliestAttainment) {
this.updateCategoryForEarlierAttainment(studentNumber, passed, attainmentDate, groupCode, student!)
} else if (student!.category === 'neverPassed' && passed) {
this.students.set(studentNumber, { ...student!, category: 'passedEventually' })
}
}
private updateCategoryForEarlierAttainment(
studentNumber: string,
passed: boolean,
attainmentDate: Date,
groupCode: number,
student: Student
) {
if (passed) {
this.students.set(studentNumber, {
earliestAttainment: attainmentDate,
category: 'passedFirst',
code: groupCode,
})
} else if (student.category === 'passedFirst') {
this.students.set(studentNumber, {
earliestAttainment: attainmentDate,
category: 'passedEventually',
code: groupCode,
})
this.addOrUpdateStudent(studentNumber, attainmentDate, groupCode)
}
}
@@ -341,8 +286,7 @@ export class CourseYearlyStatsCounter {
private parseGroupStatistics(anonymizationSalt: string | null) {
for (const [studentNumber, data] of this.students) {
this.groups[data.code].students.categories[data.category].push(studentNumber)
this.groups[data.code].students.studentnumbers.push(studentNumber)
this.groups[data.code].students.studentNumbers.push(studentNumber)
}
const groupStatistics = Object.values(this.groups).map(({ ...rest }) => {
@@ -360,7 +304,7 @@ export class CourseYearlyStatsCounter {
...rest,
students: { ...students, grades },
}
if (anonymizationSalt && normalStats.students.studentnumbers.length < 6) {
if (anonymizationSalt && normalStats.students.studentNumbers.length < 6) {
this.obfuscated = true
const gradeSpread = {} as Grades
for (const grade of Object.keys(normalStats.attempts.grades)) {
@@ -378,14 +322,9 @@ export class CourseYearlyStatsCounter {
},
grades: gradeSpread,
},
yearcode: rest.yearcode,
yearCode: rest.yearCode,
students: {
categories: {
passedFirst: [] as string[],
passedEventually: [] as string[],
neverPassed: [] as string[],
},
studentnumbers: [] as string[],
studentNumbers: [] as string[],
},
}
return obfuscatedStats
@@ -95,7 +95,7 @@ export const getEnrollmentsForCourses = async (codes: string[], unification: Uni
[Op.in]: codes,
},
enrollment_date_time: { [Op.gte]: new Date('2021-05-31') },
state: [EnrollmentState.ENROLLED, EnrollmentState.CONFIRMED],
state: EnrollmentState.ENROLLED,
is_open: getIsOpen(unification),
},
})
@@ -1,6 +1,7 @@
import { Op } from 'sequelize'
import { Name, Unification } from '../../types'
import { Name } from '../../shared/types'
import { Unification } from '../../types'
export type FormattedProgramme = {
code: string
+60 -13
View File
@@ -1,10 +1,12 @@
/* eslint-disable @typescript-eslint/naming-convention */
import crypto from 'crypto'
import { Op } from 'sequelize'
import { serviceProvider } from '../../config'
import { Course, Credit, Enrollment, Organization, SISStudyRightElement } from '../../models'
import { Name, Unification } from '../../types'
import { Name } from '../../shared/types'
import { EnrollmentState, Unification } from '../../types'
import { isOpenUniCourseCode } from '../../util'
import logger from '../../util/logger'
import { getSortRank } from '../../util/sortRank'
import { CourseYearlyStatsCounter } from './courseYearlyStatsCounter'
import {
@@ -99,7 +101,6 @@ type FormattedEnrollment = {
semesterCode: number
semesterName: Name
courseCode: string
state: string
enrollmentDateTime: Date
studentNumber: string
programmes: Array<Record<string, any>>
@@ -114,7 +115,6 @@ const parseEnrollment = (
const {
studentnumber: studentNumber,
semester,
state,
enrollment_date_time: enrollmentDateTime,
course_code: courseCode,
} = enrollment
@@ -128,7 +128,6 @@ const parseEnrollment = (
semesterCode,
semesterName,
courseCode,
state,
enrollmentDateTime,
studentNumber,
programmes: studyRightElements.map(formatStudyRightElement),
@@ -150,6 +149,10 @@ const getYearlyStatsOfNew = async (
combineSubstitutions: boolean,
studentNumberToSrElementsMap: Record<string, SISStudyRightElement[]>
) => {
if (serviceProvider === 'fd')
logger.info(
`Debugging c.y.s.: entered getYearlyStatsOfNew with courseCode ${courseCode} and unification ${unification}`
)
const courseForSubs = await Course.findOne({
where: { code: courseCode },
})
@@ -173,6 +176,11 @@ const getYearlyStatsOfNew = async (
}),
])
if (serviceProvider === 'fd')
logger.info(
`Debugging c.y.s., getYearlyStatsOfNew, courseCode ${courseCode} unification ${unification}: got credits ${credits.length}, enrollments ${enrollments.length} and course ${course ? 'yes' : 'no'}`
)
const counter = new CourseYearlyStatsCounter()
for (const credit of credits) {
@@ -218,32 +226,56 @@ const getYearlyStatsOfNew = async (
credits
)
counter.markCreditToGroup(studentNumber, passed, grade, groupCode, groupName, courseCode, yearCode)
counter.markCreditToStudentCategories(studentNumber, passed, attainmentDate, groupCode)
counter.markCreditToStudentCategories(studentNumber, attainmentDate, groupCode)
}
enrollments.forEach(enrollment => {
const { studentNumber, semesterCode, semesterName, yearCode, yearName, courseCode, state, enrollmentDateTime } =
const { studentNumber, semesterCode, semesterName, yearCode, yearName, courseCode, enrollmentDateTime } =
parseEnrollment(enrollment, anonymizationSalt, studentNumberToSrElementsMap)
const groupCode = separate ? semesterCode : yearCode
const groupName = separate ? semesterName : yearName
counter.markEnrollmentToGroup(studentNumber, state, enrollmentDateTime, groupCode, groupName, courseCode, yearCode)
counter.markEnrollmentToGroup(studentNumber, enrollmentDateTime, groupCode, groupName, courseCode, yearCode)
})
if (serviceProvider === 'fd')
logger.info(
`Debugging c.y.s., getYearlyStatsOfNew, code ${courseCode} unification ${unification}: finished looping through credits.`
)
const statistics = counter.getFinalStatistics(anonymizationSalt)
if (serviceProvider === 'fd')
logger.info(
`Debugging c.y.s., getYearlyStatsOfNew, code ${courseCode} unification ${unification}: got final statistics.`
)
let substitutionCourses: Course[] | undefined
if (combineSubstitutions && courseForSubs?.substitutions && courseForSubs.substitutions.length > 0) {
substitutionCourses = await Course.findAll({
where: {
code: {
[Op.in]: codes,
},
},
attributes: ['code', 'name'],
})
}
if (serviceProvider === 'fd')
logger.info(
`Debugging c.y.s., getYearlyStatsOfNew, code ${courseCode} unification ${unification}: got substitutionCourses ${substitutionCourses?.length} items.`
)
return {
...statistics,
coursecode: courseCode,
alternatives: codes,
alternatives: substitutionCourses ?? [{ code: courseForSubs!.code, name: courseForSubs!.name }],
name: course?.name,
}
}
export const maxYearsToCreatePopulationFrom = async (courseCodes: string[], unification: Unification) => {
const is_open = getIsOpen(unification)
const maxAttainmentDate = new Date(
Math.max(
...(
@@ -270,7 +302,7 @@ export const maxYearsToCreatePopulationFrom = async (courseCodes: string[], unif
attainment_date: {
[Op.gt]: attainmentThreshold,
},
is_open,
is_open: getIsOpen(unification),
},
order: [['attainment_date', 'ASC']],
})
@@ -311,9 +343,17 @@ export const getCourseYearlyStats = async (
})
const enrollments = await Enrollment.findAll({
attributes: ['studentnumber'],
where: { course_code: { [Op.in]: courseCodes } },
where: {
course_code: {
[Op.in]: courseCodes,
},
state: EnrollmentState.ENROLLED,
},
})
if (serviceProvider === 'fd')
logger.info(`Debugging c.y.s.: got credits ${credits.length} items and enrollments ${enrollments.length} items`)
const studentNumbers = {}
credits.forEach(credit => {
@@ -325,6 +365,11 @@ export const getCourseYearlyStats = async (
const studentNumberToSrElementsMap = await getStudentNumberToSrElementsMap(Object.keys(studentNumbers))
if (serviceProvider === 'fd')
logger.info(
`Debugging c.y.s.: got a studentNumberToSrElementsMap of size ${Object.keys(studentNumberToSrElementsMap).length}`
)
const statsRegular = await Promise.all(
courseCodes.map(async courseCode => {
const unifyStats = await getYearlyStatsOfNew(
@@ -356,6 +401,8 @@ export const getCourseYearlyStats = async (
})
)
if (serviceProvider === 'fd') logger.info('Debugging c.y.s.: finished creating statsRegular')
return statsRegular
}
@@ -2,6 +2,7 @@ import { groupBy, orderBy } from 'lodash'
import moment from 'moment'
import { InferAttributes, QueryTypes } from 'sequelize'
import { serviceProvider } from '../../config'
import { programmeCodes } from '../../config/programmeCodes'
import { dbConnections } from '../../database/connection'
import { Organization, ProgrammeModule } from '../../models'
@@ -54,7 +55,9 @@ export const getDegreeProgrammesOfOrganization = async (organizationId: string,
const programmesWithProgIds = programmesOfOrganization.map(programme => ({
...programme,
progId:
programme.code in programmeCodes ? programmeCodes[programme.code as keyof typeof programmeCodes] : programme.code,
programme.code in programmeCodes && serviceProvider !== 'fd'
? programmeCodes[programme.code as keyof typeof programmeCodes]
: programme.code,
}))
const programmesGroupedByCode = groupBy(orderBy(programmesWithProgIds, ['valid_from'], ['desc']), prog => prog.code)
const curriculumPeriods = await getCurriculumPeriods()
@@ -106,3 +109,16 @@ export const getDegreeProgrammesOfFaculty = async (facultyCode: string, onlyCurr
}
return getDegreeProgrammesOfOrganization(organization.id, onlyCurrentProgrammes)
}
export const getFacultyCodeById = async (facultyId: string) => {
const organization = await Organization.findOne({
attributes: ['code'],
where: {
id: facultyId,
},
})
if (!organization) {
throw new Error(`The organization with the id ${facultyId} was not found.`)
}
return organization.code
}
@@ -106,7 +106,7 @@ const calculateCombinedStats = async (
} else {
for (let i = 0; i < tableStatsForProgramme.length; i++) {
for (let j = 1; j < tableStatsForProgramme[i].length; j++) {
;(combinedTableStats[i][j] as number) += tableStatsForProgramme[i][j] as number
;(combinedTableStats[i][j] as number) += tableStatsForProgramme[i][j]
}
}
}
@@ -1,32 +1,33 @@
import { Name } from '../../shared/types'
import { getCreditsProduced } from '../providerCredits'
import { isRelevantProgramme } from '../studyProgramme/studyProgrammeHelpers'
import { getProgrammes } from './facultyService'
import { getDegreeProgrammesOfFaculty } from './faculty'
/**
* Returns credits produced by the programmes of the faculty but
* also the credits where the provider is the faculty itself
*/
export const getFacultyCredits = async (facultyCode: string, isAcademicYear: boolean) => {
const allProgrammes = (await getProgrammes(facultyCode)).data
const programmes = allProgrammes.filter(({ code }) => isRelevantProgramme(code))
const programmes = (await getDegreeProgrammesOfFaculty(facultyCode, true)).filter(({ code }) =>
isRelevantProgramme(code)
)
const facultyCredits = await getCreditsProduced(facultyCode, isAcademicYear)
const stats = {
[facultyCode]: facultyCredits,
codes: [facultyCode],
programmeNames: {},
programmeNames: {} as Record<string, { code: string } & Name>,
ids: [facultyCode],
}
for (const programme of programmes) {
const programmeCode = programme.code
const programmeCredits = await getCreditsProduced(programmeCode, isAcademicYear)
stats[programme.progId] = programmeCredits
stats.codes.push(programmeCode)
stats.ids.push(programme.progId)
stats.programmeNames[programme.progId] = {
code: programmeCode,
...programmes.find(programme => programme.code === programmeCode).name,
for (const { code, progId, name } of programmes) {
const programmeCredits = await getCreditsProduced(code, isAcademicYear)
stats[progId] = programmeCredits
stats.codes.push(code)
stats.ids.push(progId)
stats.programmeNames[progId] = {
code,
...name,
}
}
@@ -1,8 +1,9 @@
import { cloneDeep, omit } from 'lodash'
import { DegreeProgrammeType, Name } from '../../types'
import { getGraduationStats, getStudytrackStats, setGraduationStats, setStudytrackStats } from '../analyticsService'
import { getGraduationStatsForStudytrack, GraduationTimes } from '../studyProgramme/studyProgrammeGraduations'
import { Name } from '../../shared/types'
import { DegreeProgrammeType } from '../../types'
import { getGraduationStats, getStudyTrackStats, setGraduationStats, setStudyTrackStats } from '../analyticsService'
import { getGraduationStatsForStudyTrack, GraduationTimes } from '../studyProgramme/studyProgrammeGraduations'
import { getMedian } from '../studyProgramme/studyProgrammeHelpers'
import { getStudyRightsInProgramme } from '../studyProgramme/studyRightFinders'
import {
@@ -25,7 +26,7 @@ export const programmeTypes = {
} as const
const getStatsByGraduationYear = async (facultyProgrammes: ProgrammesOfOrganization) => {
const newStats: Array<Awaited<ReturnType<typeof getGraduationStatsForStudytrack>>> = []
const newStats: Array<Awaited<ReturnType<typeof getGraduationStatsForStudyTrack>>> = []
const medians: Record<string, LevelGraduationStats[]> = {}
const programmes: { medians: Record<string, Record<string, ProgrammeStats>> } = { medians: {} }
@@ -38,7 +39,7 @@ const getStatsByGraduationYear = async (facultyProgrammes: ProgrammesOfOrganizat
newStats.push(statsFromRedis)
continue
}
const updatedStats = await getGraduationStatsForStudytrack({
const updatedStats = await getGraduationStatsForStudyTrack({
studyProgramme: programme.code,
combinedProgramme: '',
settings: {
@@ -136,7 +137,7 @@ const getStatsByStartYear = async (facultyProgrammes: ProgrammesOfOrganization)
if (programme.degreeProgrammeType == null || !(programme.degreeProgrammeType in programmeTypes)) {
continue
}
const statsFromRedis = await getStudytrackStats(programme.code, '', 'GRADUATED_INCLUDED', 'SPECIAL_EXCLUDED')
const statsFromRedis = await getStudyTrackStats(programme.code, '', 'GRADUATED_INCLUDED', 'SPECIAL_EXCLUDED')
if (statsFromRedis) {
newStats.push(statsFromRedis)
continue
@@ -152,7 +153,7 @@ const getStatsByStartYear = async (facultyProgrammes: ProgrammesOfOrganization)
},
studyRightsOfProgramme,
})
await setStudytrackStats(updatedStats, 'GRADUATED_INCLUDED', 'SPECIAL_EXCLUDED')
await setStudyTrackStats(updatedStats, 'GRADUATED_INCLUDED', 'SPECIAL_EXCLUDED')
newStats.push(updatedStats)
}
@@ -1,13 +1,10 @@
import moment from 'moment'
import { DegreeProgrammeType, Graduated, Name, ProgrammeFilter, SpecialGroups, YearType } from '../../types'
import { Name } from '../../shared/types'
import { DegreeProgrammeType, Graduated, ProgrammeFilter, SpecialGroups, YearType } from '../../types'
import { redisClient } from '../redis'
import { getDegreeProgrammesOfFaculty } from './faculty'
import { FacultyProgressData } from './facultyStudentProgress'
const createRedisKeyForFacultyProgrammes = (id: string, programmeFilter: ProgrammeFilter) => {
return `FACULTY_PROGRAMMES_${id}_${programmeFilter}`
}
const createRedisKeyForBasicStats = (
id: string,
yearType: YearType,
@@ -48,41 +45,6 @@ const removeGraduationTimes = (data: GraduationData) => {
)
}
export const setFacultyProgrammes = async (id: string, data, programmeFilter: ProgrammeFilter) => {
const redisKey = createRedisKeyForFacultyProgrammes(id, programmeFilter)
const dataToRedis = {
data,
status: 'DONE',
lastUpdated: moment().format(),
}
const setOperationStatus = await redisClient.setAsync(redisKey, JSON.stringify(dataToRedis))
if (setOperationStatus !== 'OK') {
return null
}
return dataToRedis
}
const getFacultyProgrammesFromRedis = async (id: string, programmeFilter: ProgrammeFilter) => {
const redisKey = createRedisKeyForFacultyProgrammes(id, programmeFilter)
const dataFromRedis = await redisClient.getAsync(redisKey)
if (!dataFromRedis) {
return null
}
return JSON.parse(dataFromRedis)
}
export const getProgrammes = async (code: string, programmeFilter: ProgrammeFilter = 'NEW_STUDY_PROGRAMMES') => {
const programmes = await getFacultyProgrammesFromRedis(code, programmeFilter)
if (programmes) {
return programmes
}
let updatedProgrammes: any = await getDegreeProgrammesOfFaculty(code, programmeFilter === 'NEW_STUDY_PROGRAMMES')
if (updatedProgrammes) {
updatedProgrammes = await setFacultyProgrammes(code, updatedProgrammes, programmeFilter)
}
return updatedProgrammes
}
type GraduationInfo = {
graphStats: {
name: 'Bachelors' | 'Masters' | 'Doctors' | 'Others'
@@ -124,7 +86,7 @@ export const setBasicStats = async (
status: 'DONE',
lastUpdated: moment().format(),
}
const setOperationStatus = await redisClient.setAsync(redisKey, JSON.stringify(dataToRedis))
const setOperationStatus = await redisClient.set(redisKey, JSON.stringify(dataToRedis))
if (setOperationStatus !== 'OK') {
return null
}
@@ -138,7 +100,7 @@ export const getBasicStats = async (
specialGroups: SpecialGroups
) => {
const redisKey = createRedisKeyForBasicStats(id, yearType, programmeFilter, specialGroups)
const dataFromRedis = await redisClient.getAsync(redisKey)
const dataFromRedis = await redisClient.get(redisKey)
if (!dataFromRedis) {
return null
}
@@ -175,7 +137,7 @@ export const setThesisWritersStats = async (
status: 'DONE',
lastUpdated: moment().format(),
}
const setOperationStatus = await redisClient.setAsync(redisKey, JSON.stringify(dataToRedis))
const setOperationStatus = await redisClient.set(redisKey, JSON.stringify(dataToRedis))
if (setOperationStatus !== 'OK') return null
return dataToRedis
}
@@ -187,7 +149,7 @@ export const getThesisWritersStats = async (
specialGroups: SpecialGroups
) => {
const redisKey = createRedisKeyForThesiswriters(id, yearType, programmeFilter, specialGroups)
const dataFromRedis = await redisClient.getAsync(redisKey)
const dataFromRedis = await redisClient.get(redisKey)
if (!dataFromRedis) {
return null
}
@@ -224,20 +186,16 @@ export const setGraduationStats = async (data: GraduationData, programmeFilter:
status: 'DONE',
lastUpdated: moment().format(),
}
const setOperationStatus = await redisClient.setAsync(redisKey, JSON.stringify(dataToRedis))
const setOperationStatus = await redisClient.set(redisKey, JSON.stringify(dataToRedis))
if (setOperationStatus !== 'OK') {
return null
}
return dataToRedis
}
export const getGraduationStats = async (
id: string,
programmeFilter: ProgrammeFilter,
keepGraduationTimes: boolean = false
) => {
export const getGraduationStats = async (id: string, programmeFilter: ProgrammeFilter, keepGraduationTimes = false) => {
const redisKey = createRedisKeyForGraduationTimeStats(id, programmeFilter)
const dataFromRedis = await redisClient.getAsync(redisKey)
const dataFromRedis = await redisClient.get(redisKey)
if (!dataFromRedis) {
return null
}
@@ -260,7 +218,7 @@ export const setFacultyProgressStats = async (
status: 'DONE',
lastUpdated: moment().format(),
}
const setOperationStatus = await redisClient.setAsync(redisKey, JSON.stringify(dataToRedis))
const setOperationStatus = await redisClient.set(redisKey, JSON.stringify(dataToRedis))
if (setOperationStatus !== 'OK') {
return null
}
@@ -269,7 +227,7 @@ export const setFacultyProgressStats = async (
export const getFacultyProgressStats = async (id: string, specialGroups: SpecialGroups, graduated: Graduated) => {
const redisKey = createRedisKeyForFacultyProgress(id, specialGroups, graduated)
const dataFromRedis = await redisClient.getAsync(redisKey)
const dataFromRedis = await redisClient.get(redisKey)
if (!dataFromRedis) {
return null
}
@@ -301,7 +259,7 @@ export const setFacultyStudentStats = async (
status: 'DONE',
lastUpdated: moment().format(),
}
const setOperationStatus = await redisClient.setAsync(redisKey, JSON.stringify(dataToRedis))
const setOperationStatus = await redisClient.set(redisKey, JSON.stringify(dataToRedis))
if (setOperationStatus !== 'OK') {
return null
}
@@ -310,7 +268,7 @@ export const setFacultyStudentStats = async (
export const getFacultyStudentStats = async (id: string, specialGroups: SpecialGroups, graduated: Graduated) => {
const redisKey = createRedisKeyForFacultyStudents(id, specialGroups, graduated)
const dataFromRedis = await redisClient.getAsync(redisKey)
const dataFromRedis = await redisClient.get(redisKey)
if (!dataFromRedis) {
return null
}
@@ -3,7 +3,7 @@ import moment from 'moment'
import { rootOrgId } from '../../config'
import { Graduated, SpecialGroups, Unarray } from '../../types'
import { getStudytrackStats, setStudytrackStats } from '../analyticsService'
import { getStudyTrackStats, setStudyTrackStats } from '../analyticsService'
import { getYearsArray } from '../studyProgramme/studyProgrammeHelpers'
import { getStudyRightsInProgramme } from '../studyProgramme/studyRightFinders'
import { getStudyTrackStatsForStudyProgramme } from '../studyProgramme/studyTrackStats'
@@ -114,7 +114,7 @@ export const combineFacultyStudentProgress = async (
) {
continue
}
const statsFromRedis = await getStudytrackStats(studyProgramme, null, graduated, specialGroups)
const statsFromRedis = await getStudyTrackStats(studyProgramme, null, graduated, specialGroups)
if (statsFromRedis) {
statsOfProgrammes.push(statsFromRedis)
} else {
@@ -128,7 +128,7 @@ export const combineFacultyStudentProgress = async (
studyRightsOfProgramme,
})
statsOfProgrammes.push(updatedStats)
setStudytrackStats(updatedStats, graduated, specialGroups)
await setStudyTrackStats(updatedStats, graduated, specialGroups)
}
}
@@ -1,5 +1,5 @@
import { Unarray } from '../../types'
import { getStudytrackStats, setStudytrackStats } from '../analyticsService'
import { getStudyTrackStats, setStudyTrackStats } from '../analyticsService'
import { getPercentage, tableTitles } from '../studyProgramme/studyProgrammeHelpers'
import { getStudyRightsInProgramme } from '../studyProgramme/studyRightFinders'
import { getStudyTrackStatsForStudyProgramme } from '../studyProgramme/studyTrackStats'
@@ -65,7 +65,7 @@ export const combineFacultyStudents = async (
const newStats: StudyTrackStats[] = []
for (const studyProgramme of programmeCodes) {
const statsFromRedis = await getStudytrackStats(studyProgramme, null, graduated, specialGroups)
const statsFromRedis = await getStudyTrackStats(studyProgramme, null, graduated, specialGroups)
if (statsFromRedis) {
newStats.push(statsFromRedis)
if (!years.length) {
@@ -82,7 +82,7 @@ export const combineFacultyStudents = async (
},
studyRightsOfProgramme,
})
setStudytrackStats(updatedStats, graduated, specialGroups)
await setStudyTrackStats(updatedStats, graduated, specialGroups)
if (!years.length) {
years = updatedStats.years
}
@@ -2,7 +2,7 @@ import { cloneDeep } from 'lodash'
import { DegreeProgrammeType } from '../../types'
import { getGraduationStats, setGraduationStats } from '../analyticsService'
import { getGraduationStatsForStudytrack } from '../studyProgramme/studyProgrammeGraduations'
import { getGraduationStatsForStudyTrack } from '../studyProgramme/studyProgrammeGraduations'
import { getDegreeProgrammesOfFaculty, ProgrammesOfOrganization } from './faculty'
type ProgrammeName = {
@@ -25,7 +25,7 @@ const calculateCombinedStats = async (
]
const programmeTableStats: Record<string, Array<Array<string | number>>> = {}
const newStats: Array<Awaited<ReturnType<typeof getGraduationStatsForStudytrack>>> = []
const newStats: Array<Awaited<ReturnType<typeof getGraduationStatsForStudyTrack>>> = []
const combinedTableStats: Array<Array<string | number>> = []
let years: Array<string | number> = []
const programmeNames: Record<string, ProgrammeName> = {}
@@ -47,7 +47,7 @@ const calculateCombinedStats = async (
newStats.push(statsFromRedis)
continue
}
const updatedStats = await getGraduationStatsForStudytrack({
const updatedStats = await getGraduationStatsForStudyTrack({
studyProgramme: programme.code,
combinedProgramme: '',
settings: {
@@ -6,7 +6,6 @@ import { combineFacultyBasics } from './facultyBasics'
import { countGraduationTimes } from './facultyGraduationTimes'
import {
setBasicStats,
setFacultyProgrammes,
setFacultyProgressStats,
setFacultyStudentStats,
setGraduationStats,
@@ -63,7 +62,6 @@ export const updateFacultyOverview = async (facultyCode: string, statsType: Stat
export const updateFacultyProgressOverview = async (facultyCode: string) => {
const onlyNew = await getDegreeProgrammesOfFaculty(facultyCode, true)
const newProgrammes = await setFacultyProgrammes(facultyCode, onlyNew, 'NEW_STUDY_PROGRAMMES')
for (const graduated of graduatedOptions) {
for (const specialGroups of specialGroupOptions) {
@@ -71,7 +69,7 @@ export const updateFacultyProgressOverview = async (facultyCode: string) => {
await setFacultyStudentStats(updateFacultyStudentStats, specialGroups, graduated)
const updateFacultyProgressStats = await combineFacultyStudentProgress(
facultyCode,
newProgrammes?.data,
onlyNew,
specialGroups,
graduated
)
@@ -219,10 +219,10 @@ const computeLanguageCenterData = async () => {
}
const getLanguageCenterData = async () => {
const dataOnRedis = await redisClient.getAsync(LANGUAGE_CENTER_REDIS_KEY)
const dataOnRedis = await redisClient.get(LANGUAGE_CENTER_REDIS_KEY)
if (dataOnRedis) return JSON.parse(dataOnRedis)
const freshData = await computeLanguageCenterData()
redisClient.setAsync(LANGUAGE_CENTER_REDIS_KEY, JSON.stringify(freshData))
await redisClient.set(LANGUAGE_CENTER_REDIS_KEY, JSON.stringify(freshData))
return freshData
}

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