Compare commits

...
332 Commits
Author SHA1 Message Date
Valtteri Kantanen bf6d6af2ec [Close to graduation] Implement page with material-react-table 2024-11-27 18:48:34 +02:00
Valtteri Kantanen 264aaa25f2 [Frontend] Create TableHeaderWithTooltip component 2024-11-27 18:13:51 +02:00
Valtteri Kantanen 98eaddca5b [Frontend] Upgrade vite from v5 to v6 2024-11-27 14:01:32 +02:00
Valtteri Kantanen e8881fb8be Refactor useLanguage and language imports and types 2024-11-27 14:01:31 +02:00
Riku Rauhala 2824ede5f2 [Access rights] Remove katselmusViewer
This access right only granted access to evaluation overview which has been removed
2024-11-27 13:56:35 +02:00
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
402 changed files with 14696 additions and 9395 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)
})
+3 -3
View File
@@ -65,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)
@@ -76,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)
})
})
+112 -55
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', () => {
@@ -90,12 +107,52 @@ describe('Population Statistics tests', () => {
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)')
@@ -257,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'
@@ -286,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')
+1 -1
View File
@@ -249,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',
+59 -57
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(() => {
@@ -315,7 +315,7 @@ describe('Studyprogramme overview', () => {
})
it('Links to class statistics page with all years combined work', { retries: 2 }, () => {
cy.get('[data-cy=Table-StudytrackOverview]').within(() => {
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,17 +357,17 @@ 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='Section-KH50_001']").within(() => {
@@ -399,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],
@@ -417,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(() => {
@@ -436,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.'
@@ -444,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')
@@ -664,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(() => {
@@ -764,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')
@@ -773,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:
+5 -7
View File
@@ -8,12 +8,11 @@ Roles are defined [here](../services/backend/src/config/roles.ts). Criteria for
- courseStatistics
- facultyStatistics
- fullSisuAccess
- katselmusViewer
- openUniSearch
- 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 +22,11 @@ The roles `openUniSearch`, `teachers` and `studyGuidanceGroups` simply enable ce
- Can see statistics of every course and year
### katselemusViewer
### teachers
- Enables the _Evaluation overview_ view
- 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
@@ -46,7 +47,6 @@ The roles `openUniSearch`, `teachers` and `studyGuidanceGroups` simply enable ce
| Completed courses | ✅ | ✅ | ✅ |
| Close to graduation | | ✅ | ✅ |
| Open uni population | | ✅ | |
| Evaluation overview | | ✅ | |
| Courses | | ✅ | ✅ |
\* = anyone who can log in to Oodikone, check the [IAM groups](#iam-groups) section for more information
@@ -66,8 +66,6 @@ A user must be a member of either the `grp-oodikone-basic-users` or `grp-oodikon
| grp-oodikone-basic-users | courseStatistics |
| grp-oodikone-users | facultyStatistics |
| grp-toska | admin |
| 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',
},
},
},
]
+750 -794
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')
+1371 -300
View File
File diff suppressed because it is too large Load Diff
+18 -18
View File
@@ -5,46 +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",
"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",
"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
View File
@@ -3,7 +3,6 @@ export const roles = [
'courseStatistics',
'facultyStatistics',
'fullSisuAccess',
'katselmusViewer',
'openUniSearch',
'studyGuidanceGroups',
'teachers',
+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,
+9 -6
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.set(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!')
}
@@ -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 () => {
@@ -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,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 {
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,4 +1,3 @@
/* eslint-disable import/no-cycle */
import { InferAttributes } from 'sequelize'
import { Column, CreatedAt, DataType, ForeignKey, Model, 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, 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,4 +1,3 @@
/* eslint-disable import/no-cycle */
import { InferAttributes } from 'sequelize'
import { Column, CreatedAt, DataType, ForeignKey, Model, Table, UpdatedAt } from 'sequelize-typescript'
+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,
-1
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'
-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'
+2 -2
View File
@@ -1,8 +1,8 @@
/* 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 } from '../shared/language'
import { 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') {
+80 -88
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'
@@ -43,34 +43,31 @@ interface GetStatsRequest extends Request {
}
}
router.get(
'/:id/basicstats',
auth.roles(['facultyStatistics', 'katselmusViewer']),
async (req: GetStatsRequest, res: Response) => {
const code = req.params.id
if (!code) {
return res.status(422).end()
}
const { year_type: yearType, programme_filter: programmeFilter, special_groups: specialGroups } = req.query
const data = await getBasicStats(code, yearType, programmeFilter, specialGroups)
if (data) {
return res.json(data)
}
const programmes = await getDegreeProgrammesOfFaculty(code, programmeFilter === 'NEW_STUDY_PROGRAMMES')
if (!programmes.length) {
return res.status(422).end()
}
// TODO: The types could be handled better. Possible null value is causing issues with TS.
let updatedStats: any = await combineFacultyBasics(code, programmes, yearType, specialGroups)
if (updatedStats) {
updatedStats = await setBasicStats(updatedStats, yearType, programmeFilter, specialGroups)
}
return res.json(updatedStats)
router.get('/:id/basicstats', auth.roles(['facultyStatistics']), async (req: GetStatsRequest, res: Response) => {
const facultyId = req.params.id
const code = await getFacultyCodeById(facultyId)
if (!code) {
return res.status(422).end()
}
)
const { year_type: yearType, programme_filter: programmeFilter, special_groups: specialGroups } = req.query
const data = await getBasicStats(code, yearType, programmeFilter, specialGroups)
if (data) {
return res.json(data)
}
const programmes = await getDegreeProgrammesOfFaculty(code, programmeFilter === 'NEW_STUDY_PROGRAMMES')
if (!programmes.length) {
return res.status(422).end()
}
// TODO: The types could be handled better. Possible null value is causing issues with TS.
let updatedStats: any = await combineFacultyBasics(code, programmes, yearType, specialGroups)
if (updatedStats) {
updatedStats = await setBasicStats(updatedStats, yearType, programmeFilter, specialGroups)
}
return res.json(updatedStats)
})
interface GetCreditStatsRequest extends Request {
query: {
@@ -78,44 +75,38 @@ interface GetCreditStatsRequest extends Request {
}
}
router.get(
'/:id/creditstats',
auth.roles(['facultyStatistics', 'katselmusViewer']),
async (req: GetCreditStatsRequest, res: Response) => {
const code = req.params.id
const { year_type: yearType } = req.query
const stats = await getFacultyCredits(code, yearType === 'ACADEMIC_YEAR')
return res.json(stats)
router.get('/:id/creditstats', auth.roles(['facultyStatistics']), async (req: GetCreditStatsRequest, res: Response) => {
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)
})
router.get('/:id/thesisstats', auth.roles(['facultyStatistics']), async (req: GetStatsRequest, res: Response) => {
const facultyId = req.params.id
const code = await getFacultyCodeById(facultyId)
if (!code) {
return res.status(422).end()
}
)
router.get(
'/:id/thesisstats',
auth.roles(['facultyStatistics', 'katselmusViewer']),
async (req: GetStatsRequest, res: Response) => {
const code = req.params.id
if (!code) {
return res.status(422).end()
}
const { year_type: yearType, programme_filter: programmeFilter, special_groups: specialGroups } = req.query
const data = await getThesisWritersStats(code, yearType, programmeFilter, specialGroups)
if (data) {
return res.json(data)
}
const programmes = await getDegreeProgrammesOfFaculty(code, programmeFilter === 'NEW_STUDY_PROGRAMMES')
if (!programmes.length) {
return res.status(422).end()
}
let updatedStats: any = await combineFacultyThesisWriters(code, programmes, yearType, specialGroups)
if (updatedStats) {
updatedStats = await setThesisWritersStats(updatedStats, yearType, programmeFilter, specialGroups)
}
return res.json(updatedStats)
const { year_type: yearType, programme_filter: programmeFilter, special_groups: specialGroups } = req.query
const data = await getThesisWritersStats(code, yearType, programmeFilter, specialGroups)
if (data) {
return res.json(data)
}
)
const programmes = await getDegreeProgrammesOfFaculty(code, programmeFilter === 'NEW_STUDY_PROGRAMMES')
if (!programmes.length) {
return res.status(422).end()
}
let updatedStats: any = await combineFacultyThesisWriters(code, programmes, yearType, specialGroups)
if (updatedStats) {
updatedStats = await setThesisWritersStats(updatedStats, yearType, programmeFilter, specialGroups)
}
return res.json(updatedStats)
})
interface GetGraduationStatsRequest extends Request {
query: {
@@ -125,9 +116,10 @@ interface GetGraduationStatsRequest extends Request {
router.get(
'/:id/graduationtimes',
auth.roles(['facultyStatistics', 'katselmusViewer']),
auth.roles(['facultyStatistics']),
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()
}
@@ -160,9 +152,10 @@ interface GetProgressStatsRequest extends Request {
router.get(
'/:id/progressstats',
auth.roles(['facultyStatistics', 'katselmusViewer']),
auth.roles(['facultyStatistics']),
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()
}
@@ -190,9 +183,10 @@ interface GetStudentStatsRequest extends GetProgressStatsRequest {}
router.get(
'/:id/studentstats',
auth.roles(['facultyStatistics', 'katselmusViewer']),
auth.roles(['facultyStatistics']),
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()
}
@@ -225,9 +219,10 @@ interface GetUpdateBasicViewRequest extends Request {
router.get(
'/:id/update_basicview',
auth.roles(['facultyStatistics', 'katselmusViewer']),
auth.roles(['facultyStatistics']),
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()
}
@@ -243,23 +238,20 @@ router.get(
}
)
router.get(
'/:id/update_progressview',
auth.roles(['facultyStatistics', 'katselmusViewer']),
async (req: Request, res: Response) => {
const code = req.params.id
if (!code) {
return res.status(422).end()
}
try {
const result = await updateFacultyProgressOverview(code)
return res.json(result)
} catch (error) {
const message = `Failed to update progress tab stats for faculty ${code}`
logger.error(message, { error })
return res.status(500).json({ error: message })
}
router.get('/:id/update_progressview', auth.roles(['facultyStatistics']), async (req: Request, res: Response) => {
const facultyId = req.params.id
const code = await getFacultyCodeById(facultyId)
if (!code) {
return res.status(422).end()
}
)
try {
const result = await updateFacultyProgressOverview(code)
return res.json(result)
} catch (error) {
const message = `Failed to update progress tab stats for faculty ${code}`
logger.error(message, { error })
return res.status(500).json({ error: message })
}
})
export default router
+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
+25 -82
View File
@@ -5,19 +5,18 @@ 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'
@@ -57,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)
@@ -89,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: {
@@ -117,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)
@@ -141,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)
}
@@ -158,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)
})
@@ -173,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
@@ -202,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}` : ''}`
@@ -211,73 +223,4 @@ router.get('/:id/update_studytrackview', async (req: GetUpdateViewRequest, res:
}
})
interface GetEvaluationStatsRequest extends Request {
query: {
graduated: Graduated
year_type: YearType
special_groups: SpecialGroups
}
}
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
+27 -9
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,14 +45,19 @@ 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 {
@@ -55,7 +67,7 @@ interface PostTopTeachersRequest extends Request {
}
}
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
@@ -63,7 +75,7 @@ router.post('/top', async (req: PostTopTeachersRequest, res: Response) => {
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)
})
@@ -77,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
@@ -86,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')
}
@@ -114,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
+5 -5
View File
@@ -80,7 +80,7 @@ router.get('/refresh_redis_cache', async (req: Request, res: Response) => {
}
})
router.post('/refresh-teacher-leaderboard', async (req: Request, res: Response) => {
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`
)
@@ -90,17 +90,17 @@ router.post('/refresh-teacher-leaderboard', async (req: Request, res: Response)
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.' })
@@ -108,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 -3
View File
@@ -4,8 +4,7 @@ import { roles } from '../config/roles'
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_CODES, Language } from '../shared/language'
import logger from '../util/logger'
const router = Router()
@@ -15,7 +14,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)
})
@@ -17,7 +17,7 @@ const createRedisKeyForCreditStats = (id: string, yearType: YearType, specialGro
const createRedisKeyForGraduationStats = (id: string, yearType: YearType, specialGroups: SpecialGroups) => {
return `GRADUATION_STATS_${id}_${yearType}_${specialGroups}`
}
const createRedisKeyForStudytrackStats = (id: string, graduated: Graduated, specialGroups: SpecialGroups) => {
const createRedisKeyForStudyTrackStats = (id: string, graduated: Graduated, specialGroups: SpecialGroups) => {
return `STUDYTRACK_STATS_${id}_${graduated}_${specialGroups}`
}
@@ -34,8 +34,8 @@ export const getBasicStats = async (
yearType: YearType,
specialGroups: SpecialGroups
) => {
const searchkey = combinedProgramme ? `${id}-${combinedProgramme}` : id
const redisKey = createRedisKeyForBasicStats(searchkey, yearType, specialGroups)
const searchKey = combinedProgramme ? `${id}-${combinedProgramme}` : id
const redisKey = createRedisKeyForBasicStats(searchKey, yearType, specialGroups)
const dataFromRedis = await redisClient.get(redisKey)
if (!dataFromRedis) {
return null
@@ -102,8 +102,8 @@ export const getGraduationStats = async (
yearType: YearType,
specialGroups: SpecialGroups
) => {
const searchkey = combinedProgramme ? `${id}-${combinedProgramme}` : id
const redisKey = createRedisKeyForGraduationStats(searchkey, yearType, specialGroups)
const searchKey = combinedProgramme ? `${id}-${combinedProgramme}` : id
const redisKey = createRedisKeyForGraduationStats(searchKey, yearType, specialGroups)
const dataFromRedis = await redisClient.get(redisKey)
if (!dataFromRedis) {
return null
@@ -129,14 +129,14 @@ export const setGraduationStats = async (data, yearType: YearType, specialGroups
return dataToRedis
}
export const getStudytrackStats = async (
export const getStudyTrackStats = async (
id: string,
combinedProgramme: string | null,
graduated: Graduated,
specialGroups: SpecialGroups
) => {
const searchkey = combinedProgramme ? `${id}-${combinedProgramme}` : id
const redisKey = createRedisKeyForStudytrackStats(searchkey, graduated, specialGroups)
const searchKey = combinedProgramme ? `${id}-${combinedProgramme}` : id
const redisKey = createRedisKeyForStudyTrackStats(searchKey, graduated, specialGroups)
const dataFromRedis = await redisClient.get(redisKey)
if (!dataFromRedis) {
return null
@@ -144,9 +144,9 @@ export const getStudytrackStats = async (
return JSON.parse(dataFromRedis)
}
export const setStudytrackStats = async (data, graduated: Graduated, specialGroups: SpecialGroups) => {
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',
@@ -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 { Name } 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 = {
@@ -51,12 +46,11 @@ type Group = {
students: GroupStudents
enrollments: { studentNumber: string; enrollmentDateTime: Date }[]
allEnrollments: { studentNumber: string; enrollmentDateTime: Date }[]
yearcode: number
yearCode: number
}
type Student = {
earliestAttainment: Date
category: 'passedFirst' | 'passedEventually' | 'neverPassed'
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)
@@ -262,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)
}
}
@@ -331,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 }) => {
@@ -350,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)) {
@@ -368,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
@@ -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
+46 -5
View File
@@ -1,9 +1,12 @@
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 {
@@ -146,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 },
})
@@ -169,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) {
@@ -214,7 +226,7 @@ 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 => {
@@ -226,11 +238,20 @@ const getYearlyStatsOfNew = async (
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 && courseForSubs.substitutions && courseForSubs.substitutions.length > 0) {
if (combineSubstitutions && courseForSubs?.substitutions && courseForSubs.substitutions.length > 0) {
substitutionCourses = await Course.findAll({
where: {
code: {
@@ -241,10 +262,15 @@ const getYearlyStatsOfNew = async (
})
}
if (serviceProvider === 'fd')
logger.info(
`Debugging c.y.s., getYearlyStatsOfNew, code ${courseCode} unification ${unification}: got substitutionCourses ${substitutionCourses?.length} items.`
)
return {
...statistics,
coursecode: courseCode,
alternatives: substitutionCourses || [{ code: courseForSubs!.code, name: courseForSubs!.name }],
alternatives: substitutionCourses ?? [{ code: courseForSubs!.code, name: courseForSubs!.name }],
name: course?.name,
}
}
@@ -317,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 => {
@@ -331,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(
@@ -362,6 +401,8 @@ export const getCourseYearlyStats = async (
})
)
if (serviceProvider === 'fd') logger.info('Debugging c.y.s.: finished creating statsRegular')
return statsRegular
}
@@ -109,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,4 +1,4 @@
import { Name } from '../../types'
import { Name } from '../../shared/types'
import { getCreditsProduced } from '../providerCredits'
import { isRelevantProgramme } from '../studyProgramme/studyProgrammeHelpers'
import { getDegreeProgrammesOfFaculty } from './faculty'
@@ -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,6 +1,7 @@
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 { FacultyProgressData } from './facultyStudentProgress'
@@ -192,11 +193,7 @@ export const setGraduationStats = async (data: GraduationData, programmeFilter:
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.get(redisKey)
if (!dataFromRedis) {
@@ -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: {
@@ -36,7 +36,7 @@ export const getOpenUniSearches = async (userId: string) => {
export const createNewSearch = async (userId: string, name: string, courseCodes: string[]) => {
const savedSearches = await getOpenUniSearchesByUser(userId)
if (savedSearches && savedSearches.some(search => search.name === name)) {
if (savedSearches?.some(search => search.name === name)) {
return null
}
try {
@@ -2,7 +2,7 @@ import { InferAttributes, Op, WhereOptions } from 'sequelize'
import { Course, Credit, Enrollment, Student, SISStudyRight } from '../../models'
import { OpenUniPopulationSearch } from '../../models/kone'
import { ExtentCode } from '../../types'
import { EnrollmentState, ExtentCode } from '../../types'
import { formatCourseInfo, formatOpenCredits, formatOpenEnrollments, formatStudentInfo } from './format'
export const getCredits = async (courseCodes: string[], startdate: Date) =>
@@ -47,6 +47,7 @@ export const getEnrollments = async (courseCodes: string[], startDate: Date, end
[Op.gte]: startDate,
},
},
state: EnrollmentState.ENROLLED,
},
})
).map(formatOpenEnrollments)
@@ -28,7 +28,7 @@ type StudentStats = {
const uniq = objects => [...new Set(objects)]
const calculateTotalsForStudent = async (studentStats: StudentStats, studentNumber: string) => {
const calculateTotalsForStudent = (studentStats: StudentStats, studentNumber: string) => {
Object.keys(studentStats[studentNumber].courseInfo).forEach(course => {
const { status } = studentStats[studentNumber].courseInfo[course]
if (status.passed) {
@@ -143,7 +143,7 @@ export const getCustomOpenUniCourses = async (courseCodes: string[], startDate:
updateUnfinishedStatus(studentCourses[courseCode], enrollmentDateTime)
}
}
await calculateTotalsForStudent(studentStats, studentNumber)
calculateTotalsForStudent(studentStats, studentNumber)
}
const openUniStats = { students: studentStats, courses }
return openUniStats

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