mirror of
https://github.com/UniversityOfHelsinkiCS/oodikone.git
synced 2026-10-12 19:03:39 +03:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c03abae5d4 | ||
|
|
811dc37ef9 | ||
|
|
acb346c77a | ||
|
|
98b4cba041 | ||
|
|
c0beb94d60 | ||
|
|
c0de109c0f | ||
|
|
c0c6a6a10c | ||
|
|
033d5bd86f | ||
|
|
c8ad866635 | ||
|
|
c4f9dbdb27 | ||
|
|
0507111117 | ||
|
|
132b454937 | ||
|
|
3a7842765a | ||
|
|
9d92571770 | ||
|
|
d3340a80b1 | ||
|
|
cc0ec6fdd9 | ||
|
|
bd85a6a5ba | ||
|
|
a52063ac37 | ||
|
|
facdb85cec | ||
|
|
05673e269d | ||
|
|
e479738a27 | ||
|
|
692fab5d75 | ||
|
|
d93b52f600 | ||
|
|
efbf24c003 | ||
|
|
987c8b3a97 | ||
|
|
57cad29520 | ||
|
|
268fc7c8e9 | ||
|
|
d63e69a950 | ||
|
|
29218cd9d2 | ||
|
|
f81cbe1d4f | ||
|
|
852c91219c | ||
|
|
a94dcd2a9d | ||
|
|
874fdf83e1 | ||
|
|
5aefe58e46 | ||
|
|
9047e099dc | ||
|
|
ce03560ff8 | ||
|
|
068458d4be | ||
|
|
68767a301e | ||
|
|
b443ead517 | ||
|
|
b743e49ff1 | ||
|
|
5548940ea4 | ||
|
|
665e0ed6e4 | ||
|
|
f7247a73ec | ||
|
|
c9e259a1ea | ||
|
|
203ee46a7c | ||
|
|
afe70fdb67 | ||
|
|
df831aeef7 | ||
|
|
0021c44c2c | ||
|
|
4b4a671a70 | ||
|
|
1aed2d2ff8 | ||
|
|
3a10c22b80 | ||
|
|
60851c88e5 | ||
|
|
efdc2abbef | ||
|
|
eeab572c5d | ||
|
|
1a25f3aae6 | ||
|
|
40ea7c8752 | ||
|
|
7442513927 | ||
|
|
67ee85dd41 | ||
|
|
1f3ecb4952 | ||
|
|
8c271d0fab | ||
|
|
d678ac2a6a | ||
|
|
8ccd5cdae5 | ||
|
|
6f999fa203 | ||
|
|
5cbc827d1d | ||
|
|
4cdcd948f6 | ||
|
|
9c4ab07fb8 | ||
|
|
5e86ff05f3 | ||
|
|
d017a23ab5 | ||
|
|
250024d41d | ||
|
|
1420628f6f | ||
|
|
e1cc1f3ecc | ||
|
|
2d0d152fb7 | ||
|
|
2272890cb4 | ||
|
|
ba8471df88 | ||
|
|
575a580120 | ||
|
|
0a07dedede | ||
|
|
ed0f700fc2 | ||
|
|
616cc60b39 | ||
|
|
68b730a747 | ||
|
|
b541857980 | ||
|
|
2b3e436979 | ||
|
|
ba1224c983 | ||
|
|
aeab65358d | ||
|
|
e865d12676 | ||
|
|
96760bd0fb | ||
|
|
eb7b91a959 | ||
|
|
e07677ecaa | ||
|
|
d1a729804e | ||
|
|
bb1a43ac87 | ||
|
|
d493ac53a5 | ||
|
|
0695e031bf | ||
|
|
f799420b42 | ||
|
|
23a5ee7b08 | ||
|
|
71b5a87ef1 | ||
|
|
e0b15e457e | ||
|
|
addacd63c9 | ||
|
|
fc8c36392e | ||
|
|
1dc2c942f3 | ||
|
|
cd3ccbf5fb | ||
|
|
341b04b01a | ||
|
|
f826f5b93c | ||
|
|
137d48a483 | ||
|
|
89f31d5cdf | ||
|
|
1a8ccde9f3 | ||
|
|
96a185a59e | ||
|
|
678cd070ec | ||
|
|
a606117807 | ||
|
|
db9543edd8 | ||
|
|
eeeeb1c671 | ||
|
|
16b18bb92b | ||
|
|
2c2f98599e | ||
|
|
1f0b91a612 | ||
|
|
e3151f67d1 | ||
|
|
59ab60fc5e | ||
|
|
25233fd13e | ||
|
|
a1e0560faa | ||
|
|
7c5c785a6e | ||
|
|
61a4073d82 | ||
|
|
d19ef59fc3 | ||
|
|
986a12c27b | ||
|
|
892a366984 | ||
|
|
d87d611db7 | ||
|
|
f5ae6d42e9 | ||
|
|
40af5f0f99 | ||
|
|
e139f4cb4e | ||
|
|
71d993246b | ||
|
|
a3067c6b23 | ||
|
|
01841e104e | ||
|
|
1ae4c0b39e | ||
|
|
32e4a22c00 | ||
|
|
013da38a6b | ||
|
|
b2b2d87ff3 | ||
|
|
7e22fcd856 | ||
|
|
329466dde9 | ||
|
|
a7ebc4fb99 | ||
|
|
6e7ee36db1 | ||
|
|
ca8dc45ceb | ||
|
|
9998f2600d | ||
|
|
1f370ea9c5 | ||
|
|
f319e3e2a0 | ||
|
|
01e701b770 | ||
|
|
4542f4179d | ||
|
|
288ff2bada | ||
|
|
dce0642f08 | ||
|
|
c0a1cf99f2 | ||
|
|
f4f1ed6818 | ||
|
|
e0a7ed139f | ||
|
|
a5fe61a654 | ||
|
|
d02dff573c | ||
|
|
56faf46a1a | ||
|
|
9e09168b01 | ||
|
|
9d474ca61e | ||
|
|
9c39f00ec3 | ||
|
|
45d3178193 |
+26
-6
@@ -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*/**"]
|
||||
}
|
||||
|
||||
@@ -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 }}
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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!
|
||||
|
||||
@@ -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 ===
|
||||
|
||||
@@ -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')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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')
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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')
|
||||
|
||||
Generated
-221
@@ -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
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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,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()
|
||||
|
||||
@@ -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,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'],
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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' })
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,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]]
|
||||
|
||||
+4
-4
@@ -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,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,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,
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
@@ -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,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 => {
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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', '')
|
||||
|
||||
@@ -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(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
})
|
||||
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": "src",
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./*"]
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["src"]
|
||||
"include": ["src/**/*"]
|
||||
}
|
||||
|
||||
Generated
+5
-51
@@ -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",
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user