Compare commits

...
626 Commits
Author SHA1 Message Date
Valtteri Kantanen 54f47b8607 [Updater] Remove three attainments from excluded partial attainments CSV file that shouldn't be excluded (#4396) 2025-02-03 13:02:16 +02:00
Valtteri Kantanen db2649be6f [Filters] Fix programme filter to correctly check active study right (#4723) 2025-02-03 12:32:32 +02:00
Valtteri Kantanen 17a07de1f0 [Updater] Upgrade vulnerable @sentry/node version 2025-02-03 12:12:12 +02:00
Valtteri Kantanen 3aa6892bff [Frontend] Hide placeholder text for most column filters in material-react-table 2025-02-03 12:08:00 +02:00
Joonas Ahovalli 92405c3561 [Courses Tab] update tooltip 2025-02-01 00:47:43 +02:00
Joonas Ahovalli cfc2658078 [Courses Tab] Refactor, add HOPS icons, fix memoization & incorrect counting of completions 2025-02-01 00:45:53 +02:00
Valtteri Kantanen 209feebb5d [Class statistics] Display "Show Bachelor + Master" to all users instead of just admins 2025-01-31 16:12:25 +02:00
Valtteri Kantanen 0fb7a94eb1 [Class statistics] Change "showFullStudyPath" to "showBachelorAndMaster" everywhere 2025-01-31 16:10:39 +02:00
Valtteri KantanenandGitHub e80c0aaf85 Merge pull request #4728 from UniversityOfHelsinkiCS/renovate/all-minor-patch
Update all non-major dependencies
2025-01-31 13:33:47 +02:00
renovate[bot]andGitHub 8dda45da48 Update all non-major dependencies 2025-01-31 11:23:03 +00:00
Valtteri Kantanen e5a85500b3 Fix eslint errors 2025-01-31 13:21:07 +02:00
Valtteri Kantanen a4b8844c77 Upgrade package gelf-pro from version 1.3.12 to version 1.4.0 2025-01-31 13:14:21 +02:00
Valtteri Kantanen 7696e5bcd8 [Course statistics] Fix faculty statistics calculations 2025-01-31 12:53:43 +02:00
Valtteri Kantanen 486561789b Upgrade Redis image version in docker-compose files from 6.2.16 to 7.2.6 to match production version 2025-01-31 12:48:30 +02:00
Valtteri Kantanen 28fc185a2a Exclude Docker images from Renovate updates
We want the local Docker versions to match production versions as well as possible so automatic updates aren't good
2025-01-31 12:39:26 +02:00
Valtteri KantanenandGitHub db59d0e138 Merge pull request #4716 from UniversityOfHelsinkiCS/renovate/cypress-14.x
Update dependency cypress to v14
2025-01-31 11:43:49 +02:00
Valtteri KantanenandGitHub 1687f0a0c6 Merge pull request #4719 from UniversityOfHelsinkiCS/renovate/npm-vite-vulnerability
Update dependency vite to v6.0.9 [SECURITY]
2025-01-31 11:42:56 +02:00
Valtteri KantanenandGitHub 6ac6ad687e Merge pull request #4724 from UniversityOfHelsinkiCS/renovate/npm-sentry-node-vulnerability
Update dependency @sentry/node to v8.49.0 [SECURITY]
2025-01-31 11:42:08 +02:00
renovate[bot]andGitHub 5e1e71b559 Update dependency @sentry/node to v8.49.0 [SECURITY] 2025-01-28 21:58:26 +00:00
Joonas Ahovalli 66b5e9a65d [Completed courses of students] Display an icon for courses that are in students' HOPS but have no enrollment or completion 2025-01-28 12:08:29 +02:00
Joonas Ahovalli 30b1a31bb3 [Backend] completedCoursesSearch additionally fetches list of courses in student's HOPS 2025-01-28 10:51:00 +02:00
Santeri Rajamäki d6765bd7d6 [Class statistics] Improved performance of "Courses of Class" tab
Closes #4715
2025-01-27 16:21:12 +02:00
Valtteri Kantanen 5ce3337977 Fix Markdown error in documentation 2025-01-22 15:15:34 +02:00
Valtteri Kantanen c5e24c7409 Add documentation for db schema updates 2025-01-22 14:58:30 +02:00
Joonas Ahovalli 34f029da9e [Close to graduation] Show preferred language for student 2025-01-22 14:52:13 +02:00
Joonas Ahovalli e95414806c [Updater] add preferred language to student model 2025-01-22 12:44:03 +02:00
renovate[bot]andGitHub 456b420321 Update dependency vite to v6.0.9 [SECURITY] 2025-01-21 21:22:24 +00:00
Riku Rauhala 0eb87ea44a [Hooks] Extract remaining hooks
All hooks are now converted into TypeScript and stored in @/hooks
2025-01-20 13:53:09 +02:00
Riku Rauhala 454fabeb1d [Hooks] Extract hook useSemanticTabs
Renaming this for clarity. This will be replaced by the new useTabs hook for all MUI components
2025-01-20 12:52:04 +02:00
Riku Rauhala d3f022adc5 [Hooks] Extract hook useDeepMemo 2025-01-20 12:46:15 +02:00
Riku Rauhala 8efe3d2aa3 [Hooks] Extract hook useTabChangeAnalytics 2025-01-20 12:40:48 +02:00
Riku Rauhala d3003d5797 [Hooks] Extract hook useProgress 2025-01-20 12:36:45 +02:00
Riku Rauhala 0dd5f32f96 [Hooks] Extract hook useDidMount 2025-01-20 12:31:09 +02:00
Riku Rauhala 85b307b306 [Hooks] Extract hook useLocalStorage 2025-01-20 12:09:48 +02:00
Riku Rauhala 73eadbd1b1 [Hooks] Remove duplicate toggle hook 2025-01-20 11:59:12 +02:00
Riku Rauhala b294c560e4 [Hooks] Extract hook useTitle 2025-01-20 11:56:41 +02:00
Riku Rauhala 455d9dc532 [Hooks] Rename debouncedState for consistency 2025-01-20 11:47:32 +02:00
Riku RauhalaandGitHub df1049a436 Merge pull request #4717 from UniversityOfHelsinkiCS/material-ui-course-search
Material UI: Course search
2025-01-20 10:56:49 +02:00
Riku Rauhala ac22df8ced [Course search] Hide selected list when not selecting multiple 2025-01-20 09:52:28 +02:00
Riku Rauhala 1fd987221d [Course statistics] Update tests 2025-01-20 09:43:25 +02:00
Riku Rauhala 45985f2c44 [CourseStatistics] Convert utils into TypeScript 2025-01-17 21:27:47 +02:00
Riku Rauhala 159e11a508 [CourseStatistics] Convert to TypeScript 2025-01-17 21:08:46 +02:00
Riku Rauhala 5ff68533d7 [Courses search] Add circular progress when fetching 2025-01-17 21:04:04 +02:00
Riku Rauhala 61f20173a3 [Courses search] Place the toggles back to their original location
This seems to work better than having the toggles below
2025-01-17 20:26:56 +02:00
Riku Rauhala 987f1ec42b [Courses search] Refactor toggles 2025-01-17 20:08:19 +02:00
Riku Rauhala 3f4c39600b [Course search] Add missing search icons 2025-01-17 19:54:02 +02:00
Riku Rauhala 88ef490b1b [CourseStatistics] Rewrite course search with Material UI 2025-01-17 18:34:57 +02:00
renovate[bot]andGitHub dbc8b684c2 Update dependency cypress to v14 2025-01-17 01:32:19 +00:00
Riku Rauhala 7e135d4643 [Types] Extract type SearchHistoryItem 2025-01-16 10:03:10 +02:00
Riku Rauhala 06a8760701 [Hooks] Extract hook useSearchHistory 2025-01-15 19:16:42 +02:00
Riku Rauhala 5eaa035f1d [Hooks] Extract hook useToggle 2025-01-15 18:05:50 +02:00
Riku Rauhala ae1f0ceeb7 [Redux] Type courseSearch 2025-01-15 17:56:04 +02:00
Riku Rauhala 3ca82286b4 [Redux] Type courseStats 2025-01-15 17:09:22 +02:00
Riku Rauhala 63add43c0f [Course statistics] Move components under pages 2025-01-15 10:56:44 +02:00
Rajamäki Santeri J 72440609b4 [Study Programme] Fixed Cypress broken tests 2025-01-13 18:14:59 +02:00
Rajamäki Santeri J c946cd2405 [Study Programme] Added study right thesis counting
Closing issue #4712
2025-01-13 17:56:51 +02:00
Valtteri Kantanen 96d316ebf5 [Frontend] Rewrite Updater page with MUI components 2025-01-13 15:43:32 +02:00
Riku Rauhala b3eb7dceac [Custom population] Fix an info box text
There was an extra ' character
2025-01-10 14:18:07 +02:00
Valtteri Kantanen ee02d002ba [Course statistics] Fix tests 2025-01-10 13:16:22 +02:00
Valtteri Kantanen 34448d27d0 [Frontend] Remove unused components 2025-01-10 13:03:05 +02:00
Valtteri Kantanen 39ce64edce [Course statistics] Move minimum and maximum attainment date calculation to the backend 2025-01-10 13:02:46 +02:00
Valtteri Kantanen 0d210a607b [Course statistics] Fix a bug in the search form where every keypress triggered a new backend request 2025-01-10 13:01:23 +02:00
Valtteri Kantanen f1a6302928 [Course statistics] Use RTK Query for course search 2025-01-10 12:59:25 +02:00
Valtteri Kantanen 359db8f365 [Frontend] Add custom useDebouncedState hook 2025-01-10 12:58:12 +02:00
Valtteri Kantanen 387a3e0ef2 [Frontend] Refactor validateInputLength to always return a boolean 2025-01-10 12:56:47 +02:00
Valtteri Kantanen 4c957b347b Remove automerge from renovate config as it doesn't seem to work properly 2025-01-09 10:41:35 +02:00
Valtteri KantanenandGitHub 10d3d5aeed Merge pull request #4711 from UniversityOfHelsinkiCS/OOD-80
[Frontend] Added env variable for sisu url and changed to use that instead of hardcoded sisu urls
2025-01-09 10:26:10 +02:00
Matti Räsänen 6e365d4a23 Removed ending slash from SIS_URL in fd ci compose 2025-01-08 09:08:32 +02:00
Matti Räsänen 68292a45d0 [FD compose] Changed SIS_HOST to SIS_URL in Dockerfile and added SIS_URL to fd ci compose as well 2025-01-07 16:17:25 +02:00
Matti Räsänen f0088cf7a0 [Frontend] Added env variable for sisu url and changed to use that instead of hardcoded sisu urls 2025-01-07 16:03:09 +02:00
Valtteri KantanenandGitHub b8469a242e Merge pull request #4595 from UniversityOfHelsinkiCS/renovate/axios-1.x
Update dependency axios to v1
2025-01-07 15:21:27 +02:00
Valtteri Kantanen 8c5d5f1ee9 [General tab] Move Bachelor/Master column more to the left 2025-01-07 15:13:31 +02:00
Valtteri Kantanen 80759cacee [Frontend] Fix key spread error on RangeSelector 2025-01-07 14:25:38 +02:00
renovate[bot]andGitHub 4df2cc78ff Update dependency axios to v1 2025-01-07 11:58:36 +00:00
Valtteri Kantanen 8b34f7a5f9 [Faculties] Use StyledTable instead of regular Table 2025-01-07 13:55:09 +02:00
Valtteri Kantanen e81d6eb877 [Faculties] Fix tests 2025-01-07 10:58:32 +02:00
Valtteri Kantanen 0c47d7c21c [Faculties] Combine 'Students by starting year' and 'Progress' tabs 2025-01-07 10:46:43 +02:00
Valtteri KantanenandGitHub 175996235d Merge pull request #4705 from UniversityOfHelsinkiCS/renovate/all-minor-patch
Update all non-major dependencies
2025-01-07 09:45:05 +02:00
Valtteri Kantanen 42409d555e [Students] Add better styles to study rights table 2025-01-02 16:14:52 +02:00
Valtteri Kantanen 23b1a02d42 [Filters] Fix Programme filter showing wrong numbers
This happened in those rare cases where a student had multiple study rights for the same programme
2025-01-02 15:51:20 +02:00
Valtteri Kantanen 1be53368c6 Delete old JS version of Routes 2024-12-19 16:35:23 +02:00
Valtteri Kantanen 52e0d28a4d Remove unused static text file 2024-12-19 16:25:33 +02:00
Valtteri Kantanen 83114a26f9 [Frontend] Simplify route handling 2024-12-19 15:56:01 +02:00
Valtteri KantanenandGitHub bdc09f86e6 Merge pull request #4708 from UniversityOfHelsinkiCS/mui-completed-courses-search
Material UI: Completed courses search
2024-12-19 15:31:24 +02:00
Valtteri Kantanen 6dd9679267 [Completed courses search] Delete old components 2024-12-19 15:07:28 +02:00
Valtteri Kantanen 5fb7168d17 [Completed courses search] Update tests 2024-12-19 15:07:11 +02:00
Valtteri Kantanen 6ac4b6fb3d [Completed courses search] Rewrite with MUI 2024-12-19 15:01:17 +02:00
Valtteri Kantanen 1a8de4344c [Frontend] Rewrite SearchHistory with MUI 2024-12-19 15:00:29 +02:00
Valtteri Kantanen 1493725699 [Frontend] Rewrite RightsNotification with MUI 2024-12-19 14:57:02 +02:00
Valtteri Kantanen 6cff94e2ca [Completed courses search] Add RTK Query tag for CompletedCoursesSearchList 2024-12-19 14:37:29 +02:00
Valtteri KantanenandGitHub d51a70e259 Merge pull request #4706 from UniversityOfHelsinkiCS/frontend-build-more-max-old-space-size
[Frontend] Added max-old-space-size to package.json build
2024-12-18 13:15:26 +02:00
Valtteri Kantanen 7f84c0b9f0 Update documentation on access rights 2024-12-18 13:12:49 +02:00
Valtteri Kantanen df555ca93f Remove all remaining references to katselmusViewer role 2024-12-18 13:08:01 +02:00
Matti Räsänen b68b98b62c [Frontend] Added max-old-space-size to package.json build 2024-12-17 13:12:29 +02:00
renovate[bot]andGitHub 640dc741a4 Update all non-major dependencies 2024-12-17 08:19:50 +00:00
Valtteri Kantanen 5d02b839ee Update tests after updating students in the test db 2024-12-17 08:34:51 +02:00
Valtteri Kantanen ae60a38445 Handle cases where a student has multiple citizenships 2024-12-16 16:02:20 +02:00
Valtteri Kantanen 83d4bd26c4 Reset document title on component unmount 2024-12-16 15:56:55 +02:00
Valtteri Kantanen 481b351276 [Populations] Remove unnecessary state 2024-12-16 15:27:59 +02:00
Valtteri Kantanen 23cb8c8ee5 [Class statistics] Fix credit accumulation graph 2024-12-16 15:27:20 +02:00
Valtteri Kantanen 9bcdd81772 Update ESLint configuration 2024-12-16 15:26:58 +02:00
Valtteri KantanenandGitHub 53e443142d Merge pull request #4704 from UniversityOfHelsinkiCS/mui-students
Material UI: Students
2024-12-16 15:18:21 +02:00
Valtteri Kantanen da72107eb9 Fix tests 2024-12-16 15:02:59 +02:00
Valtteri Kantanen d2cbb89eb9 Add data-cy attribute to CurriculumPicker 2024-12-16 14:00:21 +02:00
Valtteri Kantanen 3bf0c60378 [Students] Fix tests 2024-12-16 13:58:36 +02:00
Valtteri Kantanen 59a9854ed5 Add optional cypress prop to ExternalLink 2024-12-16 13:54:58 +02:00
Valtteri Kantanen b70bc09359 [Students] Enhance student search with uninitialized state alert 2024-12-16 13:52:52 +02:00
Valtteri Kantanen 8ef82d01b1 [Students] Remove old Semantic UI components 2024-12-16 12:13:59 +02:00
Valtteri Kantanen ac683175a8 [Students] Sort students by date of university enrollment in search results 2024-12-16 11:59:03 +02:00
Valtteri Kantanen 5e86967627 Use correct components in the /students route 2024-12-16 11:58:09 +02:00
Valtteri Kantanen d971bb993f [Students] Rewrite student page with MUI 2024-12-16 11:55:54 +02:00
Valtteri Kantanen 2328f2deff [Students] Rewrite search page with MUI 2024-12-16 11:53:15 +02:00
Valtteri Kantanen ddeb65abef Rewrite CurriculumPicker with MUI 2024-12-16 11:50:59 +02:00
Valtteri Kantanen 0871d8ecbd Remove minWidth from TableHeaderWithTooltip component 2024-12-16 11:49:35 +02:00
Valtteri Kantanen 3b8aec2fef Add variant prop to ExternalLink component for customizable typography 2024-12-16 11:48:36 +02:00
Valtteri Kantanen 71dc06728f Create StyledTable 2024-12-16 11:36:55 +02:00
Valtteri Kantanen 9484ecb456 Create StyledAccordion 2024-12-16 11:36:25 +02:00
Valtteri Kantanen 25319d1c8f Rewrite StudentNameVisibilityToggle with MUI 2024-12-16 11:35:51 +02:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>Valtteri Kantanen
e7fd54b443 Update all non-major dependencies (#4703)
* Update all non-major dependencies

* Fix ESLint warnings

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Valtteri Kantanen <valtteri.kantanen@iki.fi>
2024-12-16 09:24:15 +02:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
aed2bc154b Update dependency axios to ^0.29.0 (#4684)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-15 15:59:19 +02:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
09a991669c Update dependency scheduler to ^0.25.0 (#4694)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-15 15:58:24 +02:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
41325bfa12 Update dependency @types/node to v22 (#4701)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-15 15:51:51 +02:00
Ilkka LappeteläinenandGitHub 17a1d3718f Updated react-router-dom from v5 to v7 (#4700) 2024-12-15 15:04:42 +02:00
Valtteri Kantanen 8b9c5b8789 Remove schedule from Renovate config 2024-12-12 14:12:42 +02:00
Valtteri Kantanen e11609c335 [Faculties] Students by starting year: Put cell content always on one line 2024-12-12 14:02:32 +02:00
Valtteri Kantanen 3a5b3c2d65 [Faculties] Move state from parent to child 2024-12-12 14:00:15 +02:00
Valtteri Kantanen 33916312b5 [Frontend] Make the toggle with two labels always have the same colors 2024-12-12 13:59:36 +02:00
Riku RauhalaandGitHub dab8a2679f Merge pull request #4698 from UniversityOfHelsinkiCS/faculties-accordion
[Faculties] Wrap progress sections into accordions
2024-12-12 12:49:51 +02:00
Riku Rauhala 390334c820 [Faculties] Update tests 2024-12-12 12:08:36 +02:00
Riku Rauhala 356ffc953e [Faculties] Wrap progress sections into accordions 2024-12-12 11:58:10 +02:00
Riku Rauhala a2f3a5de61 [Class statistics] Credit statistics: Change default sorting order to descending 2024-12-12 10:56:33 +02:00
Riku Rauhala 635a3fbcbc [Toggles] Extract component ToggleContainer
Wrapping all Toggles for a consistent layout
2024-12-12 10:51:41 +02:00
Riku Rauhala 7ebcb86924 [Toggle] Use bold font for the active option 2024-12-12 10:44:16 +02:00
Riku Rauhala eb1781e6af [Faculties] Add missing Code title and tweak margin 2024-12-11 15:05:55 +02:00
Riku Rauhala 6df983642b [Faculties] Add mini info button for sorting the data table 2024-12-11 14:41:52 +02:00
Riku Rauhala 634c4c9a97 [Navigation bar] Improve nav button visibility 2024-12-11 14:08:35 +02:00
Riku Rauhala 9371c7d2ab [Frontend] Rearrange Material components
Let's move components that do not have other files associated with them away from index.tsx files
2024-12-11 09:33:41 +02:00
Riku RauhalaandGitHub f6678739ad Merge pull request #4695 from UniversityOfHelsinkiCS/material-ui-faculties
Material UI: Faculties
2024-12-10 15:40:41 +02:00
Riku Rauhala 2882dd25c6 [Faculties] Skip export tests 2024-12-10 15:23:09 +02:00
Riku Rauhala ed3166d08f [Faculties] Update tests 2024-12-10 15:00:14 +02:00
Valtteri Kantanen 39340108f2 [Updater] Replace country_fi, country_sv, and country_en with citizenships array 2024-12-10 13:39:24 +02:00
Riku Rauhala 94f037b6af [Faculties] Remove Semantic UI components
Saving legacy GraduationTimes for now as it is still being used in Study track overview
2024-12-09 20:09:36 +02:00
Riku Rauhala 090e81a432 [Faculties] Add missing info boxes 2024-12-09 20:01:04 +02:00
Riku Rauhala 6e8ae9b320 [FacultyStudentDataTable] Rewrite with MUI 2024-12-09 19:45:50 +02:00
Riku Rauhala 0db094655e [PopulationLink] Rewrite with MUI 2024-12-09 17:44:25 +02:00
Riku Rauhala c437ed4e96 [Faculties] Refactor and fix exports 2024-12-09 16:55:42 +02:00
Riku Rauhala ed5a778e50 [Faculties] Extract utils 2024-12-09 16:21:30 +02:00
Riku Rauhala 13633ce11a [Faculties] Extract type DegreeProgramme 2024-12-09 15:13:24 +02:00
Riku Rauhala eab47657e6 [Backend] Fix type imports 2024-12-09 15:09:32 +02:00
Riku Rauhala 35267e8b6d [Faculties] Rename BasicInformationTab for consistency 2024-12-09 15:06:03 +02:00
Riku Rauhala 15cc9cef4a [Faculties] Rewrite the Progress tab with MUI 2024-12-09 14:30:34 +02:00
Riku Rauhala f5e1e7267f [Faculties] Rewrite Students by starting year tab with MUI 2024-12-09 13:51:51 +02:00
Riku Rauhala c6eaa18a1f [Faculties] Rewrite the Update statistics tab with MUI 2024-12-09 11:13:52 +02:00
Riku Rauhala 63e16e4332 [Faculties] Rewrite Graduation times tab with MUI 2024-12-09 09:13:58 +02:00
Riku Rauhala 074e41de59 [Section] Add optional Export button
Also, let's specify a color (deep purple) to be associated with export functionality
2024-12-06 16:24:27 +02:00
Riku Rauhala 46b6686f4b [Faculty graduations] Fix types 2024-12-06 15:46:13 +02:00
Riku Rauhala ede94708d5 [Faculties] Type faculty RTK API 2024-12-05 21:19:13 +02:00
Riku Rauhala f20953c8d2 [FacultyGraduations] Generalize name
The name refers to names of faculties in the university page and to programmes in faculties page
2024-12-05 20:29:43 +02:00
Riku Rauhala 1e728593ea [University] Fix layout 2024-12-05 19:36:14 +02:00
Riku Rauhala 21f7fcc6d2 [Faculties] Rewrite Basic information tab with MUI 2024-12-05 13:45:23 +02:00
Riku Rauhala 20c026aa56 [Section] Remove margin
The section should not have margin but instead the spacing/gap between sections should be handled in each component with Stack
2024-12-04 16:45:40 +02:00
Riku Rauhala 24eb824163 [FacultyProgressTable] Extract component ExpandableRow
This can be used in faculty statistics too, reducing copy-paste
2024-12-04 16:38:15 +02:00
Riku Rauhala 2065afe852 [Section] Make title optional and improve error message
The error message now matches the loading skeleton better -> no layout shift in case of error
2024-12-04 11:46:00 +02:00
Riku Rauhala d60480d1ae [LineGraph] Rewrite for Material UI 2024-12-04 10:52:42 +02:00
Riku Rauhala 1be0f9090e [Highcharts] Disable animations for all charts 2024-12-02 22:00:29 +02:00
Riku Rauhala f9dad0a183 [Faculties] Rewrite faculty list with MUI 2024-12-02 20:08:46 +02:00
Riku Rauhala 597f1f0c0b [University] Extract tabs to a new hook 2024-12-02 18:21:02 +02:00
Riku Rauhala c26728d24a [Navigation bar] Fix issues 2024-12-02 15:33:38 +02:00
Riku Rauhala 4c1e11692a [Navigation bar] Make the tab clickable
The tab was technically clickable but would not trigger the link. The user would have to hit the text to navigate which was not ideal
2024-12-02 14:29:41 +02:00
Riku Rauhala 132a21e113 [Navigation bar] Fix active status for subpaths 2024-12-02 12:42:18 +02:00
Riku Rauhala 324fcb71a9 [Backend] Remove oodikone-shared
Looks like we forgot to remove this one
2024-12-02 11:46:20 +02:00
Riku Rauhala 55111201e6 [Backend] Fix broken imports 2024-12-02 11:45:13 +02:00
Riku Rauhala 067040d92f [Backend] Extract faculty param types to shared 2024-12-02 11:25:05 +02:00
Riku Rauhala 0d857d716a [ESLint] Ignore no-unused-modules for shared 2024-12-02 10:59:50 +02:00
Riku Rauhala ec9b0b22b0 [Faculties] Only send relevant fields to frontend 2024-12-02 10:41:02 +02:00
Riku Rauhala 32cf9ed185 [Shared] Add GetFacultiesResponse 2024-12-02 10:32:15 +02:00
Riku Rauhala 1702e086a3 [Shared] Move API types to types/api 2024-12-02 09:38:25 +02:00
Riku Rauhala 8246818a94 [Navigation bar] Fix active path 2024-12-02 09:03:52 +02:00
Riku RauhalaandGitHub e1ee00a331 Merge pull request #4693 from UniversityOfHelsinkiCS/responsive-nav-bar
Responsive navigation bar
2024-11-29 17:57:38 +02:00
Riku Rauhala 6e22cb5e58 [Changelog] Add version number 2024-11-29 17:28:26 +02:00
Riku Rauhala f4ebb3d547 [Footer] Tweak gaps 2024-11-29 17:27:15 +02:00
Riku Rauhala 5683fbc3cb [Navigation bar] Switch to a tab based layout 2024-11-29 17:17:24 +02:00
Riku Rauhala 0c78f1c7e2 [Changelog] Increase the number of visible items to 20 2024-11-29 10:27:43 +02:00
Valtteri Kantanen 5051bbdded [Close to graduation] Fix old view with the new one 2024-11-28 17:47:32 +02:00
Valtteri Kantanen c7b86856fb [Backend] Fix study guidance group fetching when the user has no sisPersonId 2024-11-28 15:45:45 +02:00
Valtteri Kantanen afcf72e29f Improve quality on Cypress videos 2024-11-28 15:45:12 +02:00
Valtteri Kantanen a83482179a [Close to graduation] Fix excel exports 2024-11-28 15:24:11 +02:00
Riku RauhalaandGitHub 1e7a198dec Merge pull request #4692 from UniversityOfHelsinkiCS/material-ui-access-denied
Material UI: Access denied
2024-11-28 14:55:35 +02:00
Riku Rauhala 41646985e4 [Access denied] Remove old Semantic UI component 2024-11-28 14:24:23 +02:00
Valtteri Kantanen 80f6c7b1fb [Close to graduation] Use correct filters for columns 2024-11-28 14:15:43 +02:00
Valtteri Kantanen cc9465f141 Update default material-react-table options 2024-11-28 14:15:01 +02:00
Riku Rauhala d56c472e77 [Access denied] Rewrite in Material UI 2024-11-28 13:50:51 +02:00
Riku Rauhala 68e5daae13 [University] Revert to old graduation time colors 2024-11-28 11:09:15 +02:00
Riku Rauhala b259f30093 [University] Remove message about MH90_001 being excluded
This appears to no longer be the case
2024-11-28 10:51:15 +02:00
Riku Rauhala 804f5df55f [InfoBox] Switch to contained variant and increase width 2024-11-28 10:44:15 +02:00
Riku Rauhala 96ec7000fa [Faculty progress] Reduce copy-paste 2024-11-28 10:44:14 +02:00
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
Valtteri KantanenandGitHub d9e1943c51 Merge pull request #4644 from UniversityOfHelsinkiCS/OOD-59
OOD-59: [updater] [backend] try-catch-clauses for updates and if-clauses for using HY-specific programmeCode-map
2024-10-08 15:07:54 +03:00
Jan Rundt 52a8cd2e75 OOD-59: try-catch-clauses for updates and if-clauses for using HY-specific programmeCode-map 2024-10-08 12:23:31 +03:00
Valtteri Kantanen 4d712a308c [Frontend] Upgrade Sentry from v6 to v8 2024-10-07 13:02:46 +03:00
Valtteri Kantanen b81d179f27 [Study programme overview] Update tests 2024-10-07 12:23:50 +03:00
Valtteri Kantanen 083aa87d0a [Study programme overview] Add study track level breakdown for graduation times (#4468) 2024-10-07 11:58:50 +03:00
Valtteri Kantanen 420db2dd88 [Class statistics] Make progress bar label always have the same width 2024-10-07 11:25:38 +03:00
Valtteri Kantanen 5a03c2b426 [Class statistics] Fix ambiguous chart legend: Previously "150–180", now "150 ≤ credits < 180" 2024-10-07 11:24:01 +03:00
Valtteri Kantanen a227d65aa4 [Class statistics] Fix misleading infobox text 2024-10-07 11:21:52 +03:00
Valtteri Kantanen 5236638322 [Class statistics] Fix filters on Credits gained tab not working properly
Previously the filters used the Credits earned filter, but that's not correct as it uses number of all credits, no matter when they were attained. Previously the Personal study plan filter didn't have an effect on the table either even though the table header suggested it did. Now HOPS credits are used in the table if Personal study plan filter is active.
2024-10-07 11:21:01 +03:00
Riku Rauhala 16fd293290 [Course statistics] Fix missing enrollments 2024-10-07 10:10:53 +03:00
Riku RauhalaandGitHub 2d98a776c4 Merge pull request #4643 from UniversityOfHelsinkiCS/renovate/all-minor-patch
Update all non-major dependencies
2024-10-07 09:36:23 +03:00
renovate[bot]andGitHub be784074ee Update all non-major dependencies 2024-10-07 06:27:50 +00:00
Riku Rauhala 7aedb82dcf [Course statistics] Update tests 2024-10-04 17:28:43 +03:00
Riku Rauhala e93031427a [Course statistics] Remove duplicate primary course info 2024-10-04 17:01:42 +03:00
Riku Rauhala 5f98df491d [Backend] Update substitution courses logic 2024-10-04 16:58:29 +03:00
Riku Rauhala 8970d62413 [Course statistics] Update tests 2024-10-04 16:04:10 +03:00
Riku Rauhala ba5224a3b7 [Population statistics] Update tests 2024-10-04 15:43:45 +03:00
Riku Rauhala 934f5afa2a [Course statistics] Update tests 2024-10-04 15:41:18 +03:00
Riku Rauhala 8f331e6078 [Backend] Include main course in alternatives 2024-10-04 15:31:41 +03:00
Riku Rauhala e8b51cc5e3 [Course statistics] Fix broken links 2024-10-04 15:02:15 +03:00
Riku Rauhala 1bcf1baa3e [Students] Update tests 2024-10-04 14:47:26 +03:00
Riku Rauhala 9c04ca7bf1 [Backend] Fix invalid date 2024-10-04 14:28:53 +03:00
Riku Rauhala 517e67aeab [Course statistics] Extract CourseLabel component and Selected courses text 2024-10-04 14:28:53 +03:00
Riku Rauhala 13d8db6ed2 [Course statistics] Include name of alternative courses and improve styling 2024-10-04 14:28:53 +03:00
Valtteri Kantanen 2cf2d240f6 [Students] Fix grade symbol and name going on different rows in the courses table 2024-10-03 11:13:39 +03:00
Riku Rauhala 71c297d345 [Course statistics] Update enrolled, no grade calculation 2024-09-30 15:34:15 +03:00
Riku Rauhala 4b718b3cf6 [Course statistics] Update enrollments column title and helper text 2024-09-30 14:37:56 +03:00
Riku Rauhala 6375c816ac [Backend] Remove enrollment state from course statistics 2024-09-30 14:35:17 +03:00
Riku Rauhala df081ae3a4 [Course statistics] Fix missing enrollments for groups other than All 2024-09-30 14:02:36 +03:00
Riku Rauhala dba194c52e [Course statistics] Refactor 2024-09-30 09:29:51 +03:00
Riku RauhalaandGitHub 493f56c28a Merge pull request #4634 from UniversityOfHelsinkiCS/renovate/all-minor-patch
Update all non-major dependencies
2024-09-30 08:42:39 +03:00
renovate[bot]andGitHub 668c37c00c Update all non-major dependencies 2024-09-30 05:35:17 +00:00
Valtteri Kantanen 7897ef596a Remove composite column from credit_teachers and course_providers 2024-09-27 18:05:29 +03:00
Riku Rauhala 09b8f75a59 [Course statistics] Update tests 2024-09-27 17:43:52 +03:00
Valtteri Kantanen ef4f1518cb [Backend] Fix SISStudyRight model definition 2024-09-27 17:08:02 +03:00
Valtteri Kantanen 71a3686846 Fix some model definitions 2024-09-27 17:00:20 +03:00
Valtteri Kantanen c761949d68 [Updater] Improve logging of Sequelize errors 2024-09-27 16:54:46 +03:00
Valtteri Kantanen b9bee01681 [Updater] Add unique constraint to course_providers 2024-09-27 16:54:17 +03:00
Valtteri Kantanen 586985c45f [Updater] Add unique constraint to credit_teachers 2024-09-27 16:53:46 +03:00
Riku Rauhala da4e495a33 [Course statistics] Update total attempts text
Let's use multiline for show grades as well for consistency
2024-09-27 14:55:11 +03:00
Riku Rauhala 88652020ca [Course statistics] Remove enrollments by state 2024-09-27 14:50:23 +03:00
Riku Rauhala 5bd9e7ff9f [Course statistics] Remove enrollment types from Attempts tab 2024-09-27 13:33:17 +03:00
Riku Rauhala c87ea60b37 [Course statistics] Change never passed text to failed in pass rate chart 2024-09-27 13:27:45 +03:00
Valtteri Kantanen 60f5c23de4 [Frontend] Upgrade vulnerable version of rollup 2024-09-27 13:00:23 +03:00
Valtteri Kantanen 57083d03fa Add hasPersonalIdentityCode field to Student model 2024-09-27 12:59:00 +03:00
Riku Rauhala 18f5666daf [Course statistics] Add enrolled, no grade to pass rate chart in Attempts tab 2024-09-27 11:19:54 +03:00
Riku Rauhala 43e746de6f [Course statistics] Refactor countTotalStats 2024-09-27 10:47:39 +03:00
Riku Rauhala 3cc0f89dbd [Course statistics] Convert countTotalStats to TypeScript 2024-09-27 09:35:58 +03:00
Valtteri Kantanen a9013204d6 [Course statistics] Fix tests 2024-09-26 14:40:21 +03:00
Valtteri Kantanen eb1a124292 [Study programmes] Sort study tracks alphabetically 2024-09-26 14:34:06 +03:00
Valtteri Kantanen 4db0ef4897 [Study programmes] Fix combined programme stats not getting saved in Redis 2024-09-26 14:18:33 +03:00
Riku Rauhala 21cbfb18d4 [Course statistics] Simplify pass rate calculation 2024-09-26 14:08:13 +03:00
Riku Rauhala cfe27ed599 [Course statistics] Calculate pass rate with enrollments 2024-09-26 13:57:43 +03:00
Riku Rauhala 7dafd5d9c2 [Course statistics] Update tests 2024-09-23 15:08:22 +03:00
Valtteri Kantanen 76c037dd72 [Study programmes] Try to optimize Programme courses 2024-09-23 14:58:05 +03:00
Riku Rauhala 053a20f285 [Course statistics] Attempts tab: count total enrollments in total attempts
Enrollments are also considered attempts
2024-09-23 14:40:34 +03:00
Riku Rauhala ae33741452 [Course statistics] Improve the pass rate chart
For the relative passed/failed chart, let's display the bars on top of each other
2024-09-23 13:04:14 +03:00
Riku Rauhala 5c55b58278 [Study programme overview] Add retries to a flaky test 2024-09-23 10:29:55 +03:00
Valtteri Kantanen a692a2dc66 [Sortable table] Fix issue with column filtering and sorting not working in certain cases 2024-09-23 10:21:24 +03:00
Riku RauhalaandGitHub 4e97210422 Merge pull request #4628 from UniversityOfHelsinkiCS/renovate/all-minor-patch
Update all non-major dependencies
2024-09-23 09:16:30 +03:00
renovate[bot]andGitHub 4816db4791 Update all non-major dependencies 2024-09-23 06:09:59 +00:00
Riku Rauhala 66af7cfe07 [Language center view] Update tests 2024-09-20 19:10:11 +03:00
Riku Rauhala 56f3eb98a4 [Language center view] Update tests 2024-09-20 18:37:08 +03:00
Valtteri Kantanen 3ebff5d4f9 [Class statistics] Fix credit accumulation chart 2024-09-20 15:40:11 +03:00
Valtteri Kantanen f842364c1f Remove .only from a test 2024-09-20 15:39:58 +03:00
Riku Rauhala 3d5bc801ca [Language center] Update tests 2024-09-20 11:44:44 +03:00
Riku Rauhala d04476d874 [Language center] Update tests 2024-09-20 10:46:51 +03:00
Riku Rauhala 34444db544 [Programme courses] Export course name 2024-09-20 09:40:32 +03:00
Valtteri Kantanen 1c6e87ac5f [Updater] Add index for course_id column in enrollment table 2024-09-19 18:24:52 +03:00
Valtteri KantanenandGitHub a7707358d6 Merge pull request #4631 from UniversityOfHelsinkiCS/renovate/npm-vite-vulnerability
Update dependency vite to v5.4.6 [SECURITY]
2024-09-19 14:00:59 +03:00
Riku RauhalaandGitHub 949b85776a Merge pull request #4629 from UniversityOfHelsinkiCS/delete-user-in-fd-environment-merge-with-master
[Frontend][Backend] Delete user in fd environment, can not add if user already exists in db in fd environment
2024-09-19 11:08:22 +03:00
Valtteri Kantanen 41551182a4 [Close to graduation] Update new credit limits to infobox 2024-09-19 08:44:44 +03:00
Valtteri Kantanen 447142c7fc [Close to graduation] Adjust lower limits 2024-09-19 08:39:00 +03:00
renovate[bot]andGitHub 085e102bd6 Update dependency vite to v5.4.6 [SECURITY] 2024-09-17 21:16:31 +00:00
Jan Rundt 4de0da3b7a using delete as method in delete request 2024-09-17 10:26:43 +03:00
Valtteri Kantanen c0fb7208d8 Fix updating teacher leaderboards via frontend 2024-09-16 15:09:28 +03:00
Jan Rundt 47cb76d78b small fixes 2024-09-16 15:01:38 +03:00
Jan Rundt 1d03e59691 Merge branch 'master' into delete-user-in-fd-environment-merge-with-master 2024-09-16 12:55:12 +03:00
Jan Rundt 07d1db6d36 not overwriting existing user 2024-09-16 12:50:21 +03:00
Valtteri Kantanen a914b46c1c Don't try to get Sisu roles in staging 2024-09-16 12:46:18 +03:00
Valtteri Kantanen 52cca4dd01 Upgrade redis to v4 and redis-lock to v1 2024-09-16 12:19:58 +03:00
Valtteri Kantanen 2360d05f25 [Teachers] Sort leaderboard rows by number of credits 2024-09-16 11:56:15 +03:00
Riku Rauhala f5829c0c18 [Course statistics] Simplify pass rate chart 2024-09-16 10:03:51 +03:00
Jan Rundt 587ec153eb delete shown only for fd 2024-09-16 09:14:43 +03:00
Riku RauhalaandGitHub eebabf0ba1 Merge pull request #4627 from UniversityOfHelsinkiCS/renovate/all-minor-patch
Update all non-major dependencies
2024-09-16 09:08:59 +03:00
Jan Rundt fc93028e57 deleting a user, backend and frontend 2024-09-16 09:07:45 +03:00
renovate[bot]andGitHub 42bbce2f57 Update all non-major dependencies 2024-09-16 05:57:35 +00:00
Valtteri Kantanen 86430e12e1 [Study programmes] Fix irrelevant programmes data being saved to Redis 2024-09-13 13:05:35 +03:00
Valtteri Kantanen c4daf60e8c [Study programmes] Make types more specific 2024-09-13 13:00:47 +03:00
Valtteri Kantanen 66341a1bc4 [Faculties] Replace getProgrammes with getDegreeProgrammesOfFaculty 2024-09-13 10:57:39 +03:00
Riku Rauhala 2ae0d23271 Fix tests 2024-09-13 10:26:23 +03:00
Riku Rauhala 029491fb84 [Frontend] Fix grammar issues 2024-09-13 10:12:00 +03:00
Riku Rauhala 9a91b5fba7 [Frontend] Type PassRateChart.tsx 2024-09-13 10:03:48 +03:00
Riku Rauhala b16ad3d28b [Frontend] Convert PassRateChart to TypeScript 2024-09-13 09:20:14 +03:00
Riku Rauhala 2c7d9bce74 [Course statistics] Remove details from the Students tab
Fail-% is always visible, passed first/eventually is removed
2024-09-12 21:33:10 +03:00
Riku Rauhala 2f7343bdc0 [Course statistics] Improve table formatting 2024-09-12 21:33:10 +03:00
Riku Rauhala 7e474b7599 [Backend] Remove type PassedCategory 2024-09-12 21:33:10 +03:00
Valtteri Kantanen 03d9ccb8aa [Updater] Handle custom attainments in HY the way they used to be handled 2024-09-12 15:46:42 +03:00
605 changed files with 22882 additions and 18466 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
+1
View File
@@ -70,3 +70,4 @@ environments/
# Target of shared symlink
services/**/src/shared
+4 -4
View File
@@ -9,7 +9,7 @@ services:
PGDATA: /data
POSTGRES_DB: kone-db
POSTGRES_HOST_AUTH_METHOD: trust
image: postgres:15.8
image: postgres:15.10
sis-db:
<<: *defaults
@@ -18,7 +18,7 @@ services:
PGDATA: /data
POSTGRES_DB: sis-db
POSTGRES_HOST_AUTH_METHOD: trust
image: postgres:15.8
image: postgres:15.10
shm_size: 2gb
sis-importer-db:
@@ -28,7 +28,7 @@ services:
PGDATA: /data
POSTGRES_DB: sis-importer-db
POSTGRES_HOST_AUTH_METHOD: trust
image: postgres:15.8
image: postgres:15.10
user-db:
<<: *defaults
@@ -37,4 +37,4 @@ services:
PGDATA: /data
POSTGRES_DB: user-db
POSTGRES_HOST_AUTH_METHOD: trust
image: postgres:15.8
image: postgres:15.10
+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({
+82 -60
View File
@@ -1,7 +1,7 @@
/// <reference types="cypress" />
const openCompletedCoursesModal = () => {
cy.get('button').contains('Search completed courses of students').click()
cy.contains('button', 'Search completed courses of students').click()
}
const hasLanded = () => {
@@ -15,22 +15,40 @@ const openSearch = () => {
cy.init('/completedcoursessearch')
hasLanded()
}
const selectSavedCourselist = name => {
cy.get('[data-cy="history-search"]').children().eq(0).type(name).type('{enter}')
cy.get('[data-cy=history-search]').click()
cy.get('[data-cy=history-search]').type(name)
cy.get('[data-cy=history-search]').type('{downarrow}')
cy.get('[data-cy=history-search]').type('{enter}')
}
const deleteAllSearches = () => {
cy.contains('Saved courselists')
.get('.dropdown')
.then(dropdown => {
const searchItems = dropdown.find('div[role=option] > span[class=text]')
for (let i = 0; i < searchItems.length; i++) {
if (searchItems[i].textContent.includes('TEST-')) {
cy.get('[data-cy="history-search"]').children().eq(0).type(searchItems[i].textContent).type('{enter}')
cy.get('button').contains('Delete').click()
const createCourseList = (courseCodes, courseListName) => {
openSearch()
openCompletedCoursesModal()
cy.get('[data-cy=course-list-input]').type(courseCodes.join('\n'))
cy.get('[data-cy=search-name]').type(courseListName)
cy.get('[data-cy=save-courselist]').click()
}
const generateCourseListName = () => `TEST-course-list-${new Date().getTime()}`
const deleteAllPreviousSearches = () => {
cy.get('[data-cy=history-search]').click()
cy.get('.MuiAutocomplete-popper li').then($options => {
if ($options.length > 0) {
cy.wrap($options).each(($option, index, $list) => {
cy.wrap($option).click()
cy.get('[data-cy=delete-courselist]').click()
if (index < $list.length - 1) {
cy.get('[data-cy=history-search]').click()
}
}
})
})
} else {
cy.contains('You have no previous searches.')
}
})
}
describe('When search modal is opened', () => {
@@ -39,8 +57,7 @@ describe('When search modal is opened', () => {
hasLanded()
cy.url().should('include', '/completedcoursessearch')
cy.contains('Search completed courses of students')
cy.get('[data-cy="open-completed-courses-modal-button"]').click()
deleteAllSearches()
cy.get('[data-cy=open-completed-courses-modal-button]').click()
})
it('Modal opens correctly', () => {
@@ -49,26 +66,33 @@ describe('When search modal is opened', () => {
openCompletedCoursesModal()
cy.contains('Search completed courses of students')
cy.contains('Insert one or more student numbers, separated by a space, a newline, a comma, or a semicolon.')
cy.contains('Insert one or more courses, separated by a space, a newline, or a comma.')
cy.contains('Insert one or more courses, separated by a space, a newline, a comma, or a semicolon.')
cy.contains('Insert name for this course list if you wish to save it')
})
it('Modal gets the correct course codes and student numbers from the URL', () => {
cy.init('/completedcoursessearch?courseList=TKT10001&courseList=TKT10002&studentList=433237&studentList=457144')
hasLanded()
openCompletedCoursesModal()
cy.contains('[data-cy=student-no-input]', '433237, 457144')
cy.contains('[data-cy=course-list-input]', 'TKT10001, TKT10002')
})
describe('When a search is executed with invalid input', () => {
it('Returns empty table and notification', () => {
cy.init('/completedcoursessearch')
hasLanded()
openCompletedCoursesModal()
cy.get('[data-cy="student-no-input"]').type('1')
cy.get('[data-cy="course-list-input"]').type('1')
cy.get('[data-cy="completed-courses-search-button"]').click()
cy.get('[data-cy="rights-notification"]').should(
cy.get('[data-cy=student-no-input]').type('1')
cy.get('[data-cy=course-list-input]').type('1')
cy.get('[data-cy=completed-courses-search-button]').click()
cy.get('[data-cy=rights-notification]').should(
'contain.text',
'The following students information could not be displayed'
'The information for the following students could not be displayed'
)
cy.get('[data-cy="rights-notification"]').should('contain.text', '1')
cy.get('[data-cy="completed-courses-table-div"]').should('contain.text', 'Completed courses search')
cy.get('[data-cy="completed-courses-table-div"]').should('contain.text', 'Student number')
cy.get('[data-cy="completed-courses-table-div"]').should('not.contain', '1')
cy.contains('[data-cy=rights-notification]', '1')
cy.contains('[data-cy=completed-courses-table-div]', 'Student number')
cy.contains('[data-cy=completed-courses-table-div]', '1').should('not.exist')
})
})
@@ -78,9 +102,9 @@ describe('When search modal is opened', () => {
hasLanded()
openCompletedCoursesModal()
cy.fixture('completedCoursesData').then(({ studentSet1, coursesSet1 }) => {
cy.get('[data-cy="student-no-input"]').type(studentSet1.join('\n'))
cy.get('[data-cy="course-list-input"]').type(coursesSet1.join('\n'))
cy.get('[data-cy="completed-courses-search-button"]').click()
cy.get('[data-cy=student-no-input]').type(studentSet1.join('\n'))
cy.get('[data-cy=course-list-input]').type(coursesSet1.join('\n'))
cy.get('[data-cy=completed-courses-search-button]').click()
})
})
@@ -95,63 +119,61 @@ describe('When search modal is opened', () => {
it('Finds correct students and courses', () => {
cy.fixture('completedCoursesData').then(({ studentSet1, coursesSet1 }) => {
cy.get('[data-cy="completed-courses-table-div"]').should('contain.text', studentSet1[0])
cy.get('[data-cy="completed-courses-table-div"]').should('contain.text', studentSet1[1])
cy.get('[data-cy="completed-courses-table-div"]').should('contain.text', studentSet1[2])
cy.get('[data-cy="completed-courses-table-div"]').should('contain.text', studentSet1[3])
cy.get('[data-cy="completed-courses-table-div"]').should('contain.text', studentSet1[4])
cy.get('[data-cy="completed-courses-table-div"]').should('contain.text', coursesSet1[0])
cy.get('[data-cy="completed-courses-table-div"]').should('contain.text', coursesSet1[1])
cy.get('[data-cy="completed-courses-table-div"]').should('contain.text', coursesSet1[2])
cy.get('[data-cy="completed-courses-table-div"]').should('contain.text', coursesSet1[3])
for (const studentNumber of studentSet1) {
cy.contains('[data-cy=completed-courses-table-div]', studentNumber)
}
for (const courseCode of coursesSet1) {
cy.contains('[data-cy=completed-courses-table-div]', courseCode)
}
})
})
})
})
const courseListNames = ['TEST-courselist1', 'TEST-courselist2']
describe('Courselist saving-related functions work', () => {
beforeEach(() => {
openSearch()
openCompletedCoursesModal()
cy.get('[data-cy="course-list-input"]').type('CSM14204\nTKT10004')
cy.get('[data-cy="search-name"]').type(courseListNames[0])
cy.get('button').contains('Save').click()
cy.get('body').then($body => {
if ($body.find('[data-cy=history-search]').length > 0) {
deleteAllPreviousSearches()
} else {
cy.contains('You have no previous searches.')
}
})
})
it('Course list can be saved', () => {
selectSavedCourselist(courseListNames[0])
const courseList = generateCourseListName()
const courses = ['CSM14204', 'TKT10004']
createCourseList(courses, courseList)
openSearch()
openCompletedCoursesModal()
selectSavedCourselist(courseListNames[0])
cy.get('[data-cy="course-list-input"]').contains('TKT10004').contains('CSM14204')
selectSavedCourselist(courseList)
cy.contains('[data-cy=course-list-input]', courses.join(', '))
})
it('Course list can be deleted', () => {
const courseList = generateCourseListName()
createCourseList(['CSM14204', 'TKT10004'], courseList)
openSearch()
openCompletedCoursesModal()
cy.get('[data-cy="search-name"]').type(courseListNames[1])
cy.get('button').contains('Save').click()
openSearch()
openCompletedCoursesModal()
selectSavedCourselist(courseListNames[1])
cy.get('button').contains('Delete').click()
cy.get('[data-cy="history-search"]')
.type(courseListNames[1])
.type('{enter}')
.should('not.contain', courseListNames[1])
selectSavedCourselist(courseList)
cy.get('[data-cy=delete-courselist]').click()
cy.contains('You have no previous searches.')
})
it('Course list can be updated', () => {
const courseList = generateCourseListName()
createCourseList(['CSM14204', 'TKT10004'], courseList)
openSearch()
openCompletedCoursesModal()
selectSavedCourselist(courseListNames[0])
cy.get('[data-cy="course-list-input"]').type(',TKT10001')
cy.get('button').contains('Save').click()
selectSavedCourselist(courseList)
cy.get('[data-cy=course-list-input]').type(',TKT10001')
cy.get('[data-cy=save-courselist]').click()
openSearch()
openCompletedCoursesModal()
selectSavedCourselist(courseListNames[0])
cy.get('[data-cy="course-list-input"]').contains('TKT10001')
selectSavedCourselist(courseList)
cy.contains('[data-cy=course-list-input]', 'CSM14204, TKT10004, TKT10001')
})
})
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')
})
})
})
})
})
+236 -284
View File
@@ -5,331 +5,283 @@ const path = require('path')
const timestamp = new Date().toLocaleDateString('en-CA', { year: 'numeric', month: '2-digit', day: '2-digit' }) // YYYY-MM-DD, taking the local time zone into account
const downloadsFolder = Cypress.config('downloadsFolder')
describe('Faculty overview', () => {
describe('Faculty can be selected', () => {
it('Faculties are listed and one can be chosen', () => {
describe('Faculty statistics', () => {
describe('Faculty list', () => {
beforeEach(() => {
cy.init('/faculties')
cy.get('[data-cy=select-faculty]').contains('a', 'Teologinen tiedekunta')
cy.contains('a', 'H99').should('not.exist')
})
it('contains faculty names and faculty codes', () => {
cy.contains('Faculties')
cy.contains('Teologinen tiedekunta')
cy.contains('H10')
cy.contains('Oikeustieteellinen tiedekunta')
cy.contains('H20')
cy.contains('Lääketieteellinen tiedekunta')
cy.contains('H30')
cy.contains('Humanistinen tiedekunta')
cy.contains('H40')
cy.contains('Matemaattis-luonnontieteellinen tiedekunta')
cy.contains('H50')
cy.contains('Farmasian tiedekunta')
cy.contains('H55')
cy.contains('Bio- ja ympäristötieteellinen tiedekunta')
cy.contains('H57')
cy.contains('Kasvatustieteellinen tiedekunta')
cy.contains('H60')
cy.contains('Valtiotieteellinen tiedekunta')
cy.contains('H70')
cy.contains('Svenska social- och kommunalhögskolan')
cy.contains('H74')
cy.contains('Maatalous-metsätieteellinen tiedekunta')
cy.contains('H80')
cy.contains('Eläinlääketieteellinen tiedekunta')
cy.contains('H90')
})
it('contains a working link to faculty page', () => {
cy.contains('Faculties')
cy.contains('a', 'Teologinen tiedekunta').click()
cy.contains('.header', 'Teologinen tiedekunta')
cy.location('pathname').should('eq', '/faculties/hy-org-1000000580')
cy.contains('Teologinen tiedekunta')
cy.contains('H10')
})
})
describe('Basic information: admin user', () => {
beforeEach(() => {
cy.init('/faculties', 'admin')
cy.contains('a', 'Eläinlääketieteellinen tiedekunta').click()
describe('Basic information tab', () => {
describe('Admin user', () => {
beforeEach(() => {
cy.init('/faculties', 'admin')
cy.contains('a', 'Eläinlääketieteellinen tiedekunta').click()
})
it('Correct tabs are shown', () => {
cy.get('[data-cy="FacultyTabs"]').should('contain', 'Basic information')
cy.get('[data-cy="FacultyTabs"]').should('contain', 'Students by starting year')
cy.get('[data-cy="FacultyTabs"]').should('contain', 'Graduation times')
cy.get('[data-cy="FacultyTabs"]').should('contain', 'Update statistics')
})
})
it('Credits produced by faculty are shown', () => {
cy.get('[data-cy="Section-CreditsProducedByTheFaculty"]').should('be.visible')
cy.get('[data-cy="Graph-CreditsProducedByTheFaculty"]').should('be.visible')
})
describe('Basic user', () => {
beforeEach(() => {
cy.init('/faculties')
cy.contains('a', 'Matemaattis-luonnontieteellinen tiedekunta').click()
})
it('Update statistics tab is shown', () => {
cy.get('[data-cy="FacultySegmentContainer"]').should('contain', 'Update statistics')
describe('Info boxes', () => {
it('Students of the faculty', () => {
cy.get('[data-cy=StudentsOfTheFacultyInfoBoxButton]').click()
cy.get('[data-cy=StudentsOfTheFacultyInfoBoxContent]').contains('Taulukon luvut on')
})
it('Graduated of the faculty', () => {
cy.get('[data-cy=GraduatedOfTheFacultyInfoBoxButton]').click()
cy.get('[data-cy=GraduatedOfTheFacultyInfoBoxContent]').contains('Sisältää kyseisenä')
})
it('Thesis writers of the faculty', () => {
cy.get('[data-cy=ThesisWritersOfTheFacultyInfoBoxButton]').click()
cy.get('[data-cy=ThesisWritersOfTheFacultyInfoBoxContent]').contains('Sisältää kyseisenä')
})
it('Credits produced by the faculty', () => {
cy.get('[data-cy=CreditsProducedByTheFacultyInfoBoxButton]').click()
cy.get('[data-cy=CreditsProducedByTheFacultyInfoBoxContent]').contains('Sisältää opintopisteet')
})
})
it('All graphs and tables are shown', () => {
cy.get('[data-cy="StudentsOfTheFacultyGraph"]').should('be.visible')
cy.get('[data-cy="StudentsOfTheFacultyInteractiveDataTable"]').should('be.visible')
cy.get('[data-cy="GraduatedOfTheFacultyGraph"]').should('be.visible')
cy.get('[data-cy="GraduatedOfTheFacultyInteractiveDataTable"]').should('be.visible')
cy.get('[data-cy="ThesisWritersOfTheFacultyGraph"]').should('be.visible')
cy.get('[data-cy="ThesisWritersOfTheFacultyInteractiveDataTable"]').should('be.visible')
cy.get('[data-cy="CreditsProducedByTheFacultyGraph"]').should('be.visible')
cy.get('[data-cy="CreditsProducedByTheFacultyInteractiveDataTable"]').should('be.visible')
})
it('Correct tabs are shown', () => {
cy.get('[data-cy="FacultyTabs"]').should('contain', 'Basic information')
cy.get('[data-cy="FacultyTabs"]').should('contain', 'Students by starting year')
cy.get('[data-cy="FacultyTabs"]').should('contain', 'Graduation times')
cy.get('[data-cy="FacultyTabs"]').should('not.contain', 'Update statistics')
})
it('Toggle years works', () => {
cy.get('[data-cy="CreditsProducedByTheFacultyInteractiveDataTable"]').should('contain', '2022')
cy.get('[data-cy="ThesisWritersOfTheFacultyInteractiveDataTable"]').should('contain', '2022')
cy.get('[data-cy="StudentsOfTheFacultyInteractiveDataTable"]').should('contain', '2022')
cy.get('[data-cy="GraduatedOfTheFacultyInteractiveDataTable"]').should('contain', '2022')
cy.get('[data-cy="YearToggle"]').click()
cy.get('[data-cy="CreditsProducedByTheFacultyInteractiveDataTable"]').should('contain', '2022 - 2023')
cy.get('[data-cy="ThesisWritersOfTheFacultyInteractiveDataTable"]').should('contain', '2022 - 2023')
cy.get('[data-cy="StudentsOfTheFacultyInteractiveDataTable"]').should('contain', '2022 - 2023')
cy.get('[data-cy="GraduatedOfTheFacultyInteractiveDataTable"]').should('contain', '2022 - 2023')
})
it('Toggle programmes works', () => {
cy.get('[data-cy="FacultyProgrammesShownInfo"]').should('not.exist')
cy.get('[data-cy="ProgrammeToggle"]').click()
cy.get('[data-cy="FacultyProgrammesShownInfo"]').should('be.visible')
})
it('Toggle study rights works', () => {
cy.get('[data-cy="FacultyExcludeSpecialsInfo"]').should('not.exist')
cy.get('[data-cy="StudentToggle"]').click()
cy.get('[data-cy="FacultyExcludeSpecialsInfo"]').should('be.visible')
})
it.skip('Data can be exported to Excel files', { retries: 2 }, () => {
// TODO: Fix this test
const sections = [
'StudentsOfTheFaculty',
'GraduatedOfTheFaculty',
'ThesisWritersOfTheFaculty',
'CreditsProducedByTheFaculty',
]
sections.forEach(section => {
cy.get(`[data-cy="${section}Graph"]`).within(() => {
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))
})
})
})
})
})
describe('Basic information: basic user', () => {
describe('Students by starting year tab', () => {
beforeEach(() => {
cy.init('/faculties')
cy.contains('a', 'Matemaattis-luonnontieteellinen tiedekunta').click()
cy.contains('Students by starting year').click()
})
it('Basic information tab show all graphs and tables', () => {
cy.get('[data-cy="Table-CreditsProducedByTheFaculty"]').should('be.visible')
cy.get('[data-cy="Graph-CreditsProducedByTheFaculty"]').should('be.visible')
cy.get('[data-cy="Table-ThesisWritersOfTheFaculty"]').should('be.visible')
cy.get('[data-cy="Graph-ThesisWritersOfTheFaculty"]').should('be.visible')
cy.get('[data-cy="Table-StudentsOfTheFaculty"]').should('be.visible')
cy.get('[data-cy="Graph-StudentsOfTheFaculty"]').should('be.visible')
cy.get('[data-cy="Table-GraduatedOfTheFaculty"]').should('be.visible')
cy.get('[data-cy="Graph-GraduatedOfTheFaculty"]').should('be.visible')
})
it('Correct tabs are shown', () => {
cy.get('[data-cy="FacultySegmentContainer"]').should('contain', 'Basic information')
cy.get('[data-cy="FacultySegmentContainer"]').should('contain', 'Graduation times')
cy.get('[data-cy="FacultySegmentContainer"]').should('contain', 'Programmes and student populations')
cy.get('[data-cy="FacultySegmentContainer"]').should('not.contain', 'Update statistics')
})
it('Toggle years works', () => {
cy.get('[data-cy="Table-CreditsProducedByTheFaculty"]').should('contain', '2022')
cy.get('[data-cy="Table-ThesisWritersOfTheFaculty"]').should('contain', '2022')
cy.get('[data-cy="Table-StudentsOfTheFaculty"]').should('contain', '2022')
cy.get('[data-cy="Table-GraduatedOfTheFaculty"]').should('contain', '2022')
cy.get('[data-cy="YearToggle"]').click()
cy.get('[data-cy="Table-CreditsProducedByTheFaculty"]').should('contain', '2022 - 2023')
cy.get('[data-cy="Table-ThesisWritersOfTheFaculty"]').should('contain', '2022 - 2023')
cy.get('[data-cy="Table-StudentsOfTheFaculty"]').should('contain', '2022 - 2023')
cy.get('[data-cy="Table-GraduatedOfTheFaculty"]').should('contain', '2022 - 2023')
})
it('Toggle programmes works', () => {
cy.get('[data-cy="FacultyProgrammesShownInfo"]').should('not.exist')
cy.get('[data-cy="ProgrammeToggle"]').click()
cy.get('[data-cy="FacultyProgrammesShownInfo"]').should('be.visible')
})
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')
})
it('Data can be exported to Excel files', () => {
const sections = [
'StudentsOfTheFaculty',
'GraduatedOfTheFaculty',
'ThesisWritersOfTheFaculty',
'CreditsProducedByTheFaculty',
]
sections.forEach(section => {
cy.get(`[data-cy="Graph-${section}"]`).within(() => {
cy.get('[aria-label="View chart menu, Chart"]').click()
cy.contains('li.highcharts-menu-item', 'Download XLS').click()
const downloadedFile = `oodikone_${section}_H50_${timestamp}.xls`
cy.readFile(path.join(downloadsFolder, downloadedFile))
})
})
})
})
describe('Study programme information: users with access', () => {
beforeEach(() => {
cy.init('/faculties')
cy.contains('a', 'Maatalous-metsätieteellinen tiedekunta').click()
})
it('Study programme credit information is not visible in the beginning', () => {
cy.get('table[data-cy="Table-CreditsProducedByTheFaculty"]').should('be.visible')
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-0"]').should('not.be.visible')
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-1"]').should('not.be.visible')
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-2"]').should('not.be.visible')
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-3"]').should('not.be.visible')
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-4"]').should('not.be.visible')
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-5"]').should('not.be.visible')
})
it('Study programme credit information can be toggled', () => {
cy.get('[data-cy="Table-CreditsProducedByTheFaculty"]').should('be.visible')
cy.get('[data-cy="Button-Show-CreditsProducedByTheFaculty-0"]').click()
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-0"]').should('be.visible')
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-1"]').should('not.be.visible')
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-2"]').should('not.be.visible')
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-3"]').should('not.be.visible')
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-4"]').should('not.be.visible')
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-5"]').should('not.be.visible')
cy.get('[data-cy="Button-Show-CreditsProducedByTheFaculty-3"]').click()
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-0"]').should('be.visible')
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-1"]').should('not.be.visible')
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-2"]').should('not.be.visible')
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-3"]').should('be.visible')
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-4"]').should('not.be.visible')
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-5"]').should('not.be.visible')
cy.get('[data-cy="Button-Hide-CreditsProducedByTheFaculty-0"]').click()
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-0"]').should('not.be.visible')
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-1"]').should('not.be.visible')
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-2"]').should('not.be.visible')
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-3"]').should('be.visible')
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-4"]').should('not.be.visible')
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-5"]').should('not.be.visible')
})
it('Graph stays open when sorted', () => {
cy.get('[data-cy="Table-CreditsProducedByTheFaculty"]').should('be.visible')
cy.get('[data-cy="Button-Show-CreditsProducedByTheFaculty-0"]').click()
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-0"]').should('be.visible')
cy.get('[data-cy="Menu-CreditsProducedByTheFaculty-Total"]').click()
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-0"]').should('be.visible')
})
})
describe('Average graduation times', () => {
beforeEach(() => {
cy.init('/faculties')
cy.contains('a', 'Matemaattis-luonnontieteellinen tiedekunta').click()
cy.contains('Graduation times').click()
})
it('User can view graduation graphs', () => {
cy.get('[data-cy="Section-AverageGraduationTimes"]').should('be.visible')
cy.get('[data-cy="Section-bachelor"]').should('be.visible')
cy.get('[data-cy="Section-master"]').should('be.visible')
cy.get('[data-cy="Section-master"]').within(() => {
cy.contains('text.highcharts-axis-title', 'Graduation year')
cy.contains('.message', "Click a bar to view that year's programme level breakdown").should('be.visible')
})
})
it('Graphs display data', () => {
cy.get('[data-cy="Section-bachelor"]').within(() => {
cy.get('div[class="faculty-breakdown-graph"]')
})
cy.get('[data-cy="GraduationTimeToggle"]').click()
cy.get('[data-cy="Section-bachelor"]').within(() => {
cy.get('div[class="faculty-graph"]')
cy.contains('44 graduated').trigger('mouseover')
cy.contains('44 students graduated in year 2023')
cy.contains('median study time: 37.5 months')
cy.contains('21 graduated on time')
cy.contains('11 graduated max year overtime')
cy.contains('12 graduated over year late')
cy.contains('44 graduated').click()
cy.contains('Year 2023 by graduation year')
cy.get('div[class="programmes-graph"]').should('be.visible')
cy.get('div[class="programmes-graph"]').within(() => {
cy.contains('MAT')
cy.get('[aria-label="MAT, 36."]').trigger('mouseover')
cy.contains('Matemaattisten tieteiden kandiohjelma')
cy.contains('KH50_001')
cy.contains('26 students graduated in year 2023')
cy.contains('median study time: 36 months')
cy.contains('14 graduated on time')
cy.contains('6 graduated max year overtime')
cy.contains('6 graduated over year late')
})
})
cy.get('[data-cy="Section-master"]').within(() => {
cy.get('div[class="faculty-graph"]')
cy.contains('4 graduated').trigger('mouseover')
cy.contains('4 students graduated in year 2023')
cy.contains('median study time: 23 months')
cy.contains('3 graduated on time')
cy.contains('0 graduated max year overtime')
cy.contains('1 graduated over year late')
cy.contains('4 graduated').click()
cy.contains('Year 2023 by graduation year')
cy.get('div[class="programmes-graph"]').should('be.visible')
cy.get('div[class="programmes-graph"]').within(() => {
cy.contains('MAST')
cy.get('[aria-label="MAST, 23."]').trigger('mouseover')
cy.contains('Matematiikan ja tilastotieteen maisteriohjelma')
cy.contains('MH50_001')
cy.contains('3 students graduated in year 2023')
cy.contains('median study time: 23 months')
cy.contains('2 graduated on time')
cy.contains('0 graduated max year overtime')
cy.contains('1 graduated over year late')
})
})
})
it('Graduation times grouping and time types can be toggled', () => {
cy.get('[data-cy="GraduationTimeToggle"]').click()
cy.get('[data-cy="GroupByToggle"]').click()
cy.get('[data-cy="Section-master"]').should('be.visible')
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('From class of 2020 - 2021, 24/37 students have graduated')
})
cy.get('[data-cy="GraduationTimeToggle"]').click()
cy.get('[data-cy="Section-bachelor"]').within(() => {
cy.get('[aria-label="2018 - 2019, 29. On time."]').click()
cy.get('div[class="programmes-breakdown-graph"]').should('be.visible')
cy.get('div[class="programmes-breakdown-graph"]').within(() => {
cy.contains('Year 2018 - 2019 by start year')
cy.contains('MAT')
cy.get('[aria-label="MAT, 20. Max. year overtime."]').trigger('mouseover')
cy.contains('Matemaattisten tieteiden kandiohjelma')
cy.contains('KH50_001')
cy.contains('Max. year overtime: 20')
})
})
})
it('Data can be exported to an Excel file', () => {
cy.get('[data-cy="DownloadButton-AverageGraduationTimes"]').click()
const downloadedFile = `oodikone_H50_graduation_times_${timestamp}.xlsx`
it('Export button', { retries: 2 }, () => {
cy.get('[data-cy=FacultyStudentTableExportButton]').should('be.visible').and('not.be.disabled').click()
const downloadedFile = `oodikone_H50_programme_stats_${timestamp}.xlsx`
cy.readFile(path.join(downloadsFolder, downloadedFile))
})
it('Info box', () => {
cy.get('[data-cy=FacultyStudentTableInfoBoxButton]').click()
cy.get('[data-cy=FacultyStudentTableInfoBoxContent]').contains('Opiskelijat, joiden')
})
it.skip('Study right toggle', () => {
// TODO: Implement
})
it.skip('Graduated toggle', () => {
// TODO: Implement
})
it.skip('Hoverable country list', () => {
// TODO: Implement
})
it.skip('Expandable rows', () => {
// TODO: Implement
})
it.skip('Population links', () => {
// TODO: Implement
})
it('Percentage toggle works', () => {
cy.get('[data-cy="FacultyStudentStatsTable"]').should('be.visible')
cy.get('[data-cy="FacultyStudentStatsTable"]').should('not.contain', '92.3 %')
cy.get('[data-cy="HidePercentagesToggle"]').click()
cy.get('[data-cy="FacultyStudentStatsTable"]').should('contain', '92.3 %')
})
})
describe('Faculty Progress and student population tab', () => {
describe('Progress tab', () => {
beforeEach(() => {
cy.init('/faculties')
cy.contains('a', 'Matemaattis-luonnontieteellinen tiedekunta').click()
cy.contains('Programmes and student populations').click()
cy.contains('Progress').click()
})
it('User can view studentdata table without %', () => {
cy.get('[data-cy="FacultyStudentStatsTable"]').should('be.visible')
cy.get('[data-cy="FacultyStudentStatsTable"]').should('not.contain', '0 %')
it.skip('Export button', { retries: 2 }, () => {
// TODO: Fix this test
cy.get('[data-cy=FacultyProgressExportButton]').should('be.visible').and('not.be.disabled').click()
const downloadedFile = `oodikone_H50_progress_tab_${timestamp}.xlsx`
cy.readFile(path.join(downloadsFolder, downloadedFile))
})
it('User can view studentdata table with %', () => {
cy.get('[data-cy="FacultyStudentStatsTable"]').should('be.visible')
cy.get('[data-cy="HidePercentagesToggle"]').click()
cy.get('[data-cy="FacultyStudentStatsTable"]').should('contain', '0 %')
it.skip('Info boxes', () => {
cy.get('[data-cy=FacultyProgressInfoBoxButton]').click()
cy.get('[data-cy=FacultyProgressInfoBoxContent]').contains('Kuvaa tiedekuntaan')
// TODO: Click open the accordion first
cy.get('[data-cy=BachelorMastersProgressInfoBoxButton]').click()
cy.get('[data-cy=BachelorMastersProgressInfoBoxContent]').contains('The starting year is the')
})
it('Percentage Graphs exists', () => {
cy.get('[data-cy="Graph-FacultyBachelorsProgress"]').should('be.visible')
cy.get('[data-cy="Graph-FacultyBachelorMastersProgress"]').should('be.visible')
cy.get('[data-cy="Graph-FacultyMastersProgress"]').should('be.visible')
cy.get('[data-cy="Graph-FacultyBachelorsProgress"]').should('be.visible')
it.skip('Progress bar charts exist', () => {
// TODO: Click open the accordion first
cy.get('[data-cy="FacultyBachelorsProgressBarChart"]').should('be.visible')
cy.get('[data-cy="FacultyBachelorMastersProgressBarChart"]').should('be.visible')
cy.get('[data-cy="FacultyMastersProgressBarChart"]').should('be.visible')
cy.get('[data-cy="FacultyBachelorsProgressBarChart"]').should('be.visible')
})
it('Graduations can be excluded', () => {
cy.get('[data-cy="FacultyStudentStatsTable"]').should('be.visible')
cy.get('[data-cy="StudentToggle"]').click()
cy.get('[data-cy="FacultyStudentStatsTable"]').should('be.visible')
})
it('Transfers can be excluded', () => {
cy.get('[data-cy="FacultyStudentStatsTable"]').should('be.visible')
cy.get('[data-cy="GraduatedToggle"]').click()
cy.get('[data-cy="FacultyStudentStatsTable"]').should('be.visible')
})
it('Progress tables exists', () => {
it.skip('Progress tables exist', () => {
// TODO: Click open the accordion first
cy.get('[data-cy="FacultyBachelorsProgressTable"]').should('be.visible')
cy.get('[data-cy="FacultyBachelorMasterProgressTable"]').should('be.visible')
cy.get('[data-cy="FacultyMastersProgressTable"]').should('be.visible')
cy.get('[data-cy="FacultyBachelorsProgressTable"]').should('be.visible')
})
it('Progress tables can be toggled', () => {
cy.get('[data-cy="Cell-FacultyBachelorsProgressTable-5"]').should('not.be.visible')
cy.get('[data-cy="Button-Show-FacultyBachelorsProgressTable-5"]').click()
cy.get('[data-cy="Cell-FacultyBachelorsProgressTable-0"]').should('not.be.visible')
cy.get('[data-cy="Cell-FacultyBachelorsProgressTable-1"]').should('not.be.visible')
cy.get('[data-cy="Cell-FacultyBachelorsProgressTable-2"]').should('not.be.visible')
cy.get('[data-cy="Cell-FacultyBachelorsProgressTable-3"]').should('not.be.visible')
cy.get('[data-cy="Cell-FacultyBachelorsProgressTable-5"]').should('be.visible')
cy.get('[data-cy="Button-Hide-FacultyBachelorsProgressTable-5"]').click()
cy.get('[data-cy="Cell-FacultyBachelorsProgressTable-5"]').should('not.be.visible')
it.skip('Graduated toggle', () => {
// TODO: Implement
})
it('Studentstats table can be toggled', () => {
cy.get('[data-cy="FacultyStudentStatsTable"]').should('not.contain', 'TKT')
cy.get('[data-cy="Button-FacultyStudentStatsTable-6"]').click()
cy.get('[data-cy="FacultyStudentStatsTable"]').should('contain', 'TKT')
cy.get('[data-cy="Button-FacultyStudentStatsTable-6"]').click()
cy.get('[data-cy="FacultyStudentStatsTable"]').should('not.contain', 'TKT')
it.skip('Study right toggle', () => {
// TODO: Implement
})
})
describe('Graduation times tab', () => {
beforeEach(() => {
cy.init('/faculties')
cy.contains('a', 'Matemaattis-luonnontieteellinen tiedekunta').click()
cy.contains('Graduation times').click()
})
it('Progress infobox can be toggled', () => {
cy.get('[data-cy="InfoFacultyProgress-info-content"]').should('not.exist')
cy.get('[data-cy="InfoFacultyProgress-open-info"]').click()
cy.get('[data-cy="InfoFacultyProgress-info-content"]').should('be.visible')
cy.get('[data-cy="InfoFacultyProgress-close-info"]').click()
cy.get('[data-cy="InfoFacultyProgress-info-content"]').should('not.exist')
it.skip('Export button', { retries: 2 }, () => {
// TODO: Fix this test
cy.get('[data-cy=AverageGraduationTimesExportButton]').should('be.visible').and('not.be.disabled').click()
const downloadedFile = `oodikone_H50_graduation_times_${timestamp}.xlsx`
cy.readFile(path.join(downloadsFolder, downloadedFile))
})
it('Info boxes', () => {
cy.get('[data-cy=AverageGraduationTimesInfoBoxButton]').click()
cy.get('[data-cy=AverageGraduationTimesInfoBoxContent]').contains('Opiskelijoiden keskimääräiset')
})
it.skip('Study times toggle', () => {
// TODO: Implement
})
it.skip('Year toggle', () => {
// TODO: Implement
})
it.skip('Study programme toggle', () => {
// TODO: Implement
})
it('Graphs are visible', () => {
cy.get('[data-cy="bachelorGraduationTimes"]').should('be.visible')
// ! No test data for bachelor + master
cy.get('[data-cy="masterGraduationTimes"]').should('be.visible')
cy.get('[data-cy="doctorGraduationTimes"]').should('be.visible')
})
})
})
+26 -18
View File
@@ -18,7 +18,6 @@ const clearSingleDropdownSelection = dataCyAttribute => {
}
const testRangeFilter = (parentEl, min, max, expected) => {
cy.clock(MOCKED_DATE, ['Date'])
cy.cs(parentEl)
.cs('range-selector-min')
.find('input')
@@ -29,13 +28,17 @@ const testRangeFilter = (parentEl, min, max, expected) => {
.find('input')
.invoke('val')
.then(initialMax => {
cy.cs(parentEl).cs('range-selector-min').find('input').type(`{selectall}{backspace}${min}`).blur()
cy.cs(parentEl).cs('range-selector-max').find('input').type(`{selectall}{backspace}${max}`).blur()
cy.cs(parentEl).cs('range-selector-min').find('input').clear()
cy.cs(parentEl).cs('range-selector-min').find('input').type(min)
cy.cs(parentEl).cs('range-selector-max').find('input').clear()
cy.cs(parentEl).cs('range-selector-max').find('input').type(max)
checkFilteringResult(expected)
cy.cs(parentEl).cs('range-selector-min').find('input').type(`{selectall}{backspace}${initialMin}`).blur()
cy.cs(parentEl).cs('range-selector-max').find('input').type(`{selectall}{backspace}${initialMax}`).blur()
cy.cs(parentEl).cs('range-selector-min').find('input').clear()
cy.cs(parentEl).cs('range-selector-min').find('input').type(initialMin)
cy.cs(parentEl).cs('range-selector-max').find('input').clear()
cy.cs(parentEl).cs('range-selector-max').find('input').type(initialMax)
})
})
}
@@ -83,27 +86,27 @@ describe("Population statistics with a master's programme", () => {
})
})
describe('Studyright type filter', () => {
describe('Study right type filter', () => {
it('is visible', () => {
cy.contains('Studyright type')
cy.contains('Study right type')
})
it('is set to all by default', () => {
runTestStepWithPreAndPostParts('studyright-type', () => {
const card = cy.cs('studyright-type-filter-card')
runTestStepWithPreAndPostParts('studyRightTypeFilter', () => {
const card = cy.cs('studyRightTypeFilter-filter-card')
card.get('[data-cy="all"] input').should('be.checked')
})
})
it('works', () => {
runTestStepWithPreAndPostParts('studyright-type', () => {
runTestStepWithPreAndPostParts('studyRightTypeFilter', () => {
const expectedBaMaStudents = 24
const expectedMasterOnlyStudents = 2
cy.cs('studyright-type-filter-card').cs('bama').click()
cy.cs('studyRightTypeFilter-filter-card').cs('bama').click()
checkFilteringResult(expectedBaMaStudents)
cy.cs('studyright-type-filter-card').cs('master').click()
cy.cs('studyRightTypeFilter-filter-card').cs('master').click()
checkFilteringResult(expectedMasterOnlyStudents)
cy.cs('studyright-type-filter-card').cs('all').click()
cy.cs('studyRightTypeFilter-filter-card').cs('all').click()
checkFilteringResult(defaultAmountOfStudents)
})
})
@@ -169,6 +172,7 @@ describe('Population Statistics', () => {
})
it('Age filter works', { retries: 3 }, () => {
cy.clock(MOCKED_DATE, ['Date'])
runTestStepWithPreAndPostParts('Age', () => {
testRangeFilter('Age-filter-card', 23, 30, 21)
})
@@ -228,6 +232,7 @@ describe('Population Statistics', () => {
})
it('Filter combinations work', () => {
cy.clock(MOCKED_DATE, ['Date'])
runTestStepWithPreAndPostParts('GraduatedFromProgramme', () => {
runTestStepWithPreAndPostParts('Age', () => {
const getCard = () => cy.cs('GraduatedFromProgramme-filter-card')
@@ -249,8 +254,8 @@ describe('Population Statistics', () => {
checkFilteringResult(defaultAmountOfStudents)
})
it('Studyright type filter is not visible', () => {
cy.contains('Studyright type').should('not.exist')
it('Study right type filter is not visible', () => {
cy.contains('Study right type').should('not.exist')
})
})
@@ -284,13 +289,14 @@ describe('Course Statistics', () => {
it('Programme filter works', () => {
runTestStepWithPreAndPostParts('Programme', () => {
const card = cy.cs('Programme-filter-card')
const programmeDropdown = card.cs('Programme-filter-dropdown').selectFromDropdown(2)
checkFilteringResult(30)
const programmeDropdown = card.cs('Programme-filter-dropdown').selectFromDropdown(1)
checkFilteringResult(33)
programmeDropdown.get('i.delete').click()
})
})
it('Age filter works', () => {
it('Age filter works', { retries: 3 }, () => {
cy.clock(MOCKED_DATE, ['Date'])
runTestStepWithPreAndPostParts('Age', () => {
testRangeFilter('Age-filter-card', 21, 30, 36)
})
@@ -322,6 +328,7 @@ describe('Course Statistics', () => {
})
it('Filter combinations work', () => {
cy.clock(MOCKED_DATE, ['Date'])
runTestStepWithPreAndPostParts('Grade', () => {
runTestStepWithPreAndPostParts('Age', () => {
cy.cs('gradeFilter-5').click()
@@ -348,6 +355,7 @@ describe('Custom Population Statistics', () => {
})
it('Age filter works', () => {
cy.clock(MOCKED_DATE, ['Date'])
runTestStepWithPreAndPostParts('Age', () => {
testRangeFilter('Age-filter-card', 24, 28, 5)
})
+10 -9
View File
@@ -16,13 +16,14 @@ const chooseSemester = (semester, fromOrTo) => {
})
}
const checkNumbers = (numbers, numberOfColumns) => {
const checkNumbers = (numbers, numberOfColumns, mode) => {
const offset = mode === 'semesters' ? 2 : 1
cy.get('table > tbody > tr:first').within(() => {
cy.get('td').should('have.length', numberOfColumns)
cy.get('td').eq(0).contains('All courses total')
numbers.forEach((number, index) => {
cy.get('td')
.eq(index + 1)
.eq(index + offset)
.contains(number)
})
})
@@ -33,7 +34,7 @@ const checkStyle = (styles, numberOfColumns) => {
cy.get('td').should('have.length', numberOfColumns)
styles.forEach((style, index) => {
cy.get('td')
.eq(index + 1)
.eq(index + 2)
.should('have.attr', 'style')
.and('include', style)
})
@@ -64,7 +65,7 @@ describe('When language center is opened', () => {
cy.get('table > tbody > tr:first').within(() => {
cy.get('td').should('have.length', 15)
cy.get('td').eq(0).contains('All courses total')
const numbers = [36, 9, 33, 751, 15, 40, 6, 49, 1, 24, 1059, 53, 0, 2076]
const numbers = [36, 9, 33, 756, 15, 40, 6, 48, 1, 24, 1059, 49, 0, 2076]
numbers.forEach((number, index) => {
cy.get('td')
.eq(index + 1)
@@ -75,12 +76,12 @@ describe('When language center is opened', () => {
it('Faculties tab "exceeding" button works', () => {
cy.get('[data-cy="difference-button"]').click()
checkNumbers([...new Array(12).fill(0), 1, 218], 15)
checkNumbers([...new Array(12).fill(0), 1, 221], 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, 27, 496, 4, 10, 2, 33, 0, 9, 535, 34, 0, 1184], 15)
})
})
@@ -92,7 +93,7 @@ describe('When language center is opened', () => {
})
it('Semester tab shows numbers', () => {
checkNumbers([69, 26, 298, 58, 343, 98, 438, 138, 310, 90, 123, 74, 10, 1, 2076], 16)
checkNumbers([69, 26, 298, 58, 343, 98, 438, 138, 310, 90, 123, 74, 10, 1, 2076], 17, 'semesters')
})
it('Coloring mode works on semester tab', () => {
@@ -101,10 +102,10 @@ describe('When language center is opened', () => {
checkStyle(
expectedAlphas.map(alpha => `background-color: rgba(0, 170, 0, ${alpha})`),
16
17
)
cy.get('table > tbody > tr:first').within(() => {
cy.get('td').eq(15).should('have.attr', 'style').and('not.include', 'background-color')
cy.get('td').eq(16).should('have.attr', 'style').and('not.include', 'background-color')
})
})
})
+120 -56
View File
@@ -1,65 +1,82 @@
/// <reference types="cypress" />
// Now "Class statistics" in UI
describe('Population Statistics tests', () => {
const pathToMathBSc2020 =
'/populations?months=49&semesters=FALL&semesters=SPRING&studyRights=%7B"programme"%3A"KH50_001"%7D&year=2020'
const pathToMathMSc2020 =
'/populations?months=49&semesters=FALL&semesters=SPRING&studyRights=%7B"programme"%3A"MH50_001"%7D&year=2020'
describe('when using basic user', () => {
const selectStudyProgramme = programme => {
cy.contains('Select study programme')
cy.get('[data-cy=select-study-programme]').click().children().contains(programme).click()
}
const selectStudyTrack = studyTrack => {
cy.contains('Select study track')
cy.get('[data-cy=select-study-track]').click().children().contains(studyTrack).click()
}
describe('Population statistics tests', () => {
const getPath = programme => {
return `/populations?months=49&semesters=FALL&semesters=SPRING&studyRights=%7B"programme"%3A"${programme}"%7D&year=2020`
}
const pathToMathBSc2020 = getPath('KH50_001')
const pathToMathMSc2020 = getPath('MH50_001')
describe('When using basic user', () => {
beforeEach(() => {
cy.init('/populations')
})
it('Population statistics search infobox works', () => {
cy.get('[data-cy="PopulationSearch-info-content"]').should('not.exist')
cy.get('[data-cy="PopulationSearch-open-info"]').click()
cy.get('[data-cy="PopulationSearch-info-content"]').should('be.visible')
cy.get('[data-cy="PopulationSearch-info-content"]').should(
'contain',
'lukuvuosi, jolloin opiskelija on ilmoittautunut'
)
cy.get('[data-cy="PopulationSearch-close-info"]').click()
cy.get('[data-cy="PopulationSearch-info-content"]').should('not.exist')
})
describe('Population search', () => {
it('Info box works', () => {
cy.get('[data-cy="PopulationSearch-info-content"]').should('not.exist')
cy.get('[data-cy="PopulationSearch-open-info"]').click()
cy.get('[data-cy="PopulationSearch-info-content"]').should('be.visible')
cy.get('[data-cy="PopulationSearch-info-content"]').should('contain', 'Tässä osiossa voi tarkastella')
cy.get('[data-cy="PopulationSearch-close-info"]').click()
cy.get('[data-cy="PopulationSearch-info-content"]').should('not.exist')
})
it('Population statistics search form is usable', () => {
cy.contains('See class').should('be.disabled')
cy.contains('Search for class')
cy.contains('Class of')
.parent()
.within(() => {
cy.get('.form-control').as('enrollmentSelect')
it('Form is usable', () => {
cy.contains('Search for class')
cy.contains('See class').should('be.disabled')
cy.contains('Class of')
.parent()
.within(() => {
cy.get('.form-control').as('enrollmentSelect')
})
cy.get('@enrollmentSelect')
.its(`${[0]}.value`)
.then(beforeVal => {
cy.get('@enrollmentSelect').click()
cy.get('.yearSelectInput .rdtPrev').click({ force: true })
cy.get('.yearSelectInput table').contains('2018-2019').click({ force: true })
cy.get('@enrollmentSelect').should('not.have.value', beforeVal)
})
cy.contains('No study tracks available for this programme')
selectStudyProgramme('Matematiikan ja tilastotieteen maisteriohjelma')
cy.contains('See class').should('be.enabled')
selectStudyTrack('Matematiikka ja soveltava matematiikka')
cy.contains('See class').should('be.enabled')
})
describe('Correct population is shown for programme', () => {
it('without study tracks', () => {
cy.contains('Select study programme')
selectStudyProgramme('Matemaattisten tieteiden kandiohjelma')
cy.contains('See class').click()
cy.contains('Matemaattisten tieteiden kandiohjelma 2017 - 2018')
cy.contains('class size 47 students')
})
cy.get('@enrollmentSelect')
.its(`${[0]}.value`)
.then(beforeVal => {
cy.get('@enrollmentSelect').click()
cy.get('.yearSelectInput .rdtPrev').click({ force: true })
cy.get('.yearSelectInput table').contains('2018-2019').click({ force: true })
cy.get('@enrollmentSelect').should('not.have.value', beforeVal)
it('with study tracks', () => {
selectStudyProgramme('Matematiikan ja tilastotieteen maisteriohjelma')
selectStudyTrack('Matematiikka ja soveltava matematiikka')
cy.contains('See class').click()
cy.contains('Matematiikan ja tilastotieteen maisteriohjelma 2017 - 2018')
cy.contains('studytrack MAST-MSM')
cy.contains('class size 1 students')
})
cy.contains('Select study programme')
cy.get('[data-cy=select-study-programme]')
.click()
.children()
.contains('Matematiikan ja tilastotieteen maisteriohjelma')
.click()
cy.contains('See class').should('be.enabled')
})
it('Searching for population really shows population', () => {
cy.contains('Select study programme')
cy.get('[data-cy=select-study-programme]')
.click()
.children()
.contains('Matemaattisten tieteiden kandiohjelma')
.click()
cy.contains('See class').click()
cy.contains('Matemaattisten tieteiden kandiohjelma 2017 - 2018')
cy.contains('class size 47 students')
})
})
it('Population statistics is usable on general level', () => {
@@ -81,18 +98,65 @@ describe('Population Statistics tests', () => {
cy.contains('td', 'MAT11001').siblings().find('i.level.up').click()
cy.wait('@coursePage')
cy.url().should('include', '/coursestatistics')
cy.contains('MAT11001, 57033, AYMAT11001, A57033 Johdatus yliopistomatematiikkaan')
cy.contains('MAT11001 Johdatus yliopistomatematiikkaan')
cy.contains('AYMAT11001 Avoin yo: Johdatus yliopistomatematiikkaan')
cy.contains('57033 Johdatus yliopistomatematiikkaan')
cy.contains('A57033 Avoin yo: Johdatus yliopistomatematiikkaan')
})
it('Courses of class curriculum selection works', () => {
cy.visit(pathToMathBSc2020)
cy.contains('Courses of class').click()
cy.get('[data-cy=curriculum-picker]').contains('2020 - 2023')
cy.get('[data-cy=curriculum-picker]').contains('2020–2023')
cy.get('[data-cy=toggle-group-module-MAT-tyo]')
cy.get('[data-cy=curriculum-picker]').click().contains('2023 - 2026').click()
cy.get('[data-cy=curriculum-picker]').click()
cy.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()
cy.contains('2020–2023').click({ force: true })
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()
cy.contains('2023–2026').click()
cy.wait('@courseData').then(({ response }) => {
expect(response.body).to.have.property('allStudents')
expect(response.body).to.have.property('coursestatistics')
expect(response.body.allStudents).to.equal(27)
expect(response.body.coursestatistics.some(stat => stat.course.code === 'DIGI-100')).to.equal(false)
})
})
it('New fetch of courses data is done when filtered students change', () => {
cy.visit(pathToMathBSc2020)
cy.contains('Courses of class').click()
cy.intercept('/api/v2/populationstatistics/courses').as('courseData')
cy.wait('@courseData').then(({ response }) => {
expect(response.body).to.have.property('allStudents')
expect(response.body).to.have.property('coursestatistics')
expect(response.body.allStudents).to.equal(27)
})
cy.get('[data-cy=GraduatedFromProgramme-filter-card]').within(() => {
cy.get('[data-cy=GraduatedFromProgramme-header]').click()
cy.get('[data-cy=option-graduated-true]').click()
cy.wait('@courseData').then(({ response }) => {
expect(response.body).to.have.property('allStudents')
expect(response.body).to.have.property('coursestatistics')
expect(response.body.allStudents).to.equal(16)
})
})
})
it("Empty 'tags' tab has a link to the page where tags can be created", { retries: 2 }, () => {
cy.visit(pathToMathBSc2020)
cy.contains('Students (27)')
@@ -254,7 +318,7 @@ describe('Population Statistics tests', () => {
})
})
describe('when using admin', () => {
describe('When using admin', () => {
it('Student list checking works as intended', () => {
cy.init(pathToMathBSc2020, 'admin')
const existing = '433237'
@@ -283,7 +347,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')
+67 -53
View File
@@ -8,13 +8,21 @@ const student = {
email: 'sisutestidata134902@testisisudata.fi',
}
const typeToSearch = text => {
cy.get("input[placeholder='Search with a student number or name (surname firstname)']").type(text)
}
const typeStudentNumberAndClick = studentNumber => {
cy.get('.prompt').type(studentNumber)
cy.contains('td a', studentNumber).click()
typeToSearch(studentNumber)
cy.contains('td', studentNumber).click()
}
const clearSearch = () => {
cy.get("input[placeholder='Search with a student number or name (surname firstname)']").clear()
}
describe('Students tests', () => {
describe('when using basic user', () => {
describe('When using basic user', () => {
beforeEach(() => {
cy.init()
cy.contains('Students').click()
@@ -23,7 +31,7 @@ describe('Students tests', () => {
})
it('Students search form is usable', () => {
cy.get('.prompt').type(student.lastname)
typeToSearch(student.lastname)
cy.contains('Student number')
cy.contains('Started')
cy.contains('Credits')
@@ -37,20 +45,19 @@ describe('Students tests', () => {
})
it('Search term must be at least 4 characters long', () => {
cy.get('.prompt').type(student.lastname.slice(0, 3))
cy.contains('No search results or search term is not accurate enough')
cy.get('.prompt').type(student.lastname.slice(3))
typeToSearch(student.lastname.slice(0, 3))
cy.contains('Search term is not accurate enough')
typeToSearch(student.lastname.slice(3))
cy.get('table tbody tr').should('have.length', 1)
})
it('can search with studentnumber too', () => {
cy.get('.prompt').type(student.studentnumber)
it('Can search with studentnumber too', () => {
typeToSearch(student.studentnumber)
cy.contains(student.studentnumber)
})
it('Can get student specific page by clicking student', () => {
cy.get('.prompt').type(student.studentnumber)
cy.contains('td a', student.studentnumber).click()
typeStudentNumberAndClick(student.studentnumber)
cy.contains('Matemaattisten tieteiden kandiohjelma (01.08.2020–31.07.2027)')
cy.contains(student.lastname).should('not.exist')
cy.contains(student.firstnames).should('not.exist')
@@ -69,7 +76,7 @@ describe('Students tests', () => {
it("'Update student' button is not shown", () => {
typeStudentNumberAndClick(student.studentnumber)
cy.get('div.ui.fluid.card').within(() => {
cy.get('[data-cy=student-info-card]').within(() => {
cy.contains('button', 'Update student').should('not.exist')
})
})
@@ -88,15 +95,17 @@ describe('Students tests', () => {
.siblings()
.last()
.within(() => {
cy.get('.level').click()
cy.get('a').click()
})
cy.url().should('include', '/coursestatistics')
cy.contains('MAT12004, 57046, AYMAT12004 Tilastollinen päättely I')
cy.contains('MAT12004 Tilastollinen päättely I')
cy.contains('AYMAT12004 Avoin yo: Tilastollinen päättely I')
cy.contains('57046 Johdatus tilastolliseen päättelyyn')
})
it('Has correct Sisu link', () => {
typeStudentNumberAndClick(student.studentnumber)
cy.get('[data-cy=sisulink] > a')
cy.get('[data-cy=sisu-link]')
.should('have.attr', 'href')
.and('include', `https://sisu.helsinki.fi/tutor/role/staff/student/${student.sis_person_id}/basic/basic-info`)
})
@@ -108,7 +117,7 @@ describe('Students tests', () => {
'Matemaattisten tieteiden kandiohjelma',
'Oikeustieteen maisterin koulutusohjelmaOikeusnotaarin koulutusohjelma',
]
cy.get('div.ui.fluid.card').within(() => {
cy.get('[data-cy=student-info-card]').within(() => {
cy.contains('Enrollments').click()
cy.get('table tbody tr')
.should('have.length', 3)
@@ -120,18 +129,22 @@ describe('Students tests', () => {
})
})
it('Searching with bad inputs doesnt yield results', () => {
cy.get('.prompt').type('SWAG LITTINEN')
it("Searching with bad inputs doesn't yield results", () => {
typeToSearch('SWAG LITTINEN')
cy.contains('Student number').should('not.exist')
cy.get('.prompt').clear().type('01114')
clearSearch()
typeToSearch('01114')
cy.contains('Student number').should('not.exist')
})
it('Can jump to population page', () => {
typeStudentNumberAndClick(student.studentnumber)
cy.contains('.ui.table', 'Completed').within(() => {
cy.get('i.level.up.alternate.icon').click()
cy.get('[data-cy=study-rights-section]').within(() => {
cy.contains('Matemaattisten tieteiden kandiohjelma')
.parent()
.within(() => {
cy.get('a').click()
})
})
cy.contains('Matemaattisten tieteiden kandiohjelma 2020 - 2021')
cy.contains('class size 30 students')
@@ -139,33 +152,32 @@ describe('Students tests', () => {
it('Grade graph works in all three different modes', () => {
typeStudentNumberAndClick(student.studentnumber)
cy.contains('a.item', 'Grade graph').click()
cy.contains('text.highcharts-title', 'Grade plot')
cy.contains('a.item', 'Show group mean').click()
cy.get('.labeled.input').within(() => {
cy.contains('.label', 'Group size')
cy.contains('button', 'Grade graph').click()
cy.contains('button', 'Show group mean').click()
cy.get('[data-cy=group-size-input]').within(() => {
cy.contains('label', 'Group size')
cy.get('input').should('have.value', '5')
})
cy.contains('.highcharts-container text', "Nov '22")
cy.contains('.highcharts-container text', "Jul '22").should('not.exist')
cy.contains('text.highcharts-title', 'Grade plot')
cy.get('.labeled.input').within(() => {
cy.contains('.label', 'Group size')
cy.get('input').clear().type('10')
cy.get('[data-cy=group-size-input]').within(() => {
cy.contains('label', 'Group size')
cy.get('input').clear()
cy.get('input').type('10')
cy.get('input').should('have.value', '10')
})
cy.contains('.highcharts-container text', "Jul '22")
cy.contains('.highcharts-container text', "Nov '22").should('not.exist')
cy.contains('a.item', 'Show semester mean').click()
cy.contains('button', 'Show semester mean').click()
cy.contains('.highcharts-container text', "Jan '22")
})
describe('Bachelor Honours section', () => {
it("Shows 'Qualified for Honours' tag and main modules info when the student is qualified", () => {
cy.visit('/students/495976')
cy.contains('.divider h4', 'Bachelor Honours')
cy.contains('.green.tag.label', 'Qualified for Honours')
cy.contains('Main courses and other modules').click()
cy.contains('h2', 'Bachelor Honours')
cy.contains('[data-cy=honours-chip-qualified]', 'Qualified for Honours')
cy.contains('Study modules').click()
cy.get('[data-cy=main-modules] tbody tr')
.should('have.length', 3)
.each(($tr, index) => {
@@ -184,35 +196,35 @@ describe('Students tests', () => {
it("Shows 'Did not graduate in time' when the student has graduated but not in time", () => {
cy.visit('/students/540355')
cy.contains('.divider h4', 'Bachelor Honours')
cy.contains('.red.tag.label', 'Not qualified for Honours')
cy.contains('.red.tag.label', 'Did not graduate in time')
cy.contains('h2', 'Bachelor Honours')
cy.contains('[data-cy=honours-chip-not-qualified]', 'Not qualified for Honours')
cy.contains('[data-cy=honours-chip-error]', 'Did not graduate in time')
})
it("Shows 'Module grades too low' when the student has graduated in time but has too low grades", () => {
cy.visit('/students/547934')
cy.contains('.divider h4', 'Bachelor Honours')
cy.contains('.red.tag.label', 'Not qualified for Honours')
cy.contains('.red.tag.label', 'Module grades too low')
cy.contains('h2', 'Bachelor Honours')
cy.contains('[data-cy=honours-chip-not-qualified]', 'Not qualified for Honours')
cy.contains('[data-cy=honours-chip-error]', 'Module grades too low')
})
it("Shows 'Might need further inspection' when the student has graduated in time but has more than four main modules", () => {
cy.visit('/students/478837')
cy.contains('.divider h4', 'Bachelor Honours')
cy.contains('.blue.tag.label', 'Might need further inspection')
cy.contains('h2', 'Bachelor Honours')
cy.contains('[data-cy=honours-chip-inspection]', 'Might need further inspection')
})
it("Shows 'Has not graduated' when the student has not graduated", () => {
cy.visit(`students/${student.studentnumber}`)
cy.contains('.divider h4', 'Bachelor Honours')
cy.contains('.red.tag.label', 'Not qualified for Honours')
cy.contains('.red.tag.label', 'Has not graduated')
cy.contains('h2', 'Bachelor Honours')
cy.contains('[data-cy=honours-chip-not-qualified]', 'Not qualified for Honours')
cy.contains('[data-cy=honours-chip-error]', 'Has not graduated')
})
})
})
// Use admin to see all students
describe('when using admin user', () => {
describe('When using admin user', () => {
beforeEach(() => {
cy.init('/students', 'admin')
})
@@ -224,14 +236,14 @@ describe('Students tests', () => {
it("'Update student' button is shown", () => {
typeStudentNumberAndClick(student.studentnumber)
cy.get('div.ui.fluid.card').within(() => {
cy.get('[data-cy=student-info-card]').within(() => {
cy.contains('button', 'Update student')
})
})
it('Bachelor Honours section is not shown for students outside of Faculty of Science', () => {
cy.visit('/students/453146')
cy.contains('.divider h4', 'Bachelor Honours').should('not.exist')
cy.contains('h2', 'Bachelor Honours').should('not.exist')
})
it('When a study plan is selected, courses included in the study plan are highlighted with a blue background', () => {
@@ -239,15 +251,17 @@ describe('Students tests', () => {
cy.contains('table tbody tr', 'Kulttuurien tutkimuksen kandiohjelma (01.08.2020–30.06.2023)').within(() => {
cy.get('td').eq(0).click()
})
cy.contains('table tbody tr', 'Kandidaatintutkielma (KUKA-LIS222)')
.should('have.attr', 'style')
.and('equal', 'background-color: rgb(232, 244, 255);')
cy.contains('table tbody tr', 'Kandidaatintutkielma (KUKA-LIS222)').should(
'have.css',
'background-color',
'rgb(232, 244, 255)'
)
})
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()
+2 -2
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',
@@ -141,7 +141,7 @@ describe('Study guidance group tests', () => {
'Transferred from',
'Admission type',
'Gender',
'Citizenship',
'Citizenships',
'Curriculum period',
'Latest attainment date',
'Tags',
+102 -85
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', () => {
@@ -81,11 +81,11 @@ describe('Studyprogramme overview', () => {
const tableContents = [
// [Year, Graduated, Wrote thesis]
...years.map(year => [year, 0, 0]),
[2023, 26, 0],
[2022, 48, 0],
[2021, 47, 66],
[2020, 12, 18],
[2019, 1, 15],
[2023, 26, 5],
[2022, 48, 13],
[2021, 47, 76],
[2020, 12, 23],
[2019, 1, 19],
[2018, 0, 0],
[2017, 0, 1],
]
@@ -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,16 +108,16 @@ describe('Studyprogramme overview', () => {
[2017, 41, 47, 0],
]
cy.checkTableStats(studentTableContents, 'StudentsOfTheStudyprogramme')
cy.checkTableStats(studentTableContents, 'StudentsOfTheStudyProgramme')
const graduatedTableContents = [
// [Year, Graduated, Wrote thesis]
...years.map(year => [year, 0, 0]),
[2023, 24, 0],
[2022, 44, 0],
[2021, 46, 60],
[2020, 11, 17],
[2019, 1, 15],
[2023, 24, 5],
[2022, 44, 12],
[2021, 46, 69],
[2020, 11, 22],
[2019, 1, 18],
[2018, 0, 0],
[2017, 0, 1],
]
@@ -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)
@@ -175,7 +175,7 @@ describe('Studyprogramme overview', () => {
.should('contain', 'Graduated students')
.should('contain', 'Wrote thesis')
.should('contain', 47)
.should('contain', 66)
.should('contain', 76)
cy.get('[data-cy=graduation-times-graph-breakdownBachelor]')
cy.get('[data-cy=GraduationTimeToggle]').click()
@@ -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,39 +235,39 @@ 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],
['2023 - 2024', 8, 8, 0, 0, 8, 0, 5, 3, 0, 8, 1],
['2022 - 2023', 26, 25, 0, 0, 24, 2, 19, 7, 0, 25, 1],
['2021 - 2022', 38, 29, 0, 0, 33, 5, 29, 9, 0, 35, 3],
['2020 - 2021', 30, 26, 0, 0, 11, 19, 15, 15, 0, 29, 1],
['2019 - 2020', 35, 28, 0, 0, 8, 27, 22, 13, 0, 34, 1],
['2021 - 2022', 38, 29, 0, 0, 33, 5, 29, 9, 0, 36, 4],
['2020 - 2021', 30, 26, 0, 0, 11, 19, 15, 15, 0, 29, 3],
['2019 - 2020', 35, 28, 0, 0, 8, 27, 22, 13, 0, 35, 2],
['2018 - 2019', 45, 40, 0, 0, 6, 39, 26, 19, 0, 44, 1],
['2017 - 2018', 47, 41, 0, 0, 5, 42, 31, 16, 0, 47, 0],
['Total', 229, 197, 0, 0, 95, 134, 147, 82, 0, 222, 7],
['Total', 229, 197, 0, 0, 95, 134, 147, 82, 0, 224, 12],
]
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(() => {
@@ -276,7 +276,7 @@ describe('Studyprogramme overview', () => {
const dataForStudyTracks = [
['Ekonometria, MAT-EKO', 2, 2, 0, 0, 0, 2, 1, 1, 0, 2, 0],
['Matematiikka, MAT-MAT', 13, 10, 0, 0, 1, 12, 7, 6, 0, 12, 1],
['Matematiikka, MAT-MAT', 13, 10, 0, 0, 1, 12, 7, 6, 0, 12, 3],
['Tietojenkäsittelyteoria, MAT-TIE', 2, 1, 0, 0, 1, 1, 2, 0, 0, 2, 0],
['Tilastotiede, MAT-TIL', 4, 4, 0, 0, 0, 4, 1, 3, 0, 4, 0],
]
@@ -302,7 +302,7 @@ describe('Studyprogramme overview', () => {
})
it('Links to class statistics page work', () => {
cy.get('[data-cy=Table-StudytrackOverview]').within(() => {
cy.get('[data-cy=Table-StudyTrackOverview]').within(() => {
cy.get('tbody tr.header-row')
.eq(1)
.within(() => {
@@ -314,8 +314,8 @@ describe('Studyprogramme overview', () => {
cy.contains('class size 26 students')
})
it('Links to class statistics page with all years combined work', () => {
cy.get('[data-cy=Table-StudytrackOverview]').within(() => {
it('Links to class statistics page with all years combined work', { retries: 2 }, () => {
cy.get('[data-cy=Table-StudyTrackOverview]').within(() => {
cy.get('td.total-row-cell a').click()
})
@@ -324,7 +324,7 @@ describe('Studyprogramme overview', () => {
})
it('Links to class statistics page with study track info included work', () => {
cy.get('[data-cy=Table-StudytrackOverview]').within(() => {
cy.get('[data-cy=Table-StudyTrackOverview]').within(() => {
cy.get('tbody tr.header-row')
.eq(3)
.within(() => {
@@ -357,29 +357,37 @@ describe('Studyprogramme overview', () => {
['Total', 229, 28, 18, 24, 21, 10, 14, 114],
]
cy.checkTableStats(tableContents, 'StudytrackProgress')
cy.checkTableStats(tableContents, 'StudyTrackProgress')
})
it('Studytrack overview graphs render', () => {
cy.get('[data-cy=Graph-StudytrackProgress]')
it('Study track overview graphs render', () => {
cy.get('[data-cy=Graph-StudyTrackProgress]')
.should('contain', 'Less than 30 credits')
.should('contain', '30–60 credits')
.should('contain', 'At least 180 credits')
.should('contain', '49.8%') // The percentage for total, at least 180 credits, to check that the graph renders
cy.get('[data-cy=Graph-StudytrackProgress]').contains('49.8%').trigger('mouseover', { force: true })
cy.get('[data-cy=Graph-StudyTrackProgress]').contains('49.8%').trigger('mouseover', { force: true })
cy.contains('At least 180 credits: 114')
cy.get("[data-cy='graduation-times-graph-breakdownBachelor']").within(() => {
cy.get("[data-cy='Section-KH50_001']").within(() => {
cy.contains('Start year')
cy.contains('2020 - 2021')
cy.get('[aria-label="2020 - 2021, 15. On time."]').trigger('mouseover')
cy.contains('Graduated On time: 15 students')
cy.contains('On time: 15')
cy.contains("Click a bar to view that year's study track level breakdown")
cy.get('[aria-label="2019 - 2020, 22. On time."]').click()
cy.contains("Click a bar to view that year's study track level breakdown").should('not.exist')
cy.contains('Year 2019 - 2020 by start year')
cy.get('[aria-label="MAT-MAT, 14. On time."]').trigger('mouseover')
cy.contains('Matematiikka')
cy.contains('MAT-MAT')
cy.contains('On time: 14')
})
cy.get('[data-cy=GraduationTimeToggle]').click()
cy.get('[data-cy=graduation-times-graphBachelor]').within(() => {
cy.get("[data-cy='Section-KH50_001']").within(() => {
cy.contains('Start year')
cy.contains('2020 - 2021')
cy.contains('19 graduated').trigger('mouseover')
@@ -391,29 +399,31 @@ 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],
['2021 - 2022', 5, 1, 0, 0, 1, 4, 3, 2, 0, 5, 0],
['2020 - 2021', 13, 10, 0, 0, 1, 12, 7, 6, 0, 12, 1],
['2019 - 2020', 17, 14, 0, 0, 0, 17, 10, 7, 0, 16, 1],
['2020 - 2021', 13, 10, 0, 0, 1, 12, 7, 6, 0, 12, 3],
['2019 - 2020', 17, 14, 0, 0, 0, 17, 10, 7, 0, 17, 1],
['2018 - 2019', 23, 21, 0, 0, 2, 21, 11, 12, 0, 23, 0],
['2017 - 2018', 28, 24, 0, 0, 1, 27, 15, 13, 0, 28, 0],
['Total', 89, 73, 0, 0, 6, 83, 48, 41, 0, 87, 2],
['Total', 89, 73, 0, 0, 6, 83, 48, 41, 0, 88, 4],
]
cy.checkTableStats(tableContents, 'StudytrackOverview')
cy.checkTableStats(tableContents, 'StudyTrackOverview')
})
it('Links to class statistics page with study track info included work', () => {
cy.get('[data-cy=Table-StudytrackOverview]').within(() => {
cy.get('[data-cy=Table-StudyTrackOverview]').within(() => {
cy.get('tbody tr.regular-row')
.eq(2)
.within(() => {
@@ -428,7 +438,7 @@ describe('Studyprogramme overview', () => {
})
it('Info message about missing progress stats is displayed', () => {
cy.contains('.divider', 'Progress of students of the studytrack MAT-MAT by starting year')
cy.contains('.divider', 'Progress of students of the study track MAT-MAT by starting year')
cy.contains(
'.message',
'Currently progress data is only available for all students of the study programme. Please select ”All students of the programme” to view the progress data.'
@@ -436,7 +446,7 @@ describe('Studyprogramme overview', () => {
})
it('Average graduation times are displayed correctly', () => {
cy.get("[data-cy='Section-AverageGraduationTimesStudytracks']")
cy.get("[data-cy='Section-averageGraduationTimesStudyTracks']")
cy.get("[data-cy='graduation-times-graph-breakdownBachelor']").within(() => {
cy.contains('Start year')
cy.contains('2020 - 2021')
@@ -656,7 +666,7 @@ describe('Studyprogramme overview', () => {
it('content loads', () => {
cy.contains('h3', 'Select curriculum to edit:')
cy.get('[data-cy=curriculum-picker]').contains('2023 - 2026')
cy.get('[data-cy=curriculum-picker]').contains('2023–2026')
cy.contains('Change visibility of degree courses and select criteria for academic years')
cy.contains('form', 'First year (12 months) last set: 0')
cy.get('table').within(() => {
@@ -732,16 +742,23 @@ describe('Studyprogramme overview', () => {
cy.contains(studentNumber)
}
cy.go('back')
for (const studentNumber of studentNumbers) {
cy.visit(`/students/${studentNumber}`)
cy.contains('Tags')
cy.contains(name)
}
cy.get('a').contains('Matemaattisten tieteiden kandiohjelma').invoke('removeAttr', 'target').click()
cy.url().should('include', '/study-programme/KH50_001?p_tab=4')
deleteTag(name)
})
it('deleting a tag from tag view also removes it from students', () => {
cy.contains(name).should('not.exist')
for (const studentNumber of studentNumbers) {
cy.contains('Students').click()
cy.get('.prompt').type(studentNumber)
cy.contains('a', studentNumber).click()
cy.visit(`/students/${studentNumber}`)
cy.contains('Tags').should('not.exist')
cy.contains(name).should('not.exist')
}
})
@@ -756,7 +773,7 @@ describe('Studyprogramme overview', () => {
it('can access programme and correct tabs are visible', () => {
cy.contains('Basic information')
cy.contains('Studytracks and class statistics')
cy.contains('Study tracks and class statistics')
cy.contains('Update statistics').should('not.exist')
cy.contains('Degree courses').should('not.exist')
@@ -765,19 +782,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", () => {
+50 -114
View File
@@ -1,159 +1,95 @@
/// <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')
})
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')
})
})
})
+50 -41
View File
@@ -1,47 +1,53 @@
/// <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 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)
}
})
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)
}
})
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)
}
})
containsLinks(visibleLinks.basic)
userButtonWorks('basic')
})
})
@@ -51,27 +57,30 @@ 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)
}
})
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.visit('/populations')
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')
})
})
})
})
})
+14 -1
View File
@@ -15,8 +15,21 @@
/**
* @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
on('before:browser:launch', (browser = {}, launchOptions) => {
// By manually setting the browser width and height in headless
// mode, screenshots and videos will have a better quality
// https://www.cypress.io/blog/generate-high-resolution-videos-and-screenshots
const width = 2560
const height = 1440
if (browser.name === 'chrome' && browser.isHeadless) {
launchOptions.args.push(`--window-size=${width},${height}`)
launchOptions.args.push('--force-device-scale-factor=1')
}
return launchOptions
})
}
+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
})
+2 -2
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:7.2.6
ports:
- 6379:6379
@@ -83,7 +83,7 @@ services:
jami-db:
<<: *defaults
image: postgres:15.8
image: postgres:15.10
environment:
- PGDATA=/data
- POSTGRES_PASSWORD=postgres
+1
View File
@@ -21,6 +21,7 @@ services:
SENTRY_DSN: ''
SERVICE_PROVIDER: fd
LANGUAGE_CENTER_VIEW_ENABLED: 'false' # currently no intention to be enabled
SIS_URL: 'https://sis-qa.funidata.fi'
NODE_ENV: production
image: ${DOCKER_OODIKONE_NAMESPACE:-637423527834.dkr.ecr.eu-north-1.amazonaws.com/oodikone}/oodikone-frontend:${DOCKER_IMAGE_TAG:-dev}
+5 -4
View File
@@ -64,6 +64,7 @@ services:
environment:
REACT_APP_SERVICE_PROVIDER: fd
REACT_APP_LANGUAGE_CENTER_VIEW_ENABLED: 'false'
REACT_APP_SIS_URL: 'https://sis-qa.funidata.fi'
volumes:
- ./services/frontend/src/:/opt/app-root/src/src
- ./services/shared/:/opt/app-root/src/src/shared
@@ -75,7 +76,7 @@ services:
PGDATA: /data
POSTGRES_DB: kone-db
POSTGRES_HOST_AUTH_METHOD: trust
image: postgres:15.8
image: postgres:15.10
ports:
- 12349:5432
volumes:
@@ -85,7 +86,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:
@@ -98,7 +99,7 @@ services:
PGDATA: /data
POSTGRES_DB: sis-db
POSTGRES_HOST_AUTH_METHOD: trust
image: postgres:15.8
image: postgres:15.10
volumes:
- sis-db-data:/data
shm_size: 2gb
@@ -175,7 +176,7 @@ services:
PGDATA: /data
POSTGRES_DB: user-db
POSTGRES_HOST_AUTH_METHOD: trust
image: postgres:15.8
image: postgres:15.10
volumes:
- user-db-data:/data
ports:
+2 -2
View File
@@ -76,7 +76,7 @@ services:
<<: *defaults
command: 'redis-server --appendonly yes'
container_name: redis
image: redis:6.2.14
image: redis:7.2.6
ports:
- '6379:6379'
volumes:
@@ -189,7 +189,7 @@ services:
jami-db:
<<: *defaults
image: postgres:15.8
image: postgres:15.10
environment:
- PGDATA=/data
- POSTGRES_PASSWORD=postgres
+6 -11
View File
@@ -8,24 +8,22 @@ 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 `facultyStatistics`, `openUniSearch`, and `studyGuidanceGroups` simply enable certain views (_Faculties_, _Open uni student population_, and _Guidance groups_ respectively).
### courseStatistics
- Can see statistics of every course but year statistics of ≤ 5 students are hidden
- If the user has [full rights](#programmes) to **any** programme, they can also see exact statistics even if there are ≤ 5 students (the same behavior as with `admin` or `fullSisuAccess` roles)
### facultyStatistics
### teachers
- Can see statistics of every course and year
### katselemusViewer
- 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 +44,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 +63,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

+117
View File
@@ -0,0 +1,117 @@
# How to update the database schema
These instructions explain how to update the schemas for the following databases used by Oodikone:
- `sis-db`
- `user-db`
- `kone-db`
The steps to **add, remove, or modify columns** are mostly the same for each database, but the file paths and container names differ. Always **double-check** that you have the correct function name, column name, data type, and other relevant details in your migration file.
> [!NOTE]
> If you need to change the `sis-importer-db` schema, see the instructions in the [sis-importer repo](https://github.com/UniversityOfHelsinkiCS/sis-importer/blob/master/README.md#tricks--tips).
## General guidelines
### File names
- Name new migration files using the format `YYYYMMDD_XX_description.js`.
- The `XX` part is a migration number (starting from `00` each day).
- Example: `20250122_00_add_column_to_table.js`.
### Model vs. column names
- Use **camelCase** in your model definitions.
- Use **snake_case** in the actual database columns.
- If a model has `underscored: true`, Sequelize automatically converts camelCase to snake_case in all queries. Otherwise, specify the column name explicitly.
> [!IMPORTANT]
> The column name **in the database** (snake_case) must be used in the migration file, not the camelCase name from your model.
### Verifying the migration locally
- Migrations are run automatically whenever the server restarts.
- After creating a migration, ensure the relevant service is running:
- For `sis-db`: `npm run both:real` or `npm run updater:real`
- For `user-db`/`kone-db`: `npm run both:real` or `npm run oodikone:real`
- **Connect to the database container** and check:
```bash
docker exec -it <container-name> psql -U postgres <database-name>-real
```
Replace `<container-name>` and `<database-name>` with the actual values (e.g., `sis-db`, `user-db`, or `kone-db`).
- In the psql prompt, verify your migration:
```sql
SELECT * FROM migrations;
```
Also confirm that the new (or updated) column exists in the appropriate table and has the correct type.
### Deploying changes to production
- **Push changes** to the `master` branch and wait for the workflow to finish.
- For **`sis-db`** migrations (in the updater):
- You **do not** need to create a new release. Once tests pass, changes to the updater go directly to production.
- For **`user-db`** and **`kone-db`** migrations (in the backend):
- A new release must be made before the changes go to production.
- After deployment, check logs (for example, in OpenShift) to confirm the migration ran successfully.
- Optionally, use the `db-cli` tool to inspect the production database. See `how_to_db_cli.md` (in GitLab) for instructions.
## `sis-db`
> [!NOTE]
> These instructions assume that needed data is already in the `sis-importer-db`. If not, you must add the data there first. See the [sis-importer repo](https://github.com/UniversityOfHelsinkiCS/sis-importer/blob/master/README.md#tricks--tips) for more details.
1. **Update the updater model:**
- Go to `updater/sis-updater-worker/src/db/models/` and choose the model you want to change.
- Add or modify the column (with its **type** and any other necessary fields).
- Check the correct data type in the importer.
- Use camelCase in the model, snake_case in the database.
1. **Update the backend model:**
- Go to `services/backend/src/models/`.
- The models are in different formats (JavaScript in the updater, TypeScript in the backend), but follow existing examples.
1. **Create a migration file:**
- Go to `updater/sis-updater-worker/src/db/migrations/`.
- Name your file using the format described above (e.g. `20250122_00_add_column_to_table.js`).
- **Remember**: The column name **in the database** (snake_case) must be used in the migration file, not the camelCase name from your model.
1. **Run and verify the migration:**
- Ensure that the updater is running (`npm run both:real` or `npm run updater:real`).
- Use the steps in [General guidelines](#general-guidelines) to verify.
1. **Push to master and deploy:**
- Your migration will go to production automatically after tests pass.
- Check logs in production (OpenShift) or use `db-cli` to confirm success.
## `user-db` and `kone-db`
1. **Update the model:**
- Go to `services/backend/src/models/kone` (for `kone-db`) or `services/backend/src/models/user.ts` (the only model for user-db)
- Add or modify the column, type, and any other needed fields. Follow existing models for structure.
1. **Create a migration file:**
- Go to `services/backend/src/database/`.
- Use `migrations_kone` (for `kone-db`) or `migrations_user` (for `user-db`).
- Name the file using the same `YYYYMMDD_XX_description.js` format.
- **Remember**: The column name **in the database** (snake_case) must be used in the migration file, not the camelCase name from your model.
1. **Run and verify the migration:**
- Make sure Oodikone is running (`npm run both:real` or `npm run oodikone:real`).
- Follow the same verification steps as in [General Guidelines](#general-guidelines).
1. **Push to master and release:**
- Wait for the CI workflow to pass.
- **Create a new release**; then check the production logs (in OpenShift) to confirm the migration.
- Optionally, use `db-cli` to check the production database state (see `how_to_db_cli.md` in GitLab).
+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)
<!--
+184
View File
@@ -0,0 +1,184 @@
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-boolean-value': ['error', 'never'],
'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': 'warn', // 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: ['services/**/shared/**/*'],
rules: {
'import/no-unused-modules': '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',
},
},
},
]
+1642 -1318
View File
File diff suppressed because it is too large Load Diff
+19 -21
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.11",
"@eslint/js": "^9.19.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": "^14.0.1",
"eslint": "^9.19.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-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-import-resolver-typescript": "^3.7.0",
"eslint-plugin-cypress": "^4.1.0",
"eslint-plugin-import": "^2.31.0",
"eslint-plugin-prettier": "^5.2.3",
"eslint-plugin-react": "^7.37.4",
"eslint-plugin-react-hooks": "^5.1.0",
"globals": "^15.14.0",
"husky": "^9.1.7",
"lint-staged": "^15.4.3",
"node-actionlint": "^1.2.2",
"nyc": "^17.0.0",
"prettier": "3.3.3",
"stylelint": "^16.2.1",
"stylelint-config-standard": "^36.0.0"
"nyc": "^17.1.0",
"prettier": "3.4.2",
"stylelint": "^16.14.1",
"stylelint-config-standard": "^36.0.1",
"typescript-eslint": "^8.22.0"
},
"dependencies": {
"lodash": "^4.17.21",
+7 -6
View File
@@ -5,18 +5,19 @@
{
"groupName": "all non-major dependencies",
"groupSlug": "all-minor-patch",
"matchPackagePatterns": ["*"],
"matchUpdateTypes": ["minor", "patch"]
"matchUpdateTypes": ["minor", "patch"],
"matchCurrentVersion": "!/^0/",
"matchPackageNames": ["*"]
},
{
"matchDatasources": ["docker"],
"matchPackageNames": ["node"],
"versionCompatibility": "^(?<version>[^-]+)(?<compatibility>-.*)?$",
"versioning": "node"
"enabled": false
}
],
"rebaseWhen": "never",
"vulnerabilityAlerts": {
"labels": ["security"]
}
},
"addLabels": ["dependencies"],
"rangeStrategy": "bump"
}
+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')
+1099 -400
View File
File diff suppressed because it is too large Load Diff
+21 -23
View File
@@ -5,47 +5,45 @@
"build": "tsc"
},
"dependencies": {
"@sentry/node": "^5.30.0",
"@sentry/tracing": "^6.18.2",
"axios": "^0.28.1",
"bluebird": "^3.7.2",
"bullmq": "^5.0.0",
"compression": "^1.7.4",
"@sentry/node": "^8.53.0",
"@sentry/profiling-node": "^8.53.0",
"axios": "^1.7.9",
"bullmq": "^5.39.1",
"compression": "^1.7.5",
"cors": "^2.8.5",
"cron": "^3.0.0",
"express": "^4.19.2",
"cron": "^3.5.0",
"express": "^4.21.2",
"express-async-errors": "^3.1.1",
"lodash": "^4.17.21",
"lru-cache": "^11.0.0",
"lru-cache": "^11.0.2",
"moment": "^2.30.1",
"moment-timezone": "^0.5.37",
"moment-timezone": "^0.5.46",
"morgan": "^1.10.0",
"oodikone-shared": "file:../oodikone-shared",
"papaparse": "^5.4.1",
"pg": "^8.11.5",
"redis": "^3.1.2",
"papaparse": "^5.5.2",
"pg": "^8.13.1",
"redis": "^4.7.0",
"reflect-metadata": "^0.2.2",
"sequelize": "^6.37.3",
"sequelize": "^6.37.5",
"sequelize-typescript": "^2.1.6",
"umzug": "^3.8.1",
"umzug": "^3.8.2",
"unfuck-utf8-headers-middleware": "^1.0.1",
"winston": "^3.8.2",
"winston": "^3.17.0",
"winston-gelf-transporter": "^1.0.2"
},
"devDependencies": {
"@types/compression": "^1.7.5",
"@types/cors": "^2.8.17",
"@types/express": "^4.17.21",
"@types/lodash": "^4.17.6",
"@types/lodash": "^4.17.15",
"@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.12.0",
"@types/validator": "^13.12.2",
"tsx": "^4.19.2",
"typescript": "^5.7.3"
},
"overrides": {
"winston-gelf-transporter": {
"gelf-pro": "^1.3.4"
"gelf-pro": "^1.4.0"
}
}
}
-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,
+11 -8
View File
@@ -8,8 +8,8 @@ import { computeLanguageCenterData, LANGUAGE_CENTER_REDIS_KEY } from './services
import { findStudentsCloseToGraduation, CLOSE_TO_GRADUATION_REDIS_KEY } from './services/populations/closeToGraduation'
import { redisClient } from './services/redis'
import { getCurrentSemester } from './services/semesters'
import { combinedStudyprogrammes, isRelevantProgramme } from './services/studyProgramme/studyProgrammeHelpers'
import { updateBasicView, updateStudytrackView } from './services/studyProgramme/studyProgrammeUpdates'
import { combinedStudyProgrammes, isRelevantProgramme } from './services/studyProgramme/studyProgrammeHelpers'
import { updateBasicView, updateStudyTrackView } from './services/studyProgramme/studyProgrammeUpdates'
import { findAndSaveTeachers } from './services/teachers/top'
import { deleteOutdatedUsers } from './services/userService'
import logger from './util/logger'
@@ -25,7 +25,10 @@ const schedule = (cronTime: string, onTick: () => void) => {
export const refreshCloseToGraduating = async () => {
logger.info('Refreshing students close to graduating')
const updatedData = await findStudentsCloseToGraduation()
await redisClient.setAsync(CLOSE_TO_GRADUATION_REDIS_KEY, JSON.stringify(updatedData))
await redisClient.set(
CLOSE_TO_GRADUATION_REDIS_KEY,
JSON.stringify({ ...updatedData, lastUpdated: new Date().toISOString() })
)
logger.info('Students close to graduating updated!')
}
@@ -45,7 +48,7 @@ export const refreshFaculties = async () => {
export const refreshLanguageCenterData = async () => {
logger.info('Refreshing language center data')
const freshData = await computeLanguageCenterData()
await redisClient.setAsync(LANGUAGE_CENTER_REDIS_KEY, JSON.stringify(freshData))
await redisClient.set(LANGUAGE_CENTER_REDIS_KEY, JSON.stringify(freshData))
logger.info('Language center data refreshed!')
}
@@ -61,10 +64,10 @@ const refreshProgrammesAndFaculties = async () => {
export const refreshProgramme = async (code: string) => {
await updateBasicView(code, '')
await updateStudytrackView(code, '')
const combinedProgramme = combinedStudyprogrammes[code] || ''
await updateStudyTrackView(code, '')
const combinedProgramme = combinedStudyProgrammes[code] || ''
await updateBasicView(code, combinedProgramme)
await updateStudytrackView(code, combinedProgramme)
await updateStudyTrackView(code, combinedProgramme)
}
export const refreshProgrammes = async () => {
@@ -83,7 +86,7 @@ export const refreshProgrammes = async () => {
}
}
const refreshTeacherLeaderboard = async () => {
export const refreshTeacherLeaderboard = async () => {
logger.info('Refreshing statistics for teacher leaderboard')
const currentSemestersYearCode = (await getCurrentSemester()).getDataValue('yearcode')
await findAndSaveTeachers(currentSemestersYearCode, currentSemestersYearCode - 1)
@@ -40,15 +40,15 @@ const accessLogger = morgan((tokens, req: Request, res: Response): undefined =>
const message = [
`${user.mockedBy ? '(mocking) ' : ''}${user.name}`,
`${(tokens['response-time'](req, res) || '0').split('.')[0]} ms`.padEnd(8, ' '),
`${(tokens['response-time'](req, res) ?? '0').split('.')[0]} ms`.padEnd(8, ' '),
tokens.status(req, res),
tokens.method(req, res),
decodeURIComponent(tokens.url(req, res) as string),
decodeURIComponent(tokens.url(req, res)!),
].join(' ')
const fullStudyProgrammeRights = getFullStudyProgrammeRights(user.programmeRights)
const iamBasedRights = user.programmeRights.filter(right => right.isIamBased)
const usingIamRights = iamBasedRights.some(right => (tokens.url(req, res) as string).includes(right.code))
const usingIamRights = iamBasedRights.some(right => tokens.url(req, res)!.includes(right.code))
const onlyIamRights = !user.isAdmin && fullStudyProgrammeRights.length === 0
logger.info(message, {
+13 -8
View File
@@ -1,12 +1,17 @@
import { NextFunction, Request, Response } from 'express'
import { Role } from '../types'
import { Role } from '../shared/types'
export const roles = (requiredRoles: Role[]) => async (req: Request, res: Response, next: NextFunction) => {
const { roles } = req.user
if (requiredRoles.some(role => roles.includes(role)) || roles.includes('admin')) {
return next()
export const roles =
(requiredRoles: Role[], requiredIamGroups?: string[]) => (req: Request, res: Response, next: NextFunction) => {
const { roles, iamGroups } = req.user
if (
requiredRoles.some(role => roles.includes(role)) ||
requiredIamGroups?.some(group => iamGroups.includes(group)) ||
roles.includes('admin')
) {
return next()
}
res.status(403).json({ error: 'Missing required roles or IAM groups' })
}
res.status(403).json({ error: 'Missing required roles' })
}
+8 -22
View File
@@ -25,42 +25,28 @@ const getUser = async (
iamAccess: IamAccess
) => {
if (showAsUser && specialGroup?.superAdmin) {
return await getMockedUser({ userToMock: showAsUser as string, mockedBy: username as string })
return await getMockedUser({ userToMock: showAsUser, mockedBy: username })
}
return await getUserToska({
username: username as string,
name: name as string,
email: email as string,
username,
name,
email,
iamGroups,
specialGroup,
sisId: sisId as string,
sisId,
iamAccess,
})
}
type Headers = {
host?: string
'user-agent'?: string
accept?: string
'accept-language'?: string
'accept-encoding'?: string
referer?: string
displayname?: string
hygroupcn?: string
hypersonsisuid?: string
mail?: string
remote_user?: string
'shib-session-id'?: string
shib_logout_url?: string
uid?: string
'x-show-as-user'?: string
connection?: string
cookie?: string
'sec-fetch-dest'?: string
'sec-fetch-mode'?: string
'sec-fetch-site'?: string
'if-none-match'?: string
priority?: string
}
const toskaUserMiddleware = async (req: Request, _res: Response, next: NextFunction) => {
@@ -87,8 +73,8 @@ const toskaUserMiddleware = async (req: Request, _res: Response, next: NextFunct
)
}
const iamGroups = parseIamGroups(hygroupcn as string)
const { iamAccess = {}, specialGroup = {} } = await getOrganizationAccess(sisId as string, iamGroups)
const iamGroups = parseIamGroups(hygroupcn!)
const { iamAccess = {}, specialGroup = {} } = await getOrganizationAccess(sisId!, iamGroups)
const iamRights = Object.keys(iamAccess)
if (!hasRequiredIamGroup(iamGroups, iamRights)) {
@@ -107,7 +93,7 @@ const toskaUserMiddleware = async (req: Request, _res: Response, next: NextFunct
Sentry.setUser({ username: user.mockedBy ?? username! })
req.user = user
req.logoutUrl = logoutUrl as string
req.logoutUrl = logoutUrl!
next()
}
+1 -3
View File
@@ -1,4 +1,3 @@
/* eslint-disable import/no-cycle */
import { InferAttributes } from 'sequelize'
import {
BelongsTo,
@@ -50,11 +49,10 @@ export class SISStudyRight extends Model<InferAttributes<SISStudyRight>> {
@Column(DataType.BOOLEAN)
cancelled!: boolean
@ForeignKey(() => Student)
@Column(DataType.STRING)
studentNumber!: string
@BelongsTo(() => Student)
@BelongsTo(() => Student, { foreignKey: 'studentNumber', targetKey: 'studentnumber' })
student!: Student
@ForeignKey(() => StudyrightExtent)
@@ -1,4 +1,3 @@
/* eslint-disable import/no-cycle */
import { InferAttributes } from 'sequelize'
import {
BelongsTo,
@@ -12,7 +11,8 @@ import {
UpdatedAt,
} from 'sequelize-typescript'
import { DegreeProgrammeType, Name, Phase, StudyTrack } from '../types'
import { Name } from '../shared/types'
import { DegreeProgrammeType, Phase, StudyTrack } from '../types'
import { SISStudyRight } from './SISStudyRight'
@Table({
+1 -11
View File
@@ -1,4 +1,3 @@
/* eslint-disable import/no-cycle */
import { InferAttributes } from 'sequelize'
import {
BelongsTo,
@@ -14,7 +13,7 @@ import {
UpdatedAt,
} from 'sequelize-typescript'
import { Name } from '../types'
import { Name } from '../shared/types'
import { CourseProvider } from './courseProvider'
import { CourseType } from './courseType'
import { Credit } from './credit'
@@ -57,15 +56,6 @@ export class Course extends Model<InferAttributes<Course>> {
@Column(DataType.JSONB)
name!: Name
@Column(DataType.DATE)
latest_instance_date!: Date
@Column(DataType.DATE)
startdate!: Date
@Column(DataType.DATE)
enddate!: Date
@Column(DataType.DATE)
max_attainment_date!: Date
+1 -12
View File
@@ -1,6 +1,5 @@
/* eslint-disable import/no-cycle */
import { InferAttributes } from 'sequelize'
import { Column, CreatedAt, DataType, ForeignKey, Model, PrimaryKey, Table, UpdatedAt } from 'sequelize-typescript'
import { Column, CreatedAt, DataType, ForeignKey, Model, Table, UpdatedAt } from 'sequelize-typescript'
import { Course } from './course'
import { Organization } from './organization'
@@ -9,18 +8,8 @@ import { Organization } from './organization'
underscored: true,
modelName: 'course_provider',
tableName: 'course_providers',
indexes: [
{
unique: true,
fields: ['coursecode', 'organizationcode'],
},
],
})
export class CourseProvider extends Model<InferAttributes<CourseProvider>> {
@PrimaryKey
@Column(DataType.STRING)
composite!: string
@ForeignKey(() => Course)
@Column(DataType.STRING)
coursecode!: string
+1 -2
View File
@@ -1,8 +1,7 @@
/* eslint-disable import/no-cycle */
import { InferAttributes } from 'sequelize'
import { Column, CreatedAt, DataType, HasMany, Model, PrimaryKey, Table, UpdatedAt } from 'sequelize-typescript'
import { Name } from '../types'
import { Name } from '../shared/types'
import { Course } from './course'
@Table({
-1
View File
@@ -1,4 +1,3 @@
/* eslint-disable import/no-cycle */
import { InferAttributes } from 'sequelize'
import {
BelongsTo,
+1 -17
View File
@@ -1,16 +1,5 @@
/* eslint-disable import/no-cycle */
import { InferAttributes } from 'sequelize'
import {
Column,
CreatedAt,
DataType,
ForeignKey,
Model,
PrimaryKey,
Table,
Unique,
UpdatedAt,
} from 'sequelize-typescript'
import { Column, CreatedAt, DataType, ForeignKey, Model, Table, UpdatedAt } from 'sequelize-typescript'
import { Credit } from './credit'
import { Teacher } from './teacher'
@@ -21,11 +10,6 @@ import { Teacher } from './teacher'
tableName: 'credit_teachers',
})
export class CreditTeacher extends Model<InferAttributes<CreditTeacher>> {
@PrimaryKey
@Unique
@Column(DataType.STRING)
composite!: string
@ForeignKey(() => Credit)
@Column(DataType.STRING)
credit_id!: string
+2 -2
View File
@@ -1,8 +1,8 @@
/* eslint-disable import/no-cycle */
import { InferAttributes } from 'sequelize'
import { Column, CreatedAt, DataType, Model, PrimaryKey, Table, UpdatedAt } from 'sequelize-typescript'
import { CreditTypeCode, Name } from '../types'
import { Name } from '../shared/types'
import { CreditTypeCode } from '../types'
@Table({
underscored: true,
@@ -1,8 +1,7 @@
/* eslint-disable import/no-cycle */
import { InferAttributes } from 'sequelize'
import { Column, CreatedAt, DataType, Model, Table, UpdatedAt } from 'sequelize-typescript'
import { Name } from '../types'
import { Name } from '../shared/types'
@Table({
underscored: true,
@@ -1,4 +1,3 @@
/* eslint-disable import/no-cycle */
import { InferAttributes } from 'sequelize'
import {
BelongsTo,
-1
View File
@@ -1,4 +1,3 @@
/* eslint-disable import/no-cycle */
import { CreditTypeCode } from '../types'
import { Course } from './course'
import { CourseProvider } from './courseProvider'
-1
View File
@@ -1,4 +1,3 @@
/* eslint-disable import/no-cycle */
import { CreationOptional, InferAttributes, InferCreationAttributes } from 'sequelize'
import { AutoIncrement, BelongsTo, Column, DataType, ForeignKey, Model, PrimaryKey, Table } from 'sequelize-typescript'
@@ -1,4 +1,3 @@
/* eslint-disable import/no-cycle */
import { CreationOptional, InferAttributes, InferCreationAttributes } from 'sequelize'
import { AutoIncrement, BelongsTo, Column, DataType, ForeignKey, Model, PrimaryKey, Table } from 'sequelize-typescript'
+1 -2
View File
@@ -1,4 +1,3 @@
/* eslint-disable import/no-cycle */
import { InferAttributes } from 'sequelize'
import {
BelongsToMany,
@@ -12,7 +11,7 @@ import {
UpdatedAt,
} from 'sequelize-typescript'
import { Name } from '../types'
import { Name } from '../shared/types'
import { Course, CourseProvider, ProgrammeModule, SISStudyRight } from '.'
@Table({
@@ -1,4 +1,3 @@
/* eslint-disable import/no-cycle */
import { InferAttributes } from 'sequelize'
import {
BelongsTo,
@@ -13,7 +12,8 @@ import {
UpdatedAt,
} from 'sequelize-typescript'
import { DegreeProgrammeType, Name } from '../types'
import { Name } from '../shared/types'
import { DegreeProgrammeType } from '../types'
import { Organization } from './organization'
import { ProgrammeModuleChild } from './programmeModuleChild'
@@ -70,6 +70,9 @@ export class ProgrammeModule extends Model<InferAttributes<ProgrammeModule>> {
@Column(DataType.STRING)
degreeProgrammeType!: DegreeProgrammeType | null
@Column(DataType.INTEGER)
minimumCredits!: number | null
@CreatedAt
@Column(DataType.DATE)
createdAt!: Date
@@ -1,4 +1,3 @@
/* eslint-disable import/no-cycle */
import { InferAttributes } from 'sequelize'
import { Column, CreatedAt, DataType, ForeignKey, Model, PrimaryKey, Table, UpdatedAt } from 'sequelize-typescript'
+1 -2
View File
@@ -1,8 +1,7 @@
/* eslint-disable import/no-cycle */
import { InferAttributes } from 'sequelize'
import { Column, CreatedAt, DataType, Model, PrimaryKey, Table, UpdatedAt } from 'sequelize-typescript'
import { Name } from '../types'
import { Name } from '../shared/types'
@Table({
underscored: true,
+10 -19
View File
@@ -1,7 +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 '../shared/types'
import { GenderCode } from '../types'
import { Credit, Enrollment, SISStudyRight, Studyplan } from '.'
@@ -27,7 +27,7 @@ export class Student extends Model<InferAttributes<Student>> {
@HasMany(() => Enrollment, { foreignKey: 'studentnumber', sourceKey: 'studentnumber' })
enrollments!: Enrollment[]
@HasMany(() => SISStudyRight)
@HasMany(() => SISStudyRight, { foreignKey: 'studentNumber', sourceKey: 'studentnumber' })
studyRights!: SISStudyRight[]
@HasMany(() => Studyplan, { foreignKey: 'studentnumber', sourceKey: 'studentnumber' })
@@ -57,23 +57,8 @@ export class Student extends Model<InferAttributes<Student>> {
@Column(DataType.STRING)
phone_number!: string
@Column(DataType.STRING)
country_fi!: string
@Column(DataType.STRING)
country_sv!: string
@Column(DataType.STRING)
country_en!: string
@Column(DataType.STRING)
home_country_fi!: string
@Column(DataType.STRING)
home_country_sv!: string
@Column(DataType.STRING)
home_country_en!: string
@Column(DataType.JSONB)
citizenships!: Name[]
@Column(DataType.STRING)
gender_code!: GenderCode
@@ -81,6 +66,12 @@ export class Student extends Model<InferAttributes<Student>> {
@Column(DataType.STRING)
sis_person_id!: string
@Column(DataType.BOOLEAN)
hasPersonalIdentityCode!: boolean
@Column(DataType.STRING)
preferredLanguage!: string
@CreatedAt
@Column(DataType.DATE)
createdAt!: Date
-1
View File
@@ -1,4 +1,3 @@
/* eslint-disable import/no-cycle */
import { InferAttributes } from 'sequelize'
import {
BelongsTo,
@@ -1,4 +1,3 @@
/* eslint-disable import/no-cycle */
import { InferAttributes } from 'sequelize'
import { Column, CreatedAt, DataType, Model, PrimaryKey, Table, UpdatedAt } from 'sequelize-typescript'
-1
View File
@@ -1,4 +1,3 @@
/* eslint-disable import/no-cycle */
import { InferAttributes } from 'sequelize'
import { BelongsToMany, Column, CreatedAt, DataType, Model, PrimaryKey, Table, UpdatedAt } from 'sequelize-typescript'
+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)
}
+25 -16
View File
@@ -2,39 +2,48 @@ 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 3',
time: new Date().toISOString(),
version: '0.0.3',
},
{
description: "Let's not spam the GitHub API in development!",
title: 'Release 2',
time: new Date().toISOString(),
version: '0.0.2',
},
{
description: 'This release should not be visible on the frontpage',
title: 'Release 1',
time: new Date().toISOString(),
version: '0.0.1',
},
]
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,
version: release.tag_name,
}))
changelog.data = newChangelogData
res.status(200).send(newChangelogData)
changelog.data = releasesFromAPI
res.status(200).json(releasesFromAPI)
})
export default router
+38 -10
View File
@@ -1,10 +1,12 @@
import crypto from 'crypto'
import { Request, Response, Router } from 'express'
import moment from 'moment'
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()
@@ -22,24 +24,45 @@ router.get('/v2/coursesmulti', async (req: GetCoursesRequest, res: Response) =>
return res.status(400).json({ error: 'Query parameter name or code is invalid' })
}
let results: { courses: CourseWithSubsId[] } = { courses: [] }
results = await getCoursesByNameAndOrCode(name, code)
const courses = await getCoursesByNameAndOrCode(name, code)
if (combineSubstitutions === 'false') {
const courseCodes = results.courses.map(course => course.code)
const courseCodes = courses.map(course => course.code)
const substitutions = [
...new Set(
results.courses
.flatMap(course => course.substitutions)
.filter(code => code !== null && !courseCodes.includes(code))
courses.flatMap(course => course.substitutions).filter(code => code !== null && !courseCodes.includes(code))
),
]
const substitutionDetails = await getCoursesByCodes(substitutions)
for (const substitution of substitutionDetails) {
results.courses.push(substitution.dataValues)
courses.push(substitution.toJSON())
}
}
res.json(results)
const mergedCourses = {}
for (const course of courses) {
const groupId = combineSubstitutions === 'true' ? course.subsId! : course.code
if (!(course.max_attainment_date && course.min_attainment_date)) {
continue
}
if (!mergedCourses[groupId]) {
mergedCourses[groupId] = {
...course,
substitutions: combineSubstitutions === 'true' ? course.substitutions : [],
}
} else {
const mergedCourse = mergedCourses[groupId]
if (moment(mergedCourse.max_attainment_date).isBefore(course.max_attainment_date)) {
mergedCourse.max_attainment_date = course.max_attainment_date
}
if (moment(mergedCourse.min_attainment_date).isAfter(course.min_attainment_date)) {
mergedCourse.min_attainment_date = course.min_attainment_date
}
}
}
res.json({ courses: Object.values(mergedCourses) })
})
interface GetCourseYearlyStatsRequest extends Request {
@@ -54,7 +77,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 +93,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') {
+83 -91
View File
@@ -1,11 +1,11 @@
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'
import { getSortedFaculties } from '../services/faculty/facultyHelpers'
import { getFacultiesForFacultyList } from '../services/faculty/facultyHelpers'
import {
getBasicStats,
setBasicStats,
@@ -22,7 +22,7 @@ import { combineFacultyStudentProgress } from '../services/faculty/facultyStuden
import { combineFacultyStudents } from '../services/faculty/facultyStudents'
import { combineFacultyThesisWriters } from '../services/faculty/facultyThesisWriters'
import { updateFacultyOverview, updateFacultyProgressOverview } from '../services/faculty/facultyUpdates'
import { Graduated, ProgrammeFilter, SpecialGroups, StatsType, YearType } from '../types'
import { Graduated, ProgrammeFilter, SpecialGroups, StatsType, YearType } from '../shared/types'
import logger from '../util/logger'
// Faculty uses a lot of tools designed for Study programme.
@@ -31,7 +31,7 @@ import logger from '../util/logger'
const router = Router()
router.get('/', async (_req: Request, res: Response) => {
const faculties = await getSortedFaculties()
const faculties = await getFacultiesForFacultyList()
res.json(faculties)
})
@@ -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
+30 -86
View File
@@ -5,20 +5,20 @@ import {
setBasicStats,
getGraduationStats,
setGraduationStats,
getStudytrackStats,
setStudytrackStats,
getStudyTrackStats,
setStudyTrackStats,
} from '../services/analyticsService'
import { getCreditsProduced } from '../services/providerCredits'
import { getProgrammeName } from '../services/studyProgramme'
import { getBasicStatsForStudytrack } from '../services/studyProgramme/studyProgrammeBasics'
import {
getStudyProgrammeCoursesForStudyTrack,
getStudyProgrammeStatsForColorizedCoursesTable,
} from '../services/studyProgramme/studyProgrammeCourses'
import { getGraduationStatsForStudytrack } from '../services/studyProgramme/studyProgrammeGraduations'
import { updateBasicView, updateStudytrackView } from '../services/studyProgramme/studyProgrammeUpdates'
import { getStudyRightsInProgramme } from '../services/studyProgramme/studyRightFinders'
import { getGraduationStatsForStudyTrack } from '../services/studyProgramme/studyProgrammeGraduations'
import { updateBasicView, updateStudyTrackView } from '../services/studyProgramme/studyProgrammeUpdates'
import { getStudyRightsInProgramme, getStudyTracksForProgramme } from '../services/studyProgramme/studyRightFinders'
import { getStudyTrackStatsForStudyProgramme } from '../services/studyProgramme/studyTrackStats'
import { Graduated, SpecialGroups, YearType } from '../shared/types'
import logger from '../util/logger'
import { logInfoForGrafana } from '../util/logInfoForGrafana'
@@ -43,8 +43,8 @@ router.get('/creditstats', async (req: GetCreditStatsRequest, res: Response) =>
interface GetStatsRequest extends Request {
query: {
year_type: string
special_groups: string
year_type: YearType
special_groups: SpecialGroups
combined_programme: string
}
}
@@ -56,7 +56,7 @@ router.get('/:id/basicstats', async (req: GetStatsRequest, res: Response) => {
return res.status(422).end()
}
logInfoForGrafana(code, combinedProgramme)
void logInfoForGrafana(code, combinedProgramme)
const data = await getBasicStats(code, combinedProgramme, yearType, specialGroups)
if (data) {
return res.json(data)
@@ -88,7 +88,7 @@ router.get('/:id/graduationstats', async (req: GetStatsRequest, res: Response) =
return res.json(data)
}
const updatedStats = await getGraduationStatsForStudytrack({
const updatedStats = await getGraduationStatsForStudyTrack({
studyProgramme: code,
combinedProgramme,
settings: {
@@ -116,7 +116,7 @@ router.get('/:id/coursestats', async (req: GetCourseStatsRequest, res: Response)
const combinedProgramme = req.query?.combined_programme
const date = new Date()
date.setHours(23, 59, 59, 999)
logInfoForGrafana(code, combinedProgramme)
void logInfoForGrafana(code, combinedProgramme)
try {
const data = await getStudyProgrammeCoursesForStudyTrack(date.getTime(), code, showByYear, combinedProgramme)
return res.json(data)
@@ -127,8 +127,8 @@ router.get('/:id/coursestats', async (req: GetCourseStatsRequest, res: Response)
interface GetStudyTrackStatsRequest extends Request {
query: {
graduated: string
special_groups: string
graduated: Graduated
special_groups: SpecialGroups
combined_programme: string
}
}
@@ -140,8 +140,8 @@ router.get('/:id/studytrackstats', async (req: GetStudyTrackStatsRequest, res: R
return res.status(422).end()
}
logInfoForGrafana(code, combinedProgramme)
const data = await getStudytrackStats(code, combinedProgramme, graduated, specialGroups)
void logInfoForGrafana(code, combinedProgramme)
const data = await getStudyTrackStats(code, combinedProgramme, graduated, specialGroups)
if (data) {
return res.json(data)
}
@@ -157,7 +157,7 @@ router.get('/:id/studytrackstats', async (req: GetStudyTrackStatsRequest, res: R
studyRightsOfProgramme,
})
if (updated) {
await setStudytrackStats(updated, graduated, specialGroups)
await setStudyTrackStats(updated, graduated, specialGroups)
}
return res.json(updated)
})
@@ -172,6 +172,19 @@ router.get('/:id/colorizedtablecoursestats', async (req: Request, res: Response)
}
})
router.get('/:id/studytracks', async (req: Request, res: Response) => {
const code = req.params.id
if (!code) {
return res.status(422).end()
}
try {
const data = await getStudyTracksForProgramme(code)
return res.json(data)
} catch (error) {
logger.error({ message: `Failed to get study tracks for study programme ${code}`, meta: `${error}` })
}
})
interface GetUpdateViewRequest extends Request {
query: {
combined_programme: string
@@ -201,7 +214,7 @@ router.get('/:id/update_studytrackview', async (req: GetUpdateViewRequest, res:
return res.status(400).json({ error: 'Missing code' })
}
try {
const result = await updateStudytrackView(code, combinedProgramme)
const result = await updateStudyTrackView(code, combinedProgramme)
return res.json(result)
} catch (error) {
const message = `Failed to update study track stats for programme ${code}${combinedProgramme ? `+${combinedProgramme}` : ''}`
@@ -210,73 +223,4 @@ router.get('/:id/update_studytrackview', async (req: GetUpdateViewRequest, res:
}
})
interface GetEvaluationStatsRequest extends Request {
query: {
graduated: string
year_type: string
special_groups: string
}
}
router.get('/:id/evaluationstats', async (req: GetEvaluationStatsRequest, res: Response) => {
const code = req.params.id
const { graduated, year_type: yearType, special_groups: specialGroups } = req.query
if (!code) {
return res.status(422).end()
}
// Statistics for Tilannekuvalomake view
const combinedProgramme = ''
let gradData = await getGraduationStats(code, combinedProgramme, yearType, specialGroups)
if (!gradData) {
const updatedStats = await getGraduationStatsForStudytrack({
studyProgramme: code,
combinedProgramme,
settings: {
isAcademicYear: yearType === 'ACADEMIC_YEAR',
includeAllSpecials: specialGroups === 'SPECIAL_INCLUDED',
},
})
if (updatedStats) {
await setGraduationStats(updatedStats, yearType, specialGroups)
gradData = updatedStats
}
}
let progressData = await getStudytrackStats(code, combinedProgramme, graduated, specialGroups)
if (!progressData) {
const studyRightsOfProgramme = await getStudyRightsInProgramme(code, false, true)
const updated = await getStudyTrackStatsForStudyProgramme({
studyProgramme: code,
combinedProgramme,
settings: {
graduated: graduated === 'GRADUATED_INCLUDED',
specialGroups: specialGroups === 'SPECIAL_INCLUDED',
},
studyRightsOfProgramme,
})
if (updated) {
await setStudytrackStats(updated, graduated, specialGroups)
progressData = updated
}
}
const programmeName = await getProgrammeName(code)
delete gradData.tableStats
delete gradData.graphStats
delete gradData.titles
const data = {
id: code,
programmeName,
status: gradData?.status,
lastUpdated: gradData.lastUpdated,
graduations: gradData,
creditCounts: progressData?.creditCounts,
creditCountsCombo: progressData?.creditCountsCombo,
years: progressData?.years,
}
return res.json(data)
})
export default router
@@ -19,7 +19,7 @@ router.get('/', async (req: StudyProgrammeCriteriaRequest, res: Response) => {
if (studyProgramme !== '' && !studyProgramme) {
return res.status(422).end()
}
const studyProgrammeCriteria = await getCriteria(studyProgramme as string)
const studyProgrammeCriteria = await getCriteria(studyProgramme)
return res.json(studyProgrammeCriteria)
})
+1 -2
View File
@@ -14,7 +14,7 @@ import {
TagFromFrontend,
StudentTagFromFrontend,
} from '../services/tags'
import { Role } from '../types'
import { Role } from '../shared/types'
import { getFullStudyProgrammeRights, hasFullAccessToStudentData } from '../util'
const router = Router()
@@ -69,7 +69,6 @@ interface PostTagRequest extends Request {
router.post('/tags', async (req: PostTagRequest, res: Response) => {
const {
// eslint-disable-next-line @typescript-eslint/naming-convention
tag: { studytrack, tagname, year, personal_user_id },
} = req.body
const { roles, id, programmeRights } = req.user
+32 -12
View File
@@ -1,20 +1,27 @@
import { Request, Response, Router } from 'express'
import { serviceProvider } from '../config'
import * as auth from '../middleware/auth'
import { getProvidersOfFaculty, isFaculty } from '../services/organizations'
import { getTeachersBySearchTerm, getTeacherStatistics, getYearlyStatistics } from '../services/teachers'
import { CategoryID, getTeacherStats, findAndSaveTeachers, getCategoriesAndYears } from '../services/teachers/top'
import { mapToProviders } from '../shared/util/mapToProviders'
import { mapToProviders } from '../shared/util'
import { getFullStudyProgrammeRights, splitByEmptySpace } from '../util'
const router = Router()
const iamGroupsGivingFullAccess = ['hy-dekaanit', 'hy-varadekaanit-opetus']
const fullAccessAuth = () =>
serviceProvider === 'toska' ? auth.roles([], iamGroupsGivingFullAccess) : auth.roles(['teachers'])
interface GetTeachersRequest extends Request {
query: {
searchTerm: string
}
}
router.get('/', async (req: GetTeachersRequest, res: Response) => {
router.get('/', fullAccessAuth(), async (req: GetTeachersRequest, res: Response) => {
const { searchTerm } = req.query
if (!searchTerm) {
return res.status(400).json({ error: 'Search term missing' })
@@ -38,30 +45,37 @@ interface GetTopTeachersRequest extends Request {
}
}
router.get('/top', async (req: GetTopTeachersRequest, res: Response) => {
router.get('/top', fullAccessAuth(), async (req: GetTopTeachersRequest, res: Response) => {
const { yearcode, category = CategoryID.ALL } = req.query
if (!yearcode) {
return res.status(422).send('Missing required yearcode query param')
}
const result = await getTeacherStats(category, Number(yearcode))
res.json(result)
if (result) {
return res.json(result)
}
await findAndSaveTeachers(Number(yearcode), Number(yearcode))
const updatedStats = await getTeacherStats(category, Number(yearcode))
res.json(updatedStats)
})
interface PostTopTeachersRequest extends Request {
body: {
startyearcode: string
endyearcode: string
startyearcode?: string
endyearcode?: string
}
}
router.post('/top', async (req: PostTopTeachersRequest, res: Response) => {
router.post('/top', auth.roles(['admin']), async (req: PostTopTeachersRequest, res: Response) => {
const { startyearcode, endyearcode } = req.body
const endYear = endyearcode ? Number(endyearcode) : undefined
const startYear = startyearcode ? Number(startyearcode) : undefined
res.status(200).end()
await findAndSaveTeachers(Number(endyearcode), Number(startyearcode))
await findAndSaveTeachers(endYear, startYear)
})
router.get('/top/categories', async (_req: Request, res: Response) => {
router.get('/top/categories', fullAccessAuth(), async (_req: Request, res: Response) => {
const result = await getCategoriesAndYears()
res.json(result)
})
@@ -75,7 +89,7 @@ interface GetTeacherStatsRequest extends Request {
}
router.get('/stats', async (req: GetTeacherStatsRequest, res: Response) => {
const { roles, programmeRights } = req.user
const { roles, programmeRights, iamGroups } = req.user
const fullStudyProgrammeRights = getFullStudyProgrammeRights(programmeRights)
const { providers, semesterStart, semesterEnd } = req.query
@@ -84,7 +98,13 @@ router.get('/stats', async (req: GetTeacherStatsRequest, res: Response) => {
}
const providerRights = mapToProviders(fullStudyProgrammeRights)
if (!(providers.every(provider => providerRights.includes(provider)) || roles.includes('admin'))) {
if (
!(
(roles.includes('teachers') && providers.every(provider => providerRights.includes(provider))) ||
roles.includes('admin') ||
iamGroups.some(group => iamGroupsGivingFullAccess.includes(group))
)
) {
return res.status(403).send('You do not have permission to see this data')
}
@@ -112,7 +132,7 @@ interface GetTeacherByIdRequest extends Request {
}
}
router.get('/:id', async (req: GetTeacherByIdRequest, res: Response) => {
router.get('/:id', fullAccessAuth(), async (req: GetTeacherByIdRequest, res: Response) => {
const { id } = req.params
const result = await getTeacherStatistics(id)
if (!result) {
+3 -4
View File
@@ -1,4 +1,3 @@
// const router = require('express').Router()
import { Request, Response, Router } from 'express'
import { cloneDeep } from 'lodash'
@@ -16,7 +15,7 @@ import {
} from '../services/faculty/facultyService'
import { combineFacultyStudentProgress, FacultyProgressData } from '../services/faculty/facultyStudentProgress'
import { getMedian } from '../services/studyProgramme/studyProgrammeHelpers'
import { Graduated } from '../types'
import { Graduated, NameWithCode } from '../shared/types'
import logger from '../util/logger'
const router = Router()
@@ -24,7 +23,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
@@ -33,7 +32,7 @@ const getProgrammeNames = (faculties: Organization[]) => {
interface GetProgressStatsRequest extends Request {
query: {
specialsIncluded: 'true' | undefined
specialsIncluded: 'true' | 'false'
graduated: Graduated
}
}
+10 -15
View File
@@ -65,13 +65,6 @@ router.post('/update/v2/customlist/:type', async (req: PostCustomListRequest, re
}
})
router.post('/update/v2/courses', async (req: Request, res: Response) => {
const response = await updateSISCoursesByCourseCode(req.body)
if (response) {
res.status(200).json('Update SIS courses scheduled')
}
})
router.get('/update/v2/programmes', async (_req: Request, res: Response) => {
const response = await updateSISProgrammes()
if (response) {
@@ -87,25 +80,27 @@ router.get('/refresh_redis_cache', async (req: Request, res: Response) => {
}
})
router.post('/refresh_statistic_v2', async (req: Request, res: Response) => {
logger.info(`${req.user.username} requested refresh of statistics`)
jobMaker.statistics()
res.status(200).json('Teacher and study programme statistics refreshed')
router.post('/refresh-teacher-leaderboard', (req: Request, res: Response) => {
logger.info(
`${req.user.username} requested refresh of teacher leaderboard for the current and previous academic year`
)
jobMaker.teacherLeaderboard()
res.status(200).json('Teacher leaderboard for the current and previous academic year refreshed')
})
router.post('/refresh_study_programmes_v2', async (req: Request, res: Response) => {
logger.info(`${req.user.username} requested refresh of study programmes`)
refreshProgrammes()
await refreshProgrammes()
res.status(200).json('Added job for refreshing study programme overviews')
})
router.post('/refresh_faculties_v2', async (req: Request, res: Response) => {
logger.info(`${req.user.username} requested refresh of faculties`)
refreshFaculties()
await refreshFaculties()
res.status(200).json('Added job for refreshing faculties')
})
router.post('/refresh_language_center_data', async (req: Request, res: Response) => {
router.post('/refresh_language_center_data', (req: Request, res: Response) => {
logger.info(`${req.user.username} requested refresh of language center data`)
if (!languageCenterViewEnabled)
res.status(418).json({ error: 'The language center functionality is not activated in your environment.' })
@@ -113,7 +108,7 @@ router.post('/refresh_language_center_data', async (req: Request, res: Response)
res.status(200).json('Added job for refreshing language center data')
})
router.post('/refresh-close-to-graduation', async (req: Request, res: Response) => {
router.post('/refresh-close-to-graduation', (req: Request, res: Response) => {
logger.info(`${req.user.username} requested refresh of close to graduation data`)
jobMaker.closeToGraduation()
res.status(200).json('Added job for refreshing close to graduation data')
+2 -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)
})
+33 -3
View File
@@ -23,16 +23,46 @@ router.get('/from-sisu-by-eppn/:newUserEppn', auth.roles(['admin']), async (req:
res.json(person)
})
interface User {
id: string
first_name: string
last_name: string
call_name: string
eppn: string
email_address: string
student_number: string
phone_number: string
document_state: string
}
interface AddUserRequest extends Request {
body: {
user: any // TODO: Funidata, what is the type of user?
user: User
}
}
interface DeleteUserRequest extends Request {
body: {
userId: string
}
}
router.post('/add', auth.roles(['admin']), async (req: AddUserRequest, res: Response) => {
const { user } = req.body
const person = await userService.addNewUser(user)
res.json(person)
const oldUser = await userService.findByUsername(user.eppn)
if (oldUser) return res.status(400).json({ error: 'Trying to add a user already present in the user database.' })
const success = await userService.addNewUser(user)
res.json(success)
})
router.delete('/delete', auth.roles(['admin']), async (req: DeleteUserRequest, res: Response) => {
const { userId } = req.body
try {
await userService.deleteUserById(userId)
res.json({ deleted: userId })
} catch (error) {
return res.status(400).json({ error: 'Deletion was unsuccessful with the user id included in the request' })
}
})
export default router
@@ -1,44 +1,49 @@
import moment from 'moment'
import { facultyCodes, ignoredFacultyCodes } from '../config/organizationConstants'
import { redisClient } from './redis'
// Only new bachelor, masters and doctoral programmes get their data updated in redis every night, use redis for them
const isUpdatedNewProgramme = (code: string) => code.includes('KH') || code.includes('MH') || /^(T)[0-9]{6}$/.test(code)
import { facultyCodes, ignoredFacultyCodes } from '../config/organizationConstants'
import { Graduated, SpecialGroups, YearType } from '../shared/types'
import { redisClient } from './redis'
import { isRelevantProgramme } from './studyProgramme/studyProgrammeHelpers'
const filteredFacultyCodes = facultyCodes.filter(item => !ignoredFacultyCodes.includes(item))
const isFaculty = (code: string) => filteredFacultyCodes.includes(code)
const createRedisKeyForBasicStats = (id: string, yearType: string, specialGroups: string) => {
const createRedisKeyForBasicStats = (id: string, yearType: YearType, specialGroups: SpecialGroups) => {
return `BASIC_STATS_${id}_${yearType}_${specialGroups}`
}
const createRedisKeyForCreditStats = (id: string, yearType: string, specialGroups: string) => {
const createRedisKeyForCreditStats = (id: string, yearType: YearType, specialGroups: SpecialGroups) => {
return `CREDIT_STATS_${id}_${yearType}_${specialGroups}`
}
const createRedisKeyForGraduationStats = (id: string, yearType: string, specialGroups: string) => {
const createRedisKeyForGraduationStats = (id: string, yearType: YearType, specialGroups: SpecialGroups) => {
return `GRADUATION_STATS_${id}_${yearType}_${specialGroups}`
}
const createRedisKeyForStudytrackStats = (id: string, graduated: string, specialGroups: string) => {
const createRedisKeyForStudyTrackStats = (id: string, graduated: Graduated, specialGroups: SpecialGroups) => {
return `STUDYTRACK_STATS_${id}_${graduated}_${specialGroups}`
}
const shouldSaveToRedis = (id: unknown) => {
if (typeof id !== 'string') return false
if (isRelevantProgramme(id)) return true
const splitId = id.split('-')
return splitId.length === 2 && isRelevantProgramme(splitId[0]) && isRelevantProgramme(splitId[1])
}
export const getBasicStats = async (
id: string,
combinedProgramme: string | null,
yearType: string,
specialGroups: string
yearType: YearType,
specialGroups: SpecialGroups
) => {
if (!isUpdatedNewProgramme(id)) {
return null
}
const searchkey = combinedProgramme ? `${id}-${combinedProgramme}` : id
const redisKey = createRedisKeyForBasicStats(searchkey, yearType, specialGroups)
const dataFromRedis = await redisClient.getAsync(redisKey)
const searchKey = combinedProgramme ? `${id}-${combinedProgramme}` : id
const redisKey = createRedisKeyForBasicStats(searchKey, yearType, specialGroups)
const dataFromRedis = await redisClient.get(redisKey)
if (!dataFromRedis) {
return null
}
return JSON.parse(dataFromRedis)
}
export const setBasicStats = async (data, yearType: string, specialGroups: string) => {
export const setBasicStats = async (data, yearType: YearType, specialGroups: SpecialGroups) => {
const { id } = data
const redisKey = createRedisKeyForBasicStats(id, yearType, specialGroups)
const dataToRedis = {
@@ -46,10 +51,10 @@ export const setBasicStats = async (data, yearType: string, specialGroups: strin
status: 'DONE',
lastUpdated: moment().format(),
}
if (!isUpdatedNewProgramme(id)) {
if (!shouldSaveToRedis(id)) {
return dataToRedis
}
const setOperationStatus = await redisClient.setAsync(redisKey, JSON.stringify(dataToRedis))
const setOperationStatus = await redisClient.set(redisKey, JSON.stringify(dataToRedis))
if (setOperationStatus !== 'OK') {
return null
}
@@ -62,7 +67,7 @@ export const getCreditStats = async (providerCode: string, isAcademicYear: boole
isAcademicYear ? 'ACADEMIC_YEAR' : 'CALENDAR_YEAR',
specialGroups ? 'SPECIAL_INCLUDED' : 'SPECIAL_EXCLUDED'
)
const dataFromRedis = await redisClient.getAsync(redisKey)
const dataFromRedis = await redisClient.get(redisKey)
if (!dataFromRedis) {
return null
}
@@ -81,10 +86,10 @@ export const setCreditStats = async (data, isAcademicYear: boolean, specialGroup
status: 'DONE',
lastUpdated: moment().format(),
}
if (!isUpdatedNewProgramme(data.id) && !isFaculty(data.id)) {
if (!isRelevantProgramme(id) && !isFaculty(id)) {
return dataToRedis
}
const setOperationStatus = await redisClient.setAsync(redisKey, JSON.stringify(dataToRedis))
const setOperationStatus = await redisClient.set(redisKey, JSON.stringify(dataToRedis))
if (setOperationStatus !== 'OK') {
return null
}
@@ -94,22 +99,19 @@ export const setCreditStats = async (data, isAcademicYear: boolean, specialGroup
export const getGraduationStats = async (
id: string,
combinedProgramme: string | null,
yearType: string,
specialGroups: string
yearType: YearType,
specialGroups: SpecialGroups
) => {
if (!isUpdatedNewProgramme(id)) {
return null
}
const searchkey = combinedProgramme ? `${id}-${combinedProgramme}` : id
const redisKey = createRedisKeyForGraduationStats(searchkey, yearType, specialGroups)
const dataFromRedis = await redisClient.getAsync(redisKey)
const searchKey = combinedProgramme ? `${id}-${combinedProgramme}` : id
const redisKey = createRedisKeyForGraduationStats(searchKey, yearType, specialGroups)
const dataFromRedis = await redisClient.get(redisKey)
if (!dataFromRedis) {
return null
}
return JSON.parse(dataFromRedis)
}
export const setGraduationStats = async (data, yearType: string, specialGroups: string) => {
export const setGraduationStats = async (data, yearType: YearType, specialGroups: SpecialGroups) => {
const { id } = data
const redisKey = createRedisKeyForGraduationStats(id, yearType, specialGroups)
const dataToRedis = {
@@ -117,46 +119,43 @@ export const setGraduationStats = async (data, yearType: string, specialGroups:
status: 'DONE',
lastUpdated: moment().format(),
}
if (!isUpdatedNewProgramme(id)) {
if (!shouldSaveToRedis(id)) {
return dataToRedis
}
const setOperationStatus = await redisClient.setAsync(redisKey, JSON.stringify(dataToRedis))
const setOperationStatus = await redisClient.set(redisKey, JSON.stringify(dataToRedis))
if (setOperationStatus !== 'OK') {
return null
}
return dataToRedis
}
export const getStudytrackStats = async (
export const getStudyTrackStats = async (
id: string,
combinedProgramme: string | null,
graduated: string,
specialGroups: string
graduated: Graduated,
specialGroups: SpecialGroups
) => {
if (!isUpdatedNewProgramme(id)) {
return null
}
const searchkey = combinedProgramme ? `${id}-${combinedProgramme}` : id
const redisKey = createRedisKeyForStudytrackStats(searchkey, graduated, specialGroups)
const dataFromRedis = await redisClient.getAsync(redisKey)
const searchKey = combinedProgramme ? `${id}-${combinedProgramme}` : id
const redisKey = createRedisKeyForStudyTrackStats(searchKey, graduated, specialGroups)
const dataFromRedis = await redisClient.get(redisKey)
if (!dataFromRedis) {
return null
}
return JSON.parse(dataFromRedis)
}
export const setStudytrackStats = async (data, graduated: string, specialGroups: string) => {
export const setStudyTrackStats = async (data, graduated: Graduated, specialGroups: SpecialGroups) => {
const { id } = data
const redisKey = createRedisKeyForStudytrackStats(id, graduated, specialGroups)
const redisKey = createRedisKeyForStudyTrackStats(id, graduated, specialGroups)
const dataToRedis = {
...data,
status: 'DONE',
lastUpdated: moment().format(),
}
if (!isUpdatedNewProgramme(id)) {
if (!shouldSaveToRedis(id)) {
return dataToRedis
}
const setOperationStatus = await redisClient.setAsync(redisKey, JSON.stringify(dataToRedis))
const setOperationStatus = await redisClient.set(redisKey, JSON.stringify(dataToRedis))
if (setOperationStatus !== 'OK') {
return null
}
@@ -1,6 +1,6 @@
import { col, fn, Op, where } from 'sequelize'
import { Course, Credit, Enrollment, Student } from '../models/index'
import { Course, Credit, Enrollment, Student, Studyplan } from '../models/index'
import { CreditTypeCode, EnrollmentState } from '../types'
type Courses = Array<Pick<Course, 'code' | 'name' | 'substitutions'>>
@@ -15,7 +15,6 @@ interface StudentCredit {
interface StudentEnrollment {
date: Date
courseCode: string
state: EnrollmentState
substitution: string | null
}
@@ -24,9 +23,10 @@ interface StudentInfo {
lastname: string
email: string
sis_person_id: string
secondary_email: string | null
coursesInStudyPlan: string[]
secondaryEmail: string | null
credits: StudentCredit[]
enrollments: StudentEnrollment[]
enrollments: Record<string, StudentEnrollment>
}
type StudentCredits = Record<
@@ -37,16 +37,18 @@ type StudentCredits = Record<
}
>
interface FormattedStudent {
interface FormattedStudent extends StudentInfo {
studentNumber: string
sis_person_id: string
credits: StudentCredit[]
enrollments: Record<string, any>
allEnrollments: StudentEnrollment[]
firstnames: string
lastname: string
email: string
secondaryEmail: string | null
}
interface StudentWithStudyplanNested
extends Pick<Student, 'studentnumber' | 'firstnames' | 'lastname' | 'email' | 'sis_person_id' | 'secondary_email'> {
studyplans: { included_courses: string[] }[]
}
interface StudentWithCourses extends Omit<StudentWithStudyplanNested, 'studyplans'> {
coursesInStudyPlan: string[]
}
const getCourses = async (courseCodes: string[]) => {
@@ -84,7 +86,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 +97,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,30 +115,37 @@ 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
}
const getStudents = async (studentNumbers: string[]) => {
const students: Array<
Pick<Student, 'studentnumber' | 'firstnames' | 'lastname' | 'email' | 'sis_person_id' | 'secondary_email'>
> = (
await Student.findAll({
attributes: ['studentnumber', 'firstnames', 'lastname', 'email', 'sis_person_id', 'secondary_email'],
where: {
studentnumber: {
[Op.in]: studentNumbers,
},
const students = await Student.findAll({
attributes: ['studentnumber', 'firstnames', 'lastname', 'email', 'sis_person_id', 'secondary_email'],
where: {
studentnumber: {
[Op.in]: studentNumbers,
},
})
).map(student => student.toJSON())
return students
},
include: [
{
model: Studyplan,
as: 'studyplans',
attributes: ['included_courses'],
},
],
})
return students.map(student => {
const { studyplans, ...rest } = student.toJSON() as StudentWithStudyplanNested
const coursesInStudyPlan = studyplans.flatMap(studyplan => studyplan.included_courses)
return { ...rest, coursesInStudyPlan } as StudentWithCourses
})
}
export const getCompletedCourses = async (studentNumbers: string[], courseCodes: string[]) => {
@@ -161,7 +170,8 @@ export const getCompletedCourses = async (studentNumbers: string[], courseCodes:
lastname: student.lastname,
email: student.email,
sis_person_id: student.sis_person_id,
secondary_email: student.secondary_email,
secondaryEmail: student.secondary_email,
coursesInStudyPlan: student.coursesInStudyPlan,
credits: [],
enrollments: [],
}
@@ -201,7 +211,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,
})
})
@@ -217,7 +226,8 @@ export const getCompletedCourses = async (studentNumbers: string[], courseCodes:
firstnames: student.firstnames,
lastname: student.lastname,
email: student.email,
secondaryEmail: student.secondary_email,
secondaryEmail: student.secondaryEmail,
coursesInStudyPlan: student.coursesInStudyPlan,
})
return acc
}, [] as FormattedStudent[])
@@ -1,3 +1,4 @@
import { orderBy } from 'lodash'
import { Op } from 'sequelize'
import { Course } from '../../models'
@@ -52,7 +53,17 @@ const getRawCourses = async (name: string, code: string) => {
const getCourses = async (name: string, code: string) => {
const rawCourses = await getRawCourses(name, code)
const courses: CourseWithSubsId[] = rawCourses
.map(course => ({ ...course.dataValues }))
.map(course => ({
...course.toJSON(),
substitutions: orderBy(course.substitutions, [
substitution => {
if (/^A/.exec(substitution)) return 4 // open university codes come last
if (/^\d/.exec(substitution)) return 2 // old numeric codes come second
if (/^[A-Za-z]/.exec(substitution)) return 1 // new letter based codes come first
return 3 // unknown, comes before open uni?
},
]),
}))
.sort((a, b) => getSortRank(b.code) - getSortRank(a.code))
return courses
}
@@ -87,7 +98,7 @@ export const getCoursesByNameAndOrCode = async (name: string, code: string) => {
}
})
return { courses }
return courses
}
export const getCoursesByCodes = (codes: string[]) => {
@@ -1,4 +1,5 @@
import { EnrollmentState, Name } from '../../types'
import { Name } from '../../shared/types'
import { EnrollmentState } from '../../types'
import { lengthOf, percentageOf } from '../../util'
const fall: string[] = []
@@ -21,26 +22,16 @@ const initializePassingSemesters = () => {
return passingSemesters
}
const getEnrolledStudents = (enrollments: DynamicEnrollments) => {
if (enrollments) {
return Object.keys(enrollments)
.filter(key => key !== EnrollmentState.ENROLLED && key !== 'semesters')
.reduce((acc, key) => [...acc, ...[...enrollments[key]].map(studentNumber => studentNumber)], [] as string[])
const getFilteredEnrolledNoGrade = (enrollments: DynamicEnrollments, allStudents: string[]) => {
const enrolledStudents = [...(enrollments[EnrollmentState.ENROLLED] ?? [])]
const result: Record<string, boolean> = {}
for (const student of enrolledStudents) {
if (allStudents.includes(student)) {
continue
}
result[student] = true
}
return []
}
const getFilteredEnrolledNoGrade = (
enrollments: DynamicEnrollments,
enrolledStudents: string[],
allStudents: string[]
) => {
if (enrollments[EnrollmentState.ENROLLED]) {
return [...enrollments[EnrollmentState.ENROLLED]]
.filter(student => !enrolledStudents.includes(student) && !allStudents.includes(student))
.reduce((acc, student) => ({ ...acc, [student]: true }), {} as Record<string, boolean>)
}
return {} as Record<string, boolean>
return result
}
type DynamicEnrollments = {
@@ -57,8 +48,6 @@ type Stats = {
students: number
passed: number
failed: number
failedMany: number
retryPassed: number
attempts: number
improvedPassedGrade: number
percentage: number | undefined
@@ -76,8 +65,6 @@ type Students = {
all: Record<string, boolean>
passed: Record<string, boolean>
failed: Record<string, boolean>
retryPassed: Record<string, boolean>
failedMany: Record<string, boolean>
improvedPassedGrade: Record<string, boolean>
markedToSemester: Record<string, boolean>
enrolledNoGrade: Record<string, boolean>
@@ -126,8 +113,6 @@ export class CourseStatsCounter {
all: {},
passed: {},
failed: {},
retryPassed: {},
failedMany: {},
improvedPassedGrade: {},
markedToSemester: {},
enrolledNoGrade: {},
@@ -136,8 +121,6 @@ export class CourseStatsCounter {
students: 0,
passed: 0,
failed: 0,
failedMany: 0,
retryPassed: 0,
attempts: 0,
improvedPassedGrade: 0,
percentage: undefined,
@@ -216,13 +199,11 @@ export class CourseStatsCounter {
private removeFromFailed(studentNumber: string) {
delete this.students.failed[studentNumber]
delete this.students.failedMany[studentNumber]
}
private markPassingGrade(studentNumber: string) {
this.students.passed[studentNumber] = true
if (this.failedBefore(studentNumber)) {
this.students.retryPassed[studentNumber] = true
this.removeFromFailed(studentNumber)
}
}
@@ -235,12 +216,8 @@ export class CourseStatsCounter {
private markFailedGrade(studentNumber: string) {
if (this.passedBefore(studentNumber)) {
this.students.retryPassed[studentNumber] = true
this.removeFromFailed(studentNumber)
} else {
if (this.failedBefore(studentNumber)) {
this.students.failedMany[studentNumber] = true
}
this.students.failed[studentNumber] = true
}
}
@@ -275,17 +252,14 @@ export class CourseStatsCounter {
public getFinalStats() {
const { stats, students } = this
const enrolledStudents = getEnrolledStudents(this.enrollments)
const allStudents = Object.keys(students.all).map(student => student)
const filteredEnrolledNoGrade = getFilteredEnrolledNoGrade(this.enrollments, enrolledStudents, allStudents)
const allStudents = Object.keys(students.all)
const filteredEnrolledNoGrade = getFilteredEnrolledNoGrade(this.enrollments, allStudents)
students.all = { ...students.all, ...filteredEnrolledNoGrade }
students.enrolledNoGrade = filteredEnrolledNoGrade
stats.students = lengthOf(students.all)
stats.passed = lengthOf(students.passed)
stats.failed = lengthOf(students.failed)
stats.failedMany = lengthOf(students.failedMany)
stats.retryPassed = lengthOf(students.retryPassed)
stats.improvedPassedGrade = lengthOf(students.improvedPassedGrade)
stats.percentage = percentageOf(stats.passed, stats.students)
stats.passedOfPopulation = percentageOf(stats.passed, this.studentsInTotal)
@@ -1,4 +1,4 @@
import { EnrollmentState, Name, PassedCategory } from '../../types'
import { Name } from '../../shared/types'
import { FormattedProgramme, OrganizationDetails } from './helpers'
type Programme = {
@@ -34,13 +34,8 @@ type GroupAttempts = {
}
type GroupStudents = {
categories: {
passedFirst: string[]
passedEventually: string[]
neverPassed: string[]
}
grades: { [studentNumber: string]: { grade: string; passed: boolean } }
studentnumbers: string[]
studentNumbers: string[]
}
type Group = {
@@ -49,14 +44,13 @@ type Group = {
coursecode: string
attempts: GroupAttempts
students: GroupStudents
enrollments: { studentNumber: string; state: string; enrollmentDateTime: Date }[]
allEnrollments: { studentNumber: string; state: string; enrollmentDateTime: Date }[]
yearcode: number
enrollments: { studentNumber: string; enrollmentDateTime: Date }[]
allEnrollments: { studentNumber: string; enrollmentDateTime: Date }[]
yearCode: number
}
type Student = {
earliestAttainment: Date
category: PassedCategory
code: number
}
@@ -117,17 +111,12 @@ export class CourseYearlyStatsCounter {
},
},
students: {
categories: {
passedFirst: [],
passedEventually: [],
neverPassed: [],
},
grades: {},
studentnumbers: [],
studentNumbers: [],
},
enrollments: [],
allEnrollments: [],
yearcode: yearCode,
yearCode,
}
}
@@ -147,7 +136,6 @@ export class CourseYearlyStatsCounter {
if (!this.facultyStats[yearCode]) {
this.initFacultyYear(yearCode)
}
if (
facultyCode &&
!this.facultyStats[yearCode].faculties[facultyCode] &&
@@ -163,7 +151,6 @@ export class CourseYearlyStatsCounter {
this.programmes[code].passed[yearCode] = []
this.programmes[code].credits[yearCode] = 0
}
if (facultyCode && !this.facultyStats[yearCode].allStudents.includes(studentNumber)) {
this.facultyStats[yearCode].allStudents.push(studentNumber)
this.facultyStats[yearCode].faculties[facultyCode].students.push(studentNumber)
@@ -172,7 +159,6 @@ export class CourseYearlyStatsCounter {
this.programmes[code].passed[yearCode].push(studentNumber)
this.programmes[code].credits[yearCode] += credits
}
if (facultyCode && passed && !this.facultyStats[yearCode].allPassed.includes(studentNumber)) {
this.facultyStats[yearCode].allPassed.push(studentNumber)
this.facultyStats[yearCode].faculties[facultyCode].passed.push(studentNumber)
@@ -211,7 +197,6 @@ export class CourseYearlyStatsCounter {
public markEnrollmentToGroup(
studentNumber: string,
state: string,
enrollmentDateTime: Date,
groupCode: number,
groupName: string | Name,
@@ -221,22 +206,13 @@ export class CourseYearlyStatsCounter {
if (!this.groups[groupCode]) {
this.initGroup(groupCode, groupName, courseCode, yearCode)
}
const enrollment = { studentNumber, state, enrollmentDateTime }
const enrollment = { studentNumber, enrollmentDateTime }
this.groups[groupCode].allEnrollments.push(enrollment)
const oldEnrollment = this.groups[groupCode].enrollments.find(
enrollment => enrollment.studentNumber === studentNumber
)
if (!oldEnrollment) {
return this.groups[groupCode].enrollments.push({ studentNumber, state, enrollmentDateTime })
}
if (oldEnrollment.state === EnrollmentState.ENROLLED || oldEnrollment.state === EnrollmentState.CONFIRMED) {
return
}
if (state !== EnrollmentState.ENROLLED && state !== EnrollmentState.CONFIRMED) {
return
}
if (![EnrollmentState.ENROLLED, EnrollmentState.CONFIRMED].includes(state as EnrollmentState)) {
return
return this.groups[groupCode].enrollments.push({ studentNumber, enrollmentDateTime })
}
this.groups[groupCode].enrollments = this.groups[groupCode].enrollments
.filter(enrollment => enrollment.studentNumber !== studentNumber)
@@ -272,56 +248,25 @@ export class CourseYearlyStatsCounter {
this.groups[groupCode].students.grades[studentNumber] = { grade, passed }
}
public markCreditToStudentCategories(
studentNumber: string,
passed: boolean,
attainmentDate: Date,
groupCode: number
) {
public markCreditToStudentCategories(studentNumber: string, attainmentDate: Date, groupCode: number) {
if (!this.students.has(studentNumber)) {
this.addNewStudent(studentNumber, passed, attainmentDate, groupCode)
this.addOrUpdateStudent(studentNumber, attainmentDate, groupCode)
} else {
this.updateExistingStudent(studentNumber, passed, attainmentDate, groupCode)
this.updateExistingStudent(studentNumber, attainmentDate, groupCode)
}
}
private addNewStudent(studentNumber: string, passed: boolean, attainmentDate: Date, groupCode: number) {
const category = passed ? 'passedFirst' : 'neverPassed'
private addOrUpdateStudent(studentNumber: string, attainmentDate: Date, groupCode: number) {
this.students.set(studentNumber, {
earliestAttainment: attainmentDate,
category,
code: groupCode,
})
}
private updateExistingStudent(studentNumber: string, passed: boolean, attainmentDate: Date, groupCode: number) {
private updateExistingStudent(studentNumber: string, attainmentDate: Date, groupCode: number) {
const student = this.students.get(studentNumber)
if (attainmentDate < student!.earliestAttainment) {
this.updateCategoryForEarlierAttainment(studentNumber, passed, attainmentDate, groupCode, student!)
} else if (student!.category === 'neverPassed' && passed) {
this.students.set(studentNumber, { ...student!, category: 'passedEventually' })
}
}
private updateCategoryForEarlierAttainment(
studentNumber: string,
passed: boolean,
attainmentDate: Date,
groupCode: number,
student: Student
) {
if (passed) {
this.students.set(studentNumber, {
earliestAttainment: attainmentDate,
category: 'passedFirst',
code: groupCode,
})
} else if (student.category === 'passedFirst') {
this.students.set(studentNumber, {
earliestAttainment: attainmentDate,
category: 'passedEventually',
code: groupCode,
})
this.addOrUpdateStudent(studentNumber, attainmentDate, groupCode)
}
}
@@ -341,8 +286,7 @@ export class CourseYearlyStatsCounter {
private parseGroupStatistics(anonymizationSalt: string | null) {
for (const [studentNumber, data] of this.students) {
this.groups[data.code].students.categories[data.category].push(studentNumber)
this.groups[data.code].students.studentnumbers.push(studentNumber)
this.groups[data.code].students.studentNumbers.push(studentNumber)
}
const groupStatistics = Object.values(this.groups).map(({ ...rest }) => {
@@ -360,7 +304,7 @@ export class CourseYearlyStatsCounter {
...rest,
students: { ...students, grades },
}
if (anonymizationSalt && normalStats.students.studentnumbers.length < 6) {
if (anonymizationSalt && normalStats.students.studentNumbers.length < 6) {
this.obfuscated = true
const gradeSpread = {} as Grades
for (const grade of Object.keys(normalStats.attempts.grades)) {
@@ -378,14 +322,9 @@ export class CourseYearlyStatsCounter {
},
grades: gradeSpread,
},
yearcode: rest.yearcode,
yearCode: rest.yearCode,
students: {
categories: {
passedFirst: [] as string[],
passedEventually: [] as string[],
neverPassed: [] as string[],
},
studentnumbers: [] as string[],
studentNumbers: [] as string[],
},
}
return obfuscatedStats
@@ -53,7 +53,7 @@ export const getStudentNumberToSrElementsMap = async (studentNumbers: string[])
const studyRightIds = Object.keys(studyRightMap)
const studyRightElements = await SISStudyRightElement.findAll({
attributes: ['code', 'name', 'startDate', 'studyRightId'],
attributes: ['code', 'name', 'startDate', 'endDate', 'studyRightId'],
where: {
studyRightId: {
[Op.in]: studyRightIds,
@@ -95,7 +95,7 @@ export const getEnrollmentsForCourses = async (codes: string[], unification: Uni
[Op.in]: codes,
},
enrollment_date_time: { [Op.gte]: new Date('2021-05-31') },
state: [EnrollmentState.ENROLLED, EnrollmentState.CONFIRMED],
state: EnrollmentState.ENROLLED,
is_open: getIsOpen(unification),
},
})
@@ -1,6 +1,7 @@
import { Op } from 'sequelize'
import { Name, Unification } from '../../types'
import { Name } from '../../shared/types'
import { Unification } from '../../types'
export type FormattedProgramme = {
code: string
+73 -14
View File
@@ -1,10 +1,13 @@
/* eslint-disable @typescript-eslint/naming-convention */
import crypto from 'crypto'
import moment from 'moment'
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 {
@@ -65,11 +68,22 @@ const parseCredit = (
credits,
attainment_date: attainmentDate,
student_studentnumber: studentNumber,
studyright_id: studyRightId,
} = credit
const { yearcode: yearCode, yearname: yearName, semestercode: semesterCode, name: semesterName } = semester
const studyRightElements = studentNumberToSrElementsMap[studentNumber] || []
const programmes = studyRightElements.map(formatStudyRightElement)
let programmeOfCredit: SISStudyRightElement | undefined
programmeOfCredit = studyRightElements.find(studyRightElement => studyRightElement.studyRightId === studyRightId)
if (!programmeOfCredit) {
programmeOfCredit = studyRightElements
.filter(studyRightElement =>
moment(attainmentDate).isBetween(moment(studyRightElement.startDate), moment(studyRightElement.endDate))
)
.sort((a, b) => b.startDate.getTime() - a.startDate.getTime())[0] // The newest studyRightElement
}
const programmes = programmeOfCredit ? [programmeOfCredit].map(formatStudyRightElement) : []
const formattedCredit: FormattedCredit = {
yearCode,
@@ -99,7 +113,6 @@ type FormattedEnrollment = {
semesterCode: number
semesterName: Name
courseCode: string
state: string
enrollmentDateTime: Date
studentNumber: string
programmes: Array<Record<string, any>>
@@ -114,7 +127,6 @@ const parseEnrollment = (
const {
studentnumber: studentNumber,
semester,
state,
enrollment_date_time: enrollmentDateTime,
course_code: courseCode,
} = enrollment
@@ -128,7 +140,6 @@ const parseEnrollment = (
semesterCode,
semesterName,
courseCode,
state,
enrollmentDateTime,
studentNumber,
programmes: studyRightElements.map(formatStudyRightElement),
@@ -150,6 +161,10 @@ const getYearlyStatsOfNew = async (
combineSubstitutions: boolean,
studentNumberToSrElementsMap: Record<string, SISStudyRightElement[]>
) => {
if (serviceProvider === 'fd')
logger.info(
`Debugging c.y.s.: entered getYearlyStatsOfNew with courseCode ${courseCode} and unification ${unification}`
)
const courseForSubs = await Course.findOne({
where: { code: courseCode },
})
@@ -173,6 +188,11 @@ const getYearlyStatsOfNew = async (
}),
])
if (serviceProvider === 'fd')
logger.info(
`Debugging c.y.s., getYearlyStatsOfNew, courseCode ${courseCode} unification ${unification}: got credits ${credits.length}, enrollments ${enrollments.length} and course ${course ? 'yes' : 'no'}`
)
const counter = new CourseYearlyStatsCounter()
for (const credit of credits) {
@@ -218,32 +238,56 @@ const getYearlyStatsOfNew = async (
credits
)
counter.markCreditToGroup(studentNumber, passed, grade, groupCode, groupName, courseCode, yearCode)
counter.markCreditToStudentCategories(studentNumber, passed, attainmentDate, groupCode)
counter.markCreditToStudentCategories(studentNumber, attainmentDate, groupCode)
}
enrollments.forEach(enrollment => {
const { studentNumber, semesterCode, semesterName, yearCode, yearName, courseCode, state, enrollmentDateTime } =
const { studentNumber, semesterCode, semesterName, yearCode, yearName, courseCode, enrollmentDateTime } =
parseEnrollment(enrollment, anonymizationSalt, studentNumberToSrElementsMap)
const groupCode = separate ? semesterCode : yearCode
const groupName = separate ? semesterName : yearName
counter.markEnrollmentToGroup(studentNumber, state, enrollmentDateTime, groupCode, groupName, courseCode, yearCode)
counter.markEnrollmentToGroup(studentNumber, enrollmentDateTime, groupCode, groupName, courseCode, yearCode)
})
if (serviceProvider === 'fd')
logger.info(
`Debugging c.y.s., getYearlyStatsOfNew, code ${courseCode} unification ${unification}: finished looping through credits.`
)
const statistics = counter.getFinalStatistics(anonymizationSalt)
if (serviceProvider === 'fd')
logger.info(
`Debugging c.y.s., getYearlyStatsOfNew, code ${courseCode} unification ${unification}: got final statistics.`
)
let substitutionCourses: Course[] | undefined
if (combineSubstitutions && courseForSubs?.substitutions && courseForSubs.substitutions.length > 0) {
substitutionCourses = await Course.findAll({
where: {
code: {
[Op.in]: codes,
},
},
attributes: ['code', 'name'],
})
}
if (serviceProvider === 'fd')
logger.info(
`Debugging c.y.s., getYearlyStatsOfNew, code ${courseCode} unification ${unification}: got substitutionCourses ${substitutionCourses?.length} items.`
)
return {
...statistics,
coursecode: courseCode,
alternatives: codes,
alternatives: substitutionCourses ?? [{ code: courseForSubs!.code, name: courseForSubs!.name }],
name: course?.name,
}
}
export const maxYearsToCreatePopulationFrom = async (courseCodes: string[], unification: Unification) => {
const is_open = getIsOpen(unification)
const maxAttainmentDate = new Date(
Math.max(
...(
@@ -270,7 +314,7 @@ export const maxYearsToCreatePopulationFrom = async (courseCodes: string[], unif
attainment_date: {
[Op.gt]: attainmentThreshold,
},
is_open,
is_open: getIsOpen(unification),
},
order: [['attainment_date', 'ASC']],
})
@@ -311,9 +355,17 @@ export const getCourseYearlyStats = async (
})
const enrollments = await Enrollment.findAll({
attributes: ['studentnumber'],
where: { course_code: { [Op.in]: courseCodes } },
where: {
course_code: {
[Op.in]: courseCodes,
},
state: EnrollmentState.ENROLLED,
},
})
if (serviceProvider === 'fd')
logger.info(`Debugging c.y.s.: got credits ${credits.length} items and enrollments ${enrollments.length} items`)
const studentNumbers = {}
credits.forEach(credit => {
@@ -325,6 +377,11 @@ export const getCourseYearlyStats = async (
const studentNumberToSrElementsMap = await getStudentNumberToSrElementsMap(Object.keys(studentNumbers))
if (serviceProvider === 'fd')
logger.info(
`Debugging c.y.s.: got a studentNumberToSrElementsMap of size ${Object.keys(studentNumberToSrElementsMap).length}`
)
const statsRegular = await Promise.all(
courseCodes.map(async courseCode => {
const unifyStats = await getYearlyStatsOfNew(
@@ -356,6 +413,8 @@ export const getCourseYearlyStats = async (
})
)
if (serviceProvider === 'fd') logger.info('Debugging c.y.s.: finished creating statsRegular')
return statsRegular
}
@@ -2,6 +2,7 @@ import { groupBy, orderBy } from 'lodash'
import moment from 'moment'
import { InferAttributes, QueryTypes } from 'sequelize'
import { serviceProvider } from '../../config'
import { programmeCodes } from '../../config/programmeCodes'
import { dbConnections } from '../../database/connection'
import { Organization, ProgrammeModule } from '../../models'
@@ -54,7 +55,9 @@ export const getDegreeProgrammesOfOrganization = async (organizationId: string,
const programmesWithProgIds = programmesOfOrganization.map(programme => ({
...programme,
progId:
programme.code in programmeCodes ? programmeCodes[programme.code as keyof typeof programmeCodes] : programme.code,
programme.code in programmeCodes && serviceProvider !== 'fd'
? programmeCodes[programme.code as keyof typeof programmeCodes]
: programme.code,
}))
const programmesGroupedByCode = groupBy(orderBy(programmesWithProgIds, ['valid_from'], ['desc']), prog => prog.code)
const curriculumPeriods = await getCurriculumPeriods()
@@ -106,3 +109,16 @@ export const getDegreeProgrammesOfFaculty = async (facultyCode: string, onlyCurr
}
return getDegreeProgrammesOfOrganization(organization.id, onlyCurrentProgrammes)
}
export const getFacultyCodeById = async (facultyId: string) => {
const organization = await Organization.findOne({
attributes: ['code'],
where: {
id: facultyId,
},
})
if (!organization) {
throw new Error(`The organization with the id ${facultyId} was not found.`)
}
return organization.code
}
@@ -1,12 +1,11 @@
import { cloneDeep } from 'lodash'
import { NameWithCode } from '../../shared/types'
import { DegreeProgrammeType } from '../../types'
import { getBasicStats, setBasicStats } from '../analyticsService'
import { getBasicStatsForStudytrack } from '../studyProgramme/studyProgrammeBasics'
import type { ProgrammesOfOrganization } from './faculty'
type ProgrammeName = { code: string; en?: string; fi?: string; sv?: string }
type StudyTrackBasicStats = Awaited<ReturnType<typeof getBasicStatsForStudytrack>>
const calculateCombinedStats = async (
@@ -19,12 +18,12 @@ const calculateCombinedStats = async (
graphStats: [] as StudyTrackBasicStats['graphStats'],
programmeTableStats: {} as Record<string, StudyTrackBasicStats['tableStats']>,
tableStats: [] as StudyTrackBasicStats['tableStats'],
titles: ['', 'Started\nstudying', 'Accepted', 'Graduated'],
titles: ['Code', 'Started\nstudying', 'Accepted', 'Graduated'],
}
if (includeAllSpecials) {
studentInfo.titles.push('Transferred out\nof programme', 'Transferred into\nprogramme')
}
const programmeNames: Record<string, ProgrammeName> = {}
const programmeNames: Record<string, NameWithCode> = {}
let years: Array<string | number> = []
for (const programme of facultyProgrammes) {
@@ -106,7 +105,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]
}
}
}
@@ -143,7 +142,7 @@ const calculateCombinedStats = async (
graphStats: combinedGraphStats,
programmeTableStats: combinedProgrammeTableStats,
tableStats: combinedTableStats,
titles: ['', 'All', 'Bachelors', 'Masters', 'Doctors', 'Others'],
titles: ['Code', 'All', 'Bachelors', 'Masters', 'Doctors', 'Others'],
},
}
}
@@ -1,32 +1,33 @@
import { Name } from '../../shared/types'
import { getCreditsProduced } from '../providerCredits'
import { isRelevantProgramme } from '../studyProgramme/studyProgrammeHelpers'
import { getProgrammes } from './facultyService'
import { getDegreeProgrammesOfFaculty } from './faculty'
/**
* Returns credits produced by the programmes of the faculty but
* also the credits where the provider is the faculty itself
*/
export const getFacultyCredits = async (facultyCode: string, isAcademicYear: boolean) => {
const allProgrammes = (await getProgrammes(facultyCode)).data
const programmes = allProgrammes.filter(({ code }) => isRelevantProgramme(code))
const programmes = (await getDegreeProgrammesOfFaculty(facultyCode, true)).filter(({ code }) =>
isRelevantProgramme(code)
)
const facultyCredits = await getCreditsProduced(facultyCode, isAcademicYear)
const stats = {
[facultyCode]: facultyCredits,
codes: [facultyCode],
programmeNames: {},
programmeNames: {} as Record<string, { code: string } & Name>,
ids: [facultyCode],
}
for (const programme of programmes) {
const programmeCode = programme.code
const programmeCredits = await getCreditsProduced(programmeCode, isAcademicYear)
stats[programme.progId] = programmeCredits
stats.codes.push(programmeCode)
stats.ids.push(programme.progId)
stats.programmeNames[programme.progId] = {
code: programmeCode,
...programmes.find(programme => programme.code === programmeCode).name,
for (const { code, progId, name } of programmes) {
const programmeCredits = await getCreditsProduced(code, isAcademicYear)
stats[progId] = programmeCredits
stats.codes.push(code)
stats.ids.push(progId)
stats.programmeNames[progId] = {
code,
...name,
}
}

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