Compare commits

...
154 Commits
Author SHA1 Message Date
Valtteri Kantanen c03abae5d4 [Close to graduation] Calculate new data daily and cache it in Redis 2024-03-27 14:34:49 +02:00
Riku Rauhala 811dc37ef9 Disable Highcharts credits for the remaining charts 2024-03-26 21:55:16 +02:00
Riku Rauhala acb346c77a [Student statistics] Remove unnecessary padding 2024-03-26 21:41:49 +02:00
Riku Rauhala 98b4cba041 [Student statistics] Fix a typo and tweak styles 2024-03-26 21:22:08 +02:00
Valtteri Kantanen c0beb94d60 Add possibility to filter by faculty on close to graduation report 2024-03-26 20:36:41 +02:00
Valtteri Kantanen c0de109c0f Add thesis status to close to graduation report 2024-03-26 20:06:02 +02:00
Riku Rauhala c0c6a6a10c [Student statistics] Align the Credits column to the right 2024-03-26 19:59:33 +02:00
Riku Rauhala 033d5bd86f [Student details] Round grade graph mean values to 2 decimals 2024-03-26 19:33:53 +02:00
Riku Rauhala c8ad866635 [Student details] Fix style issues 2024-03-26 18:03:40 +02:00
Valtteri Kantanen c4f9dbdb27 Add first implementation for report of close to graduation students 2024-03-26 17:33:49 +02:00
Riku RauhalaandGitHub 0507111117 Merge pull request #4457 from UniversityOfHelsinkiCS/study-guidance-groups
[Study guidance groups] Add support for removing study programme or starting year
2024-03-26 16:24:36 +02:00
Riku Rauhala 132b454937 Fix e2e tests 2024-03-26 16:16:22 +02:00
Riku Rauhala 3a7842765a [Study guidance groups] Add support for removing study programme or starting year 2024-03-26 16:04:42 +02:00
Riku Rauhala 9d92571770 [Study guidance groups] Fix alignments 2024-03-26 12:15:10 +02:00
Riku Rauhala d3340a80b1 Random bullshit, go 2024-03-26 11:43:15 +02:00
Riku Rauhala cc0ec6fdd9 [Study guidance groups] Change the trash icon to a cancel icon for the cancel button 2024-03-26 11:27:08 +02:00
Valtteri Kantanen bd85a6a5ba [Updater] Don't filter out misregistrations from attainments (#4453) 2024-03-25 17:26:18 +02:00
Riku Rauhala a52063ac37 Use the same case in every title in the application 2024-03-25 15:13:15 +02:00
Riku Rauhala facdb85cec Rename variables 2024-03-25 12:22:04 +02:00
Riku Rauhala 05673e269d [Study programme] Fix colours getting mixed up
When excluding special studyrights, the colours would get mixed up. Sorting the graphStats by name returns them in the same order so the colours don't randomly change when toggling
2024-03-22 13:50:45 +02:00
Riku Rauhala e479738a27 [Study programme] Flip the specialGroups condition 2024-03-22 13:06:45 +02:00
Valtteri Kantanen 692fab5d75 Add retries to a flaky test 2024-03-22 12:49:26 +02:00
Riku RauhalaandValtteri Kantanen d93b52f600 [Study programme] Fix credits produced when special studyrights are exluded
Co-authored-by: Valtteri Kantanen <valtteri.kantanen@iki.fi>
2024-03-22 12:38:49 +02:00
Riku Rauhala efbf24c003 [Study programme] Fix the special studyright toggle 2024-03-22 11:49:27 +02:00
Riku Rauhala 987c8b3a97 Remove unused props 2024-03-22 09:47:16 +02:00
Valtteri Kantanen 57cad29520 [Updater] Fix study plans not including all the courses in student's study plan (#4390) 2024-03-20 18:36:54 +02:00
Valtteri KantanenandGitHub 268fc7c8e9 Merge pull request #4451 from UniversityOfHelsinkiCS/firefox-fix
[Class statistics] Fix broken dates on Firefox
2024-03-20 16:40:25 +02:00
Valtteri Kantanen d63e69a950 Rename two "student" variables to "studentNumber" 2024-03-20 16:26:56 +02:00
Valtteri Kantanen 29218cd9d2 [Class statistics] Fix logic for finding the correct studyright element 2024-03-20 16:22:37 +02:00
Riku Rauhala f81cbe1d4f [Class statistics] Fix broken dates on Firefox 2024-03-20 12:06:16 +02:00
Riku Rauhala 852c91219c [Population students] Rename variables 2024-03-20 11:35:55 +02:00
Valtteri Kantanen a94dcd2a9d Use a function expression in the tooltipFormatter: this is necessary as the keyword this behaves differently in arrow functions 2024-03-19 17:06:18 +02:00
Riku Rauhala 874fdf83e1 Update imports 2024-03-19 13:59:36 +02:00
Riku Rauhala 5aefe58e46 Convert the remaining functions to arrow syntax 2024-03-19 13:39:34 +02:00
Riku Rauhala 9047e099dc Move a duplicate getAge function to common 2024-03-19 13:37:56 +02:00
Riku Rauhala ce03560ff8 Rename most 's' mystery variables
These variables had the following meanings: semester, series, status, student or studyright. Some variables are harder to decipher so they are left for now
2024-03-19 13:13:27 +02:00
Riku Rauhala 068458d4be [Student statistics] Update style 2024-03-19 12:25:54 +02:00
Valtteri Kantanen 68767a301e Fix CI/CD workflow for PRs 2024-03-19 12:14:16 +02:00
Riku Rauhala b443ead517 [Changelog] Add JSDoc to getDescription 2024-03-19 11:47:37 +02:00
Riku Rauhala b743e49ff1 [Changelog] Update logic for parsing the description
It shouldn't matter how the word 'internal' is spelled or included in the description. Now the internal section of the changelog begins at any line that contains the keyword 'internal'
2024-03-19 11:45:25 +02:00
Riku Rauhala 5548940ea4 Rename 'c' mystery variables
Give all 'c' variables more meaningful names. Depending on the context, 'c' could mean any of these things: child, code, column, course, credit, curriculum or studyright
2024-03-19 10:52:26 +02:00
Riku Rauhala 665e0ed6e4 Rename more kisakoodari variables 2024-03-18 15:27:18 +02:00
Riku Rauhala f7247a73ec Rename evt variables
Rename evt to event for consistency
2024-03-18 15:20:31 +02:00
Riku Rauhala c9e259a1ea Rename srE variables
Rename srE to element or studyrightElement depending on the context
2024-03-18 15:18:55 +02:00
Riku Rauhala 203ee46a7c Rename sr variables
Rename sr to studyright for consistency
2024-03-18 15:13:39 +02:00
Riku Rauhala afe70fdb67 Rename element relates variables
Rename el or elem to element for consistency
2024-03-18 15:06:45 +02:00
Riku Rauhala df831aeef7 Rename most 'e' variables
There were tons of kisakoodari tier variables named simply e with varying meanings: element, enrollment, error, event...
2024-03-18 14:27:56 +02:00
Riku Rauhala 0021c44c2c Rename idx -> index
In most cases the variable name index is used, replace the remaining cases of idx with index for consistency with the rest of the codebase
2024-03-18 12:51:42 +02:00
Riku Rauhala 4b4a671a70 Fix SortableTable key issue 2024-03-18 12:44:05 +02:00
Riku Rauhala 1aed2d2ff8 Sort eslint rules 2024-03-18 12:17:28 +02:00
Riku Rauhala 3a10c22b80 [Course statistics] Add missing bottom margin when the direction is toggled 2024-03-18 11:51:24 +02:00
Riku Rauhala 60851c88e5 [Course statistics] Update table header formatting 2024-03-18 11:44:36 +02:00
Riku Rauhala efdc2abbef [Study guidance groups] Add missing margin and center content 2024-03-18 10:31:16 +02:00
Riku Rauhala eeab572c5d Rename Study programme and Course statistics titles for consistency 2024-03-18 10:06:04 +02:00
Valtteri Kantanen 1a25f3aae6 Add GHA workflow for running tests on PRs 2024-03-15 20:51:19 +02:00
Riku Rauhala 40ea7c8752 [Study programme] Fix null text in some expanding some years 2024-03-15 19:53:52 +02:00
Riku Rauhala 7442513927 [Study programme] Fix broken population link 2024-03-15 19:53:52 +02:00
Riku Rauhala 67ee85dd41 Refactor StudytrackDataTable 2024-03-15 19:53:52 +02:00
Valtteri Kantanen 1f3ecb4952 [GHA workflow] Require successful linting before running tests for updater 2024-03-15 15:18:54 +02:00
Valtteri Kantanen 8c271d0fab [Updater] Lint files in GHA workflow 2024-03-15 15:04:31 +02:00
Valtteri Kantanen d678ac2a6a Combine CI/CD pipeline jobs for linting backend and frontend 2024-03-15 15:03:16 +02:00
Valtteri Kantanen 8ccd5cdae5 [Updater] Update logic for finding students whose studyplan to update to avoid circular dependency 2024-03-15 14:39:22 +02:00
Riku Rauhala 6f999fa203 Bump immer to 9.0.21 2024-03-15 12:04:19 +02:00
Valtteri Kantanen 5cbc827d1d [Updater] Split updateStudents into multiple files 2024-03-15 10:55:03 +02:00
Tereshchenko Aleksandr 4cdcd948f6 Add links to updater-scheduler and -worker READMEs to the root README 2024-03-15 09:21:03 +02:00
Arttu KangasandGitHub 9c4ab07fb8 Merge pull request #4442 from UniversityOfHelsinkiCS/updater-scheduler-readme
Add workflow overview to updater-scheduler's and updater-worker's READMEs
2024-03-14 19:31:10 +02:00
Arttu KangasandGitHub 5e86ff05f3 Add explanation of what purge is, typo fixes 2024-03-14 19:30:23 +02:00
Tereshchenko Aleksandr d017a23ab5 Add workflow overview to updater-worker's README 2024-03-14 16:08:14 +02:00
Valtteri Kantanen 250024d41d Fix ESLint errors for backend and updater 2024-03-14 15:51:33 +02:00
Tereshchenko Aleksandr 1420628f6f Add workflow overview to updater-scheduler's README 2024-03-14 14:11:30 +02:00
Valtteri Kantanen e1cc1f3ecc Move many ESLint rules to the root level 2024-03-12 18:51:11 +02:00
arttu 2d0d152fb7 [Updater] Fix resolving 'is_open' in credits, improve code 2024-03-12 18:27:42 +02:00
Valtteri Kantanen 2272890cb4 [Course statistics] Fix page crashing because of missing studyrights 2024-03-12 17:50:49 +02:00
Valtteri Kantanen ba8471df88 Remove the rest of absolute imports without '@' 2024-03-12 17:42:22 +02:00
arttu 575a580120 [OpenUni] Fix credits not showing 2024-03-12 17:35:42 +02:00
Valtteri Kantanen 0a07dedede [Course population] Improve logic for finding the correct programme for student (#4187) 2024-03-12 17:18:24 +02:00
Riku Rauhala ed0f700fc2 [Study programme] Update text aligns for tables 2024-03-12 13:50:57 +02:00
Riku Rauhala 616cc60b39 [Course population] Fix InfoBox margin 2024-03-12 13:30:30 +02:00
ShootingStar91 68b730a747 Fix faculty programme getter returning multiples of some programmes, causing double credits to be count in progress stats and possibly elsewhere 2024-03-12 13:29:30 +02:00
Riku Rauhala b541857980 [Navigation bar] Format navigation items 2024-03-12 13:26:57 +02:00
Valtteri Kantanen 2b3e436979 [Course population] Clean up extra state and a useEffect 2024-03-12 13:20:22 +02:00
Valtteri Kantanen ba1224c983 [Course population] Use the same logic for credit gains as for programme distribution 2024-03-12 13:15:05 +02:00
Valtteri Kantanen aeab65358d [Course population] Extract logic for finding the correct study programme into its own function 2024-03-12 13:08:52 +02:00
Riku Rauhala e865d12676 [Navigation bar] For dropdown menu items, set the top level item as active 2024-03-12 13:05:27 +02:00
Valtteri Kantanen 96760bd0fb [Course population] Display correct programme if a student has many (#4433) 2024-03-12 12:27:10 +02:00
Riku Rauhala eb7b91a959 [Study programme] Align cells to the right in the By semester tab table 2024-03-12 10:40:03 +02:00
Riku Rauhala e07677ecaa [Study programme] Update chart/table styles 2024-03-11 15:29:50 +02:00
Riku Rauhala d1a729804e [Study programme] Fix countries hover not working properly with 0 value 2024-03-11 15:04:50 +02:00
Riku Rauhala bb1a43ac87 [Study programme] Update students table style to match the one in Faculties 2024-03-11 14:46:00 +02:00
ShootingStar91 d493ac53a5 [Progress stats] Show progamme barchart credits from left to right 2024-03-11 13:48:53 +02:00
Riku Rauhala 0695e031bf Sort props 2024-03-11 13:44:01 +02:00
Riku Rauhala f799420b42 [Faculties] Add missing margin on the Graduation times tab 2024-03-11 10:46:30 +02:00
Riku Rauhala 23a5ee7b08 [Faculties] Align starting year cells to the right 2024-03-11 10:31:08 +02:00
arttu 71b5a87ef1 [University view] Fix credit category titles in expanded table view 2024-03-09 16:42:54 +02:00
Valtteri Kantanen e0b15e457e [Users] Fix logic for filtering out old and specialized programmes 2024-03-08 15:06:50 +02:00
Valtteri Kantanen addacd63c9 [Users] Display a message on user page if they don't have a person id 2024-03-08 15:05:41 +02:00
Riku Rauhala fc8c36392e Update imports 2024-03-08 15:05:03 +02:00
Valtteri Kantanen 1dc2c942f3 Revert "[Users] Move language-related stuff to userRouter"
This reverts commit f5ae6d42e9.
2024-03-08 13:06:58 +02:00
Riku Rauhala cd3ccbf5fb [Faculty statistics] Update styles 2024-03-08 12:49:01 +02:00
Valtteri Kantanen 341b04b01a [Course stats] Fix undeterministic behaviour of combineSubstitutions toggle 2024-03-08 11:49:08 +02:00
Riku Rauhala f826f5b93c [FacultyBarChart] Add the div back 2024-03-08 09:58:22 +02:00
Riku Rauhala 137d48a483 [University] Align content cell text to the right 2024-03-07 22:06:33 +02:00
Riku Rauhala 89f31d5cdf [University] Update styles 2024-03-07 21:51:55 +02:00
arttu 1a8ccde9f3 [Open uni] Fix code that checks if student has degreestudyright 2024-03-07 19:07:57 +02:00
arttu 96a185a59e [Users] Log username if no sufficient rights to see oodikone 2024-03-06 15:01:50 +02:00
arttu 678cd070ec [Updater] Small adjustements to individual student schedule 2024-03-06 12:36:17 +02:00
arttu a606117807 [Custom population] Fix notification showing students that could not be shown to user 2024-03-06 10:29:05 +02:00
ShootingStar91 db9543edd8 Add possibility to schedule individual updates of all students in new programmes 2024-03-05 20:47:24 +02:00
arttu eeeeb1c671 Revert "Add explode backend button & route"
This reverts commit b2b2d87ff3.
2024-03-05 16:11:28 +02:00
ShootingStar91 16b18bb92b [University view] Add include/exclude special studyrights toggle 2024-03-05 14:08:12 +02:00
arttu 2c2f98599e [Credits produced] Add tooptip about transferred credit dates 2024-03-05 12:14:18 +02:00
arttu 1f0b91a612 Revert "[Credits produced] Transfers are again counted to the year of original attainment"
This reverts commit e3151f67d1.
2024-03-05 12:12:06 +02:00
ShootingStar91 e3151f67d1 [Credits produced] Transfers are again counted to the year of original attainment 2024-03-04 13:38:42 +02:00
ShootingStar91 59ab60fc5e [Credits produced] Further improve share resolving code 2024-03-04 13:15:09 +02:00
Riku Rauhala 25233fd13e Disable credits in Highcharts 2024-03-04 13:01:07 +02:00
Riku Rauhala a1e0560faa Rename defaultConfig to config for consistency with other files 2024-03-04 12:53:42 +02:00
Riku Rauhala 7c5c785a6e Get Highcharts href and text from constants 2024-03-04 12:53:42 +02:00
Riku Rauhala 61a4073d82 Move course statistics graph options away from constants 2024-03-04 12:53:42 +02:00
Riku Rauhala d19ef59fc3 Replace the Highcharts default link with toska.dev 2024-03-04 12:53:42 +02:00
Riku Rauhala 986a12c27b Make some minor code style tweaks 2024-03-04 12:53:42 +02:00
Valtteri Kantanen 892a366984 [Users] Replace rights with programmeRights both in backend and frontend 2024-03-01 17:18:45 +02:00
Valtteri Kantanen d87d611db7 [Users] Replace elementDetails with programmeRights 2024-03-01 14:45:53 +02:00
Valtteri Kantanen f5ae6d42e9 [Users] Move language-related stuff to userRouter 2024-03-01 14:45:53 +02:00
Valtteri Kantanen 40af5f0f99 [Users] Add new field to user object: programmeRights 2024-03-01 14:45:53 +02:00
ShootingStar91 e139f4cb4e Fix acual tests 2024-03-01 13:34:12 +02:00
ShootingStar91 71d993246b Revert "Run tests on PR and development"
This reverts commit a3067c6b23.
2024-03-01 13:32:16 +02:00
ShootingStar91 a3067c6b23 Run tests on PR and development 2024-03-01 13:17:26 +02:00
ShootingStar91 01841e104e Fix cypress tests again 2024-03-01 12:06:11 +02:00
ShootingStar91 1ae4c0b39e Fix tests of programme overview 2024-03-01 11:52:56 +02:00
Riku Rauhala 32e4a22c00 [University view] Add missing title 2024-03-01 10:51:32 +02:00
ShootingStar91 013da38a6b Added error listener to frontend: sends errors to sentry if they are not catched by error boundary (event handlers etc) 2024-03-01 10:49:38 +02:00
ShootingStar91 b2b2d87ff3 Add explode backend button & route 2024-03-01 10:48:40 +02:00
arttu 7e22fcd856 [Credits produced] Fix logic: Include Vaasa credits, fix provider resolving, fix determining category 2024-02-29 20:44:56 +02:00
arttu 329466dde9 [Rapodiff] Script changes for investigating every category better 2024-02-29 20:44:56 +02:00
Valtteri Kantanen a7ebc4fb99 Fix containers not being removed after setup 2024-02-28 18:05:17 +02:00
Valtteri Kantanen 6e7ee36db1 [Teachers] Fix doctoral programmes not showing up in the course provider dropdown 2024-02-28 18:03:50 +02:00
Valtteri Kantanen ca8dc45ceb Add ESLint rule for import order 2024-02-27 20:42:37 +02:00
Valtteri Kantanen 9998f2600d Add a weekly cron job for deleting users who haven't logged in for 18 months 2024-02-27 14:43:20 +02:00
Riku Rauhala 1f370ea9c5 [Course statistics] Fix the application crashing while entering course population via the Attempts table 2024-02-27 13:33:16 +02:00
Riku Rauhala f319e3e2a0 [Course statistics] Rearrange imports 2024-02-27 13:32:34 +02:00
Riku Rauhala 01e701b770 [Course population] Rearrange imports 2024-02-27 13:05:50 +02:00
Valtteri Kantanen 4542f4179d Clean up the actionHistory object before sending it to Sentry 2024-02-27 12:29:58 +02:00
Riku Rauhala 288ff2bada Remove duplicate parameters 2024-02-26 19:46:27 +02:00
Valtteri Kantanen dce0642f08 [Teachers] Fix course providers not showing 2024-02-26 14:36:22 +02:00
Valtteri Kantanen c0a1cf99f2 Remove unused file to fix ESLint error 2024-02-26 14:19:47 +02:00
Valtteri Kantanen f4f1ed6818 [Users] Add infobox to user page about IAM based access rights 2024-02-26 14:14:27 +02:00
Valtteri Kantanen e0a7ed139f Fix sorting error 2024-02-26 14:13:35 +02:00
Valtteri Kantanen a5fe61a654 [Users] Remove user faculties from frontend and backend 2024-02-26 14:13:20 +02:00
Valtteri Kantanen d02dff573c [Users] Display IAM group based study programme access rights on user page 2024-02-26 10:58:41 +02:00
Valtteri Kantanen 56faf46a1a [Users] Fix sorting by username 2024-02-26 10:57:51 +02:00
Valtteri Kantanen 9e09168b01 [Sortable table] Fix multi select filter 2024-02-26 10:32:22 +02:00
Riku Rauhala 9d474ca61e [Course statistics] Display enrolled students with no grade in the pass rate chart 2024-02-23 17:03:27 +02:00
Riku Rauhala 9c39f00ec3 Rearrange imports 2024-02-23 13:15:30 +02:00
Riku Rauhala 45d3178193 [Course statistics] Refactor panes 2024-02-23 13:12:01 +02:00
395 changed files with 7540 additions and 6931 deletions
+26 -6
View File
@@ -8,12 +8,32 @@
"ecmaVersion": "latest",
"sourceType": "commonjs"
},
"extends": ["eslint:recommended", "plugin:prettier/recommended", "plugin:cypress/recommended"],
"extends": [
"eslint:recommended",
"airbnb",
"plugin:cypress/recommended",
"plugin:import/recommended",
"plugin:prettier/recommended"
],
"rules": {
"no-console": "error",
"no-async-promise-executor": "off",
"prettier/prettier": "error",
"camelcase": "off",
"consistent-return": "off",
"cypress/no-assigning-return-values": "off",
"cypress/unsafe-to-chain-command": "off"
}
"cypress/unsafe-to-chain-command": "off",
"default-param-last": "off",
"import/order": "off",
"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": "off",
"no-return-await": "off",
"no-shadow": "off",
"prettier/prettier": "error",
"quotes": ["error", "single", { "avoidEscape": true }]
},
"ignorePatterns": ["node_modules/", "build/", "**/*migrations*/**"]
}
+73
View File
@@ -0,0 +1,73 @@
name: 'Run tests'
on:
pull_request:
branches:
- master
paths:
- 'cypress/**'
- 'cypress.config.json'
- 'services/**'
- '!services/backend/src/rapodiff/**'
- 'docker-compose.ci.yml'
- 'package*.json'
jobs:
lint_backend_and_frontend:
name: 'Lint backend and frontend'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 18
- name: 'Install dependencies'
run: npm ci && cd services/frontend && npm ci && cd ../backend && npm ci
- name: 'Run Stylelint for CSS files'
run: npm run stylelint "services/frontend/src/**/*.css"
- name: 'Run ESLint for JS files'
run: npm run eslint -- --ext .jsx,.js services --report-unused-disable-directives
test_end_to_end:
runs-on: ubuntu-latest
needs:
- lint_backend_and_frontend
strategy:
fail-fast: false
matrix:
include:
- name: 'Test course statistics'
spec: cypress/e2e/Course_statistics.js
- name: 'Test custom population'
spec: cypress/e2e/Custom_population.js
- name: 'Test population statistics'
spec: cypress/e2e/Population_statistics.js
- name: 'Test filters'
spec: cypress/e2e/Filters.js
- name: 'Test students'
spec: cypress/e2e/Students.js
- name: 'Test completed courses'
spec: cypress/e2e/Completed_courses.js
- name: 'Test studyprogramme overview'
spec: cypress/e2e/Studyprogramme_overview.js
- name: 'Test teachers and users'
spec: |
cypress/e2e/Teachers.js
cypress/e2e/Users.js
- name: 'Test faculty statistics'
spec: cypress/e2e/Faculty_statistics.js
- name: 'Test open uni search'
spec: cypress/e2e/Openuni.js
- name: 'Test language center view'
spec: cypress/e2e/Language_center.js
- name: 'Test study guidance groups'
spec: cypress/e2e/Study_guidance_groups.js
name: ${{ matrix.name }}
steps:
- uses: actions/checkout@v4
- name: 'Run tests'
uses: ./.github/actions/run-tests
with:
username: ${{ secrets.TOSKAREGISTRY_USERNAME }}
password: ${{ secrets.TOSKAREGISTRY_PASSWORD }}
spec: ${{ matrix.spec }}
+7 -20
View File
@@ -12,8 +12,8 @@ on:
jobs:
# === Static tests / linting ===
lint_backend:
name: 'Lint backend'
lint_backend_and_frontend:
name: 'Lint backend and frontend'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
@@ -21,24 +21,11 @@ jobs:
with:
node-version: 18
- name: 'Install dependencies'
run: npm ci && cd services/backend && npm ci
- name: 'Run backend linter'
run: npm run eslint services/backend
lint_frontend:
name: 'Lint frontend'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 18
- name: 'Install dependencies'
run: npm ci && cd services/frontend && npm ci
- name: 'Run frontend linter'
run: npm run concurrently \
'eslint --ext .jsx,.js services/frontend/src --report-unused-disable-directives' \
'stylelint services/frontend/src/**/*.css'
run: npm ci && cd services/frontend && npm ci && cd ../backend && npm ci
- name: 'Run Stylelint for CSS files'
run: npm run stylelint "services/frontend/src/**/*.css"
- name: 'Run ESLint for JS files'
run: npm run eslint -- --ext .jsx,.js services --report-unused-disable-directives
# === Build database images ===
build_backend:
+23 -2
View File
@@ -1,4 +1,4 @@
name: 'Test and release Updater'
name: 'Test (and release) Updater'
on:
push:
@@ -8,8 +8,27 @@ on:
- '.github/workflows/updater.yaml'
- 'updater/sis-updater-worker/**'
- 'updater/sis-updater-scheduler/**'
pull_request:
branches:
- master
paths:
- 'updater/sis-updater-worker/**'
- 'updater/sis-updater-scheduler/**'
jobs:
lint_scheduler_and_worker:
name: 'Lint scheduler and worker'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 18
- 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
build_worker:
name: 'Build Updater worker'
runs-on: ubuntu-latest
@@ -50,6 +69,7 @@ jobs:
name: 'Test Updater'
runs-on: ubuntu-latest
needs:
- lint_scheduler_and_worker
- build_worker
- build_scheduler
steps:
@@ -95,6 +115,7 @@ jobs:
name: 'Build and release Updater worker'
runs-on: ubuntu-latest
needs: test_updater
if: github.event_name == 'push'
steps:
- uses: actions/checkout@v4
@@ -121,7 +142,7 @@ jobs:
name: 'Build and release Updater scheduler'
runs-on: ubuntu-latest
needs: test_updater
if: github.event_name == 'push'
steps:
- uses: actions/checkout@v4
+4
View File
@@ -98,6 +98,10 @@ In development, backend and updater use the same redis, but in production they a
See also [older graph](documentation/Oodikone.png), which presents more information.
To read a bit more details about what updater-scheduler and updater-worker do, see their own READMEs:
- [updater-scheduler's README](./updater/sis-updater-scheduler/README.md)
- [updater-worker's README](./updater/sis-updater-worker/README.md)
### Documentation
Folder `documentation` currently contains some notes on sis-db schema and is meant as a place for storing useful information on Oodikone to make life easier for future developers. Feel free to add anything relevant!
+1 -1
View File
@@ -22,7 +22,7 @@ trap cleanup SIGINT SIGTERM ERR EXIT
cleanup() {
trap - SIGINT SIGTERM ERR EXIT
warningmsg "Trying to run docker-compose down and remove orphans"
docker-compose down --remove-orphans
docker-compose --profile oodikone --profile updater down --remove-orphans
}
# === CLI ===
+1 -1
View File
@@ -67,7 +67,7 @@ describe('When search modal is opened', () => {
)
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('contain.text', 'Student number')
cy.get('[data-cy="completed-courses-table-div"]').should('not.contain', '1')
})
})
+1 -1
View File
@@ -132,7 +132,7 @@ describe('Custom population tests', () => {
})
describe('Custom population search saving', () => {
it('Saves a custom population search', () => {
it('Saves a custom population search', { retries: 2 }, () => {
cy.fixture('customPopulations').then(({ studentNumbersForCSStudentsSet1 }) => {
const students = studentNumbersForCSStudentsSet1
openCustomPopupForm()
+4 -4
View File
@@ -83,9 +83,9 @@ describe('Population Statistics with CS master', () => {
})
})
describe('Studyright Type filter', () => {
describe('Studyright type filter', () => {
it('is visible', () => {
cy.contains('Studyright Type')
cy.contains('Studyright type')
})
it('is set to all by default', () => {
@@ -252,8 +252,8 @@ describe('Population Statistics', () => {
checkFilteringResult(defaultAmountOfStudents)
})
it('Studyright Type filter is not visible', () => {
cy.contains('Studyright Type').should('not.exist')
it('Studyright type filter is not visible', () => {
cy.contains('Studyright type').should('not.exist')
})
})
+4 -4
View File
@@ -132,14 +132,14 @@ describe('Population Statistics tests', () => {
cy.contains('Credit accumulation (for 170 students)')
})
it('Credit Statistics, Credits Gained tab works', () => {
it('Credit Statistics, Credits gained tab works', () => {
cy.selectStudyProgramme('Tietojenkäsittelytieteen kandiohjelma')
cy.contains('Credit statistics')
.parent()
.then($parentDiv => {
if (!$parentDiv.hasClass('active')) cy.contains('Credit statistics').click()
})
cy.contains('Credits Gained').click()
cy.contains('Credits gained').click()
cy.get("[data-cy='credits-gained-main-table']").should('contain', 'All students of the class')
const today = moment().endOf('month')
const months = Math.round(moment.duration(moment(today).diff(moment('2017-08-01'))).asMonths())
@@ -172,7 +172,7 @@ describe('Population Statistics tests', () => {
cy.get("[data-cy='credits-gained-table-Ei valintatapaa']").should('not.exist')
})
it('Credit Statistics, Credits Gained tab shows stats by admissions', () => {
it('Credit Statistics, Credits gained tab shows stats by admissions', () => {
cy.visit(pathToCSMaster2019)
cy.cs('filtered-students')
cy.contains('Credit statistics')
@@ -181,7 +181,7 @@ describe('Population Statistics tests', () => {
.then($parentDiv => {
if (!$parentDiv.hasClass('active')) cy.get("[data-cy='Credit statistics']").click()
})
cy.contains('Credits Gained').click()
cy.contains('Credits gained').click()
cy.get('.credits-gained-divider').click()
cy.get("[data-cy='credits-gained-table-Avoin väylä']").should('exist')
cy.get("[data-cy='credits-gained-table-Yhteispisteet']").should('exist')
+13 -13
View File
@@ -59,10 +59,10 @@ describe('Studyprogramme overview', () => {
const tableContents = [
...years.map(year => [year, 0, 0]),
[2021, 0, 0, 0, 0, 0],
[2020, 4511, 4511, 0, 0, 167],
[2019, 7097, 7097, 0, 0, 349],
[2018, 5742, 5742, 0, 0, 272],
[2017, 1910, 1910, 0, 0, 388],
[2020, 4511, 4511, 0, 0, 406],
[2019, 7117, 7097, 0, 0, 434],
[2018, 5742, 5737, 0, 0, 150],
[2017, 1882, 1882, 0, 0, 186],
]
cy.checkTableStats(tableContents, 'CreditsProducedByTheStudyprogramme')
@@ -101,10 +101,10 @@ describe('Studyprogramme overview', () => {
const creditTableContents = [
...years.map(year => [year, 0, 0]),
[2021, 0, 0, 0, 0, 0],
[2020, 4511, 4511, 0, 0, 167],
[2019, 7097, 7097, 0, 0, 349],
[2018, 5742, 5742, 0, 0, 272],
[2017, 1910, 1910, 0, 0, 388],
[2020, 4511, 4511, 0, 0, 406],
[2019, 7117, 7097, 0, 0, 434],
[2018, 5742, 5737, 0, 0, 150],
[2017, 1882, 1882, 0, 0, 186],
]
cy.checkTableStats(creditTableContents, 'CreditsProducedByTheStudyprogramme')
@@ -141,10 +141,10 @@ describe('Studyprogramme overview', () => {
const creditTableContents = [
...years.map(year => [year, 0, 0]),
['2021 - 2022', 0, 0, 0, 0, 0],
['2020 - 2021', 352, 352, 0, 0, 60],
['2019 - 2020', 6966, 6966, 0, 0, 193],
['2018 - 2019', 7777, 7777, 0, 0, 328],
['2017 - 2018', 4165, 4165, 0, 0, 344],
['2020 - 2021', 352, 352, 0, 0, 296],
['2019 - 2020', 6966, 6966, 0, 0, 430],
['2018 - 2019', 7797, 7777, 0, 0, 259],
['2017 - 2018', 4137, 4132, 0, 0, 191],
]
cy.checkTableStats(creditTableContents, 'CreditsProducedByTheStudyprogramme')
@@ -160,8 +160,8 @@ describe('Studyprogramme overview', () => {
cy.get('[data-cy=Graph-CreditsProducedByTheStudyprogramme')
.should('contain', 'Degree students')
.should('contain', 'Transferred')
.should('contain', 388)
.should('contain', 7097)
.should('contain', 434)
cy.get('[data-cy=Graph-GraduatedAndThesisWritersOfTheProgramme')
.should('contain', 'Graduated students')
-221
View File
@@ -11,7 +11,6 @@
"moment": "^2.29.4"
},
"devDependencies": {
"concurrently": "^7.4.0",
"cypress": "^13.2.0",
"eslint": "^8.56.0",
"eslint-config-airbnb": "^19.0.4",
@@ -1127,17 +1126,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/cliui": {
"version": "7.0.4",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz",
"integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==",
"dev": true,
"dependencies": {
"string-width": "^4.2.0",
"strip-ansi": "^6.0.0",
"wrap-ansi": "^7.0.0"
}
},
"node_modules/color-convert": {
"version": "1.9.3",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
@@ -1201,118 +1189,6 @@
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
"dev": true
},
"node_modules/concurrently": {
"version": "7.4.0",
"resolved": "https://registry.npmjs.org/concurrently/-/concurrently-7.4.0.tgz",
"integrity": "sha512-M6AfrueDt/GEna/Vg9BqQ+93yuvzkSKmoTixnwEJkH0LlcGrRC2eCmjeG1tLLHIYfpYJABokqSGyMcXjm96AFA==",
"dev": true,
"dependencies": {
"chalk": "^4.1.0",
"date-fns": "^2.29.1",
"lodash": "^4.17.21",
"rxjs": "^7.0.0",
"shell-quote": "^1.7.3",
"spawn-command": "^0.0.2-1",
"supports-color": "^8.1.0",
"tree-kill": "^1.2.2",
"yargs": "^17.3.1"
},
"bin": {
"conc": "dist/bin/concurrently.js",
"concurrently": "dist/bin/concurrently.js"
},
"engines": {
"node": "^12.20.0 || ^14.13.0 || >=16.0.0"
},
"funding": {
"url": "https://github.com/open-cli-tools/concurrently?sponsor=1"
}
},
"node_modules/concurrently/node_modules/ansi-styles": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"dev": true,
"dependencies": {
"color-convert": "^2.0.1"
},
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/concurrently/node_modules/chalk": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
"dev": true,
"dependencies": {
"ansi-styles": "^4.1.0",
"supports-color": "^7.1.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
"node_modules/concurrently/node_modules/chalk/node_modules/supports-color": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
"dev": true,
"dependencies": {
"has-flag": "^4.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/concurrently/node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"dev": true,
"dependencies": {
"color-name": "~1.1.4"
},
"engines": {
"node": ">=7.0.0"
}
},
"node_modules/concurrently/node_modules/color-name": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"dev": true
},
"node_modules/concurrently/node_modules/has-flag": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
"dev": true,
"engines": {
"node": ">=8"
}
},
"node_modules/concurrently/node_modules/supports-color": {
"version": "8.1.1",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
"integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==",
"dev": true,
"dependencies": {
"has-flag": "^4.0.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/chalk/supports-color?sponsor=1"
}
},
"node_modules/confusing-browser-globals": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/confusing-browser-globals/-/confusing-browser-globals-1.0.11.tgz",
@@ -1575,19 +1451,6 @@
"node": ">=0.10"
}
},
"node_modules/date-fns": {
"version": "2.29.1",
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.29.1.tgz",
"integrity": "sha512-dlLD5rKaKxpFdnjrs+5azHDFOPEu4ANy/LTh04A1DTzMM7qoajmKCBc8pkKRFT41CNzw+4gQh79X5C+Jq27HAw==",
"dev": true,
"engines": {
"node": ">=0.11"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/date-fns"
}
},
"node_modules/dayjs": {
"version": "1.11.4",
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.4.tgz",
@@ -1902,15 +1765,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/escalade": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz",
"integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==",
"dev": true,
"engines": {
"node": ">=6"
}
},
"node_modules/escape-string-regexp": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
@@ -2862,15 +2716,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/get-caller-file": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
"integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
"dev": true,
"engines": {
"node": "6.* || 8.* || >= 10.*"
}
},
"node_modules/get-intrinsic": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz",
@@ -5497,15 +5342,6 @@
"throttleit": "^1.0.0"
}
},
"node_modules/require-directory": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
"integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
"dev": true,
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/require-from-string": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
@@ -5745,12 +5581,6 @@
"node": ">=8"
}
},
"node_modules/shell-quote": {
"version": "1.7.3",
"resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.7.3.tgz",
"integrity": "sha512-Vpfqwm4EnqGdlsBFNmHhxhElJYrdfcxPThu+ryKS5J8L/fhAwLazFZtq+S+TWZ9ANj2piSQLGj6NQg+lKPmxrw==",
"dev": true
},
"node_modules/side-channel": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz",
@@ -5836,12 +5666,6 @@
"node": ">=0.10.0"
}
},
"node_modules/spawn-command": {
"version": "0.0.2-1",
"resolved": "https://registry.npmjs.org/spawn-command/-/spawn-command-0.0.2-1.tgz",
"integrity": "sha512-n98l9E2RMSJ9ON1AKisHzz7V42VDiBQGY6PB1BwRglz99wpVsSuGzQ+jOi6lFXBGVTCrRpltvjm+/XA+tpeJrg==",
"dev": true
},
"node_modules/sshpk": {
"version": "1.18.0",
"resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.18.0.tgz",
@@ -6492,15 +6316,6 @@
"node": ">= 4.0.0"
}
},
"node_modules/tree-kill": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz",
"integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==",
"dev": true,
"bin": {
"tree-kill": "cli.js"
}
},
"node_modules/tsconfig-paths": {
"version": "3.15.0",
"resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz",
@@ -6936,15 +6751,6 @@
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/y18n": {
"version": "5.0.8",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
"integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
"dev": true,
"engines": {
"node": ">=10"
}
},
"node_modules/yallist": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
@@ -6960,33 +6766,6 @@
"node": ">= 14"
}
},
"node_modules/yargs": {
"version": "17.5.1",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-17.5.1.tgz",
"integrity": "sha512-t6YAJcxDkNX7NFYiVtKvWUz8l+PaKTLiL63mJYWR2GnHq2gjEWISzsLp9wg3aY36dY1j+gfIEL3pIF+XlJJfbA==",
"dev": true,
"dependencies": {
"cliui": "^7.0.2",
"escalade": "^3.1.1",
"get-caller-file": "^2.0.5",
"require-directory": "^2.1.1",
"string-width": "^4.2.3",
"y18n": "^5.0.5",
"yargs-parser": "^21.0.0"
},
"engines": {
"node": ">=12"
}
},
"node_modules/yargs/node_modules/yargs-parser": {
"version": "21.1.1",
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
"integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
"dev": true,
"engines": {
"node": ">=12"
}
},
"node_modules/yauzl": {
"version": "2.10.0",
"resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz",
+1 -2
View File
@@ -3,8 +3,8 @@
"description": "An application for analyzing university data.",
"scripts": {
"cypress": "cypress",
"concurrently": "concurrently",
"eslint": "eslint",
"lint:all": "eslint --ext .js,.jsx services/ updater/ --report-unused-disable-directives",
"stylelint": "stylelint",
"actionlint": "node-actionlint",
"prettier": "prettier",
@@ -31,7 +31,6 @@
},
"homepage": "https://github.com/UniversityOfHelsinkiCS/oodikone#readme",
"devDependencies": {
"concurrently": "^7.4.0",
"cypress": "^13.2.0",
"eslint": "^8.56.0",
"eslint-config-airbnb": "^19.0.4",
-1
View File
@@ -204,7 +204,6 @@ set_up_oodikone_from_scratch() {
infomsg "Cleaning up docker setup and node_modules from possible previous installations"
local compose_down_args="--remove-orphans --volumes --rmi all"
"$PROJECT_ROOT"/run.sh both down "$compose_down_args" || infomsg "Cleaning errored, but will continue"
docker-compose --file "$PROJECT_ROOT"/docker-compose.test.yml "$compose_down_args" || infomsg "Cleaning errored, but will continue"
docker-compose --file "$PROJECT_ROOT"/docker-compose.ci.yml "$compose_down_args" || infomsg "Cleaning errored, but will continue"
for folder in "${folders_to_set_up[@]}"; do
cd "$folder" || die "Couldn't change directory to folder $folder"
+2 -7
View File
@@ -29,11 +29,10 @@ const frontUrl = process.env.FRONT_URL
const backendPort = 8080
// Other stuff
const DB_URL_KONE = process.env.DB_URL_KONE
const DB_URL_USER = process.env.DB_URL_USER
const { DB_URL_KONE, DB_URL_USER, SECRET_TOKEN, SIS_UPDATER_URL, CRYPT_KEY } = process.env
let DB_MAX_CONNECTIONS = parseInt(process.env.DB_MAX_CONNECTIONS, 10)
if (isNaN(DB_MAX_CONNECTIONS)) {
if (Number.isNaN(DB_MAX_CONNECTIONS)) {
DB_MAX_CONNECTIONS = 5 // sequelize's default
}
let DB_MAX_CRON_CONNECTIONS = DB_MAX_CONNECTIONS - 5
@@ -42,16 +41,12 @@ if (DB_MAX_CRON_CONNECTIONS < 1) {
}
const redis = process.env.REDIS
const SECRET_TOKEN = process.env.SECRET_TOKEN
const DB_SCHEMA_KONE = process.env.DB_SCHEMA_KONE || 'public'
const SIS_UPDATER_URL = process.env.SIS_UPDATER_URL
const KONE_PASSWORD = process.env.KONE_PASSWORD || 'postgres'
const USER_PASSWORD = process.env.USER_PASSWORD || 'postgres'
const SIS_PASSWORD = process.env.SIS_PASSWORD || 'postgres'
const CRYPT_KEY = process.env.CRYPT_KEY
module.exports = {
DB_URL_USER,
CRYPT_KEY,
+2 -1
View File
@@ -2,9 +2,10 @@ const Sequelize = require('sequelize')
const EventEmitter = require('events')
const Umzug = require('umzug')
const conf = require('../conf-backend')
const { SIS_DB_URL } = process.env
const logger = require('../util/logger')
const { SIS_DB_URL } = process.env
class DbConnection extends EventEmitter {
constructor() {
super()
+19 -1
View File
@@ -13,6 +13,11 @@ const { getAssociations } = require('./services/studyrights')
const { redisClient } = require('./services/redis')
const { computeLanguageCenterData, LANGUAGE_CENTER_REDIS_KEY } = require('./services/languageCenterData')
const { jobMaker } = require('./worker/queue')
const { deleteOutdatedUsers } = require('./services/userService')
const {
findStudentsCloseToGraduation,
CLOSE_TO_GRADUATION_REDIS_KEY,
} = require('./services/populations/closeToGraduation')
const schedule = (cronTime, func) => new CronJob({ cronTime, onTick: func, start: true, timeZone: 'Europe/Helsinki' })
@@ -68,7 +73,7 @@ const refreshProgramme = async code => {
await updateBasicView(code, '')
await updateStudytrackView(code, '', associations)
let combinedProgramme = combinedStudyprogrammes[code] || ''
const combinedProgramme = combinedStudyprogrammes[code] || ''
await updateBasicView(code, combinedProgramme)
await updateStudytrackView(code, combinedProgramme, associations)
}
@@ -95,11 +100,19 @@ const refreshLanguageCenterData = async () => {
logger.info('Language center data refreshed!')
}
const refreshCloseToGraduating = async () => {
logger.info('Updating students close to graduating...')
const updatedData = await findStudentsCloseToGraduation()
await redisClient.setAsync(CLOSE_TO_GRADUATION_REDIS_KEY, JSON.stringify(updatedData))
logger.info('Students close to graduating updated!')
}
const dailyJobs = () => {
refreshFaculties()
refreshProgrammes()
jobMaker.languagecenter()
jobMaker.statistics()
refreshCloseToGraduating()
}
const startCron = () => {
@@ -109,6 +122,11 @@ const startCron = () => {
logger.info('Running daily jobs from cron')
dailyJobs()
})
schedule('0 4 * * 3', async () => {
logger.info("Deleting users who haven't logged in for 18 months")
const numberOfDeletedUsers = await deleteOutdatedUsers()
logger.info(`Deleted ${numberOfDeletedUsers} users.`)
})
schedule('0 19 * * 1', async () => {
logger.info('Updating students whose studyplans have not been updated recently')
jobMaker.studyplansUpdate(4)
@@ -1,6 +1,7 @@
const morgan = require('morgan')
const logger = require('../util/logger')
const _ = require('lodash')
const { getFullStudyProgrammeRights } = require('../util/utils')
// So this appears to be a hack to get neatly formatted stats like response-time etc. from morgan
// without actually using morgan what it's used for (LOGGING REQUESTS!).
@@ -10,8 +11,10 @@ const accessLogger = morgan((tokens, req, res) => {
const fields = ['method', 'url', 'status', 'response-time', 'remote-addr', 'remote-user', 'user-agent', 'referrer']
const { user } = req
const meta = { ...user }
fields.forEach(field => (meta[field] = tokens[field](req, res)))
meta['time'] = tokens['date'](req, res, 'iso')
fields.forEach(field => {
meta[field] = tokens[field](req, res)
})
meta.time = tokens.date(req, res, 'iso')
if (req.route) {
meta['req-route'] = req.route.path
}
@@ -19,13 +22,15 @@ const accessLogger = morgan((tokens, req, res) => {
const message = [
`${user.mockedBy ? '(mocking) ' : ''}${user.name}`,
`${(tokens['response-time'](req, res) || '0').split('.')[0]} ms`.padEnd(8, ' '),
tokens['status'](req, res),
tokens['method'](req, res),
tokens['url'](req, res),
tokens.status(req, res),
tokens.method(req, res),
tokens.url(req, res),
].join(' ')
const usingIamRights = user.iamRights.some(programmeCode => tokens['url'](req, res).includes(programmeCode))
const onlyIamRights = !user.isAdmin && user.rights.length === 0
const fullStudyProgrammeRights = getFullStudyProgrammeRights(user.programmeRights)
const usingIamRights = user.iamRights.some(programmeCode => tokens.url(req, res).includes(programmeCode))
const onlyIamRights = !user.isAdmin && fullStudyProgrammeRights.length === 0
logger.info(message, {
// don't log student list which might be huge
@@ -41,7 +41,7 @@ const currentUserMiddleware = async (req, _res, next) => {
if (!hasRequiredIamGroup(iamGroups, iamRights)) {
logger.error({ message: 'User does not have required iam group', meta: { iamGroups, iamRights } })
throw new ApplicationError('User does not have required iam group', 403, { logoutUrl })
throw new ApplicationError(`User '${username}' does not have required iam group`, 403, { logoutUrl })
}
let user = await getUser({
@@ -52,6 +52,7 @@ const currentUserMiddleware = async (req, _res, next) => {
iamRights,
specialGroup,
sisId,
access,
})
if (showAsUser && user.isAdmin) {
@@ -39,27 +39,6 @@ const AccessGroup = sequelizeUser.define('access_group', {
},
})
const UserFaculties = sequelizeUser.define('user_faculties', {
userId: {
primaryKey: true,
type: Sequelize.BIGINT,
references: {
model: 'users',
key: 'id',
},
},
faculty_code: {
primaryKey: true,
type: Sequelize.STRING,
},
createdAt: {
type: Sequelize.DATE,
},
updatedAt: {
type: Sequelize.DATE,
},
})
const User = sequelizeUser.define(
'users',
{
@@ -98,12 +77,9 @@ User.belongsToMany(AccessGroup, {
})
AccessGroup.belongsToMany(User, { through: 'user_accessgroup' })
User.hasMany(UserFaculties, { as: 'faculty', foreignKey: 'userId' })
module.exports = {
User,
UserElementDetails,
AccessGroup,
UserFaculties,
sequelizeUser,
}
+2 -2
View File
@@ -2,8 +2,8 @@
const rapoToken = process.env.RAPO_NODEPROXY
const importerDbApiToken = process.env.IMPORTER_DB_API_TOKEN
const nodeproxyUrl = `https://toska.cs.helsinki.fi`
const importerDbApiUrl = `https://api-toska.apps.ocp-prod-0.k8s.it.helsinki.fi/importer`
const nodeproxyUrl = 'https://toska.cs.helsinki.fi'
const importerDbApiUrl = 'https://api-toska.apps.ocp-prod-0.k8s.it.helsinki.fi/importer'
const bscProgrammesOfFaculties = {
H10: ['KH10_001'],
+147 -94
View File
@@ -1,100 +1,10 @@
/* eslint-disable no-console */
const { getCreditsProduced } = require('../services/providerCredits')
const { computeCreditsProduced } = require('../services/providerCredits')
const { parseCsv } = require('./helpers')
const _ = require('lodash')
const diffs = []
let noDiffCounter = 0
const diff = (rapoData, okData, code) => {
for (const year of Object.keys(okData)) {
if (year !== '2022' && year !== '2023') continue
const rapo = rapoData[year]
const ok = okData[year]
if (!rapo) {
if (!ok) console.log(`Year ${year} not in rapo for code ${code}, skipping`)
continue
}
for (const field of ['basic', 'incoming-exchange', 'open-uni', 'agreement', 'separate', 'other']) {
const okValue = Math.round(ok[field] || 0)
const rapoValue = Math.round(rapo[field] || 0)
const diff = Math.abs(okValue - rapoValue)
const percentage =
rapoValue !== 0 || okValue !== 0 ? Number(((diff / Math.max(okValue, rapoValue)) * 100).toFixed(2)) : 0
const diffStr = `${field.padEnd(10, ' ')} ${year} ${code.padEnd(
10,
' '
)} Difference: ${diff} Rapo: ${rapoValue} Oodikone: ${okValue} Percentage: ${percentage} %`
diffs.push({ field, diff, okValue, rapoValue, code, year, diffStr, percentage })
}
}
}
let counter = 0
// Acual logic in this function
const process = async data => {
// Define here the fields to diff against each other. Must be an array, multiple fields will be summed
const rapoProgrammeData = formatData(data.slice(1))
const allProgrammeCodes = [...new Set(Object.keys(rapoProgrammeData))]
for (const programmeCode of allProgrammeCodes) {
const okProgrammeData = await getCreditsProduced(programmeCode, false, true)
counter++
if (counter % 5 === 0) {
console.log(`Done ${Math.round((counter / allProgrammeCodes.length) * 100)} %`)
}
diff(rapoProgrammeData[programmeCode], okProgrammeData.stats, programmeCode)
}
const fieldToSortBy = 'percentage'
const orderedDiffs = _.orderBy(diffs, fieldToSortBy, 'asc')
orderedDiffs.forEach(d => console.log(d.diffStr))
console.log(`${diffs.length} diffs found with current settings. Numbers with no diff: ${noDiffCounter}`)
const totalDiff = orderedDiffs.reduce((sum, cur) => cur.diff + sum, 0)
console.log('Total difference: ', totalDiff)
}
const programmeCreditsDiff = async fileName => {
console.log('Running studyprogramme credits diff')
await parseCsv(fileName, process)
console.log('Diff completed.')
}
const processIds = async (rawData, code) => {
const data = rawData.slice(1)
console.log('amount of rapo credits: ', data.length)
const rapoData = data
.map(row => ({
id: row[4],
credits: row[5],
type: row[6],
incl: row[7],
}))
.filter(row => row.type === '2' && row.incl === '0')
const rapoIds = rapoData.map(row => row.id)
const stats = await getCreditsProduced(code)
if (!stats.ids) {
console.log(stats)
console.log('Edit getCreditStats code so that it saves a list of the relevant ids you want to compare.')
return
}
const okIds = stats.ids
const notInOk = rapoIds.filter(id => id && !okIds[id.slice(11)]).map(str => str.slice(11))
const rapoIdMap = rapoIds.reduce((obj, cur) => {
obj[cur] = true
return obj
}, {})
const notInRapo = Object.keys(okIds).filter(id => !rapoIdMap[`ATTAINMENT-${id}`])
console.log('\n In rapo, but not in OK: ', notInOk.length)
console.dir(notInOk)
console.log('\n In OK, but not in Rapo: ', notInRapo.length)
console.dir(notInRapo)
delete stats.ids
console.log(stats['2022'])
const rapoCredits = rapoData.reduce((sum, cur) => parseInt(sum, 10) + parseInt(cur.credits, 10), 0)
console.log('rapoCredits: ', rapoCredits)
}
const testNewCalc = async code => {
await parseCsv('credits.csv', async data => processIds(data, code))
}
const noDiffCounter = 0
// Change weird rapo programmecodes to oodikone format, example: 300-M003 => MH30_003
const transformProgrammeCode = oldCode => `${oldCode[4]}H${oldCode.slice(0, 2)}_${oldCode.slice(5, 8)}`
@@ -115,10 +25,153 @@ const formatData = data =>
'open-uni': parseNum(openUni),
separate: parseNum(special),
total: parseNum(total),
abroad: parseNum(abroad),
other: parseNum(other),
transferred: parseNum(abroad) + parseNum(other),
}
return obj
}, {})
const diff = (rapoData, okData, code) => {
for (const year of Object.keys(okData)) {
if (year !== '2022' && year !== '2023') continue
const rapo = rapoData[year]
const ok = okData[year]
if (!rapo) {
if (!ok) console.log(`Year ${year} not in rapo for code ${code}, skipping`)
continue
}
for (const field of ['basic', 'incoming-exchange', 'open-uni', 'agreement', 'separate', 'transferred', 'other']) {
const okValue = Math.round(ok[field] || 0)
const rapoValue = Math.round(rapo[field] || 0)
const diff = Math.abs(okValue - rapoValue)
const percentage =
rapoValue !== 0 || okValue !== 0 ? Number(((diff / Math.max(okValue, rapoValue)) * 100).toFixed(2)) : 0
const diffStr = `${field.padEnd(10, ' ')} ${year} ${code.padEnd(
10,
' '
)} Difference: ${diff} Rapo: ${rapoValue} Oodikone: ${okValue} Percentage: ${percentage} %`
diffs.push({ field, diff, okValue, rapoValue, code, year, diffStr, percentage })
}
}
}
let counter = 0
const process = async data => {
const rapoProgrammeData = formatData(data.slice(1))
const allProgrammeCodes = [...new Set(Object.keys(rapoProgrammeData))]
for (const programmeCode of allProgrammeCodes) {
const okProgrammeData = await computeCreditsProduced(programmeCode, false, true)
counter++
if (counter % 5 === 0) {
console.log(`Done ${Math.round((counter / allProgrammeCodes.length) * 100)} %`)
}
diff(rapoProgrammeData[programmeCode], okProgrammeData.stats, programmeCode)
}
const fieldToSortBy = 'diff' // 'percentage'
const orderedDiffs = _.orderBy(diffs, fieldToSortBy, 'asc')
orderedDiffs.forEach(d => console.log(d.diffStr))
console.log(`${diffs.length} diffs found with current settings. Numbers with no diff: ${noDiffCounter}`)
const totalDiff = orderedDiffs.reduce((sum, cur) => cur.diff + sum, 0)
console.log('Total difference: ', totalDiff)
}
const programmeCreditsDiff = async fileName => {
console.log('Running studyprogramme credits diff')
await parseCsv(fileName, process)
console.log('Diff completed.')
}
/*
separate
Suoritus on erillisellä opiskeluoikeudella opettajankoulutuksen opintoja suorittavan opiskelijan suorittama
Suoritus on erillisellä opiskeluoikeudella opiskelevan suorittama
agreement
Suoritus on korkeakoulujen välisillä yhteistyösopimuksilla opiskelevan suorittama
open-uni
Suoritus on avoimessa korkeakouluopetuksessa suoritettu
incoming-exchange
Suoritus on tulevan kansainvälisen vaihto-opiskelijan suorittama
basic
perustutkinto-opiskelija = 1
ja
Tuntematon
tai (ehkä):
Suoritus on kotimainen harjoittelujakso
?? Opintosuoritus on täydennyskoulutuksessa suoritettu ??
other
perustutkinto-opiskelija = 0 && tuntematon
*/
const getCategory = (typeName, isBasic, hyvaksiluettu) => {
if (hyvaksiluettu) return 'transferred' // ?? unsure
if (isBasic || ['Suoritus on kotimainen harjoittelujakso', 'Tuntematon'].includes(typeName)) return 'basic'
if (typeName === 'Suoritus on avoimessa korkeakouluopetuksessa suoritettu') return 'open-uni'
if (typeName === 'Suoritus on tulevan kansainvälisen vaihto-opiskelijan suorittama') return 'incoming-exchange'
if (typeName.startsWith('Suoritus on erillisellä opiskeluoikeudella')) return 'separate'
if (typeName === 'Suoritus on korkeakoulujen välisillä yhteistyösopimuksilla opiskelevan suorittama')
return 'agreement'
return 'other'
}
const processIds = async (rawData, code, field) => {
const data = rawData.slice(0, 21628) // 11476 = KTM viimeinen
const rapoData = data
.map(row => ({
id: row[0].slice(11),
type: getCategory(row[1], row[3] === '1', row[6] === '1'),
typeCode: row[8],
isModule: row[9] !== '0',
credits: parseFloat(row[10]),
included: row[4] === '1',
}))
.filter(c => !c.isModule && (field === 'transferred' || c.included))
const rapoIds = rapoData.filter(c => c.credits > 0 && c.type === field).map(row => row.id)
const rapoCredits = rapoData.reduce(
(stats, cur) => {
stats[cur.type] += cur.credits
return stats
},
{
transferred: 0,
total: '_',
basic: 0,
'open-uni': 0,
separate: 0,
'incoming-exchange': 0,
agreement: 0,
other: 0,
}
)
const stats = await computeCreditsProduced(code, false, true, field)
if (!stats.stats.ids) {
// console.log(stats)
console.log({ rapoCredits })
console.log('Edit getCreditsProduced code so that it saves a list of the relevant ids you want to compare.')
return
}
const okIdMap = stats.stats.ids
const rapoIdMap = rapoIds.reduce((obj, cur) => {
obj[cur] = true
return obj
}, {})
const onlyInRapo = Object.keys(rapoIdMap).filter(id => !okIdMap[id])
const onlyInOk = Object.keys(okIdMap).filter(id => !rapoIdMap[id])
console.log('\n onlyInRapo: ', onlyInRapo.length)
console.dir(onlyInRapo.slice(0, 30))
console.log('\n onlyInOk: ', onlyInOk.length)
console.dir(onlyInOk.slice(0, 30))
delete stats.ids
console.log(stats.stats['2022'])
console.log('rapoCredits: ', rapoCredits)
}
const testNewCalc = async (fileName, code, field) => {
await parseCsv(fileName, async data => processIds(data, code, field))
}
module.exports = { programmeCreditsDiff, testNewCalc }
+4 -2
View File
@@ -10,7 +10,7 @@ const { programmeCreditsDiff, testNewCalc } = require('./creditsDiff')
const modes = {
credits: () => programmeCreditsDiff('data.csv'),
// Check credits with current local oodikone data against provided data.csv in rapodiff folder. data.csv can be downloaded from rapo by selecting all faculties, then from the bottom of the page clicking Export > data > csv.
check: code => testNewCalc(code),
check: (fileName, code, field) => testNewCalc(fileName, code, field),
// Provide programme code as argument. Parses credits from credits.csv in rapodiff folder. This must be obtained from Rapo people. This tells which credit ids are found in oodikone but not rapo, and vice versa.
// notice: check mode requires writing ids in to the stats object in credits calculating function.
// Add something like this there:
@@ -22,6 +22,8 @@ const main = async () => {
const mode = process.argv[2]
const modeFunction = modes[mode]
const arg = process.argv[3]
const arg2 = process.argv[4]
const arg3 = process.argv[5]
if (!modeFunction) {
console.log(
'Unknown or missing mode. Supply the mode as an argument. Supported modes:\n\n',
@@ -29,7 +31,7 @@ const main = async () => {
)
return
}
await modeFunction(arg)
await modeFunction(arg, arg2, arg3)
}
main().then(() => process.exit(0))
@@ -6,7 +6,7 @@ const _ = require('lodash')
const { optimizedStatisticsOf } = require('../services/populations')
const { Student, Transfer, StudyrightElement, Studyright } = require('../models')
const { rapoToken, tokenImporterApi, importerDbApiUrl, nodeproxyUrl } = require('./conf.js')
const { rapoToken, tokenImporterApi, importerDbApiUrl, nodeproxyUrl } = require('./conf')
const getFromRapo = async (urlStart, urlEnd) => {
console.log({ urlStart, urlEnd })
@@ -164,7 +164,7 @@ const programme_diff_year = async (programme, facultyCode, year, brief = false)
const onlyInRapo = _.difference(rapoStudents, oodikoneStudents)
const wrongMissingInRapo = []
for (let student of onlyInOodikone) {
for (const student of onlyInOodikone) {
const status = await isAcuallyActive(student)
if (status) {
wrongMissingInRapo.push(student)
@@ -173,7 +173,7 @@ const programme_diff_year = async (programme, facultyCode, year, brief = false)
// these are in wrong RAPO-population
const transferred = []
for (let student of onlyInRapo) {
for (const student of onlyInRapo) {
const isTransfer = await hasTransferred(student, programme)
if (isTransfer) {
transferred.push(student)
@@ -243,7 +243,7 @@ const facultyDiff = async (faculty, programmes, brief) => {
console.log('\n')
console.log(faculty)
console.log('==')
for (let programme of programmes) {
for (const programme of programmes) {
await programme_diff(programme, faculty, brief)
}
}
@@ -277,16 +277,16 @@ const msc_programme_diff_year = async (programme, facultyCode, year, brief = fal
const onlyInRapo = _.difference(rapoStudents, oodikoneStudents)
const wrongInRapo = []
for (let student of onlyInOodikone) {
for (const student of onlyInOodikone) {
const status = await isAcuallyActive(student)
if (status) {
//wrongInRapo.push(student)
// wrongInRapo.push(student)
}
}
// in wrong RAPO population
const transferred = []
for (let student of onlyInRapo) {
for (const student of onlyInRapo) {
const status = await hasTransferred(student, programme)
if (status) {
transferred.push(student)
+6 -2
View File
@@ -1,11 +1,13 @@
const Sentry = require('@sentry/node')
const express = require('express')
const cors = require('cors')
const { frontUrl } = require('./conf-backend')
const shibbolethCharsetMiddleware = require('./middleware/shibbolethCharsetMiddleware')
const currentUserMiddleware = require('./middleware/currentUserMiddleware')
const accessLogger = require('./middleware/accessLogger')
const auth = require('./middleware/auth')
const errorMiddleware = require('./middleware/errorMiddleware')
const changelog = require('./routes/changelog')
const courses = require('./routes/courses')
const students = require('./routes/students')
@@ -30,9 +32,10 @@ const studyGuidanceGroups = require('./routes/studyGuidanceGroups')
const studyProgramme = require('./routes/studyProgramme')
const customOpenUniSearch = require('./routes/customOpenUniSearch')
const studyProgrammeCriteria = require('./routes/studyProgrammeCriteria')
const initializeSentry = require('./util/sentry')
const completedCoursesSearch = require('./routes/completedCoursesSearch')
const errorMiddleware = require('./middleware/errorMiddleware')
const closeToGraduation = require('./routes/closeToGraduation')
const initializeSentry = require('./util/sentry')
module.exports = (app, url) => {
initializeSentry(app)
app.use(Sentry.Handlers.requestHandler())
@@ -69,6 +72,7 @@ module.exports = (app, url) => {
app.use(`${url}/feedback`, feedback)
app.use(`${url}/custom-population-search`, customPopulationSearch)
app.use(`${url}/studyguidancegroups`, auth.roles(['studyGuidanceGroups']), studyGuidanceGroups)
app.use(`${url}/close-to-graduation`, auth.roles(['admin']), closeToGraduation)
app.get('*', async (_, res) => {
const results = { error: 'unknown endpoint' }
res.status(404).json(results)
+4 -2
View File
@@ -1,14 +1,16 @@
const router = require('express').Router()
const axios = require('axios')
const { isDev } = require('../conf-backend')
const changelog = {}
router.get('/', async (req, res) => {
router.get('/', async (_req, res) => {
if (changelog?.data) return res.status(200).send(changelog.data)
if (isDev)
return res.status(200).json([
{
description: "### Fake release\nLet's not spam github api in development!",
description: "### Fake release\nLet's not spam the GitHub API in development!",
title: 'Fake title for fake release',
time: new Date(),
},
@@ -0,0 +1,10 @@
const { getCloseToGraduationData } = require('../services/populations/closeToGraduation')
const router = require('express').Router()
router.get('/', async (req, res) => {
const result = await getCloseToGraduationData()
res.json(result)
})
module.exports = router
@@ -1,6 +1,7 @@
const router = require('express').Router()
const { getCompletedCourses } = require('../services/completedCoursesSearch')
const { getImporterClient } = require('../util/importerClient')
const importerClient = getImporterClient()
const _ = require('lodash')
const {
@@ -27,7 +28,7 @@ router.get('/', async (req, res) => {
// have enrolled to their courses in last 8 months
// (acual logic a bit different, see importer)
const teacherRightsToStudents = await Promise.race([
importerClient.post(`/teacher-rights/`, { teacherId, studentNumbers }),
importerClient.post('/teacher-rights/', { teacherId, studentNumbers }),
answerTimeout,
])
if (teacherRightsToStudents && Array.isArray(teacherRightsToStudents.data))
+5 -3
View File
@@ -3,6 +3,7 @@ const crypto = require('crypto')
const courseService = require('../services/courses')
const { faculties } = require('../services/organisations')
const { validateParamLength } = require('../util')
const { getFullStudyProgrammeRights } = require('../util/utils')
router.get('/v2/coursesmulti', async (req, res) => {
let results = { courses: [] }
@@ -34,14 +35,15 @@ router.get('/v2/coursesmulti', async (req, res) => {
router.get('/v3/courseyearlystats', async (req, res) => {
const {
user: { rights, roles },
user: { roles, programmeRights },
} = req
const allowedRoles = roles && ['admin', 'courseStatistics'].find(role => roles.includes(role))
const fullStudyProgrammeRights = getFullStudyProgrammeRights(programmeRights)
// If user has rights to see at least one programme, then they are allowed
// to see all of them
if (!allowedRoles && rights.length < 1) {
if (!allowedRoles && fullStudyProgrammeRights.length === 0) {
return res.status(403).json({ error: 'No programmes so no access to course stats' })
}
@@ -54,7 +56,7 @@ router.get('/v3/courseyearlystats', async (req, res) => {
} else {
// Studentnumbers should be obfuscated to all other users except admins
// and users with rights to any specific study programmes
const anonymize = allowedRoles !== 'admin' && rights.length < 1
const anonymize = allowedRoles !== 'admin' && fullStudyProgrammeRights.length === 0
const anonymizationSalt = anonymize ? crypto.randomBytes(12).toString('hex') : null
const results = await courseService.courseYearlyStats(codes, separate, anonymizationSalt, combineSubstitutions)
res.json(results)
@@ -51,7 +51,7 @@ router.put('/searches/:id', async (req, res) => {
if (!id || !userId) return res.status(422).end()
const updatedSearch = await updateSearch(userId, id, courseCodes)
if (!updatedSearch) return res.status(404).json({ error: `Open uni search could not be found` })
if (!updatedSearch) return res.status(404).json({ error: 'Open uni search could not be found' })
return res.json({
id: updatedSearch.id,
userId: updatedSearch.userId,
@@ -67,7 +67,7 @@ router.delete('/searches/:id', async (req, res) => {
if (!id || !userId) return res.status(422).end()
const deletedSearch = await deleteSearch(userId, id)
if (!deletedSearch) return res.status(404).json({ error: `Open uni search could not be found` })
if (!deletedSearch) return res.status(404).json({ error: 'Open uni search could not be found' })
return res.json(id)
})
+2 -2
View File
@@ -42,8 +42,8 @@ router.get('/:id/basicstats', async (req, res) => {
const wantedProgrammes = await getProgrammes(code, programmeFilter)
if (!wantedProgrammes) return res.status(422).end()
//all programmes are required for correct sorting of transfers
let allProgrammeCodes = []
// all programmes are required for correct sorting of transfers
const allProgrammeCodes = []
if (programmeFilter === 'NEW_STUDY_PROGRAMMES') {
const allProgs = await getProgrammes(code, 'ALL_PROGRAMMES')
allProgs?.data.forEach(prog => allProgrammeCodes.push(prog.code))
+22 -17
View File
@@ -7,7 +7,7 @@ const studentService = require('../services/students')
const StudyrightService = require('../services/studyrights')
const CourseService = require('../services/courses')
const StatMergeService = require('../services/statMerger')
const { mapToProviders } = require('../util/utils')
const { mapToProviders, getFullStudyProgrammeRights } = require('../util/utils')
const { encrypt, decrypt } = require('../services/encrypt')
const { mapCodesToIds } = require('../services/studyprogramme/studyprogrammeHelpers')
@@ -148,7 +148,9 @@ router.post('/v2/populationstatistics/coursesbystudentnumberlist', async (req, r
router.get('/v3/populationstatistics', async (req, res) => {
const { year, semesters, studyRights: studyRightsJSON } = req.query
const { user } = req
const {
user: { isAdmin, programmeRights, id: userId },
} = req
if (!year || !semesters || !studyRightsJSON) {
res.status(400).json({ error: 'The query should have a year, semester and studyRights defined' })
@@ -156,17 +158,15 @@ router.get('/v3/populationstatistics', async (req, res) => {
}
let studyRights = null
studyRights = JSON.parse(studyRightsJSON)
const {
user: { rights, iamRights, isAdmin },
} = req
const fullProgrammeRights = getFullStudyProgrammeRights(programmeRights)
const programmeRightsCodes = programmeRights.map(({ code }) => code)
try {
if (
!isAdmin &&
!rights.includes(studyRights.programme) &&
!rights.includes(studyRights.combinedProgramme) &&
!iamRights.includes(studyRights.programme) &&
!iamRights.includes(studyRights.combinedProgramme)
!programmeRightsCodes.includes(studyRights.programme) &&
!programmeRightsCodes.includes(studyRights.combinedProgramme)
) {
res.status(403).json([])
return
@@ -207,7 +207,7 @@ router.get('/v3/populationstatistics', async (req, res) => {
return
}
res.json(filterPersonalTags(result, user.id))
res.json(filterPersonalTags(result, userId))
} else {
const result = await populationService.optimizedStatisticsOf({
...req.query,
@@ -220,8 +220,12 @@ router.get('/v3/populationstatistics', async (req, res) => {
return
}
// Obfuscate if user has only iam rights
if (!isAdmin && !rights.includes(studyRights.programme) && !rights.includes(studyRights.combinedProgramme)) {
// Obfuscate if user has only limited study programme rights
if (
!isAdmin &&
!fullProgrammeRights.includes(studyRights.programme) &&
!fullProgrammeRights.includes(studyRights.combinedProgramme)
) {
result.students = result.students.map(student => {
const { iv, encryptedData } = encrypt(student.studentNumber)
const obfuscatedBirthDate = new Date(new Date(student.birthdate).setMonth(0, 0)) // only birth year for age distribution
@@ -242,14 +246,14 @@ router.get('/v3/populationstatistics', async (req, res) => {
})
}
res.json(filterPersonalTags(result, user.id))
res.json(filterPersonalTags(result, userId))
}
})
router.get('/v3/populationstatisticsbycourse', async (req, res) => {
const { coursecodes, from, to, separate: sep, unifyCourses } = req.query
const {
user: { id, isAdmin, studentsUserCanAccess: allStudentsUserCanAccess, rights },
user: { id, isAdmin, studentsUserCanAccess: allStudentsUserCanAccess, programmeRights },
} = req
const separate = sep ? JSON.parse(sep) : false
@@ -288,7 +292,8 @@ router.get('/v3/populationstatisticsbycourse', async (req, res) => {
if (!isAdmin) {
courseproviders = await CourseService.getCourseProvidersForCourses(JSON.parse(coursecodes))
}
const rightsMappedToProviders = mapToProviders(rights)
const fullStudyProgrammeRights = getFullStudyProgrammeRights(programmeRights)
const rightsMappedToProviders = mapToProviders(fullStudyProgrammeRights)
const found = courseproviders.some(r => rightsMappedToProviders.includes(r))
const studentsUserCanAccess =
@@ -356,14 +361,14 @@ router.post('/v3/populationstatisticsbystudentnumbers', async (req, res) => {
router.get('/v3/populationstatistics/studyprogrammes', async (req, res) => {
const {
user: { rights, iamRights, roles },
user: { roles, programmeRights },
} = req
if (roles?.includes('admin')) {
const studyrights = await StudyrightService.getAssociations()
mapCodesToIds(studyrights.programmes)
res.json(studyrights)
} else {
const allRights = rights.concat(iamRights)
const allRights = _.uniq(programmeRights.map(p => p.code))
// For combined programme
// If more programmes are combined, then a function might be a better idea to add moar rights
if (allRights.includes('KH90_001') || allRights.includes('MH90_001')) allRights.push('KH90_001', 'MH90_001')
@@ -23,7 +23,7 @@ const { ElementDetail } = require('../models')
// For grafana statistics (idea stolen from Norppa)
const logInfoForGrafana = async (code, combinedProgramme) => {
const programme = await ElementDetail.findOne({ where: { code: code } })
const programme = await ElementDetail.findOne({ where: { code } })
const programmeCode = combinedProgramme ? `${programme.code}-${combinedProgramme}` : programme.code
logger.info('Study Programme', {
studyprogrammeName: combinedProgramme ? `${programme.name.fi} + maisteri` : programme.name.fi,
@@ -59,7 +59,7 @@ router.get('/v2/studyprogrammes/creditstats', async (req, res) => {
const codes = JSON.parse(codesListString)
const stats = {}
for (const code of codes) {
stats[code] = await getCreditsProduced(code, isAcademicYear !== 'false', includeSpecials)
stats[code] = await getCreditsProduced(code, isAcademicYear !== 'false', includeSpecials !== 'false')
}
return res.json({ stats })
})
@@ -150,9 +150,8 @@ router.get('/v2/studyprogrammes/:id/update_basicview', async (req, res) => {
logger.error({ message: `Failed to update code ${code} ${combinedProgramme} basic stats`, meta: `${e}` })
}
return res.json(result)
} else {
res.status(422).end()
}
res.status(422).end()
})
router.get('/v2/studyprogrammes/:id/update_studytrackview', async (req, res) => {
@@ -167,9 +166,8 @@ router.get('/v2/studyprogrammes/:id/update_studytrackview', async (req, res) =>
logger.error({ message: `Failed to update code ${code} ${combinedProgramme} studytrack stats`, meta: `${e}` })
}
return res.json(result)
} else {
res.status(422).end()
}
res.status(422).end()
})
router.get('/v2/studyprogrammes/:id/evaluationstats', async (req, res) => {
+43 -27
View File
@@ -3,6 +3,7 @@ const Tags = require('../services/tags')
const TagStudent = require('../services/tagstudent')
const Students = require('../services/students')
const { difference } = require('lodash')
const { getFullStudyProgrammeRights } = require('../util/utils')
const filterRelevantTags = (tags, userId) => {
return tags.filter(tag => !tag.personal_user_id || tag.personal_user_id === userId)
@@ -12,6 +13,11 @@ const filterRelevantStudentTags = (studentTags, userId) => {
return studentTags.filter(({ tag }) => !tag.personal_user_id || tag.personal_user_id === userId)
}
const userIsUnauthorized = (programmeRights, programmeCodes, roles) =>
!programmeRights.includes(programmeCodes[0]) &&
!programmeRights.includes(programmeCodes[1]) &&
!roles?.includes('admin')
router.get('/tags', async (_req, res) => {
const tags = await Tags.findTags()
res.status(200).json(tags)
@@ -20,13 +26,14 @@ router.get('/tags', async (_req, res) => {
router.get('/tags/:studytrack', async (req, res) => {
const { studytrack } = req.params
const {
user: { rights, roles, id },
user: { roles, id, programmeRights },
} = req
const fullStudyProgrammeRights = getFullStudyProgrammeRights(programmeRights)
const programmeCodes = studytrack.includes('KH') && studytrack.includes('MH') ? studytrack.split('-') : [studytrack]
// Respond with null and 200 instead of 403 if the user isn't authorized to view the tags. This is to avoid unnecessary noise in Sentry
if (!rights.includes(programmeCodes[0]) && !rights.includes(programmeCodes[1]) && !roles?.includes('admin'))
return res.json(null)
if (userIsUnauthorized(fullStudyProgrammeRights, programmeCodes, roles)) return res.json(null)
const tags = await Tags.findTagsByStudytrack(studytrack)
res.status(200).json(filterRelevantTags(tags, id))
@@ -37,12 +44,12 @@ router.post('/tags', async (req, res) => {
tag: { studytrack, tagname, year, personal_user_id },
} = req.body
const {
user: { rights, roles, id },
user: { roles, id, programmeRights },
} = req
const fullStudyProgrammeRights = getFullStudyProgrammeRights(programmeRights)
const programmeCodes = studytrack.includes('KH') && studytrack.includes('MH') ? studytrack.split('-') : [studytrack]
if (!rights.includes(programmeCodes[0]) && !rights.includes(programmeCodes[1]) && !roles?.includes('admin'))
return res.status(403).end()
if (userIsUnauthorized(fullStudyProgrammeRights, programmeCodes, roles)) return res.status(403).end()
await Tags.createNewTag({ studytrack, tagname, year, personal_user_id })
const tags = await Tags.findTagsByStudytrack(studytrack)
@@ -52,18 +59,15 @@ router.post('/tags', async (req, res) => {
router.delete('/tags', async (req, res) => {
const { tag } = req.body
const {
user: { rights, roles, id },
user: { roles, id, programmeRights },
} = req
const fullStudyProgrammeRights = getFullStudyProgrammeRights(programmeRights)
const programmeCodes =
tag.studytrack.includes('KH') && tag.studytrack.includes('MH') ? tag.studytrack.split('-') : [tag.studytrack]
if (
!rights.includes(programmeCodes[0]) &&
!rights.includes(programmeCodes[1]) &&
!roles?.includes('admin') &&
!(tag.personal_user_id === id)
)
if (userIsUnauthorized(fullStudyProgrammeRights, programmeCodes, roles) && !(tag.personal_user_id === id))
return res.status(403).end()
await Tags.deleteTag(tag)
@@ -79,13 +83,19 @@ router.get('/studenttags', async (req, res) => {
router.get('/studenttags/:studytrack', async (req, res) => {
const { studytrack } = req.params
const {
user: { rights, roles, id },
user: { roles, id, programmeRights },
} = req
const fullStudyProgrammeRights = getFullStudyProgrammeRights(programmeRights)
const programmeCodes = studytrack.includes('KH') && studytrack.includes('MH') ? studytrack.split('-') : [studytrack]
// Respond with null and 200 instead of 403 if the user isn't authorized to view the tags. This is to avoid unnecessary noise in Sentry
if (!rights.includes(programmeCodes[0]) && !rights.includes(programmeCodes[1]) && !roles?.includes('admin'))
if (
!fullStudyProgrammeRights.includes(programmeCodes[0]) &&
!fullStudyProgrammeRights.includes(programmeCodes[1]) &&
!roles?.includes('admin')
)
return res.json(null)
const result = await TagStudent.getStudentTagsByStudytrack(studytrack)
@@ -102,11 +112,12 @@ router.get('/studenttags/:studentnumber', async (req, res) => {
router.post('/studenttags/:studentnumber', async (req, res) => {
const { tag, studytrack, combinedProgramme } = req.body
const {
user: { roles, rights, id },
user: { roles, id, programmeRights },
} = req
if (!rights.includes(studytrack) && !rights.includes(combinedProgramme) && !roles?.includes('admin'))
return res.status(403).end()
const fullStudyProgrammeRights = getFullStudyProgrammeRights(programmeRights)
if (userIsUnauthorized(fullStudyProgrammeRights, [studytrack, combinedProgramme], roles)) return res.status(403).end()
await TagStudent.createStudentTag(tag)
const studytrackCode = combinedProgramme ? `${studytrack}-${combinedProgramme}` : studytrack
@@ -117,11 +128,12 @@ router.post('/studenttags/:studentnumber', async (req, res) => {
router.post('/studenttags', async (req, res) => {
const { tags, studytrack, combinedProgramme } = req.body
const {
user: { rights, roles, id },
user: { roles, id, programmeRights },
} = req
if (!rights.includes(studytrack) && !rights.includes(combinedProgramme) && !roles?.includes('admin'))
return res.status(403).end()
const fullStudyProgrammeRights = getFullStudyProgrammeRights(programmeRights)
if (userIsUnauthorized(fullStudyProgrammeRights, [studytrack, combinedProgramme], roles)) return res.status(403).end()
const studytrackCode = combinedProgramme ? `${studytrack}-${combinedProgramme}` : studytrack
const existingTags = await Tags.findTagsByStudytrack(studytrackCode)
@@ -146,11 +158,13 @@ router.post('/studenttags', async (req, res) => {
router.delete('/studenttags/delete_one', async (req, res) => {
const { tag_id, studentnumber, studytrack, combinedProgramme } = req.body
const {
user: { rights, roles, id },
user: { roles, id, programmeRights },
} = req
if (!rights.includes(studytrack) && !rights.includes(combinedProgramme) && !roles?.includes('admin'))
return res.status(403).end()
const fullStudyProgrammeRights = getFullStudyProgrammeRights(programmeRights)
if (userIsUnauthorized(fullStudyProgrammeRights, [studytrack, combinedProgramme], roles)) return res.status(403).end()
const studytrackCode = combinedProgramme ? `${studytrack}-${combinedProgramme}` : studytrack
const tags = await Tags.findTagsFromStudytrackById(studytrackCode, [tag_id])
if (tags.length === 0) return res.status(403).json({ error: 'The tag does not exist' })
@@ -163,11 +177,13 @@ router.delete('/studenttags/delete_one', async (req, res) => {
router.delete('/studenttags/delete_many', async (req, res) => {
const { tagId, studentnumbers, studytrack, combinedProgramme } = req.body
const {
user: { rights, roles, id },
user: { roles, id, programmeRights },
} = req
if (!rights.includes(studytrack) && !rights.includes(combinedProgramme) && !roles?.includes('admin'))
return res.status(403).end()
const fullStudyProgrammeRights = getFullStudyProgrammeRights(programmeRights)
if (userIsUnauthorized(fullStudyProgrammeRights, [studytrack, combinedProgramme], roles)) return res.status(403).end()
const studytrackCode = combinedProgramme ? `${studytrack}-${combinedProgramme}` : studytrack
const tags = await Tags.findTagsFromStudytrackById(studytrackCode, [tagId])
if (tags.length === 0) return res.status(403).json({ error: 'The tag does not exist' })
+5 -3
View File
@@ -2,7 +2,7 @@ const teachers = require('../services/teachers')
const topteachers = require('../services/topteachers')
const { isFaculty, providersOfFaculty } = require('../services/organisations')
const router = require('express').Router()
const { mapToProviders } = require('../util/utils')
const { mapToProviders, getFullStudyProgrammeRights } = require('../util/utils')
router.get('/', async (req, res) => {
const { searchTerm } = req.query
@@ -43,14 +43,16 @@ router.get('/top/categories', async (req, res) => {
router.get('/stats', async (req, res) => {
const {
user: { rights, roles },
user: { roles, programmeRights },
} = req
const fullStudyProgrammeRights = getFullStudyProgrammeRights(programmeRights)
const { providers, semesterStart, semesterEnd } = req.query
if (!providers || !semesterStart) {
return res.status(422).send('Missing required query parameters.')
}
const providerRights = mapToProviders(rights)
const providerRights = mapToProviders(fullStudyProgrammeRights)
if (!(providers.every(p => providerRights.includes(p)) || roles.includes('admin'))) {
return res.status(403).send('You do not have rights to see this data')
+9 -5
View File
@@ -3,10 +3,11 @@ const { getFacultyList } = require('../services/faculty/facultyHelpers')
const { getGraduationStats, getFacultyProgressStats } = require('../services/faculty/facultyService')
const { getMedian } = require('../services/studyprogramme/studyprogrammeHelpers')
const degreeNames = ['bachelor', 'bachelorMaster', 'master', 'licentiate', 'doctor']
router.get('/allprogressstats', async (req, res) => {
const specialGroups = 'SPECIAL_EXCLUDED'
const specialGroups = req.query?.specialsIncluded === 'true' ? 'SPECIAL_INCLUDED' : 'SPECIAL_EXCLUDED'
const graduated = req.query?.graduated
const allFaculties = await getFacultyList()
const facultyCodes = allFaculties.map(f => f.code)
@@ -15,7 +16,7 @@ router.get('/allprogressstats', async (req, res) => {
for (const facultyCode of facultyCodes) {
const data = await getFacultyProgressStats(facultyCode, specialGroups, graduated)
if (!data) {
return res.status(500).end()
return res.status(404).send('Data missing from server: Refreshing faculty data required')
}
codeToData[facultyCode] = data
}
@@ -23,6 +24,9 @@ router.get('/allprogressstats', async (req, res) => {
const universityData = {
years: codeToData.H50.years,
yearlyBachelorTitles: codeToData.H50.yearlyBachelorTitles,
yearlyBcMsTitles: codeToData.H50.yearlyBcMsTitles,
yearlyMasterTitles: codeToData.H50.yearlyMasterTitles,
yearlyLicentiateTitles: codeToData.H50.yearlyLicentiateTitles,
programmeNames: allFaculties.reduce((obj, fac) => {
const { name, ...rest } = fac.dataValues
obj[fac.code] = { ...rest, ...name }
@@ -66,8 +70,8 @@ router.get('/allprogressstats', async (req, res) => {
'mastersProgStats',
'doctoralProgStats',
]) {
if (Object.keys(facultyData[fieldName]).length === 0) continue
if (!universityData[fieldName]) universityData[fieldName] = {}
if (Object.keys(facultyData[fieldName]).length === 0) continue
universityData[fieldName][facultyCode] = unifyProgressStats(Object.values(facultyData[fieldName]))
}
}
@@ -75,7 +79,7 @@ router.get('/allprogressstats', async (req, res) => {
// Remove ELL bachelor+master progresstats for now, because it is problematic due to different credit limits
// when expanding the table rows to show 'programme'(faculty) specific progress bars
delete universityData.bcMsProgStats['H90']
delete universityData.bcMsProgStats.H90
return res.status(200).json(universityData)
})
@@ -176,7 +180,7 @@ router.get('/allgraduationstats', async (req, res) => {
})
}
}
// eslint-disable-next-line no-unused-vars
const { programmes, ...rest } = data.classSizes
universityData.classSizes.programmes[facultyCode] = rest
}
+7 -1
View File
@@ -8,6 +8,7 @@ const {
updateStudentsByStudentNumber,
abort,
updateCoursesByCourseCode,
updateStudentsIndividually,
} = require('../services/sisUpdaterService')
const logger = require('../util/logger')
@@ -37,8 +38,13 @@ router.get('/update/v2/students', async (req, res) => {
}
})
router.get('/update/v2/students_individually', async (req, res) => {
await updateStudentsIndividually()
res.status(200).send()
})
router.post('/update/v2/customlist/:type', async (req, res) => {
const type = req.params.type
const { type } = req.params
const list = req.body
const typeToJob = {
students: updateStudentsByStudentNumber,
-7
View File
@@ -65,11 +65,4 @@ router.delete('/:uid/elements', async (req, res) => {
res.json(user)
})
router.post('/:uid/faculties', async (req, res) => {
const { uid } = req.params
const { faculties } = req.body
const user = await userService.setFaculties(uid, faculties)
res.json(user)
})
module.exports = router
@@ -1,4 +1,4 @@
const { redisClient } = require('../services/redis')
const { redisClient } = require('./redis')
const moment = require('moment')
// Only new bachelor, masters and doctoral programmes get their data updated in redis every night, use redis for them
@@ -14,7 +14,7 @@ const createRedisKeyForStudytrackStats = (id, graduated, specialGroups) =>
const getBasicStats = async (id, combinedProgramme, yearType, specialGroups) => {
if (!isUpdatedNewProgramme(id)) return null
let searchkey = combinedProgramme ? `${id}-${combinedProgramme}` : id
const searchkey = combinedProgramme ? `${id}-${combinedProgramme}` : id
const redisKey = createRedisKeyForBasicStats(searchkey, yearType, specialGroups)
const dataFromRedis = await redisClient.getAsync(redisKey)
if (!dataFromRedis) return null
@@ -66,7 +66,7 @@ const setCreditStats = async (data, isAcademicYear, specialGroups) => {
const getGraduationStats = async (id, combinedProgramme, yearType, specialGroups) => {
if (!isUpdatedNewProgramme(id)) return null
let searchkey = combinedProgramme ? `${id}-${combinedProgramme}` : id
const searchkey = combinedProgramme ? `${id}-${combinedProgramme}` : id
const redisKey = createRedisKeyForGraduationStats(searchkey, yearType, specialGroups)
const dataFromRedis = await redisClient.getAsync(redisKey)
if (!dataFromRedis) return null
@@ -89,7 +89,7 @@ const setGraduationStats = async (data, yearType, specialGroups) => {
const getStudytrackStats = async (id, combinedProgramme, graduated, specialGroups) => {
if (!isUpdatedNewProgramme(id)) return null
let searchkey = combinedProgramme ? `${id}-${combinedProgramme}` : id
const searchkey = combinedProgramme ? `${id}-${combinedProgramme}` : id
const redisKey = createRedisKeyForStudytrackStats(searchkey, graduated, specialGroups)
const dataFromRedis = await redisClient.getAsync(redisKey)
if (!dataFromRedis) return null
@@ -11,7 +11,7 @@ const getCompletedCourses = async (studentNumbers, courseCodes) => {
},
})
let fullCourseCodes = [
const fullCourseCodes = [
...courseCodes,
...new Set([...courses.reduce((acc, course) => [...acc, course.code, ...course.substitutions], [])]),
]
@@ -63,7 +63,7 @@ const getCompletedCourses = async (studentNumbers, courseCodes) => {
? null
: courses.find(course => course.substitutions.includes(course_code))?.code
return {
courseCode: originalCode ? originalCode : course_code,
courseCode: originalCode || course_code,
substitution: originalCode ? course_code : null,
studentNumber: student_studentnumber,
creditType: credittypecode,
@@ -74,7 +74,7 @@ const getCompletedCourses = async (studentNumbers, courseCodes) => {
enrollments = enrollments.map(enrollment => {
const originalCode = courses.find(course => course.substitutions.includes(enrollment.course_code))?.code
return {
courseCode: originalCode ? originalCode : enrollment.course_code,
courseCode: originalCode || enrollment.course_code,
substitution: originalCode ? enrollment.course_code : null,
studentNumber: enrollment.studentnumber,
date: enrollment.enrollment_date_time,
@@ -100,7 +100,8 @@ const getCompletedCourses = async (studentNumbers, courseCodes) => {
const previous = studentCredits[credit.studentNumber].credits?.find(c => credit.courseCode === c.courseCode)
if (previous && previous.date > credit.date) {
return
} else if (previous) {
}
if (previous) {
studentCredits[credit.studentNumber].credits = studentCredits[credit.studentNumber].credits.filter(
c => credit.courseCode !== c.courseCode
)
@@ -148,6 +149,8 @@ const getCompletedCourses = async (studentNumbers, courseCodes) => {
students.forEach(student => {
courseCodes.forEach(courseCode => {
// TODO: Fix this, sort function always takes two parameters
// eslint-disable-next-line prefer-destructuring
student.enrollments[courseCode] = student.allEnrollments
.filter(e => e.courseCode === courseCode || e.substitution === courseCode)
.sort(e => new Date(e.date).getDate())[0]
@@ -158,7 +161,6 @@ const getCompletedCourses = async (studentNumbers, courseCodes) => {
// the user does not have rights to see all enrollments.
return {
students: students.map(student => {
// eslint-disable-next-line no-unused-vars
const { allEnrollments, ...rest } = student
return { ...rest }
}),
@@ -49,6 +49,7 @@ class CourseStatsCounter {
this.grades = {}
}
// eslint-disable-next-line class-methods-use-this
initializePassingSemesters(initialValue = 0) {
const passingSemesters = {
BEFORE: initialValue,
@@ -63,7 +64,7 @@ class CourseStatsCounter {
}
markAttempt() {
this.stats.attempts = this.stats.attempts + 1
this.stats.attempts += 1
}
markToAll(studentnumber) {
@@ -164,12 +165,12 @@ class CourseStatsCounter {
}
getPassingSemestersCumulative() {
const passingSemesters = this.stats.passingSemesters
const { passingSemesters } = this.stats
const attemptStats = {
BEFORE: passingSemesters['BEFORE'],
BEFORE: passingSemesters.BEFORE,
}
attemptStats['0-FALL'] = passingSemesters['BEFORE'] + passingSemesters['0-FALL']
attemptStats['0-FALL'] = passingSemesters.BEFORE + passingSemesters['0-FALL']
attemptStats['0-SPRING'] = attemptStats['0-FALL'] + passingSemesters['0-SPRING']
for (let i = 1; i < 7; i++) {
@@ -177,22 +178,21 @@ class CourseStatsCounter {
attemptStats[spring[i]] = attemptStats[fall[i]] + passingSemesters[spring[i]]
}
attemptStats['LATER'] = attemptStats['6-SPRING'] + passingSemesters['LATER']
attemptStats.LATER = attemptStats['6-SPRING'] + passingSemesters.LATER
return attemptStats
}
getFinalStats() {
const stats = this.stats
const students = this.students
const { stats, students } = this
const studs = this.enrollments
? Object.keys(this.enrollments)
.filter(key => key !== 'ENROLLED' && key !== 'semesters')
.reduce((acc, key) => [...acc, ...[...this.enrollments[key]].map(studentnumber => studentnumber)], [])
: []
const allStudents = Object.keys(students.all).map(student => student)
const fileredEnrolledNoGrade = this.enrollments['ENROLLED']
? [...this.enrollments['ENROLLED']]
const fileredEnrolledNoGrade = this.enrollments.ENROLLED
? [...this.enrollments.ENROLLED]
.filter(student => !studs.includes(student) && !allStudents.includes(student))
.reduce((acc, student) => ({ ...acc, [student]: true }), {})
: {}
@@ -199,20 +199,16 @@ class CourseYearlyStatsCounter {
category: 'passedFirst',
code: groupcode,
})
} else {
if (student.category == 'passedFirst') {
this.students.set(studentnumber, {
earliestAttainment: attainment_date,
category: 'passedEventually',
code: groupcode,
})
}
} else if (student.category === 'passedFirst') {
this.students.set(studentnumber, {
earliestAttainment: attainment_date,
category: 'passedEventually',
code: groupcode,
})
}
} else {
if (student.category === 'neverPassed') {
if (passed) {
this.students.set(studentnumber, { ...student, category: 'passedEventually' })
}
} else if (student.category === 'neverPassed') {
if (passed) {
this.students.set(studentnumber, { ...student, category: 'passedEventually' })
}
}
}
@@ -258,7 +254,8 @@ class CourseYearlyStatsCounter {
// totals cannot be calculated
this.obfuscated = true
let gradeSpread = {}
const gradeSpread = {}
// eslint-disable-next-line guard-for-in
for (const grade in normalStats.attempts.grades) {
gradeSpread[grade] = []
}
@@ -1,9 +1,8 @@
const Sequelize = require('sequelize')
const { Op } = require('sequelize')
const { Credit, Course, Organization, Enrollment } = require('../../models')
const { parseCredit } = require('./parseCredits')
const { parseEnrollment } = require('./parseEnrollments')
const Op = Sequelize.Op
const { CourseYearlyStatsCounter } = require('./course_yearly_stats_counter')
const { sortMainCode } = require('../../util/utils')
const {
@@ -23,7 +22,7 @@ const unifyOpenUniversity = code => {
const allCodeAlternatives = async code => {
const course = await Course.findOne({
where: { code: code },
where: { code },
})
return sortMainCode([...course.substitutions, code])
@@ -62,7 +61,7 @@ const yearlyStatsOfNew = async (
const counter = new CourseYearlyStatsCounter()
for (let credit of credits) {
for (const credit of credits) {
const {
studentnumber,
grade,
@@ -1,6 +1,5 @@
const Sequelize = require('sequelize')
const { Op } = require('sequelize')
const { ExcludedCourse } = require('../models/models_kone')
const { Op } = Sequelize
const addExcludedCourses = async (programmecode, coursecodes, curriculum) => {
return ExcludedCourse.bulkCreate(
@@ -1,5 +1,4 @@
const Sequelize = require('sequelize')
const { Op } = Sequelize
const { Op } = require('sequelize')
const {
ElementDetail,
Organization,
@@ -22,26 +21,11 @@ const {
mapCodesToIds,
facultyProgrammeStudents,
} = require('./facultyHelpers')
const { uniqBy } = require('lodash')
const {
dbConnections: { sequelize },
} = require('../../database/connection')
const getTransferredToAndAway = async (programmeCodes, allProgrammeCodes, since) => {
const awayTransfers = await transferredAway(programmeCodes, allProgrammeCodes, since)
const toTransfers = await transferredTo(programmeCodes, allProgrammeCodes, since)
return [...toTransfers, ...awayTransfers]
}
const getTransferredInside = async (programmeCodes, allProgrammeCodes, since) => {
return await transferredInsideFaculty(programmeCodes, allProgrammeCodes, since)
}
const getTransfersOut = async (programmeCode, start, end) => {
return await transferredFaculty([], [programmeCode], start, end)
}
const getTransfersIn = async (programmeCode, start, end) => {
return await transferredFaculty([programmeCode], [], start, end)
}
const transferredFaculty = async (programmeCodeIn, programmeCodeOut, start, end) =>
(
await Transfer.findAll({
@@ -69,7 +53,7 @@ const startedStudyrights = async (faculty, code, since, studyRightWhere) =>
model: StudyrightElement,
required: true,
where: {
code: code,
code,
},
include: {
model: ElementDetail,
@@ -95,7 +79,7 @@ const graduatedStudyrights = async (faculty, code, since, studyrightWhere) =>
model: StudyrightElement,
required: true,
where: {
code: code,
code,
},
include: {
model: ElementDetail,
@@ -122,7 +106,7 @@ const studyrightsByRightStartYear = async (faculty, code, since, graduated = 1)
model: StudyrightElement,
attributes: ['code', 'startdate'],
where: {
code: code,
code,
},
required: true,
include: {
@@ -136,7 +120,7 @@ const studyrightsByRightStartYear = async (faculty, code, since, graduated = 1)
startdate: {
[Op.gte]: since,
},
graduated: graduated,
graduated,
},
})
).map(facultyFormatStudyright)
@@ -149,7 +133,7 @@ const getStudyRightsByExtent = async (faculty, startDate, endDate, code, extents
attributes: [],
required: true,
where: {
code: code,
code,
},
include: {
model: ElementDetail,
@@ -191,7 +175,7 @@ const getStudyRightsByBachelorStart = async (faculty, startDate, endDate, code,
model: StudyrightElement,
required: true,
where: {
code: code,
code,
},
},
where: {
@@ -278,20 +262,25 @@ const transferredTo = async (programmes, allProgrammeCodes, since) =>
).map(formatFacultyTransfer)
const degreeProgrammesOfFaculty = async facultyCode =>
(
await ProgrammeModule.findAll({
attributes: ['code', 'name'],
include: {
model: Organization,
where: {
code: {
[Op.startsWith]: facultyCode,
// Some programmenames are different, causing this to return multiples of same codes.
// Hence the uniqBy
uniqBy(
(
await ProgrammeModule.findAll({
attributes: ['code', 'name'],
include: {
model: Organization,
where: {
code: {
[Op.startsWith]: facultyCode,
},
},
},
},
group: ['programme_module.code', 'programme_module.name', 'organization.id'],
})
).map(facultyFormatProgramme)
group: ['programme_module.code', 'programme_module.name', 'organization.id'],
})
).map(facultyFormatProgramme),
'code'
)
const facultyOrganizationId = async faculty => {
return await Organization.findOne({
@@ -366,6 +355,7 @@ const findFacultyProgrammeCodes = async (faculty, programmeFilter) => {
for (const org of facultyChildOrganizations) {
const childAssociationProgrammes = await degreeProgrammesOfFaculty(org.code)
if (childAssociationProgrammes.length > 0) {
// eslint-disable-next-line no-loop-func
childAssociationProgrammes.forEach(prog => {
if (!(prog.code in allProgrammeCodes)) {
allProgrammes = allProgrammes.concat([prog])
@@ -379,6 +369,7 @@ const findFacultyProgrammeCodes = async (faculty, programmeFilter) => {
for (const gcOrg of grandChildren) {
const associatedProgrammes = await degreeProgrammesOfFaculty(gcOrg.code)
if (associatedProgrammes.length > 0) {
// eslint-disable-next-line no-loop-func
associatedProgrammes.forEach(prog => {
if (!(prog.code in allProgrammeCodes)) {
allProgrammes = allProgrammes.concat([prog])
@@ -400,6 +391,24 @@ const findFacultyProgrammeCodes = async (faculty, programmeFilter) => {
return allProgrammes
}
const getTransferredToAndAway = async (programmeCodes, allProgrammeCodes, since) => {
const awayTransfers = await transferredAway(programmeCodes, allProgrammeCodes, since)
const toTransfers = await transferredTo(programmeCodes, allProgrammeCodes, since)
return [...toTransfers, ...awayTransfers]
}
const getTransferredInside = async (programmeCodes, allProgrammeCodes, since) => {
return await transferredInsideFaculty(programmeCodes, allProgrammeCodes, since)
}
const getTransfersOut = async (programmeCode, start, end) => {
return await transferredFaculty([], [programmeCode], start, end)
}
const getTransfersIn = async (programmeCode, start, end) => {
return await transferredFaculty([programmeCode], [], start, end)
}
module.exports = {
startedStudyrights,
graduatedStudyrights,
@@ -18,7 +18,7 @@ const { codes } = require('../../../config/programmeCodes')
const filterDuplicateStudyrights = studyrights => {
// bachelor+master students have two studyrights (separated by two last digits in studyrightid)
// choose only bachelor one, so we don't count start of masters as starting in faculty
let rightsToCount = {}
const rightsToCount = {}
studyrights.forEach(right => {
const id = right.studyrightid.slice(0, -2)
@@ -39,18 +39,22 @@ const addProgramme = async (
facultyProgrammes,
includeAllSpecials
) => {
let { progId, name, code } = facultyProgrammes.filter(prog => prog.code === programme)[0]
const { progId, name, code } = facultyProgrammes.filter(prog => prog.code === programme)[0]
if (!(progId in stats)) {
stats[progId] = {}
if (includeAllSpecials) {
Object.keys(tableStats).forEach(year => (stats[progId][year] = [0, 0, 0]))
Object.keys(tableStats).forEach(year => {
stats[progId][year] = [0, 0, 0]
})
} else {
Object.keys(tableStats).forEach(year => (stats[progId][year] = [0]))
Object.keys(tableStats).forEach(year => {
stats[progId][year] = [0]
})
}
}
stats[progId][transferYear][index] += 1
if (!(progId in allBasics.programmeNames)) {
allBasics.programmeNames[progId] = { ...name, code: code }
allBasics.programmeNames[progId] = { ...name, code }
}
}
@@ -116,7 +120,7 @@ const getFacultyStarters = async (
})
}
allBasics.studentInfo.graphStats.push({ name: 'Started studying (new in faculty)', data: startedGraphStats })
programmeData['started'] = programmeTableStats
programmeData.started = programmeTableStats
Object.keys(startedTableStats).forEach(year => {
counts[year] = [startedTableStats[year]]
@@ -143,9 +147,11 @@ const getFacultyGraduates = async (
const graduatedGraphStats = [[...graphStats], [...graphStats], [...graphStats], [...graphStats], [...graphStats]]
const graduatedTableStats = {}
const programmeTableStats = {}
Object.keys(tableStats).forEach(year => (graduatedTableStats[year] = [0, 0, 0, 0, 0]))
Object.keys(tableStats).forEach(year => {
graduatedTableStats[year] = [0, 0, 0, 0, 0]
})
const keys = Object.keys(codes)
let studyrightWhere = getExtentFilter(includeAllSpecials)
const studyrightWhere = getExtentFilter(includeAllSpecials)
for (const code of programmes) {
let graduatedRights = await graduatedStudyrights(faculty, code, since, studyrightWhere)
if (!includeAllSpecials) {
@@ -169,7 +175,9 @@ const getFacultyGraduates = async (
if (!(programmeId in programmeTableStats)) {
programmeTableStats[programmeId] = {}
Object.keys(tableStats).forEach(year => (programmeTableStats[programmeId][year] = [0, 0, 0, 0, 0]))
Object.keys(tableStats).forEach(year => {
programmeTableStats[programmeId][year] = [0, 0, 0, 0, 0]
})
}
programmeTableStats[programmeId][endYear][0] += 1
@@ -204,7 +212,7 @@ const getFacultyGraduates = async (
allBasics.graduationInfo.graphStats.push({ name: 'Doctors', data: graduatedGraphStats[3] })
allBasics.graduationInfo.graphStats.push({ name: 'Others', data: graduatedGraphStats[4] })
programmeData['graduated'] = programmeTableStats
programmeData.graduated = programmeTableStats
Object.keys(graduatedTableStats).forEach(year => {
counts[year] = counts[year].concat(graduatedTableStats[year][0])
@@ -246,13 +254,17 @@ const getFacultyTransfers = async (
faculty
)
let transferGraphStats = []
let transferTableStats = {}
const transferTableStats = {}
if (includeAllSpecials) {
transferGraphStats = [[...graphStats], [...graphStats], [...graphStats]]
Object.keys(tableStats).forEach(year => (transferTableStats[year] = [0, 0, 0]))
Object.keys(tableStats).forEach(year => {
transferTableStats[year] = [0, 0, 0]
})
} else {
transferGraphStats = [[...graphStats]]
Object.keys(tableStats).forEach(year => (transferTableStats[year] = [0]))
Object.keys(tableStats).forEach(year => {
transferTableStats[year] = [0]
})
}
const programmeTableStats = {}
@@ -320,7 +332,7 @@ const getFacultyTransfers = async (
allBasics.studentInfo.graphStats.push({ name: 'Transferred away', data: transferGraphStats[1] })
allBasics.studentInfo.graphStats.push({ name: 'Transferred to', data: transferGraphStats[2] })
}
programmeData['transferred'] = programmeTableStats
programmeData.transferred = programmeTableStats
Object.keys(transferTableStats).forEach(year => {
counts[year] = counts[year].concat(transferTableStats[year])
@@ -328,10 +340,10 @@ const getFacultyTransfers = async (
}
const combineFacultyBasics = async (faculty, programmes, yearType, allProgrammeCodes, programmeFilter, special) => {
let counts = {}
let countsGraduations = {}
let years = []
let programmeData = {}
const counts = {}
const countsGraduations = {}
const years = []
const programmeData = {}
const isAcademicYear = yearType === 'ACADEMIC_YEAR'
const includeAllSpecials = special === 'SPECIAL_INCLUDED'
const since = isAcademicYear ? new Date('2017-08-01') : new Date('2017-01-01')
@@ -340,7 +352,7 @@ const combineFacultyBasics = async (faculty, programmes, yearType, allProgrammeC
Object.keys(tableStats).forEach(year => years.push(year))
const wantedProgrammeCodes = programmes.map(prog => prog.code)
let allBasics = {
const allBasics = {
id: faculty,
years: [],
programmeNames: {},
@@ -424,7 +436,7 @@ const combineFacultyBasics = async (faculty, programmes, yearType, allProgrammeC
allBasics.studentInfo.tableStats.reverse()
allBasics.graduationInfo.tableStats.reverse()
//combine programme level tablestats
// combine programme level tablestats
// all programmes are not present in all data types, check all found programmes and patch missing details with 0
let allCodes = Object.keys(programmeData.started).concat(
@@ -433,8 +445,8 @@ const combineFacultyBasics = async (faculty, programmes, yearType, allProgrammeC
)
allCodes = [...new Set(allCodes)]
let studentInfo = {}
let graduationInfo = {}
const studentInfo = {}
const graduationInfo = {}
allCodes.forEach(code => {
if (!(code in studentInfo)) studentInfo[code] = {}
@@ -457,12 +469,10 @@ const combineFacultyBasics = async (faculty, programmes, yearType, allProgrammeC
// trasferred
if (code in programmeData.transferred) {
studentInfo[code][year] = studentInfo[code][year].concat(programmeData.transferred[code][year])
} else if (includeAllSpecials) {
studentInfo[code][year] = studentInfo[code][year].concat([0, 0, 0])
} else {
if (includeAllSpecials) {
studentInfo[code][year] = studentInfo[code][year].concat([0, 0, 0])
} else {
studentInfo[code][year] = studentInfo[code][year].concat([0])
}
studentInfo[code][year] = studentInfo[code][year].concat([0])
}
}
})
@@ -8,7 +8,7 @@ const { getProgrammes } = require('./facultyService')
const sortProgrammes = data => {
const check = name => {
if (isNaN(name[0])) return -1
if (Number.isNaN(name[0])) return -1
return 1
}
data.sort((a, b) => {
@@ -22,8 +22,8 @@ const getSortedProgIds = progs => {
for (const prog of progs) {
if (Object.keys(codes).includes(prog)) {
const name = prog === 'T923103' ? codes[prog].toUpperCase() + '-DP' : codes[prog].toUpperCase()
programmes = [...programmes, { name: name, code: prog }]
const name = prog === 'T923103' ? `${codes[prog].toUpperCase()}-DP` : codes[prog].toUpperCase()
programmes = [...programmes, { name, code: prog }]
} else {
programmes = [...programmes, { name: prog, code: prog }]
}
@@ -137,12 +137,10 @@ const getClassSizes = async (faculty, programmeCodes, since, classSizes, program
if (studyrightid.slice(-2) === '-2') {
level = 'bcMsCombo'
actualStartdate = await findBachelorStartdate(studyrightid.replace(/-2$/, '-1'))
} else if (code.includes('KH')) {
level = 'bachelor' // Some rare faculties have one/two outliers with extent 2 for bachelor
} else {
if (code.includes('KH')) {
level = 'bachelor' // Some rare faculties have one/two outliers with extent 2 for bachelor
} else {
level = 'master'
}
level = 'master'
}
} else if (extentcode === 4) {
level = 'doctor'
@@ -158,15 +156,15 @@ const getClassSizes = async (faculty, programmeCodes, since, classSizes, program
if (level !== 'bcMsCombo') {
classSizes[level][startYear] += 1
if (level === 'bachelor' && (await hasMS(code, studyrightElements, studyrightid))) {
classSizes['bcMsCombo'][startYear] += 1
classSizes.bcMsCombo[startYear] += 1
}
}
if (!(code in classSizes.programmes)) {
if (extentcode === 2) {
classSizes.programmes[code] = {}
classSizes.programmes[code]['bcMsCombo'] = getYearsObject({ years, emptyArrays: false })
classSizes.programmes[code]['master'] = getYearsObject({ years, emptyArrays: false })
classSizes.programmes[code].bcMsCombo = getYearsObject({ years, emptyArrays: false })
classSizes.programmes[code].master = getYearsObject({ years, emptyArrays: false })
} else {
classSizes.programmes[code] = getYearsObject({ years, emptyArrays: false })
}
@@ -193,10 +191,10 @@ const count = async (
goals,
mode
) => {
let graduationAmounts = {}
let graduationTimes = {}
let programmes = getProgrammeObjectBasis(yearsList, levels)
let data = {
const graduationAmounts = {}
const graduationTimes = {}
const programmes = getProgrammeObjectBasis(yearsList, levels)
const data = {
medians: {},
programmes: {
medians: getProgrammeObjectBasis(yearsList, levels),
@@ -265,7 +263,9 @@ const count = async (
}
const progStatistics = countTimeCategories(programmes[level][year][prog.code].graduationTimes, goal)
Object.keys(progStatistics).forEach(key => (statistics[key] += progStatistics[key]))
Object.keys(progStatistics).forEach(key => {
statistics[key] += progStatistics[key]
})
data.programmes.medians[level][year].programmes = [
...data.programmes.medians[level][year].programmes,
@@ -318,7 +318,7 @@ const countGraduationTimes = async (faculty, programmeFilter) => {
doctor: 48,
licentiate: 78,
exceptions: {
MH30_004: 6, //months more
MH30_004: 6, // months more
'420420-ma': 6,
MH30_001: 12,
'320011-ma': 12,
@@ -363,7 +363,7 @@ const countGraduationTimes = async (faculty, programmeFilter) => {
)
const classSizes = getProgrammeObjectBasis(yearsList, levels, false)
classSizes['programmes'] = {}
classSizes.programmes = {}
await getClassSizes(faculty, programmeCodes, since, classSizes, programmeFilter, years)
return { id: faculty, goals, byGradYear, byStartYear, programmeNames, classSizes }
@@ -1,5 +1,4 @@
const Sequelize = require('sequelize')
const { Op } = Sequelize
const { Op } = require('sequelize')
const { codes } = require('../../../config/programmeCodes')
const { faculties } = require('../organisations')
@@ -124,7 +123,7 @@ const getExtentFilter = includeAllSpecials => {
if (!includeAllSpecials) {
filteredExtents.push(6, 7, 9, 13, 14, 18, 22, 23, 34, 99)
}
let studyrightWhere = {
const studyrightWhere = {
extentcode: {
[Op.notIn]: filteredExtents,
},
@@ -139,7 +138,7 @@ const mapCodesToIds = data => {
if (keys.includes(prog.code)) {
prog.progId = codes[prog.code].toUpperCase()
if (prog.code === 'T923103' && !prog.progId.includes('-DP')) {
prog.progId = prog.progId + '-DP'
prog.progId += '-DP'
}
} else {
prog.progId = prog.code
@@ -26,13 +26,15 @@ const removeGraduationTimes = data => {
yearStat.times = null
})
)
// TODO: Does this do something? Seems like an unnecessary line.
// eslint-disable-next-line no-unused-expressions
data.byGradYear.medians
}
const setFacultyProgrammes = async (id, data, programmeFilter) => {
const redisKey = createRedisKeyForFacultyProgrammes(id, programmeFilter)
const dataToRedis = {
data: data,
data,
status: 'DONE',
lastUpdated: moment().format(),
}
@@ -41,6 +43,13 @@ const setFacultyProgrammes = async (id, data, programmeFilter) => {
return dataToRedis
}
const getFacultyProgrammesFromRedis = async (id, programmeFilter) => {
const redisKey = createRedisKeyForFacultyProgrammes(id, programmeFilter)
const dataFromRedis = await redisClient.getAsync(redisKey)
if (!dataFromRedis) return null
return JSON.parse(dataFromRedis)
}
const getProgrammes = async (code, programmeFilter = 'NEW_STUDY_PROGRAMMES') => {
const programmes = await getFacultyProgrammesFromRedis(code, programmeFilter)
if (programmes) return programmes
@@ -50,13 +59,6 @@ const getProgrammes = async (code, programmeFilter = 'NEW_STUDY_PROGRAMMES') =>
return updatedProgrammes
}
const getFacultyProgrammesFromRedis = async (id, programmeFilter) => {
const redisKey = createRedisKeyForFacultyProgrammes(id, programmeFilter)
const dataFromRedis = await redisClient.getAsync(redisKey)
if (!dataFromRedis) return null
return JSON.parse(dataFromRedis)
}
const setBasicStats = async (data, yearType, programmeFilter, specialGroups) => {
const { id } = data
const redisKey = createRedisKeyForBasicStats(id, yearType, programmeFilter, specialGroups)
@@ -15,15 +15,19 @@ const getStudentData = (
thresholdKeys = [],
thresholdAmounts = []
) => {
let data = {}
let programmeData = {}
const data = {}
const programmeData = {}
const creditCounts = students.map(({ credits }) =>
credits
.filter(credit => moment(credit.attainment_date).isSameOrAfter(startDate))
.reduce((total, credit) => total + credit.credits, 0)
)
thresholdKeys.forEach(t => (data[t] = 0))
limitKeys.forEach(t => (programmeData[t] = 0))
thresholdKeys.forEach(t => {
data[t] = 0
})
limitKeys.forEach(t => {
programmeData[t] = 0
})
// Data is only used for doctoral programmes, otherwise only programmeData is needed
if (prog === 'T') {
@@ -129,7 +133,7 @@ const combineFacultyStudentProgress = async (faculty, programmes, specialGroups,
if (year === 'Total') continue
const { startDate, endDate } = getAcademicYearDates(year, since)
const lastDayOfMonth = moment().endOf('month')
let months = Math.round(moment.duration(moment(lastDayOfMonth).diff(moment(startDate))).asMonths())
const months = Math.round(moment.duration(moment(lastDayOfMonth).diff(moment(startDate))).asMonths())
if (months >= 36) {
bachelorlimits = createLimits(36, 0)
} else {
@@ -181,7 +185,7 @@ const combineFacultyStudentProgress = async (faculty, programmes, specialGroups,
// Get credit threshold values: 'combined study programme' is false and 'only master studyright' is true
if (!(progId in progressStats.programmeNames)) {
progressStats.programmeNames[progId] = { code: code, ...name }
progressStats.programmeNames[progId] = { code, ...name }
}
if (!includeAllSpecials) {
studyrights = await filterOutTransfers(studyrights, code, startDate, endDate)
@@ -246,7 +250,7 @@ const combineFacultyStudentProgress = async (faculty, programmes, specialGroups,
} else {
const all = studyrights.filter(sr => sr.extentcode === 4 || sr.extentcode === 3).map(sr => sr.studentnumber)
const doctoralStudents = await studytrackStudents(all)
let { creditThresholdKeys, creditThresholdAmounts } = getCreditThresholds(code)
const { creditThresholdKeys, creditThresholdAmounts } = getCreditThresholds(code)
const { data, creditCounts } = getStudentData(
startDate,
@@ -10,6 +10,7 @@ const { getStudyRightsByExtent, getStudentsByStudentnumbers, getTransfersIn } =
const { checkTransfers } = require('./facultyHelpers')
const { inactiveStudyrights, graduatedStudyRights } = require('../studyprogramme/studyrightFinders')
const { enrolledStudents, absentStudents } = require('../studyprogramme/studentGetters')
const emptyTotals = () => {
return {
total: 0,
@@ -58,7 +59,7 @@ const addTotalsProgramme = (programmeTableStats, progId, year, totals) => {
}
const getStudentData = (students, facultyExtra, year, code) => {
let data = { female: 0, male: 0, finnish: 0, otherCountries: 0, otherUnknown: 0 }
const data = { female: 0, male: 0, finnish: 0, otherCountries: 0, otherUnknown: 0 }
students.forEach(({ genderCode, homeCountryEn }) => {
data.male += genderCode === '1' ? 1 : 0
data.female += genderCode === '2' ? 1 : 0
@@ -67,11 +68,11 @@ const getStudentData = (students, facultyExtra, year, code) => {
data.otherCountries += homeCountryEn !== 'Finland' ? 1 : 0
if (!Object.keys(facultyExtra[year][code]).includes(homeCountryEn) && homeCountryEn !== 'Finland')
facultyExtra[year][code][homeCountryEn] = 0
if (!Object.keys(facultyExtra['Total'][code]).includes(homeCountryEn) && homeCountryEn !== 'Finland')
facultyExtra['Total'][code][homeCountryEn] = 0
if (!Object.keys(facultyExtra.Total[code]).includes(homeCountryEn) && homeCountryEn !== 'Finland')
facultyExtra.Total[code][homeCountryEn] = 0
if (homeCountryEn !== 'Finland') {
facultyExtra[year][code][homeCountryEn] += 1
facultyExtra['Total'][code][homeCountryEn] += 1
facultyExtra.Total[code][homeCountryEn] += 1
}
})
return data
@@ -105,7 +106,7 @@ const getFacultyDataForYear = async ({
const totals = emptyTotals()
for (const { progId, code, name } of programmes) {
if (!Object.keys(facultyExtra[year]).includes(code)) facultyExtra[year][code] = {}
if (!Object.keys(facultyExtra['Total']).includes(code)) facultyExtra['Total'][code] = {}
if (!Object.keys(facultyExtra.Total).includes(code)) facultyExtra.Total[code] = {}
const toTransfers = await getTransfersIn(code, startDate, endDate)
const allStudyrights = await getStudyRightsByExtent(faculty, startDate, endDate, code, extents, graduated)
@@ -113,7 +114,7 @@ const getFacultyDataForYear = async ({
if (includeAllSpecials) {
allStudents = [...new Set([...allStudyrights.map(sr => sr.studentnumber)])]
}
let studyrights = allStudyrights.filter(s => !checkTransfers(s, toTransfers, toTransfers))
const studyrights = allStudyrights.filter(s => !checkTransfers(s, toTransfers, toTransfers))
// Get all the studyrights and students for the calculations
const studentNbrs = studyrights.map(sr => sr.studentnumber)
@@ -134,7 +135,7 @@ const getFacultyDataForYear = async ({
progTotals[progId] = emptyTotals()
}
if (!(progId in programmeNames)) {
programmeNames[progId] = { code: code, ...name }
programmeNames[progId] = { code, ...name }
progTotals[progId] = emptyTotals()
}
@@ -232,12 +233,12 @@ const combineFacultyStudents = async (code, programmes, specialGroups, graduated
const studentsData = {
id: code,
years: years,
facultyTableStats: facultyTableStats,
years,
facultyTableStats,
facultyTableStatsExtra: facultyExtra,
programmeStats: programmeTableStats,
titles: [...tableTitles.studytracksStart, ...tableTitles.studytracksBasic, ...tableTitles.studytracksEnd],
programmeNames: programmeNames,
programmeNames,
}
return studentsData
}
@@ -13,10 +13,10 @@ const {
const getFacultyThesisWriters = async ({ since, years, isAcademicYear, facultyProgrammes, includeAllSpecials }) => {
const thesisTypes = ['urn:code:course-unit-type:bachelors-thesis', 'urn:code:course-unit-type:masters-thesis']
let bachelors = getStatsBasis(years)
let masters = getStatsBasis(years)
let programmeCounts = {}
let programmeNames = {}
const bachelors = getStatsBasis(years)
const masters = getStatsBasis(years)
const programmeCounts = {}
const programmeNames = {}
for (const { progId, code, name } of facultyProgrammes) {
if (code === 'MH70_008_2' || code.startsWith('LIS') || code.startsWith('T')) continue
@@ -36,8 +36,10 @@ const getFacultyThesisWriters = async ({ since, years, isAcademicYear, facultyPr
if (!(progId in programmeCounts)) {
programmeCounts[progId] = {}
Object.keys(bachelors.tableStats).forEach(year => (programmeCounts[progId][year] = [0, 0, 0]))
programmeNames[progId] = { ...name, code: code }
Object.keys(bachelors.tableStats).forEach(year => {
programmeCounts[progId][year] = [0, 0, 0]
})
programmeNames[progId] = { ...name, code }
}
programmeCounts[progId][thesisYear][0] += 1
@@ -94,7 +96,7 @@ const getFacultyThesisWritersForProgrammes = async (
}
const combineFacultyThesisWriters = async (faculty, facultyProgrammes, yearType, specialGroups) => {
let allThesisWriters = {
const allThesisWriters = {
id: faculty,
years: [],
tableStats: [],
@@ -74,7 +74,7 @@ const updateFacultyOverview = async (faculty, statsType) => {
]
let allProgrammes = []
let newProgrammes = []
let allProgrammeCodes = []
const allProgrammeCodes = []
try {
const all = await findFacultyProgrammeCodes(faculty, 'ALL_PROGRAMMES')
@@ -1,5 +1,4 @@
const Sequelize = require('sequelize')
const { Op } = Sequelize
const { Op } = require('sequelize')
const moment = require('moment')
const { Semester, SemesterEnrollment, Studyright } = require('../models')
@@ -39,7 +38,7 @@ const statutoryAbsences = async (studentnumber, startdate, enddate) =>
},
},
where: {
studentnumber: studentnumber,
studentnumber,
statutory_absence: true,
},
})
@@ -2,6 +2,7 @@ const { Course, Credit, Enrollment, StudyrightElement, Studyright } = require('.
const { Op } = require('sequelize')
const { redisClient } = require('./redis')
const { orderBy } = require('lodash')
const LANGUAGE_CENTER_REDIS_KEY = 'LANGUAGE_CENTER_DATA'
const isBetween = (start, date, end) =>
@@ -25,12 +26,59 @@ const getLanguageCenterCourses = async () => {
return courses
}
const getLanguageCenterData = async () => {
const dataOnRedis = await redisClient.getAsync(LANGUAGE_CENTER_REDIS_KEY)
if (dataOnRedis) return JSON.parse(dataOnRedis)
const freshData = await computeLanguageCenterData()
redisClient.setAsync(LANGUAGE_CENTER_REDIS_KEY, JSON.stringify(freshData))
return freshData
const createArrayOfCourses = async (attempts, courses) => {
const fields = { completions: 0, enrollments: 0, difference: 0, rejected: 0 }
const semestersObject = {}
const facultyObject = {}
const semesterStatsMap = attempts.reduce((obj, cur) => {
const semester = cur.semestercode
facultyObject[cur.faculty] = true
semestersObject[semester] = true
if (!obj[cur.courseCode]) {
obj[cur.courseCode] = {}
}
if (!obj[cur.courseCode][semester]) {
obj[cur.courseCode][semester] = { ...fields }
}
if (cur.faculty && !obj[cur.courseCode][semester][cur.faculty]) {
obj[cur.courseCode][semester][cur.faculty] = { ...fields }
}
const allFacultiesTotal = obj[cur.courseCode][semester]
const allStats = [allFacultiesTotal]
if (cur.faculty) {
const semesterFacultyStats = obj[cur.courseCode][semester][cur.faculty]
allStats.push(semesterFacultyStats)
}
for (const stats of allStats) {
if (cur.completed) {
stats.completions += 1
} else if (cur.enrolled) {
stats.enrollments += 1
} else {
stats.rejected += 1
}
}
return obj
}, {})
const courseList = courses
.map(c => ({ ...c, bySemesters: { ...semesterStatsMap[c.code], cellStats: {} } }))
.filter(course => course.bySemesters)
courseList.forEach(course => {
Object.keys(semestersObject).forEach(sem => {
const stats = course.bySemesters[sem]
if (!stats) return
stats.difference = getDifference(stats)
Object.keys(facultyObject).forEach(fac => {
const stats = course.bySemesters[sem]?.[fac]
if (!stats) return
stats.difference = getDifference(stats)
})
})
})
return courseList
}
const computeLanguageCenterData = async () => {
@@ -162,7 +210,7 @@ const computeLanguageCenterData = async () => {
if (item.faculty) {
if (isOpenUni(item.faculty)) {
item.faculty = 'OPEN'
} else if (isMisc(item.faculty) || !item.faculty.substring(0, 3).match(`^H\\d`)) {
} else if (isMisc(item.faculty) || !item.faculty.substring(0, 3).match('^H\\d')) {
item.faculty = 'OTHER'
}
return
@@ -174,7 +222,7 @@ const computeLanguageCenterData = async () => {
const faculty = getFaculty(item.studentNumber, item.date)
if (isOpenUni(faculty)) {
item.faculty = 'OPEN'
} else if (faculty?.substring(0, 3).match(`^H\\d`)) {
} else if (faculty?.substring(0, 3).match('^H\\d')) {
item.faculty = !isMisc(faculty) ? faculty : 'OTHER'
} else {
item.faculty = 'OTHER'
@@ -190,61 +238,12 @@ const computeLanguageCenterData = async () => {
return { tableData, faculties }
}
const createArrayOfCourses = async (attempts, courses) => {
const fields = { completions: 0, enrollments: 0, difference: 0, rejected: 0 }
const semestersObject = {}
const facultyObject = {}
const semesterStatsMap = attempts.reduce((obj, cur) => {
const semester = cur.semestercode
facultyObject[cur.faculty] = true
semestersObject[semester] = true
if (!obj[cur.courseCode]) {
obj[cur.courseCode] = {}
}
if (!obj[cur.courseCode][semester]) {
obj[cur.courseCode][semester] = { ...fields }
}
if (cur.faculty && !obj[cur.courseCode][semester][cur.faculty]) {
obj[cur.courseCode][semester][cur.faculty] = { ...fields }
}
const allFacultiesTotal = obj[cur.courseCode][semester]
const allStats = [allFacultiesTotal]
if (cur.faculty) {
const semesterFacultyStats = obj[cur.courseCode][semester][cur.faculty]
allStats.push(semesterFacultyStats)
}
for (const stats of allStats) {
if (cur.completed) {
stats.completions += 1
} else {
if (cur.enrolled) {
stats.enrollments += 1
} else {
stats.rejected += 1
}
}
}
return obj
}, {})
const courseList = courses
.map(c => ({ ...c, bySemesters: { ...semesterStatsMap[c.code], cellStats: {} } }))
.filter(course => course.bySemesters)
courseList.forEach(course => {
Object.keys(semestersObject).forEach(sem => {
const stats = course.bySemesters[sem]
if (!stats) return
stats.difference = getDifference(stats)
Object.keys(facultyObject).forEach(fac => {
const stats = course.bySemesters[sem]?.[fac]
if (!stats) return
stats.difference = getDifference(stats)
})
})
})
return courseList
const getLanguageCenterData = async () => {
const dataOnRedis = await redisClient.getAsync(LANGUAGE_CENTER_REDIS_KEY)
if (dataOnRedis) return JSON.parse(dataOnRedis)
const freshData = await computeLanguageCenterData()
redisClient.setAsync(LANGUAGE_CENTER_REDIS_KEY, JSON.stringify(freshData))
return freshData
}
module.exports = {
@@ -17,7 +17,6 @@ const getCredits = async (courseCodes, startdate) =>
course_code: {
[Op.in]: courseCodes,
},
is_open: true,
attainment_date: {
[Op.gte]: startdate,
},
@@ -80,6 +79,9 @@ const getStudyRights = async students =>
student_studentnumber: {
[Op.in]: students.length > 0 ? students : { [Op.not]: null },
},
extentcode: {
[Op.in]: [1, 2, 3, 4],
},
},
})
).map(mapStudyRights)
@@ -103,8 +105,8 @@ const createOpenUniPopulationSearch = async (userId, name, courseCodes) => {
const updateOpenUniPopulationSearch = async (userId, id, courseCodes) => {
const searchToUpdate = await OpenUniPopulationSearch.findOne({
where: {
userId: userId,
id: id,
userId,
id,
},
})
@@ -115,8 +117,8 @@ const updateOpenUniPopulationSearch = async (userId, id, courseCodes) => {
const deleteOpenUniSearch = async (userId, id) => {
return await OpenUniPopulationSearch.destroy({
where: {
userId: userId,
id: id,
userId,
id,
},
})
}
@@ -3,7 +3,7 @@ const { getCredits, getStudyRights, getEnrollments, getStudentInfo, getCourseNam
const uniq = objects => [...new Set(objects)]
const calulateTotalsForStudent = async (studentStats, studentnumber) => {
const calculateTotalsForStudent = async (studentStats, studentnumber) => {
Object.keys(studentStats[studentnumber].courseInfo).forEach(course => {
if (studentStats[studentnumber].courseInfo[course].status.passed) {
studentStats[studentnumber].totals.passed += 1
@@ -15,7 +15,7 @@ const calulateTotalsForStudent = async (studentStats, studentnumber) => {
})
}
const getCustomOpenUniCourses = async (courseCodes, startdate, enddate) => {
const ayCourseCodes = courseCodes.map(courseCode => 'AY' + courseCode)
const ayCourseCodes = courseCodes.map(courseCode => `AY${courseCode}`)
const allCourseCodes = courseCodes.concat(ayCourseCodes)
const allCredits = await getCredits(allCourseCodes, startdate)
const allEnrollments = await getEnrollments(allCourseCodes, startdate, enddate)
@@ -60,7 +60,7 @@ const getCustomOpenUniCourses = async (courseCodes, startdate, enddate) => {
(acc, code) => ({ ...acc, [code.replace('AY', '')]: getEmptyCourseInfo() }),
{}
),
email: email,
email,
secondaryEmail: secondary_email,
totals: { passed: 0, failed: 0, unfinished: 0 },
}
@@ -100,10 +100,10 @@ const getCustomOpenUniCourses = async (courseCodes, startdate, enddate) => {
}
}
}
await calulateTotalsForStudent(studentStats, studentnumber)
await calculateTotalsForStudent(studentStats, studentnumber)
}
}
const openUniStats = { students: studentStats, courses: courses }
const openUniStats = { students: studentStats, courses }
return openUniStats
}
+1 -191
View File
@@ -1,196 +1,7 @@
const Sequelize = require('sequelize')
const { Op } = require('sequelize')
const { Organization } = require('../models')
const Op = Sequelize.Op
const { dbConnections } = require('../database/connection')
// temp refactoring faculty <-> userservice mappings away from userservice
const facultiesAndProgrammesInUserService = [
['H10', 'MH10_001'],
['H10', 'MH40_011'],
['H10', 'KH10_001'],
['H20', 'KH20_001'],
['H20', 'MH20_001'],
['H20', 'MH20_003'],
['H20', 'MH20_002'],
['H30', 'KH30_001'],
['H30', 'KH30_002'],
['H30', 'MH30_002'],
['H30', 'MH30_003'],
['H30', 'MH30_004'],
['H30', 'MH30_005'],
['H30', 'MH30_001'],
['H40', 'KH40_001'],
['H40', 'KH40_002'],
['H40', 'KH40_003'],
['H40', 'KH40_004'],
['H40', 'KH40_005'],
['H40', 'KH40_006'],
['H40', 'MH40_001'],
['H40', 'MH40_002'],
['H40', 'MH40_003'],
['H40', 'MH40_004'],
['H40', 'MH40_005'],
['H40', 'MH40_006'],
['H40', 'MH40_007'],
['H40', 'MH40_008'],
['H40', 'MH40_009'],
['H40', 'MH40_010'],
['H40', 'MH40_011'],
['H40', 'MH40_012'],
['H40', 'MH40_013'],
['H40', 'MH40_014'],
['H40', 'MH40_015'],
['H40', 'MH70_001'],
['H40', 'MH70_006'],
['H50', 'KH50_001'],
['H50', 'KH50_002'],
['H50', 'KH50_003'],
['H50', 'KH50_004'],
['H50', 'KH50_005'],
['H50', 'KH50_006'],
['H50', 'KH50_007'],
['H50', 'KH50_008'],
['H50', 'MH50_001'],
['H50', 'MH50_002'],
['H50', 'MH50_003'],
['H50', 'MH50_004'],
['H50', 'MH50_005'],
['H50', 'MH50_006'],
['H50', 'MH50_007'],
['H50', 'MH50_008'],
['H50', 'MH50_009'],
['H50', 'MH50_010'],
['H50', 'MH50_011'],
['H50', 'MH50_012'],
['H50', 'MH50_013'],
['H50', 'MH50_014'],
['H55', 'KH55_001'],
['H55', 'MH55_001'],
['H55', 'MH55_002'],
['H57', 'KH57_001'],
['H57', 'KH57_002'],
['H57', 'KH57_003'],
['H57', 'MH50_002'],
['H57', 'MH50_013'],
['H57', 'MH57_001'],
['H57', 'MH57_002'],
['H57', 'MH57_003'],
['H57', 'MH57_004'],
['H57', 'MH57_005'],
['H57', 'MH80_007'],
['H60', 'KH60_001'],
['H60', 'MH60_001'],
['H60', 'MH60_002'],
['H70', 'KH40_001'],
['H70', 'KH50_001'],
['H70', 'KH70_001'],
['H70', 'KH70_002'],
['H70', 'KH70_003'],
['H70', 'KH70_004'],
['H70', 'MH50_001'],
['H70', 'MH57_005'],
['H70', 'MH70_001'],
['H70', 'MH70_002'],
['H70', 'MH70_003'],
['H70', 'MH70_004'],
['H70', 'MH70_005'],
['H70', 'MH70_006'],
['H70', 'MH70_007'],
['H70', 'MH70_008'],
['H70', 'MH70_009'],
['H70', 'MH70_010'],
['H70', 'MH70_011'],
['H74', 'KH74_001'],
['H80', 'KH57_002'],
['H80', 'KH80_001'],
['H80', 'KH80_002'],
['H80', 'KH80_003'],
['H80', 'KH80_004'],
['H80', 'MH57_002'],
['H80', 'MH57_003'],
['H80', 'MH57_005'],
['H80', 'MH80_001'],
['H80', 'MH80_002'],
['H80', 'MH80_003'],
['H80', 'MH80_004'],
['H80', 'MH80_005'],
['H80', 'MH80_006'],
['H80', 'MH80_007'],
['H80', 'MH80_008'],
['H90', 'KH90_001'],
['H90', 'MH90_001'],
['H92', 'T920101'],
['H92', 'T920102'],
['H92', 'T920103'],
['H92', 'T920104'],
['H92', 'T920105'],
['H92', 'T920106'],
['H92', 'T920107'],
['H92', 'T920108'],
['H92', 'T920109'],
['H92', 'T920110'],
['H92', 'T920111'],
['H92', 'T921101'],
['H92', 'T921102'],
['H92', 'T921103'],
['H92', 'T921104'],
['H92', 'T921105'],
['H92', 'T921106'],
['H92', 'T921107'],
['H92', 'T921108'],
['H92', 'T921109'],
['H92', 'T922101'],
['H92', 'T922102'],
['H92', 'T922103'],
['H92', 'T922104'],
['H92', 'T922105'],
['H92', 'T922106'],
['H92', 'T923101'],
['H92', 'T923102'],
['H92', 'T923103'],
['H92', 'T923104'],
['H92', 'T923105'],
['H92', 'T923106'],
['H92', 'T923107'],
['H30', '447000'],
['H30', '447001'],
['H30', '447002'],
['H60', 'EDUK730'],
['H60', 'K-MUUT-ERIL'],
['H60', 'ED400'],
['H60', 'ED200'],
['H60', 'ED700'],
['H60', 'ED100'],
['H60', 'ED300'],
['H60', 'ED600'],
['H60', '8009'],
['H60', '8118'],
['H60', '8600'],
['H60', 'DUK-PED'],
['H60', '65'],
['H60', '1102'],
['H60', '20018'],
['H60', '8601'],
['H60', '9290'],
['H60', '8172'],
['H60', '83600'],
['H60', 'DUM-PED'],
['H60', 'ED500'],
['H60', '83601'],
['H60', '0130'],
['H60', '20005-e'],
['H60', '1040'],
['H60', '8703'],
['H60', '0005'],
['H60', '7900'],
['H60', '0152 '],
]
const facultiesAndProgrammesForTrends = facultiesAndProgrammesInUserService.map(f => ({
faculty_code: f[0],
programme_code: f[1],
}))
// Have facultyfetching to work like it worked during oodi-db time
const facultiesInOodi = [
'H10',
@@ -240,5 +51,4 @@ module.exports = {
faculties,
isFaculty,
providersOfFaculty,
facultiesAndProgrammesForTrends,
}
@@ -50,7 +50,9 @@ const bottlenecksOf = async (query, studentnumberlist, encryptdata = false) => {
const getStudentsAndCourses = async (selectedStudents, studentnumberlist, courseCodes) => {
if (!studentnumberlist) {
// TODO: Check where this params object is coming from as ESLint is complaining about it
const { months, studyRights, startDate, endDate, exchangeStudents, nondegreeStudents, transferredStudents, tag } =
// eslint-disable-next-line no-use-before-define
params
const studentnumbers =
selectedStudents ||
@@ -80,25 +82,28 @@ const bottlenecksOf = async (query, studentnumberlist, encryptdata = false) => {
filteredCourseCodes
)
return [allstudents, courses, courseEnrollements]
} else {
const { months, startDate } = params
const beforeDate = months && startDate ? dateMonthsFromNow(startDate, months) : new Date()
const allstudents = studentnumberlist.reduce((numbers, num) => {
numbers[num] = true
return numbers
}, {})
const courses = await findCourses(studentnumberlist, beforeDate, courseCodes)
const foundCourseCodes = Object.keys(keyBy(courses, 'code'))
const filteredCourseCodes = courseCodes?.filter(code => !foundCourseCodes.includes(code))
const courseEnrollements = await findCourseEnrollments(studentnumberlist, beforeDate, filteredCourseCodes)
return [allstudents, courses, courseEnrollements]
}
// TODO: Check where this params object is coming from as ESLint is complaining about it
// eslint-disable-next-line no-use-before-define
const { months, startDate } = params
const beforeDate = months && startDate ? dateMonthsFromNow(startDate, months) : new Date()
const allstudents = studentnumberlist.reduce((numbers, num) => {
numbers[num] = true
return numbers
}, {})
const courses = await findCourses(studentnumberlist, beforeDate, courseCodes)
const foundCourseCodes = Object.keys(keyBy(courses, 'code'))
const filteredCourseCodes = courseCodes?.filter(code => !foundCourseCodes.includes(code))
const courseEnrollements = await findCourseEnrollments(studentnumberlist, beforeDate, filteredCourseCodes)
return [allstudents, courses, courseEnrollements]
}
const encryptStudentnumbers = bottlenecks => {
// eslint-disable-next-line guard-for-in
for (const course in bottlenecks.coursestatistics) {
const encryptedStudentStats = {}
// eslint-disable-next-line guard-for-in
for (const data in bottlenecks.coursestatistics[course].students) {
encryptedStudentStats[data] = {}
const studentnumbers = Object.keys(bottlenecks.coursestatistics[course].students[data])
@@ -158,11 +163,11 @@ const bottlenecksOf = async (query, studentnumberlist, encryptdata = false) => {
const coursesByCode = keyBy(coursesToLoop, 'code')
for (const course of coursesToLoop) {
let { course_type } = course
const { course_type } = course
let maincourse = course
if (course.main_course_code && course.main_course_code !== course.code) {
let newmain = coursesByCode[course.main_course_code]
const newmain = coursesByCode[course.main_course_code]
if (newmain) {
maincourse = newmain
@@ -0,0 +1,132 @@
const { Op } = require('sequelize')
const {
ElementDetail,
Student,
Studyplan,
Studyright,
StudyrightElement,
Credit,
Course,
Organization,
} = require('../../models')
const { mapToProviders } = require('../../util/utils')
const { redisClient } = require('../redis')
const CLOSE_TO_GRADUATION_REDIS_KEY = 'CLOSE_TO_GRADUATION_DATA'
const formatStudent = student => {
const { studyright_elements: studyrightElements, startdate: startOfStudyright } = student.studyplans[0].studyright
const programmeCode = student.studyplans[0].programme_code
const programme = studyrightElements?.find(programme => programme.code === programmeCode)?.element_detail
const programmeCodeToProviderCode = mapToProviders([programmeCode])[0]
const thesisData = student.credits.find(credit =>
credit.course.organizations.some(org => org.code === programmeCodeToProviderCode)
)
return {
student: {
studentNumber: student.studentnumber,
name: student.abbreviatedname,
sis_person_id: student.sis_person_id,
},
startOfStudyright,
thesisInfo: thesisData
? {
grade: thesisData.grade,
attainmentDate: thesisData.attainment_date,
courseCode: thesisData.course.code,
}
: null,
programme: {
code: programme?.code,
name: programme?.name,
},
credits: {
hops: student.studyplans[0].completed_credits,
all: student.creditcount,
},
}
}
const findStudentsCloseToGraduation = async () =>
(
await Student.findAll({
attributes: ['abbreviatedname', 'creditcount', 'sis_person_id', 'studentnumber'],
include: [
{
model: Studyplan,
attributes: ['completed_credits', 'programme_code'],
where: {
completed_credits: {
[Op.gte]: 160,
},
programme_code: {
[Op.like]: 'KH%',
},
},
include: [
{
model: Studyright,
attributes: ['startdate', 'studyrightid'],
where: {
graduated: 0,
cancelled: false,
},
include: [
{
model: StudyrightElement,
attributes: ['code'],
include: [
{
model: ElementDetail,
where: {
type: 20,
},
attributes: ['code', 'name'],
},
],
},
],
},
],
},
{
model: Credit,
attributes: ['attainment_date', 'grade'],
required: false,
where: {
credittypecode: 4,
},
include: {
model: Course,
attributes: ['code'],
where: {
course_unit_type: 'urn:code:course-unit-type:bachelors-thesis',
},
include: {
model: Organization,
attributes: ['code'],
through: {
attributes: [],
},
},
},
},
],
})
).map(student => formatStudent(student.toJSON()))
const getCloseToGraduationData = async () => {
const dataOnRedis = await redisClient.getAsync(CLOSE_TO_GRADUATION_REDIS_KEY)
if (dataOnRedis) return JSON.parse(dataOnRedis)
const freshData = await findStudentsCloseToGraduation()
redisClient.setAsync(CLOSE_TO_GRADUATION_REDIS_KEY, JSON.stringify(freshData))
return freshData
}
module.exports = {
findStudentsCloseToGraduation,
getCloseToGraduationData,
CLOSE_TO_GRADUATION_REDIS_KEY,
}
@@ -111,7 +111,7 @@ const getStudentsIncludeCoursesBetween = async (studentnumbers, startDate, endDa
raw: true,
}),
Enrollment.findAll({
attributes: ['course_code', 'state', 'enrollment_date_time', 'studentnumber', 'semestercode'],
attributes: ['course_code', 'state', 'enrollment_date_time', 'studentnumber', 'semestercode', 'studyright_id'],
where: {
enrollment_date_time: {
[Op.between]: [attainmentDateFrom, endDate],
@@ -162,6 +162,7 @@ const getStudentsIncludeCoursesBetween = async (studentnumbers, startDate, endDa
'cancelled',
'is_ba_ma',
'semester_enrollments',
'actual_studyrightid',
],
separate: true,
include: [
@@ -207,6 +208,7 @@ const getStudentsIncludeCoursesBetween = async (studentnumbers, startDate, endDa
'student_studentnumber',
'course_code',
'language',
'studyright_id',
],
where: creditsOfStudent,
raw: true,
@@ -3,6 +3,7 @@ const { getStudentsIncludeCoursesBetween } = require('./getStudentsIncludeCourse
const { optimizedStatisticsOf } = require('./optimizedStatisticsOf')
const { studentnumbersWithAllStudyrightElements } = require('./studentnumbersWithAllStudyrightElements')
const { getEarliestYear } = require('./shared')
const { getStudentsCloseToGraduation } = require('./closeToGraduation')
module.exports = {
bottlenecksOf,
@@ -10,4 +11,5 @@ module.exports = {
optimizedStatisticsOf,
studentnumbersWithAllStudyrightElements,
getEarliestYear,
getStudentsCloseToGraduation,
}
@@ -31,19 +31,19 @@ const optimizedStatisticsOf = async (query, studentnumberlist) => {
// db startdate is formatted to utc so need to change it when querying
const formattedStartDate = new Date(moment.tz(startDate, 'Europe/Helsinki').format()).toUTCString()
const studentnumbers = studentnumberlist
? studentnumberlist
: await studentnumbersWithAllStudyrightElements({
studyRights,
startDate: formattedStartDate,
endDate,
exchangeStudents,
nondegreeStudents,
transferredOutStudents: transferredStudents,
tag: null,
transferredToStudents: true,
graduatedStudents: true,
})
const studentnumbers =
studentnumberlist ||
(await studentnumbersWithAllStudyrightElements({
studyRights,
startDate: formattedStartDate,
endDate,
exchangeStudents,
nondegreeStudents,
transferredOutStudents: transferredStudents,
tag: null,
transferredToStudents: true,
graduatedStudents: true,
}))
const code = studyRights[0] || ''
let optionData = {}
@@ -1,5 +1,4 @@
const Sequelize = require('sequelize')
const { Op } = Sequelize
const { Op } = require('sequelize')
const moment = require('moment')
const { Credit, Course, Studyright, ElementDetail, StudyrightElement } = require('../../models')
const {
@@ -97,7 +96,16 @@ const formatStudentForPopulationStatistics = (
code,
currentSemester
) => {
const toCourse = ({ grade, attainment_date, credits, course_code, credittypecode, isStudyModule, language }) => {
const toCourse = ({
grade,
attainment_date,
credits,
course_code,
credittypecode,
isStudyModule,
language,
studyright_id,
}) => {
const attainment_date_normailized =
attainment_date < startDate ? startDateMoment.clone().add(1, 'day').toISOString() : attainment_date
const passed = Credit.passed({ credittypecode })
@@ -111,11 +119,14 @@ const formatStudentForPopulationStatistics = (
isStudyModuleCredit: isStudyModule,
credittypecode,
language,
studyright_id,
}
}
const criteriaCoursesBySubstitutions = criteria?.allCourses
? Object.keys(criteria.allCourses).reduce((acc, code) => {
acc[code] = code
// TODO: Check this line, arrow functions shouldn't return assignments
// eslint-disable-next-line no-return-assign
criteria.allCourses[code].map(subst => (acc[subst] = code))
return acc
}, {})
@@ -264,7 +275,7 @@ const getOptionsForStudents = async (students, code, level) => {
currentExtent = 2
optionExtent = 1
} else {
throw new Error('Invalid study level ' + level)
throw new Error(`Invalid study level ${level}`)
}
const programmes = await getAllProgrammes()
@@ -277,7 +288,7 @@ const getOptionsForStudents = async (students, code, level) => {
studentnumber: {
[Op.in]: students,
},
code: code,
code,
},
},
],
@@ -440,7 +451,7 @@ const formatStudentsForApi = async (
}
const formatQueryParamArrays = (query, params) => {
let res = { ...query }
const res = { ...query }
params.forEach(p => {
if (!res[p]) return
res[p] = Array.isArray(res[p]) ? res[p] : [res[p]]
@@ -26,7 +26,7 @@ const studentnumbersWithAllStudyrightElements = async ({
filteredExtents.push(6, 9, 13, 14, 18, 22, 23, 99)
}
let studyrightWhere = {
const studyrightWhere = {
extentcode: {
[Op.notIn]: filteredExtents,
},
@@ -35,7 +35,7 @@ const studentnumbersWithAllStudyrightElements = async ({
},
}
let studentWhere = {}
const studentWhere = {}
if (tag) {
const taggedStudentnumbers = await TagStudent.findAll({
attributes: ['studentnumber'],
@@ -70,7 +70,7 @@ const studentnumbersWithAllStudyrightElements = async ({
where: {
[Op.or]: [
{
['$studyright_elements->element_detail.type$']: {
'$studyright_elements->element_detail.type$': {
[Op.ne]: 20,
},
},
@@ -92,7 +92,7 @@ const studentnumbersWithAllStudyrightElements = async ({
raw: true,
})
let studentnumbers = [...new Set(students.map(s => s.student_studentnumber))]
const studentnumbers = [...new Set(students.map(s => s.student_studentnumber))]
// bit hacky solution, but this is used to filter out studentnumbers who have since changed studytracks
const rights = await Studyright.findAll({
attributes: ['studyrightid'],
@@ -1,8 +1,7 @@
const Sequelize = require('sequelize')
const { Op } = require('sequelize')
const { ExcludedCourse } = require('../models/models_kone')
const { Op } = Sequelize
const logger = require('../util/logger')
const { combinedStudyprogrammes } = require('./studyprogramme/studyprogrammeHelpers.js')
const { combinedStudyprogrammes } = require('./studyprogramme/studyprogrammeHelpers')
const { dbConnections } = require('../database/connection')
const { ProgrammeModule } = require('../models')
@@ -12,7 +11,7 @@ const getCurriculumVersions = async code => {
if (!result) return
const modified = result.map(r => ({
...r,
curriculum_period_ids: r.curriculum_period_ids.map(id => parseInt(id.substring(6)) + 1949),
curriculum_period_ids: r.curriculum_period_ids.map(id => parseInt(id.substring(6), 10) + 1949),
}))
return modified
} catch (e) {
@@ -66,6 +65,19 @@ const modifyParent = (course, moduleMap) => {
return { ...course, parent_id: parent.id, parent_code: parent.code, parent_name: parent.name }
}
const labelProgammes = (modules, excludedCourses) => {
return modules.map(module => {
const label = {
id: module.parent_name.fi,
label: `${module.parent_code}\n${module.parent_name.fi}`,
orderNumber: module.module_order,
}
const foundCourse = excludedCourses.find(course => course.course_code === module.code)
const visible = { visibility: !foundCourse, id: foundCourse?.id ?? null }
return { ...module, label, visible }
})
}
const getCoursesAndModulesForProgramme = async (code, period_ids) => {
if (!period_ids) return {}
const result = await recursivelyGetModuleAndChildren(
@@ -107,17 +119,4 @@ const getCoursesAndModules = async (code, period_ids) => {
return { defaultProgrammeCourses, secondProgrammeCourses: { courses: [], modules: [] } }
}
const labelProgammes = (modules, excludedCourses) => {
return modules.map(module => {
const label = {
id: module.parent_name.fi,
label: `${module.parent_code}\n${module.parent_name.fi}`,
orderNumber: module.module_order,
}
const foundCourse = excludedCourses.find(course => course.course_code === module.code)
const visible = { visibility: !foundCourse, id: foundCourse?.id ?? null }
return { ...module, label, visible }
})
}
module.exports = { getCoursesAndModules, getCurriculumVersions }
@@ -28,19 +28,24 @@ const { getCourseCodesOfProvider } = require('./providers')
'separate'
Erillisoikeus: opintosuoritukset, joiden suorituksen luokittelu on "erillisellä opiskeluoikeudella" tai
"opettajankoulutuksen erillisellä opiskeluikeudella" suoritettu. Ei sisällä kansainvälisten vaihto-opiskelijoiden suorituksia.
Ulkomailta hyväksiluetut opintopisteet: ulkomailta hyväksilutetut opintopisteet.
'transferred'
Ulkomailta hyväksiluetut opintopisteet: ulkomailta hyväksilutetut opintopisteet.
Muut hyväksiluetut opintopisteet: kotimaassa suoritetut, hyväksiluetut opintopisteet.
Possible ambiguities / mistakes:
Rapo may use createddate instead of attainment_date to determine if student had active studyright.
This is probably very rare to cause differences, only one such was found in MH60_001
*/
const getCategory = (extent, hadDegreeStudyright) => {
if ([1, 2, 3, 4].includes(extent) || hadDegreeStudyright) return 'basic' // Rapo-kategoria: Perustutkinto-opiskelijat
if (extent === 7 || extent === 34) return 'incoming-exchange' // Saapuvat vaihto-opiskelijat
if (extent === 9 && !hadDegreeStudyright) return 'open-uni' // Avoimen opiskeluoikeus, ei tutkinto-opiskelija
if (extent === 14) return 'agreement' // Korkeakoulujen väliset yhteistyöopinnot
if (extent === 99) return 'separate' // Erillisoikeus
return 'other'
const getCategory = (extent, degreeStudyright) => {
if ([7, 34].includes(extent)) return 'incoming-exchange' // Saapuvat vaihto-opiskelijat
if ([9, 31].includes(extent) && !degreeStudyright) return 'open-uni' // Avoimen opiskeluoikeus, ei tutkinto-opiskelija
if ([14, 16].includes(extent)) return 'agreement' // Korkeakoulujen väliset yhteistyöopinnot
if ([13, 18, 22, 23, 99].includes(extent)) return 'separate' // Erillisoikeus
if ([1, 2].includes(extent) || (degreeStudyright && [1, 2].includes(degreeStudyright.extentcode))) return 'basic' // Rapo-kategoria: Perustutkinto-opiskelijat
return 'other' // TODO: These still happen, but not a lot. Should be investigated which go here and where they should go instead
}
/*
@@ -59,32 +64,36 @@ const findRelevantStudyright = (attainmentDate, studyrights) => {
})
}
// At the given date, student had SOME degree-studyright.
const isDegreeStudent = (studyrights, date) =>
studyrights?.some(sr => {
const getDegreeStudyright = (studyrights, date, semestercode) =>
studyrights?.find(sr => {
const rightExtentCode = [1, 2, 3, 4].includes(sr.extentcode)
const rightDates =
new Date(sr.startdate).getTime() <= new Date(date).getTime() &&
new Date(date).getTime() <= new Date(sr.enddate).getTime()
// it is possible to also check if student had active semester enrollment to the studyright here.
// However, it is unsure if rapo requires this (seems like maybe not). shouldn't make a huge difference.
return rightExtentCode && rightDates
const enrolled = sr.semesterEnrollments?.some(e => e.semestercode === semestercode && e.enrollmenttype === 1)
return rightExtentCode && rightDates && enrolled
})
const getCreditsProduced = async (provider, isAcademicYear, specialIncluded = true) => {
let data = await getCreditStats(provider, isAcademicYear, specialIncluded)
if (data) return data
data = await computeCreditsProduced(provider, isAcademicYear, specialIncluded)
await setCreditStats(data, isAcademicYear, specialIncluded)
return data
}
/* Calculates credits produced by provider (programme or faculty) */
const computeCreditsProduced = async (providerCode, isAcademicYear, specialIncluded = true) => {
const since = new Date('2017-01-01')
const providercode = mapToProviders([providerCode])[0]
const courses = await getCourseCodesOfProvider(providercode)
const credits = await getCreditsForProvider(providercode, courses, since)
const rapoFormattedProviderCode = mapToProviders([providerCode])[0]
const courses = await getCourseCodesOfProvider(rapoFormattedProviderCode)
const credits = await getCreditsForProvider(rapoFormattedProviderCode, courses, since)
// This part also adds oikis vaasa which is provided by different organization.
// Uknown if there are other similar cases!
const vaasaCodes = {
'200-K001': '200-K0012',
'200-M001': '200-M0012',
}
const vaasaProvider = vaasaCodes[rapoFormattedProviderCode]
if (vaasaProvider) {
const courses = await getCourseCodesOfProvider(vaasaProvider)
const vaasaCredits = await getCreditsForProvider(vaasaProvider, courses, since)
credits.push(...vaasaCredits)
}
const students = [...new Set(credits.map(({ studentNumber }) => studentNumber))]
const transfers = (await allTransfers(providerCode, since)).map(t => t.studyrightid)
@@ -107,13 +116,17 @@ const computeCreditsProduced = async (providerCode, isAcademicYear, specialInclu
const stats = {}
credits.forEach(({ attainmentDate, studentNumber, credits, studyrightId }) => {
credits.forEach(({ attainmentDate, studentNumber, credits, studyrightId, semestercode }) => {
const relevantStudyright = studyrightId
? studyrightIdToStudyrightMap[studyrightId]
: findRelevantStudyright(attainmentDate, studentNumberToStudyrightsMap[studentNumber])
const attainmentYear = defineYear(attainmentDate, isAcademicYear)
const hadDegreeStudyright = isDegreeStudent(studentNumberToStudyrightsMap[studentNumber], attainmentDate)
const category = relevantStudyright ? getCategory(relevantStudyright.extentcode, hadDegreeStudyright) : 'other'
const degreeStudyright = getDegreeStudyright(
studentNumberToStudyrightsMap[studentNumber],
attainmentDate,
semestercode
)
const category = relevantStudyright ? getCategory(relevantStudyright.extentcode, degreeStudyright) : 'other'
if (!stats[attainmentYear]) stats[attainmentYear] = { transferred: 0 }
stats[attainmentYear].total += credits
if (!stats[attainmentYear][category]) {
@@ -122,10 +135,13 @@ const computeCreditsProduced = async (providerCode, isAcademicYear, specialInclu
stats[attainmentYear][category] += credits
})
const transferredCredits = await getTransferredCredits(providercode, since)
transferredCredits.forEach(({ attainment_date, credits }) => {
const attainmentYear = defineYear(attainment_date, isAcademicYear)
const transferredCredits = await getTransferredCredits(rapoFormattedProviderCode, since)
if (vaasaProvider) {
const vaasaTransferredCredits = await getTransferredCredits(vaasaProvider, since)
transferredCredits.push(...vaasaTransferredCredits)
}
transferredCredits.forEach(({ createdate, credits }) => {
const attainmentYear = defineYear(createdate, isAcademicYear)
if (!stats[attainmentYear]) stats[attainmentYear] = { transferred: 0 }
stats[attainmentYear].transferred += credits || 0
// Transferred not counted in total
@@ -134,6 +150,14 @@ const computeCreditsProduced = async (providerCode, isAcademicYear, specialInclu
return { stats, id: providerCode }
}
const getCreditsProduced = async (provider, isAcademicYear, specialIncluded = true) => {
let data = await getCreditStats(provider, isAcademicYear, specialIncluded)
if (data) return data
data = await computeCreditsProduced(provider, isAcademicYear, specialIncluded)
await setCreditStats(data, isAcademicYear, specialIncluded)
return data
}
module.exports = {
getCreditsProduced,
computeCreditsProduced,
+1 -1
View File
@@ -1,6 +1,6 @@
const redis = require('redis')
const bluebird = require('bluebird')
const conf = require('../conf-backend.js')
const conf = require('../conf-backend')
const logger = require('../util/logger')
bluebird.promisifyAll(redis.RedisClient.prototype)
+1 -2
View File
@@ -1,7 +1,6 @@
const moment = require('moment')
const sequelize = require('sequelize')
const { Op } = require('sequelize')
const { Semester } = require('../models')
const Op = sequelize.Op
const getCurrentSemester = async () => {
const today = new Date()
@@ -1,7 +1,8 @@
const axios = require('axios')
const { SIS_UPDATER_URL, SECRET_TOKEN } = require('../conf-backend')
const { Studyplan } = require('../models')
const { Studyplan, Student, StudyrightElement } = require('../models')
const { Op } = require('sequelize')
const logger = require('../util/logger')
const client = axios.create({ baseURL: SIS_UPDATER_URL })
@@ -38,6 +39,37 @@ const updateSISRedisCache = async () => {
const response = await client.get('/v1/rediscache', params)
return response.data
}
const delay = time => {
return new Promise(res => {
setTimeout(res, time)
})
}
const updateStudentsIndividually = async () => {
try {
const studentNumbers = await Student.findAll({
attributes: ['studentnumber'],
include: {
model: StudyrightElement,
where: {
code: { [Op.regexp]: '(KH*|MH*)' },
},
},
})
const uniqueStudents = [...new Set(studentNumbers.map(s => s.studentnumber))]
const chunkSize = 4000
for (let from = 0; from < uniqueStudents.length - chunkSize; from += chunkSize) {
await client.post(
'v1/students',
{ studentnumbers: uniqueStudents.slice(from, from + chunkSize), individualMode: true },
params
)
await delay(200)
}
} catch (e) {
logger.error(e)
}
}
const studyplansUpdate = async days => {
const limitDate = new Date()
@@ -67,4 +99,5 @@ module.exports = {
abort,
updateCoursesByCourseCode,
studyplansUpdate,
updateStudentsIndividually,
}
+8 -10
View File
@@ -1,4 +1,4 @@
const Sequelize = require('sequelize')
const { Op } = require('sequelize')
const {
dbConnections: { sequelize },
} = require('../database/connection')
@@ -16,7 +16,6 @@ const {
Transfer,
} = require('../models')
const { TagStudent, Tag } = require('../models/models_kone')
const Op = Sequelize.Op
const logger = require('../util/logger')
const createStudent = student => Student.create(student)
@@ -288,7 +287,7 @@ const formatStudent = async ({
updatedAt: updatedAt || createdAt,
studyplans,
tags,
sis_person_id: sis_person_id,
sis_person_id,
}
}
@@ -321,13 +320,12 @@ const nameLike = terms => {
const [first, second] = terms
if (!second) {
return columnLike('abbreviatedname', first)
} else {
return {
[Op.or]: [
columnLike('abbreviatedname', `%${first}%${second}%`),
columnLike('abbreviatedname', `%${second}%${first}%`),
],
}
}
return {
[Op.or]: [
columnLike('abbreviatedname', `%${first}%${second}%`),
columnLike('abbreviatedname', `%${second}%${first}%`),
],
}
}
@@ -1,4 +1,5 @@
const { getImporterClient } = require('../util/importerClient')
const importerClient = getImporterClient()
const { StudyGuidanceGroupTag } = require('../models/models_kone')
const logger = require('../util/logger')
@@ -38,9 +39,16 @@ const getAllStudentsUserHasInGroups = async sisPersonId =>
.map(member => member.personStudentNumber)
)
const getTagToUpdate = (studyProgramme, year) => {
if (studyProgramme || studyProgramme === null) {
return { studyProgramme }
}
return { year }
}
const changeGroupTags = async ({ groupId, tags }) => {
const { studyProgramme, year } = tags
const tagToUpdate = studyProgramme ? { studyProgramme } : { year }
const tagToUpdate = getTagToUpdate(studyProgramme, year)
const [result] = await StudyGuidanceGroupTag.upsert(
{
@@ -1,56 +1,93 @@
const { Op } = require('sequelize')
const { Credit, Course, Organization } = require('../../models')
const { Credit, Course, Organization, CourseProvider } = require('../../models')
const { whereStudents } = require('.')
const {
dbConnections: { sequelize },
} = require('../../database/connection')
const { formatCredit } = require('./studyprogrammeHelpers')
const getCreditsForProvider = async (provider, codes, since) =>
(
await sequelize.query(
`
SELECT
cr.id,
cr.course_code,
cr.attainment_date,
cr.student_studentnumber,
cr.credits * COALESCE(share_element.share::numeric, 1) AS credits,
cr.studyright_id,
cr.semestercode
FROM
credit cr
JOIN
course_providers cp ON cr.course_id = cp.coursecode
JOIN
organization o ON cp.organizationcode = o.id
LEFT JOIN LATERAL
(
SELECT (jae ->> 'share')::numeric AS share
FROM jsonb_array_elements(cp.shares) AS jae
WHERE (cr.attainment_date >= (jae ->> 'startDate')::date OR jae ->> 'startDate' IS NULL)
AND (cr.attainment_date <= (jae ->> 'endDate')::date OR jae ->> 'endDate' IS NULL)
ORDER BY
(jae ->> 'startDate')::date DESC NULLS LAST,
(jae ->> 'endDate')::date DESC NULLS LAST
LIMIT 1
) AS share_element ON TRUE
WHERE
(o.code = :provider)
AND cr.attainment_date >= :since
AND cr.credittypecode = 4
AND cr."isStudyModule" IS NOT TRUE
`,
{
type: sequelize.QueryTypes.SELECT,
replacements: { provider, codes, since },
}
)
).map(formatCredit)
const getCreditsForProvider = async (provider, codes, since) => {
const credits = await Credit.findAll({
where: {
course_code: { [Op.in]: codes },
attainment_date: { [Op.gte]: since },
isStudyModule: { [Op.not]: true },
credittypecode: 4,
},
raw: true,
})
const courseIds = credits.map(cr => cr.course_id)
const providers = await CourseProvider.findAll({
where: {
coursecode: { [Op.in]: courseIds },
},
raw: true,
})
const organizationIds = providers.map(p => p.organizationcode)
const organizations = await Organization.findAll({
where: {
id: {
[Op.in]: organizationIds,
},
},
raw: true,
})
const organizationIdToCodeMap = organizations.reduce((obj, org) => {
obj[org.id] = org.code
return obj
}, {})
const courseIdToShareMap = providers.reduce((obj, provider) => {
if (!obj[provider.coursecode]) obj[provider.coursecode] = []
obj[provider.coursecode].push(provider)
return obj
}, {})
const courseIdToShare = (courseId, attainmentDate) => {
const providers = courseIdToShareMap[courseId]
const relevantProvider = providers.find(p => organizationIdToCodeMap[p.organizationcode] === provider)
if (!relevantProvider?.shares) return 0
const relevantShare = relevantProvider.shares
.filter(share => {
const startMatches = new Date(share.startDate) <= attainmentDate || !share.startDate
const endMatches = new Date(share.endDate) >= attainmentDate || !share.endDate
const bothArentNull = share.startDate || share.endDate
return startMatches && endMatches && bothArentNull
})
// The next mess is for cases where there are multiple shares with only startdate or enddate.
// This isn't pretty and it may not be 100% correct but because the
// data is all over the place, this is best approximation for now.
.sort((a, b) => {
if (!b.startDate || !a.startDate) return 0
const bTime = b.startDate ? new Date(b.startDate).getTime() : 0
const aTime = a.startDate ? new Date(a.startDate).getTime() : 0
return bTime - aTime
})
.sort((a, b) => {
if (!b.endDate || !a.endDate) return 0
const bTime = b.endDate ? new Date(b.endDate).getTime() : 0
const aTime = a.endDate ? new Date(a.endDate).getTime() : 0
return bTime - aTime
})[0]
// Odd logic, but if there are multiple providers for same course code, if no fitting dates are
// not found for our relevant provider, we can assume the share of that date is of some other provider.
// But if only 1 provider exists, we can assume it has share of 1.
if (!relevantShare) {
if (providers.length > 1) return 0
return 1
}
return relevantShare.share
}
return credits
.map(cr => ({ ...cr, credits: cr.credits * courseIdToShare(cr.course_id, cr.attainment_date, cr.id) }))
.map(formatCredit)
}
const getTransferredCredits = async (provider, since) =>
await Credit.findAll({
attributes: ['id', 'course_code', 'credits', 'attainment_date'],
attributes: ['id', 'course_code', 'credits', 'attainment_date', 'createdate'],
include: {
model: Course,
attributes: ['code'],
@@ -1,15 +1,14 @@
const _ = require('lodash')
const Sequelize = require('sequelize')
const { Op } = require('sequelize')
const {
dbConnections: { sequelize },
} = require('../../database/connection')
const { Op } = Sequelize
const { Credit, Course, Organization, ProgrammeModule, Transfer, Enrollment } = require('../../models')
const { formatTransfer } = require('./studyprogrammeHelpers')
const logger = require('../../util/logger')
const whereStudents = studentnumbers => {
return studentnumbers ? studentnumbers : { [Op.not]: null }
return studentnumbers || { [Op.not]: null }
}
const sinceDate = since => {
@@ -142,8 +142,8 @@ const getStudentsForProgrammeCourses = async (from, to, programmeCourses) => {
const result = res.map(course => ({
code: course.code,
name: course.course_name,
totalPassed: parseInt(course.total_students),
totalAllcredits: parseInt(course.total_credits),
totalPassed: parseInt(course.total_students, 10),
totalAllcredits: parseInt(course.total_credits, 10),
type: 'passed',
isStudyModule: course.isStudyModule,
}))
@@ -182,8 +182,8 @@ const getOwnStudentsForProgrammeCourses = async (from, to, programmeCourses, stu
const res = {
code: course.code,
name: course.course_name,
totalProgrammeStudents: parseInt(course.total_students),
totalProgrammeCredits: parseInt(course.total_credits),
totalProgrammeStudents: parseInt(course.total_students, 10),
totalProgrammeCredits: parseInt(course.total_credits, 10),
type: 'ownProgramme',
}
return res
@@ -226,8 +226,8 @@ const getOtherStudentsForProgrammeCourses = async (from, to, programmeCourses, s
return res.map(course => ({
code: course.code,
name: course.course_name,
totalOtherProgrammeStudents: parseInt(course.total_students),
totalOtherProgrammeCredits: parseInt(course.total_credits),
totalOtherProgrammeStudents: parseInt(course.total_students, 10),
totalOtherProgrammeCredits: parseInt(course.total_credits, 10),
type: 'otherProgramme',
isStudyModule: course.isStudyModule,
}))
@@ -257,8 +257,8 @@ const getTransferStudentsForProgrammeCourses = async (from, to, programmeCourses
return res.map(course => ({
code: course.code,
name: course.course_name,
totalTransferStudents: parseInt(course.total_students),
totalTransferCredits: parseInt(course.total_credits),
totalTransferStudents: parseInt(course.total_students, 10),
totalTransferCredits: parseInt(course.total_credits, 10),
isStudyModule: course.isStudyModule,
type: 'transfer',
}))
@@ -293,8 +293,8 @@ const getStudentsWithoutStudyrightForProgrammeCourses = async (from, to, program
return res.map(course => ({
code: course.code,
name: course.course_name,
totalWithoutStudyrightStudents: parseInt(course.total_students),
totalWithoutStudyrightCredits: parseInt(course.total_credits),
totalWithoutStudyrightStudents: parseInt(course.total_students, 10),
totalWithoutStudyrightCredits: parseInt(course.total_credits, 10),
type: 'noStudyright',
}))
}
@@ -6,7 +6,7 @@ const getCriteriaByStudyProgramme = async code => {
if (code === '') return null
return await ProgressCriteria.findOne({
where: {
code: code,
code,
},
})
}
@@ -71,22 +71,22 @@ const saveYearlyCreditCriteria = async (studyProgramme, credits) => {
if (!studyProgrammeToUpdate) {
const courseObj = { year1: [], year2: [], year3: [], year4: [], year5: [], year6: [] }
const creditsObj = {
year1: parseInt(credits.year1),
year2: parseInt(credits.year2),
year3: parseInt(credits.year3),
year4: parseInt(credits.year4),
year5: parseInt(credits.year5),
year6: parseInt(credits.year6),
year1: parseInt(credits.year1, 10),
year2: parseInt(credits.year2, 10),
year3: parseInt(credits.year3, 10),
year4: parseInt(credits.year4, 10),
year5: parseInt(credits.year5, 10),
year6: parseInt(credits.year6, 10),
}
return await createCriteria(studyProgramme, courseObj, creditsObj)
}
const yearlyCredits = {
creditsYearOne: parseInt(credits.year1),
creditsYearTwo: parseInt(credits.year2),
creditsYearThree: parseInt(credits.year3),
creditsYearFour: parseInt(credits.year4),
creditsYearFive: parseInt(credits.year5),
creditsYearSix: parseInt(credits.year6),
creditsYearOne: parseInt(credits.year1, 10),
creditsYearTwo: parseInt(credits.year2, 10),
creditsYearThree: parseInt(credits.year3, 10),
creditsYearFour: parseInt(credits.year4, 10),
creditsYearFive: parseInt(credits.year5, 10),
creditsYearSix: parseInt(credits.year6, 10),
}
try {
const updatedData = await studyProgrammeToUpdate.update({ ...yearlyCredits })
@@ -223,7 +223,7 @@ const getBasicStatsForStudytrack = async ({ studyprogramme, combinedProgramme, s
const reversedYears = getYearsArray(since.getFullYear(), isAcademicYear).reverse()
const key = includeAllSpecials ? 'SPECIAL_INCLUDED' : 'SPECIAL_EXCLUDED'
const titles = tableTitles['basics'][combinedProgramme ? `${key}_COMBINED_PROGRAMME` : key]
const titles = tableTitles.basics[combinedProgramme ? `${key}_COMBINED_PROGRAMME` : key]
const tableStats = reversedYears.map(year =>
initializeTableStats(
year,
@@ -24,8 +24,8 @@ const getAllStudyprogrammeCourses = async studyprogramme => {
const normalCourses = await getAllProgrammeCourses(providerCode)
return normalCourses.reduce((acc, curr) => {
acc.push(curr.code)
if (curr.substitutions && curr.substitutions.includes('AY' + curr.code)) {
acc.push('AY' + curr.code)
if (curr.substitutions && curr.substitutions.includes(`AY${curr.code}`)) {
acc.push(`AY${curr.code}`)
}
return acc
}, [])
@@ -34,7 +34,6 @@ const getAllStudyprogrammeCourses = async studyprogramme => {
const makeYearlyPromises = (years, academicYear, type, programmeCourses, studyprogramme) => {
return years.map(
year =>
// eslint-disable-next-line no-async-promise-executor
new Promise(async res => {
const from = academicYear === 'ACADEMIC_YEAR' ? new Date(year, 7, 1, 0, 0, 0) : new Date(year, 0, 1, 0, 0, 0)
const to =
@@ -66,7 +65,7 @@ const makeYearlyPromises = (years, academicYear, type, programmeCourses, studypr
res(
result.map(c => {
c['year'] = year
c.year = year
return c
})
)
@@ -227,8 +226,8 @@ const getStudyprogrammeCoursesForStudytrack = async (unixMillis, studyprogramme,
}
}
if (!acc[curr.code]['years'][curr.year]) {
acc[curr.code]['years'][curr.year] = {
if (!acc[curr.code].years[curr.year]) {
acc[curr.code].years[curr.year] = {
totalAllStudents: 0,
totalPassed: 0,
totalNotCompleted: 0,
@@ -246,30 +245,32 @@ const getStudyprogrammeCoursesForStudytrack = async (unixMillis, studyprogramme,
}
switch (curr.type) {
case 'passed':
acc[curr.code]['years'][curr.year]['totalPassed'] += curr.totalPassed
acc[curr.code]['years'][curr.year]['totalAllStudents'] += acc[curr.code]['years'][curr.year]['totalPassed']
acc[curr.code]['years'][curr.year]['totalAllCredits'] += curr.totalAllcredits
acc[curr.code].years[curr.year].totalPassed += curr.totalPassed
acc[curr.code].years[curr.year].totalAllStudents += acc[curr.code].years[curr.year].totalPassed
acc[curr.code].years[curr.year].totalAllCredits += curr.totalAllcredits
break
case 'notCompleted':
acc[curr.code]['years'][curr.year]['totalNotCompleted'] += curr.totalNotCompleted
acc[curr.code]['years'][curr.year]['totalAllStudents'] +=
acc[curr.code]['years'][curr.year]['totalNotCompleted']
acc[curr.code].years[curr.year].totalNotCompleted += curr.totalNotCompleted
acc[curr.code].years[curr.year].totalAllStudents += acc[curr.code].years[curr.year].totalNotCompleted
break
case 'ownProgramme':
acc[curr.code]['years'][curr.year]['totalProgrammeStudents'] += curr.totalProgrammeStudents
acc[curr.code]['years'][curr.year]['totalProgrammeCredits'] += curr.totalProgrammeCredits
acc[curr.code].years[curr.year].totalProgrammeStudents += curr.totalProgrammeStudents
acc[curr.code].years[curr.year].totalProgrammeCredits += curr.totalProgrammeCredits
break
case 'otherProgramme':
acc[curr.code]['years'][curr.year]['totalOtherProgrammeStudents'] += curr.totalOtherProgrammeStudents
acc[curr.code]['years'][curr.year]['totalOtherProgrammeCredits'] += curr.totalOtherProgrammeCredits
acc[curr.code].years[curr.year].totalOtherProgrammeStudents += curr.totalOtherProgrammeStudents
acc[curr.code].years[curr.year].totalOtherProgrammeCredits += curr.totalOtherProgrammeCredits
break
case 'noStudyright':
acc[curr.code]['years'][curr.year]['totalWithoutStudyrightStudents'] += curr.totalWithoutStudyrightStudents
acc[curr.code]['years'][curr.year]['totalWithoutStudyrightCredits'] += curr.totalWithoutStudyrightCredits
acc[curr.code].years[curr.year].totalWithoutStudyrightStudents += curr.totalWithoutStudyrightStudents
acc[curr.code].years[curr.year].totalWithoutStudyrightCredits += curr.totalWithoutStudyrightCredits
break
case 'transfer':
acc[curr.code]['years'][curr.year]['totalTransferStudents'] += curr.totalTransferStudents
acc[curr.code]['years'][curr.year]['totalTransferCredits'] += curr.totalTransferCredits
acc[curr.code].years[curr.year].totalTransferStudents += curr.totalTransferStudents
acc[curr.code].years[curr.year].totalTransferCredits += curr.totalTransferCredits
break
default:
break
}
return acc
@@ -294,15 +295,15 @@ const getStudyprogrammeCoursesForStudytrack = async (unixMillis, studyprogramme,
if (allCourses[normCode]) {
const mergedCourse = {}
mergedCourse['code'] = allCourses[normCode].code
mergedCourse['name'] = allCourses[normCode].name
mergedCourse['years'] = {}
mergedCourse.code = allCourses[normCode].code
mergedCourse.name = allCourses[normCode].name
mergedCourse.years = {}
yearRange
.filter(year => year <= maxYear)
.forEach(year => {
if (!allCourses[normCode]['years'][year]) {
mergedCourse['years'][year] = {
if (!allCourses[normCode].years[year]) {
mergedCourse.years[year] = {
totalAllStudents: 0,
totalPassed: 0,
totalNotCompleted: 0,
@@ -317,13 +318,12 @@ const getStudyprogrammeCoursesForStudytrack = async (unixMillis, studyprogramme,
totalTransferStudents: 0,
}
} else {
mergedCourse['years'][year] = { ...allCourses[normCode]['years'][year] }
mergedCourse.years[year] = { ...allCourses[normCode].years[year] }
}
if (allCourses[ayCourse]['years'][year]) {
if (allCourses[ayCourse].years[year]) {
properties.forEach(prop => {
mergedCourse['years'][year][prop] =
mergedCourse['years'][year][prop] + allCourses[ayCourse]['years'][year][prop]
mergedCourse.years[year][prop] += allCourses[ayCourse].years[year][prop]
})
}
})
@@ -333,7 +333,7 @@ const getGoal = programme => {
if (programme.startsWith('LI')) {
return 78
}
return 48 //unknown, likely old doctor or licentiate
return 48 // unknown, likely old doctor or licentiate
}
module.exports = {
@@ -23,11 +23,12 @@ const { studytrackStudents, enrolledStudents, absentStudents } = require('./stud
const getUnique = studentnumbers => [...new Set(studentnumbers)]
const getStudentData = (startDate, students) => {
let data = { female: 0, male: 0, otherUnkown: 0, finnish: 0, otherCountries: 0, otherCountriesCounts: {} }
const data = { female: 0, male: 0, otherUnkown: 0, finnish: 0, otherCountries: 0, otherCountriesCounts: {} }
const creditCounts = []
students.forEach(({ genderCode, homeCountryEn, credits }) => {
const creditcount = credits
.filter(credit => moment(credit.attainment_date).isAfter(startDate))
// eslint-disable-next-line no-return-assign
.reduce((prev, curr) => (prev += curr.credits), 0)
creditCounts.push(creditcount)
@@ -103,7 +104,7 @@ const getGraduationTimeStats = async ({
...graduationTimes[track].medians.combo,
{
y: medianCombo,
amount: graduationAmounts[track]['combo'][year],
amount: graduationAmounts[track].combo[year],
name: year,
statistics: statisticsCombo,
classSize,
@@ -112,7 +113,7 @@ const getGraduationTimeStats = async ({
graduationTimes[track].medians.basic = [
...graduationTimes[track].medians.basic,
{ y: median, amount: graduationAmounts[track]['basic'][year], name: year, statistics, classSize },
{ y: median, amount: graduationAmounts[track].basic[year], name: year, statistics, classSize },
]
}
@@ -288,11 +289,12 @@ const getStudytrackDataForTheYear = async ({
return acc
}, {})
otherCountriesCount[track]['Total'] = countryStatsFromYears
otherCountriesCount[track].Total = countryStatsFromYears
}
// If the track has no stats for that year, it should be removed from the table and dropdown options
if (all.length === 0) {
// eslint-disable-next-line no-unused-expressions
emptyTracks.has(track) ? emptyTracks.set(track, emptyTracks.get(track) + 1) : emptyTracks.set(track, 1)
return
}
@@ -319,7 +321,7 @@ const getStudytrackDataForTheYear = async ({
// Count stats for the main studytrack table grouped by year
mainStatsByYear[year] = [
[
studytrackNames[track]?.name['fi'] ? `${studytrackNames[track]?.name['fi']}, ${track}` : year,
studytrackNames[track]?.name.fi ? `${studytrackNames[track]?.name.fi}, ${track}` : year,
...getStats(
all,
started,
@@ -484,7 +486,7 @@ const getStudytrackStatsForStudyprogramme = async ({ studyprogramme, combinedPro
const graduatedTitles = combinedProgramme ? getCorrectCombinedTitles() : tableTitles.studytracksBasic
return {
id: combinedProgramme ? `${studyprogramme}-${combinedProgramme}` : studyprogramme,
years: years,
years,
...data,
doCombo,
studytrackOptions,
+12 -13
View File
@@ -3,10 +3,11 @@ const {
} = require('../database/connection')
const { Studyright, StudyrightElement, ElementDetail, Transfer } = require('../models')
const moment = require('moment')
const { redisClient } = require('../services/redis')
const { redisClient } = require('./redis')
const _ = require('lodash')
const { Op, col, where, fn } = require('sequelize')
const logger = require('../util/logger')
const REDIS_KEY = 'STUDYRIGHT_ASSOCIATIONS_V2'
const byStudent = studentNumber => {
@@ -107,9 +108,8 @@ const allUniqueStudyrightCodeArrays = () =>
const uniqueStudyrightAssocations = elementcodes => {
if (elementcodes === undefined) {
return allUniqueStudyrightCodeArrays()
} else {
return uniqueStudyrightCodeArrays(elementcodes)
}
return uniqueStudyrightCodeArrays(elementcodes)
}
const getAssociatedStudyrights = async elementcodes => {
@@ -199,7 +199,7 @@ const calculateAssociationsFromDb = async (chunksize = 100000) => {
return 'FALL'
}
const getEnrollmentStartYear = momentstartdate => {
if (getSemester(momentstartdate) == 'SPRING') return momentstartdate.utc(2).year() - 1
if (getSemester(momentstartdate) === 'SPRING') return momentstartdate.utc(2).year() - 1
return momentstartdate.year()
}
const total = await Studyright.count()
@@ -221,15 +221,15 @@ const calculateAssociationsFromDb = async (chunksize = 100000) => {
}
} else if (type === StudyRightType.PROGRAMME) {
associations.programmes[code] = associations.programmes[code] || {
type: type,
name: name,
code: code,
type,
name,
code,
enrollmentStartYears: {},
studytracks: [],
}
const momentstartdate = moment(startdate)
const enrollment = momentstartdate.isValid() ? getEnrollmentStartYear(momentstartdate) : null
const enrollmentStartYears = associations.programmes[code].enrollmentStartYears
const { enrollmentStartYears } = associations.programmes[code]
if (!enrollmentStartYears[enrollment])
enrollmentStartYears[enrollment] = {
studyTracks: {},
@@ -269,9 +269,9 @@ const calculateAssociationsFromDb = async (chunksize = 100000) => {
programmes: {},
}
associations.studyTracks[e.code].programmes[code] = {
type: type,
name: name,
code: code,
type,
name,
code,
}
}
})
@@ -304,9 +304,8 @@ const getAssociations = async (doRefresh = false) => {
const associations = await calculateAssociationsFromDb()
await saveAssociationsToRedis(associations)
return associations
} else {
return studyrights
}
return studyrights
}
const getFilteredAssociations = async codes => {
+2 -4
View File
@@ -1,8 +1,6 @@
const Sequelize = require('sequelize')
const { Op } = require('sequelize')
const { Tag } = require('../models/models_kone')
const Op = Sequelize.Op
const findTagById = tag_id =>
Tag.findOne({
where: {
@@ -34,7 +32,7 @@ const findTagsFromStudytrackById = async (studytrack, tag_ids) => {
}
const createNewTag = async tag => {
if (isNaN(tag.year)) {
if (Number.isNaN(tag.year)) {
const newTag = {
...tag,
year: null,
+1 -3
View File
@@ -1,8 +1,6 @@
const Sequelize = require('sequelize')
const { Op } = require('sequelize')
const { TagStudent, Tag } = require('../models/models_kone')
const Op = Sequelize.Op
const getStudentTags = () => TagStudent.findAll({})
const getStudentTagsByStudytrack = studytrack => {
+4 -3
View File
@@ -76,7 +76,8 @@ const markCredit = (stats, passed, failed, credits, transferred) => {
passed: stats.passed + 1,
transferred: transferred ? stats.transferred + credits : stats.transferred,
}
} else if (failed) {
}
if (failed) {
return {
...stats,
failed: stats.failed + 1,
@@ -134,6 +135,8 @@ const markCreditForCourse = (courses, credit) => {
}
}
const isRegularCourse = credit => !credit.isStudyModule
const teacherStats = async teacherid => {
const teacher = await findTeacherCredits(teacherid)
const statistics = teacher.credits.filter(isRegularCourse).reduce(
@@ -230,8 +233,6 @@ const getCredits = (teacherIds, semestercodeStart, semestercodeEnd) =>
},
})
const isRegularCourse = credit => !credit.isStudyModule
const calculateCreditStatistics = credits =>
credits.reduce(
(stats, credit) => {
+11 -11
View File
@@ -1,4 +1,4 @@
const { redisClient } = require('../services/redis')
const { redisClient } = require('./redis')
const { getSemestersAndYears, getCurrentSemester } = require('./semesters')
const { Teacher, Semester, Credit, Course } = require('../models')
const { Op } = require('sequelize')
@@ -78,14 +78,14 @@ const updatedStats = (statistics, teacher, passed, failed, credits, transferred)
credits: transferred ? stats.credits : stats.credits + credits,
transferred: transferred ? stats.transferred + credits : stats.transferred,
}
} else if (failed) {
}
if (failed) {
return {
...stats,
failed: stats.failed + 1,
}
} else {
return stats
}
return stats
}
const isRegularCourse = credit => !credit.isStudyModule
@@ -130,19 +130,19 @@ const findTopTeachers = async yearcode => {
}
}
const findAndSaveTopTeachers = async yearcode => {
const { all, openuni } = await findTopTeachers(yearcode)
await setTeacherStats(ID.OPENUNI, yearcode, openuni)
await setTeacherStats(ID.ALL, yearcode, all)
}
const findAndSaveTeachers = async (startcode = 1, to) => {
const endcode = to || (await getCurrentSemester()).getDataValue('yearcode')
for (let code = startcode; code <= endcode; code++) {
await findAndSaveTopTeachers(code)
logger.info(`Teacher leaderboard for yearcode ${code} calculated`)
}
logger.info(`Teacher leaderboard calculations done`)
}
const findAndSaveTopTeachers = async yearcode => {
const { all, openuni } = await findTopTeachers(yearcode)
await setTeacherStats(ID.OPENUNI, yearcode, openuni)
await setTeacherStats(ID.ALL, yearcode, all)
logger.info('Teacher leaderboard calculations done')
}
module.exports = {
+116 -96
View File
@@ -3,12 +3,12 @@ const LRU = require('lru-cache')
const _ = require('lodash')
const { getStudentnumbersByElementdetails } = require('./students')
const { facultiesAndProgrammesForTrends } = require('../services/organisations')
const { sendNotificationAboutNewUser } = require('../services/mailservice')
const { checkStudyGuidanceGroupsAccess, getAllStudentsUserHasInGroups } = require('../services/studyGuidanceGroups')
const { User, UserElementDetails, AccessGroup, UserFaculties, sequelizeUser } = require('../models/models_user')
const { sendNotificationAboutNewUser } = require('./mailservice')
const { checkStudyGuidanceGroupsAccess, getAllStudentsUserHasInGroups } = require('./studyGuidanceGroups')
const { User, UserElementDetails, AccessGroup } = require('../models/models_user')
const { getUserIams, getAllUserAccess, getUserIamAccess } = require('../util/jami')
const { createLocaleComparator } = require('../util/utils')
const { createLocaleComparator, getFullStudyProgrammeRights } = require('../util/utils')
const { sequelizeUser } = require('../database/connection')
const courseStatisticsGroup = 'grp-oodikone-basic-users'
const facultyStatisticsGroup = 'grp-oodikone-users'
@@ -32,11 +32,6 @@ const userIncludes = [
as: 'accessgroup',
attributes: ['id', 'group_code', 'group_info'],
},
{
separate: true,
model: UserFaculties,
as: 'faculty',
},
]
const accessGroupsByCodes = codes =>
@@ -64,13 +59,33 @@ const byId = async id =>
include: userIncludes,
})
// Crud things from old userservice
const deleteOutdatedUsers = async () =>
await sequelizeUser.transaction(async t => {
const outdatedUsersCTE = `
WITH outdated_users AS (
SELECT id FROM users
WHERE last_login IS NULL OR last_login < CURRENT_DATE - INTERVAL '18 months'
)`
// some fields that frontend needs
const formatUserForAdminView = user => ({
...user.get(),
elementdetails: user.programme.map(p => p.elementDetailCode),
})
await sequelizeUser.query(
`${outdatedUsersCTE} DELETE FROM user_faculties WHERE "userId" IN (SELECT id FROM outdated_users)`,
{ transaction: t }
)
await sequelizeUser.query(
`${outdatedUsersCTE} DELETE FROM user_accessgroup WHERE "userId" IN (SELECT id FROM outdated_users)`,
{ transaction: t }
)
await sequelizeUser.query(
`${outdatedUsersCTE} DELETE FROM user_elementdetails WHERE "userId" IN (SELECT id FROM outdated_users)`,
{ transaction: t }
)
const [results] = await sequelizeUser.query(
`${outdatedUsersCTE} DELETE FROM users WHERE id IN (SELECT id FROM outdated_users) RETURNING id`,
{ transaction: t }
)
return results.length
})
const addProgrammes = async (id, codes) => {
for (const code of codes) {
@@ -96,6 +111,7 @@ const modifyRights = async (username, rights) => {
if (val === true) {
return code
}
return undefined
})
.filter(code => code)
const rightsToRemove = Object.entries(rights)
@@ -103,6 +119,7 @@ const modifyRights = async (username, rights) => {
if (val === false) {
return code
}
return undefined
})
.filter(code => code)
@@ -119,33 +136,22 @@ const modifyAccess = async body => {
const { username, accessgroups } = body
await modifyRights(username, accessgroups)
userDataCache.del(username)
return formatUserForAdminView(await byUsername(username))
const updatedUser = await byUsername(username)
return updatedUser
}
const enableElementDetails = async (id, codes) => {
await addProgrammes(id, codes)
const user = await byId(id)
userDataCache.del(user.username)
return formatUserForAdminView(user)
}
const setFaculties = async (id, faculties) => {
await sequelizeUser.transaction(async transaction => {
await UserFaculties.destroy({ where: { userId: id }, transaction })
for (const faculty of faculties) {
await UserFaculties.create({ userId: id, faculty_code: faculty }, { transaction })
}
})
const user = await byId(id)
userDataCache.del(user.username)
return formatUserForAdminView(user)
return user
}
const removeElementDetails = async (id, codes) => {
await removeProgrammes(id, codes)
const user = await byId(id)
userDataCache.del(user.userId)
return formatUserForAdminView(user)
return user
}
const updateUser = async (username, fields) => {
@@ -158,21 +164,87 @@ const updateUser = async (username, fields) => {
userDataCache.del(username)
return formatUserForAdminView(await byUsername(username))
const updatedUser = await byUsername(username)
return updatedUser
}
const getAccessGroups = async () => await AccessGroup.findAll()
const getStudyProgrammeRights = (iamAccess, specialGroup, userProgrammes) => {
const iamBasedProgrammes = []
for (const [programmeCode, rights] of Object.entries(iamAccess || {})) {
if (specialGroup.kosu || rights.admin) {
iamBasedProgrammes.push({ code: programmeCode, limited: false, isIamBased: true })
} else {
iamBasedProgrammes.push({ code: programmeCode, limited: true, isIamBased: true })
}
}
return [
...iamBasedProgrammes,
...userProgrammes.map(({ elementDetailCode }) => ({
code: elementDetailCode,
limited: false,
isIamBased: false,
})),
]
}
const formatUser = async (userFromDb, extraRights, programmeRights, getStudentAccess = true) => {
const accessGroups = userFromDb.accessgroup.map(entry => entry.group_code)
const fullStudyProgrammeRights = getFullStudyProgrammeRights(programmeRights)
const {
dataValues: {
id,
username: userId,
full_name: name,
email,
language,
sisu_person_id: sisPersonId,
iam_groups: iamGroups,
},
} = userFromDb
const studentsUserCanAccess = getStudentAccess
? _.uniqBy(
(
await Promise.all([
getStudentnumbersByElementdetails(fullStudyProgrammeRights),
getAllStudentsUserHasInGroups(sisPersonId),
])
).flat()
)
: []
// attribute naming is a bit confusing, but it's used so widely in oodikone, that it's not probably worth changing
return {
id, // our internal id, not the one from sis, (e.g. 101)
userId, // acually username (e.g. mluukkai)
name,
language,
sisPersonId, //
iamGroups,
email,
roles: accessGroups,
studentsUserCanAccess, // studentnumbers used in various parts of backend. For admin this is usually empty, since no programmes / faculties are set.
isAdmin: accessGroups.includes('admin'),
programmeRights,
}
}
const updateAccessGroups = async (username, iamGroups = [], specialGroup = {}, sisId, user) => {
const { jory, hyOne, superAdmin, openUni, katselmusViewer } = specialGroup
const userFromDb = user || (await byUsername(username))
const formattedUser = await formatUser(userFromDb, [], Boolean(sisId))
const formattedUser = await formatUser(userFromDb, [], [], Boolean(sisId))
const { roles: currentAccessGroups } = formattedUser
// Modify accessgroups based on current groups and iamGroups
let newAccessGroups = []
const newAccessGroups = []
if (currentAccessGroups.includes('studyGuidanceGroups') || (sisId && (await checkStudyGuidanceGroupsAccess(sisId))))
newAccessGroups.push('studyGuidanceGroups')
if (iamGroups.includes(courseStatisticsGroup)) newAccessGroups.push('courseStatistics')
@@ -202,10 +274,6 @@ const updateAccessGroups = async (username, iamGroups = [], specialGroup = {}, s
return { newAccessGroups, accessGroups }
}
// newer logic
const enrichProgrammesFromFaculties = faculties =>
facultiesAndProgrammesForTrends.filter(f => faculties.includes(f.faculty_code)).map(f => f.programme_code)
const findAll = async () => {
const allUsers = await User.findAll({
include: userIncludes,
@@ -220,18 +288,15 @@ const findAll = async () => {
const formattedUsers = await Promise.all(
allUsers.map(async user => {
const { iamGroups, specialGroup } = userAccessMap[user.sisu_person_id] || {}
const { iamGroups, specialGroup, access } = userAccessMap[user.sisu_person_id] || {}
const { accessGroups } = await updateAccessGroups(user.username, iamGroups, specialGroup, null, user)
return {
...user.get(),
iam_groups: iamGroups || [],
elementdetails: _.uniqBy([
...user.programme.map(p => p.elementDetailCode),
...enrichProgrammesFromFaculties(user.faculty.map(f => f.faculty_code)),
]),
accessgroup: accessGroups,
programmeRights: getStudyProgrammeRights(access, specialGroup, user.programme),
}
})
)
@@ -255,56 +320,8 @@ const findOne = async id => {
return {
...user.get(),
iam_groups: iamAccess || [],
elementdetails: _.uniqBy([
...user.programme.map(p => p.elementDetailCode),
...enrichProgrammesFromFaculties(user.faculty.map(f => f.faculty_code)),
]),
accessgroup: accessGroups,
}
}
const formatUser = async (userFromDb, extraRights, getStudentAccess = true) => {
const [accessGroups, programmes, faculties] = [
['accessgroup', 'group_code'],
['programme', 'elementDetailCode'],
['faculty', 'faculty_code'],
].map(([field, code]) => userFromDb[field].map(entry => entry[code]))
const rights = _.uniqBy([...programmes, ...enrichProgrammesFromFaculties(faculties)]).concat(extraRights)
const {
dataValues: {
id,
username: userId,
full_name: name,
email,
language,
sisu_person_id: sisPersonId,
iam_groups: iamGroups,
},
} = userFromDb
const studentsUserCanAccess = getStudentAccess
? _.uniqBy(
(
await Promise.all([getStudentnumbersByElementdetails(rights), getAllStudentsUserHasInGroups(sisPersonId)])
).flat()
)
: []
// attribute naming is a bit confusing, but it's used so widely in oodikone, that it's not probably worth changing
return {
id, // our internal id, not the one from sis, (e.g. 101)
userId, // acually username (e.g. mluukkai)
name,
language,
sisPersonId, //
iamGroups,
email,
rights,
roles: accessGroups,
studentsUserCanAccess, // studentnumbers used in various parts of backend. For admin this is usually empty, since no programmes / faculties are set.
isAdmin: accessGroups.includes('admin'),
programmeRights: getStudyProgrammeRights(iamAccess, specialGroup, user.programme),
}
}
@@ -345,7 +362,9 @@ const getMockedUser = async ({ userToMock, mockedBy }) => {
const iamRights = Object.keys(access)
const formattedUser = await formatUser(userFromDb, specialGroup.kosu ? iamRights : [])
const programmeRights = getStudyProgrammeRights(access, specialGroup, userFromDb.programme)
const formattedUser = await formatUser(userFromDb, specialGroup.kosu ? iamRights : [], programmeRights)
const toReturn = {
...formattedUser,
@@ -359,7 +378,7 @@ const getMockedUser = async ({ userToMock, mockedBy }) => {
return toReturn
}
const getUser = async ({ username, name, email, iamGroups, iamRights, specialGroup, sisId }) => {
const getUser = async ({ username, name, email, iamGroups, iamRights, specialGroup, sisId, access }) => {
if (userDataCache.has(username)) {
return userDataCache.get(username)
}
@@ -378,7 +397,8 @@ const getUser = async ({ username, name, email, iamGroups, iamRights, specialGro
const userFromDb = await byUsername(username)
userFromDb.iamGroups = await getUserIams(userFromDb.sisu_person_id)
const formattedUser = await formatUser(userFromDb, specialGroup.kosu ? iamRights : [])
const programmeRights = getStudyProgrammeRights(access, specialGroup, userFromDb.programme)
const formattedUser = await formatUser(userFromDb, specialGroup.kosu ? iamRights : [], programmeRights)
const { newAccessGroups } = await updateAccessGroups(username, iamGroups, specialGroup, sisId)
@@ -402,8 +422,8 @@ module.exports = {
findOne,
modifyAccess,
getAccessGroups,
setFaculties,
getUser,
getMockedUser,
getOrganizationAccess,
deleteOutdatedUsers,
}
+2 -2
View File
@@ -1,12 +1,12 @@
const os = require('os')
const winston = require('winston')
const { WinstonGelfTransporter } = require('winston-gelf-transporter')
const { isProduction } = require('../conf-backend')
const { combine, timestamp, printf, splat } = winston.format
let transports = []
const transports = []
const formatDate = timestamp => {
if (!isProduction) return new Date(timestamp).toLocaleTimeString('fi-FI').replace(' klo', '')
+6 -6
View File
@@ -33,15 +33,15 @@ const getPassingSemester = (startYear, date) => {
const getAcademicYearDates = (year, since) => {
if (year === 'Total') {
return {
startDate: new Date(`${since.getFullYear()}-${semesterStart['FALL']}`).toUTCString(),
endDate: new Date(`${moment(new Date()).add(1, 'years').format('YYYY')}-${semesterEnd['SPRING']}`).toUTCString(),
startDate: new Date(`${since.getFullYear()}-${semesterStart.FALL}`).toUTCString(),
endDate: new Date(`${moment(new Date()).add(1, 'years').format('YYYY')}-${semesterEnd.SPRING}`).toUTCString(),
}
}
const startYear = year.slice(0, 4)
const endYear = moment(startYear, 'YYYY').add(1, 'years').format('YYYY')
return {
startDate: new Date(moment.tz(`${startYear}-${semesterStart['FALL']}`, 'Europe/Helsinki').format()).toUTCString(),
endDate: new Date(`${endYear}-${semesterEnd['SPRING']}`).toUTCString(),
startDate: new Date(moment.tz(`${startYear}-${semesterStart.FALL}`, 'Europe/Helsinki').format()).toUTCString(),
endDate: new Date(`${endYear}-${semesterEnd.SPRING}`).toUTCString(),
}
}
@@ -49,9 +49,9 @@ const getYearStartAndEndDates = (year, isAcademicYear) => {
if (isAcademicYear) {
const firstYear = year.slice(0, 4)
return {
startDate: new Date(`${firstYear}-${semesterStart['FALL']}`).toISOString(),
startDate: new Date(`${firstYear}-${semesterStart.FALL}`).toISOString(),
endDate: new Date(
`${moment(firstYear, 'YYYY').add(1, 'years').format('YYYY')}-${semesterEnd['SPRING']}`
`${moment(firstYear, 'YYYY').add(1, 'years').format('YYYY')}-${semesterEnd.SPRING}`
).toISOString(),
}
}
+8 -5
View File
@@ -16,8 +16,9 @@ const mapToProviders = programmeCodes => {
// that got the wrong provider
if (asNum < 7920111 && asNum > 7920102) {
return `${asNum + 1}`
} else if (asNum === 7920111) {
return `7920103`
}
if (asNum === 7920111) {
return '7920103'
}
return `${asNum}`
}
@@ -46,9 +47,7 @@ const getSortRank = code => {
const sortMainCode = codeArray => {
if (!codeArray) return []
return codeArray.sort(function (x, y) {
return getSortRank(y) - getSortRank(x)
})
return codeArray.sort((x, y) => getSortRank(y) - getSortRank(x))
}
/** Returns a sorting function that can be used to sort strings so that Finnish alphabetical order is respected.
@@ -59,9 +58,13 @@ const createLocaleComparator = (field = null) => {
return (val1, val2) => val1[field].localeCompare(val2[field], 'fi', { sensitivity: 'accent' })
}
const getFullStudyProgrammeRights = programmeRights =>
programmeRights.filter(({ limited }) => !limited).map(({ code }) => code)
module.exports = {
mapToProviders,
sortMainCode,
getSortRank,
createLocaleComparator,
getFullStudyProgrammeRights,
}
+1 -1
View File
@@ -22,7 +22,7 @@ worker.on('completed', job => {
// If there is no error event listener, the worker stops taking jobs after any error.
worker.on('error', err => {
logger.error(`Job returned error:`)
logger.error('Job returned error:')
logger.error(err.toString())
})
+36 -29
View File
@@ -2,52 +2,59 @@
"root": true,
"env": {
"browser": true,
"es2020": true
"es2021": true
},
"parserOptions": {
"ecmaVersion": "latest",
"sourceType": "module"
},
"extends": ["airbnb", "plugin:prettier/recommended", "plugin:import/recommended"],
"extends": "../../.eslintrc.json",
"rules": {
"react/forbid-prop-types": "off",
"jsx-a11y/no-static-element-interactions": "off",
"camelcase": "error",
"consistent-return": "error",
"import/no-commonjs": "error",
"import/no-default-export": "error",
"import/no-unused-modules": ["error", { "missingExports": true, "unusedExports": true }],
"import/order": [
"error",
{
"alphabetize": { "order": "asc", "caseInsensitive": true },
"groups": ["builtin", "external", "internal", "parent", "sibling"],
"pathGroups": [{ "pattern": "@/**", "group": "internal", "position": "before" }]
}
],
"import/prefer-default-export": "off",
"jsx-a11y/click-events-have-key-events": "off",
"jsx-a11y/label-has-for": "off",
"jsx-a11y/label-has-associated-control": "off",
"no-plusplus": "off",
"no-shadow": "off",
"react/jsx-props-no-spreading": "off",
"react/jsx-filename-extension": ["error", { "allow": "as-needed" }],
"jsx-a11y/label-has-for": "off",
"jsx-a11y/no-static-element-interactions": "off",
"no-async-promise-executor": "error",
"no-await-in-loop": "error",
"no-implied-eval": "off",
"no-param-reassign": ["error", { "props": false }],
"no-promise-executor-return": "error",
"no-restricted-syntax": "error",
"no-return-await": "error",
"react/forbid-prop-types": "off",
"react/function-component-definition": [
"error",
{ "namedComponents": "arrow-function", "unnamedComponents": "arrow-function" }
],
"default-param-last": "off",
"no-implied-eval": "off",
"react/no-unescaped-entities": "off",
"react/prop-types": "off",
"react/prefer-stateless-function": "error",
"react/no-unstable-nested-components": "off",
"react/jsx-no-constructed-context-values": "off",
"react/jsx-filename-extension": ["error", { "allow": "as-needed" }],
"react/jsx-key": "error",
"react/jsx-no-constructed-context-values": "off",
"react/jsx-props-no-spreading": "off",
"react/jsx-sort-props": ["error", { "reservedFirst": false }],
"react/no-unescaped-entities": "off",
"react/no-unknown-property": "off",
"import/no-default-export": "error",
"import/prefer-default-export": "off",
"import/no-unused-modules": ["error", { "missingExports": true, "unusedExports": true }],
"import/no-commonjs": "error",
"quotes": ["error", "single", { "avoidEscape": true }],
"react-hooks/rules-of-hooks": "error",
"no-console": "error",
"no-param-reassign": ["error", { "props": false }],
"no-continue": "off"
"react/no-unstable-nested-components": "off",
"react/prefer-stateless-function": "error",
"react/prop-types": "off",
"react-hooks/rules-of-hooks": "error"
},
"plugins": ["react", "react-hooks"],
"settings": {
"import/resolver": {
"node": {
"moduleDirectory": ["node_modules", "src/"]
},
"eslint-import-resolver-custom-alias": {
"alias": {
"@": "./services/frontend/src"
@@ -56,7 +63,7 @@
}
},
"react": {
"version": "detect"
"version": "16.14.0"
}
}
}
+3 -3
View File
@@ -1,9 +1,9 @@
{
"compilerOptions": {
"baseUrl": "src",
"baseUrl": ".",
"paths": {
"@/*": ["./*"]
"@/*": ["./src/*"]
}
},
"include": ["src"]
"include": ["src/**/*"]
}
+5 -51
View File
@@ -10,7 +10,7 @@
"@sentry/browser": "^6.18.2",
"@sentry/tracing": "^6.18.2",
"axios": "^0.21.4",
"immer": "^9.0.12",
"immer": "^9.0.21",
"lodash": "^4.17.21",
"moment": "^2.29.4",
"prop-types": "^15.8.1",
@@ -32,8 +32,7 @@
},
"devDependencies": {
"@vitejs/plugin-react-swc": "^3.6.0",
"vite": "^5.1.3",
"vite-tsconfig-paths": "^4.3.1"
"vite": "^5.1.3"
}
},
"node_modules/@babel/runtime": {
@@ -1212,12 +1211,6 @@
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/globrex": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/globrex/-/globrex-0.1.2.tgz",
"integrity": "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==",
"dev": true
},
"node_modules/highcharts": {
"version": "9.3.3",
"resolved": "https://registry.npmjs.org/highcharts/-/highcharts-9.3.3.tgz",
@@ -1245,9 +1238,9 @@
}
},
"node_modules/immer": {
"version": "9.0.15",
"resolved": "https://registry.npmjs.org/immer/-/immer-9.0.15.tgz",
"integrity": "sha512-2eB/sswms9AEUSkOm4SbV5Y7Vmt/bKRwByd52jfLkW4OLYeaTP3EEiJ9agqU0O/tq6Dk62Zfj+TJSqfm1rLVGQ==",
"version": "9.0.21",
"resolved": "https://registry.npmjs.org/immer/-/immer-9.0.21.tgz",
"integrity": "sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA==",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/immer"
@@ -1970,26 +1963,6 @@
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/tsconfck": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/tsconfck/-/tsconfck-3.0.2.tgz",
"integrity": "sha512-6lWtFjwuhS3XI4HsX4Zg0izOI3FU/AI9EGVlPEUMDIhvLPMD4wkiof0WCoDgW7qY+Dy198g4d9miAqUHWHFH6Q==",
"dev": true,
"bin": {
"tsconfck": "bin/tsconfck.js"
},
"engines": {
"node": "^18 || >=20"
},
"peerDependencies": {
"typescript": "^5.0.0"
},
"peerDependenciesMeta": {
"typescript": {
"optional": true
}
}
},
"node_modules/tslib": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
@@ -2210,25 +2183,6 @@
}
}
},
"node_modules/vite-tsconfig-paths": {
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/vite-tsconfig-paths/-/vite-tsconfig-paths-4.3.1.tgz",
"integrity": "sha512-cfgJwcGOsIxXOLU/nELPny2/LUD/lcf1IbfyeKTv2bsupVbTH/xpFtdQlBmIP1GEK2CjjLxYhFfB+QODFAx5aw==",
"dev": true,
"dependencies": {
"debug": "^4.1.1",
"globrex": "^0.1.2",
"tsconfck": "^3.0.1"
},
"peerDependencies": {
"vite": "*"
},
"peerDependenciesMeta": {
"vite": {
"optional": true
}
}
},
"node_modules/xlsx": {
"version": "0.20.1",
"resolved": "https://cdn.sheetjs.com/xlsx-0.20.1/xlsx-0.20.1.tgz",
+2 -3
View File
@@ -11,7 +11,7 @@
"@sentry/browser": "^6.18.2",
"@sentry/tracing": "^6.18.2",
"axios": "^0.21.4",
"immer": "^9.0.12",
"immer": "^9.0.21",
"lodash": "^4.17.21",
"moment": "^2.29.4",
"prop-types": "^15.8.1",
@@ -33,8 +33,7 @@
},
"devDependencies": {
"@vitejs/plugin-react-swc": "^3.6.0",
"vite": "^5.1.3",
"vite-tsconfig-paths": "^4.3.1"
"vite": "^5.1.3"
},
"overrides": {
"react-highcharts": {
+16 -15
View File
@@ -1,8 +1,9 @@
import axios from 'axios'
import * as Sentry from '@sentry/browser'
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
import { showAsUserKey } from 'common'
import { apiBasePath, isDev } from '../conf'
import * as Sentry from '@sentry/browser'
import axios from 'axios'
import { showAsUserKey } from '@/common'
import { apiBasePath, isDev } from '@/conf'
const getHeaders = () => {
// Set up dev user for development environment, mimicking production admin user
@@ -73,15 +74,15 @@ export const callController = (route, prefix, data, method = 'get', query, param
return { type: attempt, requestSettings }
}
const handleError = (err, actionHistory = []) => {
const { response } = err
const handleError = (error, actionHistory = []) => {
const { response } = error
if (response && response.status) {
Sentry.withScope(s => {
s.setExtra('config', err.config)
s.setExtra('request', err.request)
s.setExtra('response', err.response)
s.setExtra('actionHistory', JSON.stringify(actionHistory))
Sentry.captureException(err)
Sentry.withScope(scope => {
scope.setExtra('config', error.config)
scope.setExtra('request', error.request)
scope.setExtra('response', error.response)
scope.setExtra('actionHistory', JSON.stringify(actionHistory))
Sentry.captureException(error)
})
}
}
@@ -95,9 +96,9 @@ export const handleRequest = store => next => async action => {
try {
const res = await callApi(route, method, data, params, 0, onProgress)
store.dispatch({ type: success, response: res.data, query })
} catch (e) {
store.dispatch({ type: failure, response: e, query })
handleError(e, store.getState().actionHistory)
} catch (error) {
store.dispatch({ type: failure, response: error, query })
handleError(error, store.getState().actionHistory)
}
}
}
@@ -32,7 +32,7 @@ export const facultyToolTips = {
**Exchange students**: Saapuvat vaihto-opiskelijat: opintosuoritukset, jotka ovat saapuvan kansainvälisen opiskelijan suorittamia.
Sisältää kaikki vaihto-opiskelijan opiskeluoikeuden aikana syntyneet opintopisteet.
**Transferred**: Hyväksiluetut opintopisteet
**Transferred**: Hyväksiluetut opintopisteet. Tilastointipäivämäärä on hyväksilukupäivämäärä, eli vaikka suoritus olisi vuodelta 2021, jos opintopiste hyväksiluetaan 2024, se lasketaan vuodelle 2024.
Lisäksi painamalla "Show special categories" -valintaa, saat näkyviin myös harvinaisemmat kategoriat:
@@ -5,3 +5,4 @@ export { filterToolTips } from './filters'
export { populationStatisticsToolTips } from './populationStatistics'
export { studentToolTips } from './students'
export { studyProgrammeToolTips } from './studyprogramme'
export { userToolTips } from './users'
@@ -59,7 +59,7 @@ export const populationStatisticsToolTips = {
Opiskelijat on mahdollista suodattaa kurssin perusteella käyttämällä kurssirivin vasemmassa laidassa olevaa suodatinpainiketta.
Kurssisuodatin aukeaa ikkunan vasempaan laitaan ja tarjoaa lisää vaihtoehtoja suodatukseen. Voit myös valita monta kurssia kerralla suodattimeen.
Klikkaamalla kurssin nimen vieressä olevaa sinistä nuolta, siirryt ko. kurssin **Course Statistics** -näkymään.
Klikkaamalla kurssin nimen vieressä olevaa sinistä nuolta, siirryt ko. kurssin **Course statistics** -näkymään.
Lisäksi kurssin nimi **name** ja koodisarakkeissa **code** suodatinkuvaketta klikkaamalla tulee esiin hakukentät suodattamista varten.
Suodattimet saa tyhjennettyä painamalla kentän vieressä olevaa ruksia tai valitsemall valikon alaosasta **Reset column filter**.
@@ -67,7 +67,7 @@ export const populationStatisticsToolTips = {
**Huom! Nämä suodattimet vaikuttavat vain tähän taulukkoon, eivät koko populatioon.**
**Taulukot**
- **pass/fail** - näkymä sisältää kurssisuoritukset, hylätyt, yritykset sekä ilmoittautumistiedot. Huom.! ilmoittautumistiedot ovat saatavilla vain Sisun käyttöönoton jälkeiseltä ajalta.
- **Pass/fail** - näkymä sisältää kurssisuoritukset, hylätyt, yritykset sekä ilmoittautumistiedot. Huom.! ilmoittautumistiedot ovat saatavilla vain Sisun käyttöönoton jälkeiseltä ajalta.
- **Total students** - sarake näyttää kaikkien opiskelijoiden määrän, mukaanlukien ilmoittautuneet ilman arvosanaa.
- **Enrolled, no grade** - sarake näyttää niiden opiskelijoiden määrän, joilla on ilmoittautuminen muttei arvosanaa eikä hylättyä suoritusta.
- **grades** - arvosanajakauma.
@@ -81,7 +81,7 @@ export const populationStatisticsToolTips = {
Opiskelijat on mahdollista suodattaa kurssin perusteella käyttämällä kurssirivin vasemmassa laidassa olevaa suodatinpainiketta.
Kurssisuodatin aukeaa ikkunan vasempaan laitaan ja tarjoaa lisää vaihtoehtoja suodatukseen. Voit myös valita monta kurssia kerralla suodattimeen.
Klikkaamalla kurssin nimen vieressä olevaa sinistä nuolta, siirryt ko. kurssin **Course Statistics** -näkymään.
Klikkaamalla kurssin nimen vieressä olevaa sinistä nuolta, siirryt ko. kurssin **Course statistics** -näkymään.
Taulukon tietoja voi suodattaa antamalla opiskelijoiden vähimmäismäärän kurssilla kenttään *Limit to courses where student number at least*.
Lisäksi kurssin nimi **name** ja koodisarakkeissa **code** suodatinkuvaketta klikkaamalla tulee esiin hakukentät suodattamista varten.
@@ -90,7 +90,7 @@ export const populationStatisticsToolTips = {
**Huom! Nämä suodattimet vaikuttavat vain tähän taulukkoon, eivät koko populatioon.**
**Taulukot**
- **pass/fail** - näkymä sisältää kurssisuoritukset, hylätyt, yritykset sekä ilmoittautumistiedot. Huom.! ilmoittautumistiedot ovat saatavilla vain Sisun käyttöönoton jälkeiseltä ajalta.
- **Pass/fail** - näkymä sisältää kurssisuoritukset, hylätyt, yritykset sekä ilmoittautumistiedot. Huom.! ilmoittautumistiedot ovat saatavilla vain Sisun käyttöönoton jälkeiseltä ajalta.
- **grades** - arvosanajakauma.
`,
Search: `Tässä osiossa voi tarkastella koulutusohjelmakohtaisia populaatioita sisäänotto-lukuvuoden mukaisesti,
@@ -36,7 +36,7 @@ export const studyProgrammeToolTips = {
**Exchange students**: Saapuvat vaihto-opiskelijat: opintosuoritukset, jotka ovat saapuvan kansainvälisen opiskelijan suorittamia.
Sisältää kaikki vaihto-opiskelijan opiskeluoikeuden aikana syntyneet opintopisteet.
**Transferred**: Hyväksiluetut opintopisteet
**Transferred**: Hyväksiluetut opintopisteet. Tilastointipäivämäärä on hyväksilukupäivämäärä, eli vaikka suoritus olisi vuodelta 2021, jos opintopiste hyväksiluetaan 2024, se lasketaan vuodelle 2024.
Lisäksi painamalla "Show special categories" -valintaa, saat näkyviin myös harvinaisemmat kategoriat:
@@ -0,0 +1,23 @@
export const userToolTips = {
IamGroupBasedAccess: `
# Currently this description is more like how things should be, not how they are. In particular, full rights based on IAM groups are not implemented.
The exclamation mark means that the user has **limited rights** to the study programme. The check mark means that the user has **full rights** to the study programme. Full rights behave exactly like manually added rights.
Both of these icons are displayed: the relevant one is green, the irrelevant one is greyed out.
**Limited rights** are granted to the following people:
- member of the steering group (johtoryhmä) of the study programme.
- dean or vice dean (dekaani tai varadekaani) of the corresponding faculty.
- The following people get limited rights to **all** study programmes:
- heads of academic affairs (opintoasiainpäälliköt)
- employees of ospa (opetuksen strategiset palvelut)
- rector, vice-rectors and director of administration (rehtori, vararehtorit ja hallintojohtaja)
- members of the academic affairs council (opintoasiainneuvosto)
**Full rights** are granted to the following people:
- education planning officer (koulutussuunnittelija) of the corresponding faculty.
- vice dean responsible for education (opetusvaradekaani) of the corresponding faculty.
- head **and** member of the steering group of the studyprogramme (johtoryhmän jäsen ja koulutusohjelman johtaja).
`,
}

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