mirror of
https://github.com/UniversityOfHelsinkiCS/oodikone.git
synced 2026-10-13 03:13:40 +03:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1af42bf012 | ||
|
|
f606a7e8b7 | ||
|
|
6d938a34af | ||
|
|
77c5721dff | ||
|
|
c5561ab6dc | ||
|
|
0b647e1b32 | ||
|
|
1437bb9d0e | ||
|
|
66249aabd8 | ||
|
|
02c63d1d9f | ||
|
|
00b7dc933e | ||
|
|
9f089fb6db | ||
|
|
542267042e | ||
|
|
9e282ae634 | ||
|
|
6a625b1da7 | ||
|
|
4982bd8f3c | ||
|
|
908e33ccd3 | ||
|
|
d429480f80 | ||
|
|
dada995398 | ||
|
|
69c9b05657 | ||
|
|
0b5ac0b61b | ||
|
|
a902961942 | ||
|
|
7c2fa532d4 | ||
|
|
36267f9fca | ||
|
|
c2d99d6944 | ||
|
|
0b1be20780 | ||
|
|
cc9f5f6906 | ||
|
|
d5bca4dc57 | ||
|
|
5f8c468ceb | ||
|
|
434d45987d | ||
|
|
0cb8acc46a | ||
|
|
6e996fbc4c | ||
|
|
86fbaaf1ca | ||
|
|
517c216a5d | ||
|
|
a04fb68aaa | ||
|
|
72c01fa345 | ||
|
|
62f1cc5459 | ||
|
|
c9eceaebe4 | ||
|
|
92e8709ef6 | ||
|
|
9c5c135d0f | ||
|
|
c4f65a1f29 | ||
|
|
96f1c9d9cf | ||
|
|
a8cfcb3371 | ||
|
|
63cbe523b1 | ||
|
|
7092fbbda0 | ||
|
|
82573c9d17 | ||
|
|
1b98b0a847 | ||
|
|
b73174f8a5 | ||
|
|
b1167eebf3 | ||
|
|
35391f9b36 | ||
|
|
4fde6c319a | ||
|
|
ebd1a98ead | ||
|
|
b3ef64fc2e | ||
|
|
7bbc3c0cb0 | ||
|
|
90abe7eb22 | ||
|
|
08dd4019af | ||
|
|
2841fd69a3 | ||
|
|
827d45c481 | ||
|
|
52ba8d582b | ||
|
|
d151521eae | ||
|
|
6188fe6fe4 | ||
|
|
1f0eb93174 | ||
|
|
f50e632cdb | ||
|
|
bbd83b501b | ||
|
|
48048e5ecf | ||
|
|
5be0a74a60 | ||
|
|
e8e10bcb9a | ||
|
|
3e3a95a113 | ||
|
|
9a190b301d | ||
|
|
f976eca1a5 | ||
|
|
a32b4695a9 | ||
|
|
d55e27f1d9 | ||
|
|
b749ad4033 | ||
|
|
eddc514907 | ||
|
|
e0c4db7bd9 | ||
|
|
3b384cceb6 | ||
|
|
6e15b8c355 | ||
|
|
f1196b49ae | ||
|
|
e5b89dc844 | ||
|
|
5b27c42945 | ||
|
|
5ae10d2695 | ||
|
|
c4f7529973 | ||
|
|
5d84d2f9a3 | ||
|
|
40fbcd9750 | ||
|
|
e06f12017d | ||
|
|
c686d2ad73 | ||
|
|
7306d261ce | ||
|
|
db0e924f36 | ||
|
|
10d13b751b | ||
|
|
87ef487fe8 | ||
|
|
50968b4b66 | ||
|
|
791e295eb7 | ||
|
|
6818ab94fe | ||
|
|
4bef9c8daa | ||
|
|
8050a56847 | ||
|
|
04e2d44665 | ||
|
|
32b13df638 | ||
|
|
04f2244337 | ||
|
|
93c7267942 | ||
|
|
94ff7c1218 | ||
|
|
161fdc25e7 | ||
|
|
9c069c8082 | ||
|
|
21e3848207 | ||
|
|
251db49a05 | ||
|
|
07d9ef9901 | ||
|
|
bf414f856b | ||
|
|
b8a0fff57e | ||
|
|
dd8ecb7152 | ||
|
|
0c3c6a3442 | ||
|
|
006984930f | ||
|
|
cab91265b0 | ||
|
|
e48247d80b | ||
|
|
9e0265c439 | ||
|
|
c5fd7bf26c | ||
|
|
2a68c60892 | ||
|
|
f9a974fdd2 | ||
|
|
d64ac35a4f | ||
|
|
484f4ecf94 | ||
|
|
10ef70a22e | ||
|
|
fa82b720f5 | ||
|
|
3b17923761 | ||
|
|
e42a348ec7 | ||
|
|
d33f28c776 | ||
|
|
3fd883c17b | ||
|
|
e980035e11 | ||
|
|
2c51e5fb07 | ||
|
|
ea792758d6 | ||
|
|
998caed914 | ||
|
|
595025c976 | ||
|
|
e277175e5f | ||
|
|
c510ac2258 | ||
|
|
e9d7df45db | ||
|
|
492475722c | ||
|
|
afdab20014 | ||
|
|
d51b357b38 | ||
|
|
f477b3ef50 | ||
|
|
17cecd0d5e | ||
|
|
9624b06c4f | ||
|
|
ab19b429ca | ||
|
|
51f0409d26 | ||
|
|
003f8e2293 | ||
|
|
2757e34338 | ||
|
|
2e6fdb4970 | ||
|
|
5ea08b3e20 | ||
|
|
99b8cdabcd | ||
|
|
d04fa26cdd | ||
|
|
1237cb61bf | ||
|
|
39731667ec | ||
|
|
c918f862e3 | ||
|
|
d79b5b3f81 | ||
|
|
94af91274f | ||
|
|
af5c5209c5 | ||
|
|
a96d7dbdea | ||
|
|
e1ca5bac5c | ||
|
|
295c3b6311 | ||
|
|
c1a66a32f8 | ||
|
|
69942c2c17 | ||
|
|
7f0fa7fb63 | ||
|
|
56ee488b9b | ||
|
|
6f8cee5153 | ||
|
|
2e00415a40 | ||
|
|
8ef53a58be | ||
|
|
f9abd1c028 | ||
|
|
f912ccb27d | ||
|
|
6224ec2307 | ||
|
|
87e5bd9ef6 | ||
|
|
1beb4892fc | ||
|
|
ccb2722b6e | ||
|
|
e9fcf3dd8c | ||
|
|
376848fb70 | ||
|
|
82f0fe0cdd | ||
|
|
871507d310 | ||
|
|
0ab5b70acc | ||
|
|
1cf05f4e1d | ||
|
|
0bae2d36f5 | ||
|
|
29da912df9 | ||
|
|
64a211e17f | ||
|
|
776b74c777 | ||
|
|
4a35505af3 | ||
|
|
2a20befb67 | ||
|
|
3824e43345 | ||
|
|
59bfbfcca3 | ||
|
|
6181f89cc7 | ||
|
|
ffa2fa1076 | ||
|
|
d79037a2f9 | ||
|
|
d51ad67e4e | ||
|
|
22e11948f5 | ||
|
|
5b5884b25d | ||
|
|
c206b94455 | ||
|
|
01b15c3dc1 | ||
|
|
7b02c64e92 | ||
|
|
f694b6b77f | ||
|
|
2db43e0c87 | ||
|
|
6a419dc5a7 | ||
|
|
8c4a08eb97 | ||
|
|
2a095a7c18 | ||
|
|
089cbc5cae | ||
|
|
53166a1771 | ||
|
|
9cea69618d | ||
|
|
69df95200c | ||
|
|
9c60e78349 | ||
|
|
ecedf3e73a | ||
|
|
9e5da80e4a | ||
|
|
825940e226 | ||
|
|
0c2a378f2c | ||
|
|
a5b65f8648 | ||
|
|
038876401f | ||
|
|
1b40319f4a | ||
|
|
91f847d72e | ||
|
|
1f5908dd86 | ||
|
|
c30a404074 |
+1
-1
@@ -1,4 +1,4 @@
|
||||
# Populate and change this to .env
|
||||
IMPORTER_DB_API_TOKEN=
|
||||
IMPORTER_DB_API_URL=
|
||||
IMPORTER_DB_API_URL=https://importer.cs.helsinki.fi
|
||||
PATE_TOKEN=
|
||||
|
||||
+3
-10
@@ -8,17 +8,10 @@
|
||||
"parserOptions": {
|
||||
"requireConfigFile": false
|
||||
},
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:prettier/recommended",
|
||||
"plugin:cypress/recommended"
|
||||
],
|
||||
"plugins": [
|
||||
"prettier",
|
||||
"cypress"
|
||||
],
|
||||
"extends": ["eslint:recommended", "plugin:prettier/recommended", "plugin:cypress/recommended"],
|
||||
"plugins": ["prettier", "cypress"],
|
||||
"rules": {
|
||||
"no-console": "off",
|
||||
"no-console": "error",
|
||||
"react/no-unescaped-entities": "off",
|
||||
"no-async-promise-executor": "off",
|
||||
"prettier/prettier": "error"
|
||||
|
||||
@@ -87,7 +87,7 @@ jobs:
|
||||
environment: production
|
||||
set_commits: 'skip'
|
||||
sourcemaps: './build/static/js'
|
||||
url_prefix: '~/oodikone/static/js'
|
||||
url_prefix: '~/static/js'
|
||||
version: ${{ steps.extract_version.outputs.VERSION }}
|
||||
|
||||
build_and_release_userservice:
|
||||
|
||||
@@ -545,7 +545,7 @@ jobs:
|
||||
environment: staging
|
||||
set_commits: 'skip'
|
||||
sourcemaps: './build/static/js'
|
||||
url_prefix: '~/oodikone/static/js'
|
||||
url_prefix: '~/static/js'
|
||||
version: ${{ github.sha }}
|
||||
- name: 'Push to docker hub'
|
||||
run: |
|
||||
|
||||
@@ -126,6 +126,10 @@ Continuous integration (CI) works with Github actions and is defined in workflow
|
||||
|
||||
You should always install the dependencies **inside** the container to have the application **inside** the container access them. This might be the case when someone else installs a new library and you only pull the changes in package.json. Use `docker exec -it <service> sh` to get inside the container and run `npm ci` to install modules.
|
||||
|
||||
### I get the "You are not authorized to use Oodikone" -message when I try to work with anon-data
|
||||
|
||||
Make sure you have your VPN on
|
||||
|
||||
### Everything is broken, can't get oodikone running, data is not there etc.
|
||||
|
||||
Just do clean install by launching cli with `npm cli` and running option 1: _Set up oodikone from scratch_.
|
||||
@@ -135,3 +139,7 @@ Just do clean install by launching cli with `npm cli` and running option 1: _Set
|
||||
Toska of course.
|
||||
|
||||
University of Helsinki
|
||||
|
||||
## Tietosuoja / data protection
|
||||
|
||||
See [tietosuoja.md](tietosuoja.md).
|
||||
|
||||
Binary file not shown.
@@ -120,7 +120,8 @@ describe('Population Statistics', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('Admission type filter works', () => {
|
||||
// Return when we have +2020 population with admission types in data
|
||||
it.skip('Admission type filter works', () => {
|
||||
runTestStepWithPreAndPostParts('admissionTypeFilter-header', () => {
|
||||
cy.selectFromDropdown('admissionTypeFilter-dropdown', 0)
|
||||
checkFilteringResult(5)
|
||||
|
||||
@@ -79,7 +79,7 @@ describe('Studyprogramme overview', () => {
|
||||
let originalProductivityCalculatedText
|
||||
const doRecalculation = () => {
|
||||
// Wait for page to load before clicking to admin page, TODO: change to cy.intercept
|
||||
cy.wait(2000)
|
||||
cy.wait(5000)
|
||||
cy.get('.attached').contains('Admin').click()
|
||||
cy.contains('recalculate productivity').click()
|
||||
cy.contains('recalculate throughput').click()
|
||||
|
||||
@@ -10,6 +10,7 @@ const adminUserHeaders = {
|
||||
hygroupcn: 'grp-oodikone-users;grp-oodikone-basic-users',
|
||||
edupersonaffiliation: 'member;employee;faculty',
|
||||
mail: 'grp-toska+mockadminuser@helsinki.fi',
|
||||
hypersonsisuid: 'hy-hlo-6666666',
|
||||
}
|
||||
|
||||
const basicUserHeaders = {
|
||||
@@ -19,6 +20,7 @@ const basicUserHeaders = {
|
||||
hygroupcn: 'grp-oodikone-users;grp-oodikone-basic-users',
|
||||
edupersonaffiliation: 'member',
|
||||
mail: 'grp-toska+mockbasicuser@helsinki.fi',
|
||||
hypersonsisuid: 'hy-hlo-1234567',
|
||||
}
|
||||
|
||||
const onlycoursestatisticsUserHeaders = {
|
||||
@@ -28,6 +30,7 @@ const onlycoursestatisticsUserHeaders = {
|
||||
hyGroupCn: 'grp-oodikone-basic-users',
|
||||
eduPersonAffiliation: 'member',
|
||||
mail: 'grp-toska+mockonlycoursestatisticsuser@helsinki.fi',
|
||||
hypersonsisuid: 'hy-hlo-7654321',
|
||||
}
|
||||
|
||||
const norightsUserHeaders = {
|
||||
@@ -37,6 +40,7 @@ const norightsUserHeaders = {
|
||||
hygroupcn: 'grp-oodikone-users',
|
||||
edupersonaffiliation: 'member',
|
||||
mail: 'grp-toska+mocknorightuser@helsinki.fi',
|
||||
hypersonsisuid: 'hy-hlo-0000000',
|
||||
}
|
||||
|
||||
const userHeaders = [adminUserHeaders, basicUserHeaders, onlycoursestatisticsUserHeaders, norightsUserHeaders]
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
"concurrently": "concurrently",
|
||||
"eslint": "eslint",
|
||||
"stylelint": "stylelint",
|
||||
"prettier": "prettier",
|
||||
"cli": "./cli.sh",
|
||||
"oodikone": "./run.sh oodikone anon up --detach",
|
||||
"oodikone:real": "./run.sh oodikone real up --detach",
|
||||
|
||||
@@ -64,8 +64,7 @@ parse_profiles() {
|
||||
return 0
|
||||
}
|
||||
|
||||
# Set which docker-compose files to use based on version. In ci, profiles are also
|
||||
# emptied, since they're passed from github actions.
|
||||
# Set which docker-compose files to use based on version.
|
||||
parse_env() {
|
||||
env=""
|
||||
if [[ "$version" == "real" ]]; then
|
||||
|
||||
@@ -20,10 +20,10 @@ USER_DB_NAME="user-db"
|
||||
DATABASES=("$KONE_DB_NAME" "$SIS_DB_NAME" "$SIS_IMPORTER_DB_NAME" "$USER_DB_NAME")
|
||||
|
||||
## Urls should be in same order as databases as both are iterated through by indexes.
|
||||
KONE_DB_REAL_DUMP_URL="oodikone.cs.helsinki.fi:/home/tkt_oodi/backups/latest-kone-pg.sqz"
|
||||
KONE_DB_REAL_DUMP_URL="svm-101.cs.helsinki.fi:/home/oodikone_user/backups/latest-kone-db.sqz"
|
||||
SIS_DB_REAL_DUMP_URL="svm-96.cs.helsinki.fi:/home/updater_user/backups/latest-sis.sqz"
|
||||
SIS_IMPORTER_DB_REAL_DUMP_URL="importer:/home/importer_user/importer-db/backup/importer-db.sqz"
|
||||
USER_DB_REAL_DUMP_URL="oodikone.cs.helsinki.fi:/home/tkt_oodi/backups/latest-user-pg.sqz"
|
||||
SIS_IMPORTER_DB_REAL_DUMP_URL="svm-92.cs.helsinki.fi:/home/importer_user/importer-db/backup/importer-db.sqz"
|
||||
USER_DB_REAL_DUMP_URL="svm-101.cs.helsinki.fi:/home/oodikone_user/backups/latest-user-db.sqz"
|
||||
REAL_DUMP_URLS=("$KONE_DB_REAL_DUMP_URL" "$SIS_DB_REAL_DUMP_URL" "$SIS_IMPORTER_DB_REAL_DUMP_URL" "$USER_DB_REAL_DUMP_URL")
|
||||
|
||||
# Source utility functions
|
||||
@@ -152,10 +152,10 @@ linting and formatting"
|
||||
npm run prepare
|
||||
|
||||
infomsg "Pulling images"
|
||||
"$PROJECT_ROOT"/run.sh oodikone anon pull
|
||||
"$PROJECT_ROOT"/run.sh both anon pull
|
||||
|
||||
infomsg "Building images"
|
||||
"$PROJECT_ROOT"/run.sh oodikone anon build
|
||||
"$PROJECT_ROOT"/run.sh both anon build
|
||||
|
||||
successmsg "Setup ready, oodikone can be started! See README for more info."
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
require('./src/app')
|
||||
const logger = require('./src/util/logger')
|
||||
|
||||
process.on('unhandledRejection', reason => {
|
||||
console.log(reason)
|
||||
logger.error({ message: 'unhandledRejection: ', meta: reason })
|
||||
})
|
||||
|
||||
Generated
+49
-4433
File diff suppressed because it is too large
Load Diff
@@ -15,11 +15,12 @@
|
||||
"@sentry/tracing": "^6.11.0",
|
||||
"async": "^3.2.1",
|
||||
"async-lock": "^1.3.0",
|
||||
"axios": "^0.21.1",
|
||||
"axios": "^0.21.2",
|
||||
"bluebird": "^3.7.2",
|
||||
"cors": "^2.8.5",
|
||||
"cron": "^1.8.2",
|
||||
"express": "^4.17.1",
|
||||
"express-async-errors": "^3.1.1",
|
||||
"jsonwebtoken": "^8.5.1",
|
||||
"lodash": "^4.17.21",
|
||||
"lru-cache": "^5.1.1",
|
||||
@@ -32,9 +33,7 @@
|
||||
"umzug": "^2.3.0",
|
||||
"unfuck-utf8-headers-middleware": "^1.0.1",
|
||||
"uuid": "^3.4.0",
|
||||
"winston": "^3.3.3",
|
||||
"winston-log2gelf": "^2.4.0",
|
||||
"winston-transport": "^4.4.0"
|
||||
"winston": "^3.3.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"nodemon": "^1.19.4"
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
require('express-async-errors')
|
||||
const express = require('express')
|
||||
const cors = require('cors')
|
||||
const Sentry = require('@sentry/node')
|
||||
@@ -6,17 +7,19 @@ const routes = require('./routes')
|
||||
const { startCron } = require('./events')
|
||||
const { initializeDatabaseConnection, dbConnections } = require('./database/connection')
|
||||
const initializeSentry = require('./util/sentry')
|
||||
const errorMiddleware = require('./middleware/errormiddleware')
|
||||
const logger = require('./util/logger')
|
||||
|
||||
initializeDatabaseConnection()
|
||||
.then(() => {
|
||||
dbConnections.connect()
|
||||
|
||||
dbConnections.on('connect', () => {
|
||||
console.log('Connected to sis db successfully')
|
||||
logger.info('Connected to sis db successfully')
|
||||
})
|
||||
|
||||
dbConnections.on('error', () => {
|
||||
console.log('Failed connecting to sis db')
|
||||
logger.error('Failed to connect to sis db!')
|
||||
})
|
||||
|
||||
const app = express()
|
||||
@@ -31,21 +34,23 @@ initializeDatabaseConnection()
|
||||
|
||||
routes(app, baseUrl)
|
||||
|
||||
app.get('*', async (req, res) => {
|
||||
app.get('*', async (_, res) => {
|
||||
const results = { error: 'unknown endpoint' }
|
||||
res.status(404).json(results)
|
||||
})
|
||||
|
||||
app.use(Sentry.Handlers.errorHandler())
|
||||
|
||||
const server = app.listen(backendPort, () => console.log(`Backend listening on port ${backendPort}!`))
|
||||
app.use(errorMiddleware)
|
||||
|
||||
const server = app.listen(backendPort, () => logger.info(`Backend listening on port ${backendPort}!`))
|
||||
process.on('SIGTERM', () => {
|
||||
server.close(() => {
|
||||
console.log('Process terminated')
|
||||
logger.info('Process terminated')
|
||||
})
|
||||
})
|
||||
})
|
||||
.catch(e => {
|
||||
process.exitCode = 1
|
||||
console.log(e)
|
||||
logger.error(e)
|
||||
})
|
||||
|
||||
@@ -62,6 +62,8 @@ const addSlashToEnd = url => (url.endsWith('/') ? url : url + '/')
|
||||
|
||||
const ACCESS_TOKEN_HEADER_KEY = 'x-access-token'
|
||||
|
||||
const PWD = process.env.PWD || '/usr/src/app'
|
||||
|
||||
module.exports = {
|
||||
DB_URL_KONE,
|
||||
DB_MAX_CONNECTIONS,
|
||||
@@ -90,4 +92,5 @@ module.exports = {
|
||||
backendPort,
|
||||
importerToken,
|
||||
importerUrl,
|
||||
PWD,
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@ const EventEmitter = require('events')
|
||||
const Umzug = require('umzug')
|
||||
const conf = require('../conf-backend')
|
||||
const { SIS_DB_URL } = process.env
|
||||
const logger = require('../util/logger')
|
||||
|
||||
class DbConnection extends EventEmitter {
|
||||
constructor() {
|
||||
super()
|
||||
@@ -31,8 +33,7 @@ class DbConnection extends EventEmitter {
|
||||
this.emit('error', e)
|
||||
return
|
||||
}
|
||||
console.log(`Sis database connection failed! Attempt ${attempt}/${this.RETRY_ATTEMPTS}`)
|
||||
console.log(e)
|
||||
logger.error({ message: `Sis database connection failed! Attempt ${attempt}/${this.RETRY_ATTEMPTS}`, meta: e })
|
||||
setTimeout(() => this.connect(attempt + 1), 1000 * attempt)
|
||||
}
|
||||
}
|
||||
@@ -54,10 +55,9 @@ const initializeDatabaseConnection = async () => {
|
||||
break
|
||||
} catch (e) {
|
||||
if (i === waitSeconds) {
|
||||
console.log(`Could not connect to database in ${waitSeconds} seconds`)
|
||||
logger.error(`Could not connect to database in ${waitSeconds} seconds`)
|
||||
throw e
|
||||
}
|
||||
console.log('.')
|
||||
await sleep(1000)
|
||||
}
|
||||
}
|
||||
@@ -69,7 +69,6 @@ const initializeDatabaseConnection = async () => {
|
||||
tableName: 'migrations',
|
||||
schema: conf.DB_SCHEMA_KONE,
|
||||
},
|
||||
logging: console.log,
|
||||
migrations: {
|
||||
params: [sequelizeKone.getQueryInterface(), Sequelize],
|
||||
path: `${process.cwd()}/src/database/migrations_kone`,
|
||||
@@ -78,27 +77,16 @@ const initializeDatabaseConnection = async () => {
|
||||
},
|
||||
})
|
||||
const migrations = await migrator.up()
|
||||
|
||||
console.log('Kone Migrations up to date', migrations)
|
||||
logger.info({ message: 'Kone Migrations up to date', meta: migrations })
|
||||
} catch (e) {
|
||||
console.log('Kone Migration error')
|
||||
logger.error('Kone Migration error')
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
const forceSyncDatabase = async () => {
|
||||
try {
|
||||
await sequelizeKone.getQueryInterface().createSchema(conf.DB_SCHEMA_KONE)
|
||||
} catch (e) {
|
||||
//console.log(e)
|
||||
}
|
||||
await sequelizeKone.sync({ force: true })
|
||||
}
|
||||
|
||||
const dbConnections = new DbConnection()
|
||||
module.exports = {
|
||||
dbConnections,
|
||||
sequelizeKone,
|
||||
initializeDatabaseConnection,
|
||||
forceSyncDatabase,
|
||||
}
|
||||
|
||||
+120
-160
@@ -9,209 +9,168 @@ const {
|
||||
getStartYears,
|
||||
} = require('./services/trends')
|
||||
const { refreshAssociationsInRedis } = require('./services/studyrights')
|
||||
const { getAllProgrammes, nonGraduatedStudentsOfElementDetail } = require('./services/studyrights')
|
||||
const { getAllProgrammes } = require('./services/studyrights')
|
||||
const { productivityStatsForStudytrack, throughputStatsForStudytrack } = require('./services/studyprogramme')
|
||||
const { findAndSaveTeachers } = require('./services/topteachers')
|
||||
const {
|
||||
setProductivity,
|
||||
setThroughput,
|
||||
patchProductivity,
|
||||
patchThroughput,
|
||||
patchNonGraduatedStudents,
|
||||
} = require('./services/analyticsService')
|
||||
const { isNewHYStudyProgramme } = require('./util')
|
||||
const { setProductivity, setThroughput, patchProductivity, patchThroughput } = require('./services/analyticsService')
|
||||
const { isProduction } = require('./conf-backend')
|
||||
const { getCurrentSemester } = require('./services/semesters')
|
||||
const logger = require('./util/logger')
|
||||
|
||||
const schedule = (cronTime, func) => new CronJob({ cronTime, onTick: func, start: true, timeZone: 'Europe/Helsinki' })
|
||||
|
||||
const refreshStudyrightAssociations = async () => {
|
||||
try {
|
||||
console.log('Refreshing studyright associations...')
|
||||
await refreshAssociationsInRedis()
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
await refreshAssociationsInRedis()
|
||||
logger.info('Studyright associations refreshed!')
|
||||
}
|
||||
|
||||
const refreshOverview = async () => {
|
||||
try {
|
||||
console.log('Refreshing throughput and productivity for programmes...')
|
||||
const codes = (await getAllProgrammes()).map(p => p.code)
|
||||
let ready = 0
|
||||
for (const code of codes) {
|
||||
let programmeStatsSince = new Date('2017-07-31')
|
||||
if (code.includes('MH') || code.includes('KH')) {
|
||||
programmeStatsSince = new Date('2017-07-31')
|
||||
} else {
|
||||
programmeStatsSince = new Date('2000-07-31')
|
||||
}
|
||||
try {
|
||||
await patchThroughput({ [code]: { status: 'RECALCULATING' } })
|
||||
const data = await throughputStatsForStudytrack(code, programmeStatsSince.getFullYear())
|
||||
await setThroughput(data)
|
||||
} catch (e) {
|
||||
try {
|
||||
await patchThroughput({ [code]: { status: 'RECALCULATION ERRORED' } })
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
console.error(e)
|
||||
console.log(`Failed to update throughput stats for code: ${code}, reason: ${e.message}`)
|
||||
}
|
||||
try {
|
||||
await patchProductivity({ [code]: { status: 'RECALCULATING' } })
|
||||
const data = await productivityStatsForStudytrack(code, programmeStatsSince)
|
||||
await setProductivity(data)
|
||||
} catch (e) {
|
||||
try {
|
||||
await patchProductivity({
|
||||
[code]: { status: 'RECALCULATION ERRORED' },
|
||||
})
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
console.error(e)
|
||||
console.log(`Failed to update productivity stats for code: ${code}, reason: ${e.message}`)
|
||||
}
|
||||
ready += 1
|
||||
console.log(`${ready}/${codes.length} programmes done`)
|
||||
logger.info('Refreshing throughput and productivity for programmes...')
|
||||
const codes = (await getAllProgrammes()).map(p => p.code).filter(code => code.includes('KH') || code.includes('MH'))
|
||||
let ready = 0
|
||||
for (const code of codes) {
|
||||
let programmeStatsSince = new Date('2017-07-31')
|
||||
if (code.includes('MH') || code.includes('KH')) {
|
||||
programmeStatsSince = new Date('2017-07-31')
|
||||
} else {
|
||||
programmeStatsSince = new Date('2000-07-31')
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
try {
|
||||
await patchThroughput({ [code]: { status: 'RECALCULATING' } })
|
||||
const data = await throughputStatsForStudytrack(code, programmeStatsSince.getFullYear())
|
||||
await setThroughput(data)
|
||||
} catch (e) {
|
||||
logger.error({ message: `failed to recalculate throughput for ${code}`, meta: e })
|
||||
try {
|
||||
await patchThroughput({ [code]: { status: 'RECALCULATION ERRORED' } })
|
||||
} catch (e) {
|
||||
logger.error({ message: 'failed to update throughtput status to error', meta: e })
|
||||
}
|
||||
}
|
||||
try {
|
||||
await patchProductivity({ [code]: { status: 'RECALCULATING' } })
|
||||
const data = await productivityStatsForStudytrack(code, programmeStatsSince)
|
||||
await setProductivity(data)
|
||||
} catch (e) {
|
||||
logger.error({ message: `failed to recalculate productivity for ${code}`, meta: e })
|
||||
try {
|
||||
await patchProductivity({
|
||||
[code]: { status: 'RECALCULATION ERRORED' },
|
||||
})
|
||||
} catch (e) {
|
||||
logger.error({ message: 'failed to update productivity status to error', meta: e })
|
||||
}
|
||||
}
|
||||
ready += 1
|
||||
logger.info(`${ready}/${codes.length} programmes done`)
|
||||
}
|
||||
logger.info('Throughput and productivity for programmes refreshed')
|
||||
}
|
||||
|
||||
const refreshTeacherLeaderboard = async () => {
|
||||
try {
|
||||
const startyearcode = new Date().getFullYear() - 1950
|
||||
const endyearcode = startyearcode + 1
|
||||
console.log('Refreshing teacher leaderboard...')
|
||||
await findAndSaveTeachers(startyearcode, endyearcode)
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
}
|
||||
}
|
||||
|
||||
const refreshNonGraduatedStudentsOfOldProgrammes = async () => {
|
||||
try {
|
||||
const oldProgrammeCodes = (await getAllProgrammes()).map(p => p.code).filter(c => !isNewHYStudyProgramme(c))
|
||||
let i = 0
|
||||
console.log('Refreshing non-graduated students of old programmes...')
|
||||
await Promise.all(
|
||||
oldProgrammeCodes.map(
|
||||
c =>
|
||||
new Promise(async res => {
|
||||
try {
|
||||
const [nonGraduatedStudents, studentnumbers] = await nonGraduatedStudentsOfElementDetail(c)
|
||||
await patchNonGraduatedStudents({ [c]: { formattedData: nonGraduatedStudents, studentnumbers } })
|
||||
console.log(`${++i}/${oldProgrammeCodes.length}`)
|
||||
} catch (e) {
|
||||
console.log(`Failed refreshing non-graduated students of programme ${c}!`)
|
||||
}
|
||||
res()
|
||||
})
|
||||
)
|
||||
)
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
}
|
||||
// refresh this and previous year
|
||||
const currentSemestersYearCode = (await getCurrentSemester()).getDataValue('yearcode')
|
||||
await findAndSaveTeachers(currentSemestersYearCode - 1, currentSemestersYearCode)
|
||||
}
|
||||
|
||||
const refreshProtoCtoRedis = async () => {
|
||||
try {
|
||||
const defaultQuery = { include_old_attainments: 'false', exclude_non_enrolled: 'false' }
|
||||
const onlyOld = { include_old_attainments: 'true', exclude_non_enrolled: 'false' }
|
||||
const onlyEnr = { include_old_attainments: 'false', exclude_non_enrolled: 'true' }
|
||||
const bothToggles = { include_old_attainments: 'true', exclude_non_enrolled: 'true' }
|
||||
console.log('Refreshing CDS ProtoC')
|
||||
await refreshProtoC(defaultQuery)
|
||||
await refreshProtoC(onlyOld)
|
||||
await refreshProtoC(onlyEnr)
|
||||
await refreshProtoC(bothToggles)
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
}
|
||||
logger.info('Refreshing CDS ProtoC')
|
||||
const defaultQuery = { include_old_attainments: 'false', exclude_non_enrolled: 'false' }
|
||||
const onlyOld = { include_old_attainments: 'true', exclude_non_enrolled: 'false' }
|
||||
const onlyEnr = { include_old_attainments: 'false', exclude_non_enrolled: 'true' }
|
||||
const bothToggles = { include_old_attainments: 'true', exclude_non_enrolled: 'true' }
|
||||
await refreshProtoC(defaultQuery)
|
||||
await refreshProtoC(onlyOld)
|
||||
await refreshProtoC(onlyEnr)
|
||||
await refreshProtoC(bothToggles)
|
||||
logger.info('Refreshing CDS ProtoC doned')
|
||||
}
|
||||
|
||||
const refreshStatusToRedis = async () => {
|
||||
try {
|
||||
const unixMillis = moment().valueOf()
|
||||
const date = new Date(Number(unixMillis))
|
||||
const unixMillis = moment().valueOf()
|
||||
const date = new Date(Number(unixMillis))
|
||||
|
||||
date.setHours(23, 59, 59, 999)
|
||||
const showByYearOff = 'false'
|
||||
const showByYear = 'true'
|
||||
console.log('Refreshing CDS Status')
|
||||
await refreshStatus(date.getTime(), showByYearOff)
|
||||
await refreshStatus(date.getTime(), showByYear)
|
||||
date.setHours(23, 59, 59, 999)
|
||||
const showByYearOff = 'false'
|
||||
const showByYear = 'true'
|
||||
logger.info('Refreshing CDS Status')
|
||||
await refreshStatus(date.getTime(), showByYearOff)
|
||||
await refreshStatus(date.getTime(), showByYear)
|
||||
logger.info('Refreshing CDS Status doned')
|
||||
|
||||
console.log('Refreshing CDS Graduated')
|
||||
await refreshStatusGraduated(date.getTime(), showByYearOff)
|
||||
await refreshStatusGraduated(date.getTime(), showByYear)
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
}
|
||||
logger.info('Refreshing CDS Graduated')
|
||||
await refreshStatusGraduated(date.getTime(), showByYearOff)
|
||||
await refreshStatusGraduated(date.getTime(), showByYear)
|
||||
logger.info('Refreshing CDS graduated doned')
|
||||
}
|
||||
|
||||
const refreshUberToRedis = async () => {
|
||||
try {
|
||||
const years = await getStartYears()
|
||||
const mappedYears = years.map(({ studystartdate }) => studystartdate)
|
||||
mappedYears.forEach(async year => {
|
||||
console.log('Refreshing CDS Uber data for date', year)
|
||||
const defaultQuery = { include_old_attainments: 'false', start_date: year }
|
||||
const oldAttainmentsQuery = { include_old_attainments: 'true', start_date: year }
|
||||
const years = (await getStartYears()).map(({ studystartdate }) => studystartdate)
|
||||
for (const year of years) {
|
||||
const formattedYear = new Date(year).getFullYear()
|
||||
logger.info(`Refreshing CDS uber year ${formattedYear}`)
|
||||
const defaultQuery = { include_old_attainments: 'false', start_date: year }
|
||||
const oldAttainmentsQuery = { include_old_attainments: 'true', start_date: year }
|
||||
try {
|
||||
await refreshUber(defaultQuery)
|
||||
await refreshUber(oldAttainmentsQuery)
|
||||
})
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
logger.info(`Refreshing CDS uber year ${formattedYear} doned`)
|
||||
} catch (e) {
|
||||
logger.error({ message: `Error when refreshing CDS uber year ${formattedYear}`, meta: e })
|
||||
}
|
||||
}
|
||||
logger.info(`Refreshing CDS Uber data doned`)
|
||||
}
|
||||
|
||||
const refreshProtoCProgrammeToRedis = async () => {
|
||||
try {
|
||||
const codes = (await getAllProgrammes()).map(p => p.code).filter(code => code.includes('KH'))
|
||||
codes.forEach(async code => {
|
||||
console.log('refreshing code', code)
|
||||
const defaultQuery = { include_old_attainments: 'false', exclude_non_enrolled: 'false', code }
|
||||
const onlyOld = { include_old_attainments: 'true', exclude_non_enrolled: 'false', code }
|
||||
const onlyEnr = { include_old_attainments: 'false', exclude_non_enrolled: 'true', code }
|
||||
const bothToggles = { include_old_attainments: 'true', exclude_non_enrolled: 'true', code }
|
||||
console.log('Refreshing CDS ProtoCProgramme for code ', code)
|
||||
await refreshProtoCProgramme(defaultQuery)
|
||||
await refreshProtoCProgramme(onlyOld)
|
||||
await refreshProtoCProgramme(onlyEnr)
|
||||
await refreshProtoCProgramme(bothToggles)
|
||||
})
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
const codes = (await getAllProgrammes()).map(p => p.code).filter(code => code.includes('KH'))
|
||||
for (const code of codes) {
|
||||
logger.info(`Refreshing ProtoCProgramme code ${code}`)
|
||||
const defaultQuery = { include_old_attainments: 'false', exclude_non_enrolled: 'false', code }
|
||||
const onlyOld = { include_old_attainments: 'true', exclude_non_enrolled: 'false', code }
|
||||
const onlyEnr = { include_old_attainments: 'false', exclude_non_enrolled: 'true', code }
|
||||
const bothToggles = { include_old_attainments: 'true', exclude_non_enrolled: 'true', code }
|
||||
const queries = [defaultQuery, onlyOld, onlyEnr, bothToggles]
|
||||
for (const query of queries) {
|
||||
try {
|
||||
await refreshProtoCProgramme(query)
|
||||
} catch (e) {
|
||||
logger.error({
|
||||
message: `Error when refreshing ProtocProgramme code ${code}, query ${Object.keys({ query })[0]}`,
|
||||
meta: e,
|
||||
}) // object.keys is hax to print query variable name
|
||||
}
|
||||
}
|
||||
logger.info(`Refreshing ProtoCProgramme code ${code} doned`)
|
||||
}
|
||||
logger.info(`Refreshing protocprogramme doned`)
|
||||
}
|
||||
|
||||
const refreshStatistics = async () => {
|
||||
console.log('Starting refreshing statistics')
|
||||
await refreshStudyrightAssociations()
|
||||
await refreshOverview()
|
||||
await refreshNonGraduatedStudentsOfOldProgrammes()
|
||||
await refreshTeacherLeaderboard()
|
||||
console.log('Statistics refreshed!')
|
||||
const statfuncs = [refreshStudyrightAssociations, refreshOverview, refreshTeacherLeaderboard]
|
||||
logger.info('Refreshing statistics')
|
||||
for (const func of statfuncs) {
|
||||
await func()
|
||||
}
|
||||
logger.info('Statistics refreshed!')
|
||||
}
|
||||
|
||||
const refreshTrends = async () => {
|
||||
console.log('Starting refreshing trends')
|
||||
await refreshProtoCtoRedis()
|
||||
await refreshStatusToRedis()
|
||||
await refreshUberToRedis()
|
||||
await refreshProtoCProgrammeToRedis()
|
||||
console.log('Trends refreshed!')
|
||||
const trendfuncs = [refreshProtoCtoRedis, refreshStatusToRedis, refreshUberToRedis, refreshProtoCProgrammeToRedis]
|
||||
logger.info('Refreshing trends')
|
||||
for (const func of trendfuncs) {
|
||||
await func()
|
||||
}
|
||||
logger.info('Trends refreshed!')
|
||||
}
|
||||
|
||||
const startCron = () => {
|
||||
if (isProduction) {
|
||||
schedule('0 6 * * *', async () => {
|
||||
await refreshStatistics()
|
||||
await refreshTrends()
|
||||
// refresh 3am every day
|
||||
schedule('0 3 * * *', async () => {
|
||||
for (const func of [refreshStatistics, refreshTrends]) {
|
||||
await func()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -219,4 +178,5 @@ const startCron = () => {
|
||||
module.exports = {
|
||||
startCron,
|
||||
refreshStatistics,
|
||||
refreshTrends,
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ const { getUserDataFor } = require('../services/userService')
|
||||
const { ACCESS_TOKEN_HEADER_KEY } = require('../conf-backend')
|
||||
const { hasRequiredGroup, parseHyGroups } = require('../util/utils')
|
||||
|
||||
const TOKEN_VERSION = 1 // When token structure changes, increment in userservice, backend and frontend
|
||||
const TOKEN_VERSION = 1.1 // When token structure changes, increment in userservice, backend and frontend
|
||||
|
||||
const checkAuth = async (req, res, next) => {
|
||||
const token = req.headers[ACCESS_TOKEN_HEADER_KEY]
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
const Sentry = require('@sentry/node')
|
||||
const { ApplicationError } = require('../util/customErrors')
|
||||
const logger = require('../util/logger')
|
||||
|
||||
const errorHandler = (error, req, res, next) => {
|
||||
logger.error(`${error.message} ${error.name} ${error.stack}`)
|
||||
|
||||
Sentry.captureException(error)
|
||||
|
||||
if (res.headersSent) {
|
||||
return next(error)
|
||||
}
|
||||
|
||||
const normalizedError = error instanceof ApplicationError ? error : new ApplicationError(error.message)
|
||||
|
||||
return res.status(normalizedError.status).json(normalizedError)
|
||||
}
|
||||
|
||||
module.exports = errorHandler
|
||||
@@ -30,6 +30,9 @@ Student.hasMany(SemesterEnrollment, { foreignKey: 'studentnumber', sourceKey: 's
|
||||
SemesterEnrollment.belongsTo(Semester, { foreignKey: 'semestercomposite', targetKey: 'composite' })
|
||||
Semester.hasMany(SemesterEnrollment, { foreignKey: 'semestercomposite', sourceKey: 'composite' })
|
||||
|
||||
ProgrammeModule.belongsTo(Organization, { foreignKey: 'organization_id' })
|
||||
Organization.hasMany(ProgrammeModule, { foreignKey: 'organization_id' })
|
||||
|
||||
Credit.notUnnecessary = credit => {
|
||||
return credit.credits > 0 && credit.credits <= 12
|
||||
}
|
||||
@@ -96,6 +99,8 @@ ProgrammeModule.belongsToMany(ProgrammeModule, {
|
||||
foreignKey: 'parent_id',
|
||||
})
|
||||
|
||||
Organization.hasMany(Organization, { foreignKey: 'parent_id', as: 'children' })
|
||||
|
||||
module.exports = {
|
||||
Organization,
|
||||
Course,
|
||||
|
||||
@@ -19,6 +19,9 @@ Organization.init(
|
||||
name: {
|
||||
type: JSONB,
|
||||
},
|
||||
parent_id: {
|
||||
type: STRING,
|
||||
},
|
||||
createdAt: {
|
||||
type: DATE,
|
||||
},
|
||||
|
||||
@@ -18,6 +18,16 @@ ProgrammeModule.init(
|
||||
type: {
|
||||
type: STRING,
|
||||
},
|
||||
studyLevel: {
|
||||
type: STRING,
|
||||
},
|
||||
organization_id: {
|
||||
type: STRING,
|
||||
references: {
|
||||
model: 'organization',
|
||||
key: 'id',
|
||||
},
|
||||
},
|
||||
createdAt: {
|
||||
type: DATE,
|
||||
},
|
||||
|
||||
@@ -57,7 +57,7 @@ module.exports = (app, url) => {
|
||||
app.use(`${url}/faculties`, faculties)
|
||||
app.use(`${url}/updater`, auth.roles(['admin']), updater)
|
||||
app.use(`${url}/teachers`, auth.roles(['teachers']), teachers)
|
||||
app.use(`${url}/users`, auth.roles(['users']), users)
|
||||
app.use(`${url}/users`, auth.roles(['admin']), users)
|
||||
app.use(`${url}/feedback`, feedback)
|
||||
app.use(`${url}/mandatory_courses`, mandatoryCourses)
|
||||
app.use(`${url}/mandatory-course-labels`, mandatoryCourseLabels)
|
||||
|
||||
@@ -2,7 +2,6 @@ const router = require('express').Router()
|
||||
const crypto = require('crypto')
|
||||
const Course = require('../services/courses')
|
||||
const { validateParamLength } = require('../util')
|
||||
const logger = require('../util/logger')
|
||||
|
||||
router.get('/v2/coursesmulti', async (req, res) => {
|
||||
let results = { courses: [] }
|
||||
@@ -17,35 +16,29 @@ router.get('/v2/coursesmulti', async (req, res) => {
|
||||
})
|
||||
|
||||
router.get('/v3/courseyearlystats', async (req, res) => {
|
||||
try {
|
||||
const { rights, roles } = req
|
||||
const { rights, roles } = req
|
||||
|
||||
const allowedRoles = roles && ['admin', 'courseStatistics'].find(role => roles.includes(role))
|
||||
const allowedRoles = roles && ['admin', 'courseStatistics'].find(role => roles.includes(role))
|
||||
|
||||
// If user has rights to see at least one programme, then they are allowed
|
||||
// to see all of them
|
||||
if (!allowedRoles && rights.length < 1) {
|
||||
return res.status(403).json({ error: 'No programmes so no access to course stats' })
|
||||
}
|
||||
// If user has rights to see at least one programme, then they are allowed
|
||||
// to see all of them
|
||||
if (!allowedRoles && rights.length < 1) {
|
||||
return res.status(403).json({ error: 'No programmes so no access to course stats' })
|
||||
}
|
||||
|
||||
const { codes, separate: sep, unifyOpenUniCourses: unify } = req.query
|
||||
const { codes, separate: sep, unifyOpenUniCourses: unify } = req.query
|
||||
|
||||
const separate = !sep ? false : JSON.parse(sep)
|
||||
const unifyOpenUniCourses = !unify ? false : JSON.parse(unify)
|
||||
if (!codes) {
|
||||
res.status(422).send('Missing required query parameters')
|
||||
} 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 anonymizationSalt = anonymize ? crypto.randomBytes(12).toString('hex') : null
|
||||
const results = await Course.courseYearlyStats(codes, separate, unifyOpenUniCourses, anonymizationSalt)
|
||||
res.json(results)
|
||||
}
|
||||
} catch (e) {
|
||||
logger.error(e.message)
|
||||
console.log(e)
|
||||
res.status(500).send('Something went wrong with handling the request.')
|
||||
const separate = !sep ? false : JSON.parse(sep)
|
||||
const unifyOpenUniCourses = !unify ? false : JSON.parse(unify)
|
||||
if (!codes) {
|
||||
res.status(422).send('Missing required query parameters')
|
||||
} 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 anonymizationSalt = anonymize ? crypto.randomBytes(12).toString('hex') : null
|
||||
const results = await Course.courseYearlyStats(codes, separate, unifyOpenUniCourses, anonymizationSalt)
|
||||
res.json(results)
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -7,76 +7,56 @@ const {
|
||||
} = require('../services/customPopulationSearch')
|
||||
|
||||
router.get('/', async (req, res) => {
|
||||
try {
|
||||
const {
|
||||
decodedToken: { id },
|
||||
} = req
|
||||
const customPopulationSearches = await getCustomPopulationSearchesByUser(id)
|
||||
res.json(customPopulationSearches)
|
||||
} catch (e) {
|
||||
console.error('e', e)
|
||||
res.status(500).json({ error: 'error' })
|
||||
}
|
||||
const {
|
||||
decodedToken: { id },
|
||||
} = req
|
||||
const customPopulationSearches = await getCustomPopulationSearchesByUser(id)
|
||||
res.json(customPopulationSearches)
|
||||
})
|
||||
|
||||
router.post('/', async (req, res) => {
|
||||
try {
|
||||
const {
|
||||
decodedToken: { id },
|
||||
body: { name, students },
|
||||
} = req
|
||||
const {
|
||||
decodedToken: { id },
|
||||
body: { name, students },
|
||||
} = req
|
||||
|
||||
if (!name) return res.status(400).json({ error: 'name missing' })
|
||||
if (students && !Array.isArray(students)) res.status(400).json({ error: 'students must be of type array' })
|
||||
if (!name) return res.status(400).json({ error: 'name missing' })
|
||||
if (students && !Array.isArray(students)) res.status(400).json({ error: 'students must be of type array' })
|
||||
|
||||
const customPopulationSearch = await createCustomPopulationSearch(name, id, students || [])
|
||||
res.json(customPopulationSearch)
|
||||
} catch (e) {
|
||||
console.error('e', e)
|
||||
res.status(500).json({ error: 'error' })
|
||||
}
|
||||
const customPopulationSearch = await createCustomPopulationSearch(name, id, students || [])
|
||||
res.json(customPopulationSearch)
|
||||
})
|
||||
|
||||
router.put('/:id', async (req, res) => {
|
||||
try {
|
||||
const {
|
||||
decodedToken: { id: userId },
|
||||
body: { students },
|
||||
} = req
|
||||
const { id } = req.params
|
||||
const {
|
||||
decodedToken: { id: userId },
|
||||
body: { students },
|
||||
} = req
|
||||
const { id } = req.params
|
||||
|
||||
if (!id) return res.status(400).json({ error: 'id missing' })
|
||||
if (!students) return res.status(400).json({ error: 'students missing' })
|
||||
if (!Array.isArray(students)) res.status(400).json({ error: 'students must be of type array' })
|
||||
if (!id) return res.status(400).json({ error: 'id missing' })
|
||||
if (!students) return res.status(400).json({ error: 'students missing' })
|
||||
if (!Array.isArray(students)) res.status(400).json({ error: 'students must be of type array' })
|
||||
|
||||
const updatedPopulationSearch = await updateCustomPopulationSearch(userId, id, students)
|
||||
if (!updatedPopulationSearch) return res.status(404).json({ error: 'custom population search not found' })
|
||||
const updatedPopulationSearch = await updateCustomPopulationSearch(userId, id, students)
|
||||
if (!updatedPopulationSearch) return res.status(404).json({ error: 'custom population search not found' })
|
||||
|
||||
res.json(updatedPopulationSearch)
|
||||
} catch (e) {
|
||||
console.error('e', e)
|
||||
res.status(500).json({ error: 'error' })
|
||||
}
|
||||
res.json(updatedPopulationSearch)
|
||||
})
|
||||
|
||||
router.delete('/:id', async (req, res) => {
|
||||
try {
|
||||
const {
|
||||
decodedToken: { id: userId },
|
||||
} = req
|
||||
const { id } = req.params
|
||||
const {
|
||||
decodedToken: { id: userId },
|
||||
} = req
|
||||
const { id } = req.params
|
||||
|
||||
if (!id) return res.status(400).json({ error: 'id missing' })
|
||||
if (!id) return res.status(400).json({ error: 'id missing' })
|
||||
|
||||
const deletedSuccessfully = (await deleteCustomPopulationSearch(userId, id)) > 0
|
||||
const deletedSuccessfully = (await deleteCustomPopulationSearch(userId, id)) > 0
|
||||
|
||||
if (!deletedSuccessfully) return res.status(404).json({ error: 'custom population search not found' })
|
||||
if (!deletedSuccessfully) return res.status(404).json({ error: 'custom population search not found' })
|
||||
|
||||
res.json(id)
|
||||
} catch (e) {
|
||||
console.error('e', e)
|
||||
res.status(500).json({ error: 'error' })
|
||||
}
|
||||
res.json(id)
|
||||
})
|
||||
|
||||
module.exports = router
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
const router = require('express').Router()
|
||||
const { getStudentsUserCanAccess } = require('../services/userService')
|
||||
const MandatoryCourses = require('../services/mandatoryCourses')
|
||||
const {
|
||||
productivityStatsForStudytrack,
|
||||
@@ -7,7 +6,6 @@ const {
|
||||
optionData,
|
||||
} = require('../services/studyprogramme')
|
||||
const { findProgrammeTheses, createThesisCourse, deleteThesisCourse } = require('../services/thesis')
|
||||
|
||||
const {
|
||||
getProductivity,
|
||||
setProductivity,
|
||||
@@ -15,28 +13,20 @@ const {
|
||||
setThroughput,
|
||||
patchProductivity,
|
||||
patchThroughput,
|
||||
getNonGraduatedStudents,
|
||||
} = require('../services/analyticsService')
|
||||
const { getAllProgrammes, getAllElementDetails } = require('../services/studyrights')
|
||||
const logger = require('../util/logger')
|
||||
|
||||
const programmeStatsSince = new Date('2017-07-31')
|
||||
|
||||
router.get('/elementdetails/all', async (req, res) => {
|
||||
try {
|
||||
const elementdetails = await getAllElementDetails()
|
||||
res.json(elementdetails)
|
||||
} catch (e) {
|
||||
res.status(500).json(e)
|
||||
}
|
||||
const elementdetails = await getAllElementDetails()
|
||||
res.json(elementdetails)
|
||||
})
|
||||
|
||||
router.get('/studyprogrammes', async (req, res) => {
|
||||
try {
|
||||
const studyrights = await getAllProgrammes()
|
||||
res.json(studyrights)
|
||||
} catch (err) {
|
||||
res.status(500).json(err)
|
||||
}
|
||||
const studyrights = await getAllProgrammes()
|
||||
res.json(studyrights)
|
||||
})
|
||||
|
||||
router.get('/v2/studyprogrammes/:id/optiondata', async (req, res) => {
|
||||
@@ -55,34 +45,6 @@ router.get('/v2/studyprogrammes/:id/optiondata', async (req, res) => {
|
||||
res.json(data)
|
||||
})
|
||||
|
||||
router.get('/v2/studyprogrammes/:id/present_students', async (req, res) => {
|
||||
try {
|
||||
const {
|
||||
roles,
|
||||
decodedToken,
|
||||
params: { id },
|
||||
} = req
|
||||
if (!id) return res.status(400).json({ error: 'programme id missing' })
|
||||
|
||||
const nonGraduatedStudents = await getNonGraduatedStudents(id)
|
||||
if (!nonGraduatedStudents) return res.status(200).json({})
|
||||
const {
|
||||
data: { formattedData, studentnumbers },
|
||||
} = nonGraduatedStudents
|
||||
|
||||
const filteredData = { ...formattedData }
|
||||
const studentsUserCanAccess = await getStudentsUserCanAccess(studentnumbers, roles, decodedToken.userId)
|
||||
Object.keys(filteredData).forEach(year => {
|
||||
filteredData[year] = filteredData[year].filter(({ studentNumber }) => studentsUserCanAccess.has(studentNumber))
|
||||
})
|
||||
|
||||
res.json(filteredData)
|
||||
} catch (e) {
|
||||
console.error('e', e)
|
||||
res.status(500).json({ error: 'error' })
|
||||
}
|
||||
})
|
||||
|
||||
router.get('/v2/studyprogrammes/:id/mandatory_courses', async (req, res) => {
|
||||
if (req.params.id) {
|
||||
const codes = await MandatoryCourses.byStudyprogramme(req.params.id)
|
||||
@@ -99,14 +61,14 @@ router.get('/v2/studyprogrammes/:id/productivity', async (req, res) => {
|
||||
try {
|
||||
data = await getProductivity(code)
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
logger.error(`Failed to get code ${code} productivity`)
|
||||
}
|
||||
if (!data) {
|
||||
try {
|
||||
const stats = await productivityStatsForStudytrack(code, programmeStatsSince)
|
||||
data = await setProductivity(stats)
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
logger.error(`Failed to update code ${code} patching status to recalculation erroring`)
|
||||
}
|
||||
}
|
||||
return res.json(data)
|
||||
@@ -118,14 +80,8 @@ router.get('/v2/studyprogrammes/:id/productivity', async (req, res) => {
|
||||
router.get('/v2/studyprogrammes/productivity/recalculate', async (req, res) => {
|
||||
const code = req.query.code
|
||||
|
||||
console.log('Productivity stats recalculation starting')
|
||||
try {
|
||||
await patchThroughput({ [code]: { status: 'RECALCULATING' } })
|
||||
res.status(200).end()
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
return res.status(500).end()
|
||||
}
|
||||
await patchThroughput({ [code]: { status: 'RECALCULATING' } })
|
||||
res.status(200).end()
|
||||
|
||||
try {
|
||||
if (code.includes('MH') || code.includes('KH')) {
|
||||
@@ -141,13 +97,13 @@ router.get('/v2/studyprogrammes/productivity/recalculate', async (req, res) => {
|
||||
[code]: { status: 'RECALCULATION ERRORED' },
|
||||
})
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
logger.error(`Failed to update code ${code} patching status to recalculation erroring`)
|
||||
return
|
||||
}
|
||||
console.error(e)
|
||||
console.log(`Failed to update productivity stats for code: ${code}, reason: ${e.message}`)
|
||||
|
||||
logger.error(`Failed to update productivity stats for code: ${code}, reason: ${e.message}`)
|
||||
}
|
||||
console.log(`Productivity stats recalculation for studyprogramme ${code} done`)
|
||||
logger.info(`Produdtiviy stats recalculation for studyprogramme ${code} done`)
|
||||
})
|
||||
|
||||
router.get('/v2/studyprogrammes/:id/throughput', async (req, res) => {
|
||||
@@ -159,14 +115,14 @@ router.get('/v2/studyprogrammes/:id/throughput', async (req, res) => {
|
||||
try {
|
||||
data = await getThroughput(code)
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
logger.error(`Failed to get code ${code} throughput`)
|
||||
}
|
||||
if (!data) {
|
||||
try {
|
||||
const result = await throughputStatsForStudytrack(req.params.id, programmeStatsSince.getFullYear())
|
||||
data = await setThroughput(result)
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
logger.error(`Failed to update code ${code} patching status to recalculation erroring`)
|
||||
}
|
||||
}
|
||||
return res.json({ ...data, thesis: await thesisPromise })
|
||||
@@ -178,14 +134,8 @@ router.get('/v2/studyprogrammes/:id/throughput', async (req, res) => {
|
||||
router.get('/v2/studyprogrammes/throughput/recalculate', async (req, res) => {
|
||||
const code = req.query.code
|
||||
|
||||
console.log('Throughput stats recalculation starting')
|
||||
try {
|
||||
await patchThroughput({ [code]: { status: 'RECALCULATING' } })
|
||||
res.status(200).end()
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
return res.status(500).end()
|
||||
}
|
||||
await patchThroughput({ [code]: { status: 'RECALCULATING' } })
|
||||
res.status(200).end()
|
||||
|
||||
try {
|
||||
if (code.includes('MH') || code.includes('KH')) {
|
||||
@@ -199,13 +149,12 @@ router.get('/v2/studyprogrammes/throughput/recalculate', async (req, res) => {
|
||||
try {
|
||||
await patchThroughput({ [code]: { status: 'RECALCULATION ERRORED' } })
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
logger.error(`Failed to update code ${code} patching status to recalculation erroring`)
|
||||
return
|
||||
}
|
||||
console.error(e)
|
||||
console.log(`Failed to update throughput stats for code: ${code}, reason: ${e.message}`)
|
||||
logger.error(`Failed to update throughput stats for code: ${code}, reason: ${e.message}`)
|
||||
}
|
||||
console.log(`Throughput stats recalculation for studyprogramme ${code} done`)
|
||||
logger.info(`Throughput stats recalculation for studyprogramme ${code} done`)
|
||||
})
|
||||
|
||||
router.get('/v2/studyprogrammes/:id/thesis', async (req, res) => {
|
||||
|
||||
@@ -3,7 +3,6 @@ const { faculties } = require('../services/organisations')
|
||||
|
||||
router.get('/', async (req, res) => {
|
||||
const facultyList = await faculties()
|
||||
console.log('faculties')
|
||||
res.json(facultyList)
|
||||
})
|
||||
|
||||
|
||||
@@ -1,22 +1,27 @@
|
||||
const router = require('express').Router()
|
||||
const userService = require('../services/userService')
|
||||
const { getUserDataFor } = require('../services/userService')
|
||||
const { sendFeedbackToToska } = require('../services/mailservice')
|
||||
const logger = require('../util/logger')
|
||||
|
||||
router.post('/email', async (req, res) => {
|
||||
const { content } = req.body
|
||||
const { uid } = req.headers
|
||||
const { email, full_name } = await userService.byUsername(uid)
|
||||
const {
|
||||
decodedToken: { userId },
|
||||
} = req
|
||||
|
||||
const { email, full_name } = await getUserDataFor(userId)
|
||||
|
||||
const result = await sendFeedbackToToska({
|
||||
feedbackContent: content,
|
||||
userId: uid,
|
||||
userId,
|
||||
userEmail: email,
|
||||
userFullName: full_name,
|
||||
})
|
||||
if (result.error) {
|
||||
return res.status(400).json(result).end()
|
||||
}
|
||||
console.log('Message sent succesfully!')
|
||||
logger.info(`${userId} succesfully sent some feedback mail to toska`)
|
||||
|
||||
res.status(200).json('success').end()
|
||||
})
|
||||
|
||||
|
||||
@@ -2,55 +2,46 @@ const router = require('express').Router()
|
||||
const userService = require('../services/userService')
|
||||
const { parseHyGroups } = require('../util/utils')
|
||||
const { sendNotificationAboutNewUser } = require('../services/mailservice')
|
||||
const logger = require('../util/logger')
|
||||
|
||||
router.post('/login', async (req, res) => {
|
||||
try {
|
||||
const uid = req.headers['uid']
|
||||
if (req.headers['shib-session-id'] && uid) {
|
||||
const full_name = req.headers.displayname || ''
|
||||
const mail = req.headers.mail || ''
|
||||
const hyGroups = parseHyGroups(req.headers['hygroupcn'])
|
||||
const affiliations = parseHyGroups(req.headers['edupersonaffiliation'])
|
||||
const hyPersonSisuId = req.headers.hypersonsisuid || ''
|
||||
|
||||
console.log(uid, 'trying to login, referring to userservice.')
|
||||
let { token, isNew } = await userService.login(uid, full_name, hyGroups, affiliations, mail, hyPersonSisuId)
|
||||
if (isNew) {
|
||||
const result = await sendNotificationAboutNewUser({ userId: uid, userFullName: full_name })
|
||||
if (result.error) {
|
||||
return res.status(400).json(result).end()
|
||||
}
|
||||
const uid = req.headers['uid']
|
||||
if (req.headers['shib-session-id'] && uid) {
|
||||
const full_name = req.headers.displayname || ''
|
||||
const mail = req.headers.mail || ''
|
||||
const hyGroups = parseHyGroups(req.headers['hygroupcn'])
|
||||
const affiliations = parseHyGroups(req.headers['edupersonaffiliation'])
|
||||
const hyPersonSisuId = req.headers.hypersonsisuid || ''
|
||||
logger.info(`${uid} trying to login, referring to userservice`)
|
||||
let { token, isNew } = await userService.login(uid, full_name, hyGroups, affiliations, mail, hyPersonSisuId)
|
||||
if (isNew) {
|
||||
const result = await sendNotificationAboutNewUser({ userId: uid, userFullName: full_name })
|
||||
if (result.error) {
|
||||
return res.status(400).json(result).end()
|
||||
}
|
||||
res.status(200).json({ token })
|
||||
} else {
|
||||
res
|
||||
.status(401)
|
||||
.json({
|
||||
message: `Not enough headers login, uid: ${req.headers.uid}
|
||||
session-id ${req.headers['shib-session-id']}`,
|
||||
})
|
||||
.end()
|
||||
}
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
res.status(401).json({ message: 'problem with login', err })
|
||||
res.status(200).json({ token })
|
||||
} else {
|
||||
res
|
||||
.status(401)
|
||||
.json({
|
||||
message: `Not enough headers login, uid: ${req.headers.uid}
|
||||
session-id ${req.headers['shib-session-id']}`,
|
||||
})
|
||||
.end()
|
||||
}
|
||||
})
|
||||
|
||||
router.delete('/logout', async (req, res) => {
|
||||
try {
|
||||
const logoutUrl = req.headers.shib_logout_url
|
||||
const { returnUrl } = req.body
|
||||
if (logoutUrl) {
|
||||
return res
|
||||
.status(200)
|
||||
.send({ logoutUrl: `${logoutUrl}?return=${returnUrl}` })
|
||||
.end()
|
||||
}
|
||||
res.status(200).send({ logoutUrl: returnUrl }).end()
|
||||
} catch (err) {
|
||||
res.status(500).json({ message: 'Error with logout', err })
|
||||
const logoutUrl = req.headers.shib_logout_url
|
||||
const { returnUrl } = req.body
|
||||
if (logoutUrl) {
|
||||
return res
|
||||
.status(200)
|
||||
.send({ logoutUrl: `${logoutUrl}?return=${returnUrl}` })
|
||||
.end()
|
||||
}
|
||||
res.status(200).send({ logoutUrl: returnUrl }).end()
|
||||
})
|
||||
|
||||
module.exports = router
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
const router = require('express').Router()
|
||||
const MandatoryCourseLabels = require('../services/mandatoryCourseLabels')
|
||||
const logger = require('../util/logger')
|
||||
|
||||
router.get('/:programme/labels', async (req, res) => {
|
||||
const { programme } = req.params
|
||||
@@ -8,7 +9,7 @@ router.get('/:programme/labels', async (req, res) => {
|
||||
const labels = await MandatoryCourseLabels.labelsByStudyprogramme(programme)
|
||||
res.status(200).json(labels)
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
logger.error(err)
|
||||
res.status(400).json(err.message)
|
||||
}
|
||||
} else {
|
||||
@@ -25,7 +26,7 @@ router.delete('/:programme/labels', async (req, res) => {
|
||||
const labels = await MandatoryCourseLabels.labelsByStudyprogramme(programme)
|
||||
res.status(200).json(labels)
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
logger.error(err)
|
||||
res.status(400).json(err.message)
|
||||
}
|
||||
} else {
|
||||
@@ -42,7 +43,7 @@ router.post('/:programme/move', async (req, res) => {
|
||||
const labels = await MandatoryCourseLabels.labelsByStudyprogramme(programme)
|
||||
res.status(200).json(labels)
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
logger.error(err)
|
||||
res.status(400).json(err.message)
|
||||
}
|
||||
} else {
|
||||
@@ -59,7 +60,7 @@ router.post('/:programme/labels', async (req, res) => {
|
||||
const labels = await MandatoryCourseLabels.labelsByStudyprogramme(programme)
|
||||
res.status(200).json(labels)
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
logger.error(err)
|
||||
res.status(400).json(err.message)
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -5,47 +5,32 @@ router.delete('/:programme', async (req, res) => {
|
||||
const { programme } = req.params
|
||||
const { course } = req.body
|
||||
if (!programme || !course) return res.status(400).end()
|
||||
try {
|
||||
await MandatoryCourses.remove(programme, course)
|
||||
res.status(200).json(course)
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
res.status(500).json(err.message)
|
||||
}
|
||||
await MandatoryCourses.remove(programme, course)
|
||||
res.status(200).json(course)
|
||||
})
|
||||
|
||||
router.post('/:programme', async (req, res) => {
|
||||
const { programme } = req.params
|
||||
const { course } = req.body
|
||||
if (!programme || !course) return res.status(400).end()
|
||||
try {
|
||||
await MandatoryCourses.create(programme, course)
|
||||
const newCourse = await MandatoryCourses.find(programme, course)
|
||||
if (!newCourse) return res.status(400).end()
|
||||
res.status(201).json({ name: newCourse.course.name, code: newCourse.course_code, label: newCourse.label })
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
res.status(500).json(err.message)
|
||||
}
|
||||
await MandatoryCourses.create(programme, course)
|
||||
const newCourse = await MandatoryCourses.find(programme, course)
|
||||
if (!newCourse) return res.status(400).end()
|
||||
res.status(201).json({ name: newCourse.course.name, code: newCourse.course_code, label: newCourse.label })
|
||||
})
|
||||
|
||||
router.post('/:programme/label/:course', async (req, res) => {
|
||||
const { programme, course } = req.params
|
||||
const { label } = req.body
|
||||
if (!programme || !course) return res.status(400).end()
|
||||
try {
|
||||
await MandatoryCourses.updateLabel(programme, course, label)
|
||||
const updatedCourse = await MandatoryCourses.find(programme, course)
|
||||
if (!updatedCourse) return res.status(400).end()
|
||||
res.status(200).json({
|
||||
name: updatedCourse.course.name,
|
||||
code: updatedCourse.course_code,
|
||||
label: updatedCourse.mandatory_course_label,
|
||||
})
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
res.status(500).json(err.message)
|
||||
}
|
||||
await MandatoryCourses.updateLabel(programme, course, label)
|
||||
const updatedCourse = await MandatoryCourses.find(programme, course)
|
||||
if (!updatedCourse) return res.status(400).end()
|
||||
res.status(200).json({
|
||||
name: updatedCourse.course.name,
|
||||
code: updatedCourse.course_code,
|
||||
label: updatedCourse.mandatory_course_label,
|
||||
})
|
||||
})
|
||||
|
||||
module.exports = router
|
||||
|
||||
@@ -8,6 +8,8 @@ const UserService = require('../services/userService')
|
||||
const TagService = require('../services/tags')
|
||||
const CourseService = require('../services/courses')
|
||||
const StatMergeService = require('../services/statMerger')
|
||||
const { getAllStudentsUserHasInGroups } = require('../services/studyGuidanceGroups')
|
||||
const { logger } = require('../util/logger')
|
||||
|
||||
const filterPersonalTags = (population, userId) => {
|
||||
return {
|
||||
@@ -21,265 +23,238 @@ const filterPersonalTags = (population, userId) => {
|
||||
|
||||
// POST instead of GET because of too long params and "sensitive" data
|
||||
router.post('/v2/populationstatistics/courses', async (req, res) => {
|
||||
try {
|
||||
if (!req.body.year || !req.body.semesters || !req.body.studyRights) {
|
||||
Sentry.captureException(new Error('The body should have a year, semester and study rights defined'))
|
||||
res.status(400).json({ error: 'The body should have a year, semester and study rights defined' })
|
||||
if (!req.body.year || !req.body.semesters || !req.body.studyRights) {
|
||||
Sentry.captureException(new Error('The body should have a year, semester and study rights defined'))
|
||||
res.status(400).json({ error: 'The body should have a year, semester and study rights defined' })
|
||||
return
|
||||
}
|
||||
if (!Array.isArray(req.body.studyRights)) {
|
||||
// studyRights should always be an array
|
||||
req.body.studyRights = [req.body.studyRights]
|
||||
}
|
||||
|
||||
if (req.body.months == null) {
|
||||
req.body.months = 12
|
||||
}
|
||||
|
||||
if (req.body.years) {
|
||||
const upperYearBound = new Date().getFullYear() + 1
|
||||
const multicoursestatPromises = Promise.all(
|
||||
req.body.years.map(year => {
|
||||
if (req.body.selectedStudentsByYear) {
|
||||
req.body.selectedStudents = req.body.selectedStudentsByYear[year]
|
||||
}
|
||||
const newMonths = (upperYearBound - Number(year)) * 12
|
||||
const query = { ...req.body, year, months: newMonths }
|
||||
const coursestatistics = Population.bottlenecksOf(query)
|
||||
return coursestatistics
|
||||
})
|
||||
)
|
||||
const multicoursestats = await multicoursestatPromises
|
||||
const result = StatMergeService.populationCourseStatsMerger(multicoursestats)
|
||||
if (result.error) {
|
||||
res.status(400).json(result)
|
||||
return
|
||||
}
|
||||
if (!Array.isArray(req.body.studyRights)) {
|
||||
// studyRights should always be an array
|
||||
req.body.studyRights = [req.body.studyRights]
|
||||
|
||||
res.json(result)
|
||||
} else {
|
||||
const result = await Population.bottlenecksOf(req.body)
|
||||
|
||||
if (result.error) {
|
||||
Sentry.captureException(new Error(result.error))
|
||||
res.status(400).json(result)
|
||||
return
|
||||
}
|
||||
|
||||
if (req.body.months == null) {
|
||||
req.body.months = 12
|
||||
}
|
||||
|
||||
if (req.body.years) {
|
||||
const upperYearBound = new Date().getFullYear() + 1
|
||||
const multicoursestatPromises = Promise.all(
|
||||
req.body.years.map(year => {
|
||||
if (req.body.selectedStudentsByYear) {
|
||||
req.body.selectedStudents = req.body.selectedStudentsByYear[year]
|
||||
}
|
||||
const newMonths = (upperYearBound - Number(year)) * 12
|
||||
const query = { ...req.body, year, months: newMonths }
|
||||
const coursestatistics = Population.bottlenecksOf(query)
|
||||
return coursestatistics
|
||||
})
|
||||
)
|
||||
const multicoursestats = await multicoursestatPromises
|
||||
const result = StatMergeService.populationCourseStatsMerger(multicoursestats)
|
||||
console.log('result: ', result)
|
||||
if (result.error) {
|
||||
res.status(400).json(result)
|
||||
return
|
||||
}
|
||||
|
||||
res.json(result)
|
||||
} else {
|
||||
console.log('bottleneck väylä')
|
||||
const result = await Population.bottlenecksOf(req.body)
|
||||
|
||||
if (result.error) {
|
||||
Sentry.captureException(new Error(result.error))
|
||||
res.status(400).json(result)
|
||||
return
|
||||
}
|
||||
|
||||
res.json(result)
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
Sentry.captureException(e)
|
||||
res.status(500).json({ error: e })
|
||||
res.json(result)
|
||||
}
|
||||
})
|
||||
|
||||
router.post('/v2/populationstatistics/coursesbycoursecode', async (req, res) => {
|
||||
try {
|
||||
const { from, to, coursecodes } = req.body
|
||||
if (!from || !to || !coursecodes) {
|
||||
return res.status(400).json({ error: 'The body should have a yearcode and coursecode defined' })
|
||||
}
|
||||
const maxYearsToCreatePopulationFrom = await CourseService.maxYearsToCreatePopulationFrom(coursecodes)
|
||||
if (Math.abs(to - from + 1) > maxYearsToCreatePopulationFrom) {
|
||||
return res.status(400).json({ error: `Max years to create population from is ${maxYearsToCreatePopulationFrom}` })
|
||||
}
|
||||
let studentnumberlist
|
||||
const studentnumbers = await Student.findByCourseAndSemesters(coursecodes, from, to)
|
||||
const {
|
||||
decodedToken: { userId },
|
||||
roles,
|
||||
} = req
|
||||
|
||||
if (roles && roles.includes('admin')) {
|
||||
studentnumberlist = studentnumbers
|
||||
} else {
|
||||
const unitsUserCanAccess = await UserService.getUnitsFromElementDetails(userId)
|
||||
const codes = unitsUserCanAccess.map(unit => unit.id)
|
||||
studentnumberlist = await Student.filterStudentnumbersByAccessrights(studentnumbers, codes)
|
||||
}
|
||||
|
||||
const result = await Population.bottlenecksOf(
|
||||
{
|
||||
year: 1900,
|
||||
studyRights: [],
|
||||
semesters: ['FALL', 'SPRING'],
|
||||
months: 10000,
|
||||
},
|
||||
studentnumberlist
|
||||
)
|
||||
|
||||
if (result.error) {
|
||||
res.status(400).json(result)
|
||||
return
|
||||
}
|
||||
|
||||
res.json(result)
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
res.status(500).json({ error: e })
|
||||
const { from, to, coursecodes } = req.body
|
||||
if (!from || !to || !coursecodes) {
|
||||
return res.status(400).json({ error: 'The body should have a yearcode and coursecode defined' })
|
||||
}
|
||||
const maxYearsToCreatePopulationFrom = await CourseService.maxYearsToCreatePopulationFrom(coursecodes)
|
||||
if (Math.abs(to - from + 1) > maxYearsToCreatePopulationFrom) {
|
||||
return res.status(400).json({ error: `Max years to create population from is ${maxYearsToCreatePopulationFrom}` })
|
||||
}
|
||||
let studentnumberlist
|
||||
const studentnumbers = await Student.findByCourseAndSemesters(coursecodes, from, to)
|
||||
const {
|
||||
decodedToken: { userId },
|
||||
roles,
|
||||
} = req
|
||||
|
||||
if (roles && roles.includes('admin')) {
|
||||
studentnumberlist = studentnumbers
|
||||
} else {
|
||||
const unitsUserCanAccess = await UserService.getUnitsFromElementDetails(userId)
|
||||
const codes = unitsUserCanAccess.map(unit => unit.id)
|
||||
studentnumberlist = await Student.filterStudentnumbersByAccessrights(studentnumbers, codes)
|
||||
}
|
||||
|
||||
const result = await Population.bottlenecksOf(
|
||||
{
|
||||
year: 1900,
|
||||
studyRights: [],
|
||||
semesters: ['FALL', 'SPRING'],
|
||||
months: 10000,
|
||||
},
|
||||
studentnumberlist
|
||||
)
|
||||
|
||||
if (result.error) {
|
||||
Sentry.captureException(new Error(result.error))
|
||||
res.status(400).json(result)
|
||||
return
|
||||
}
|
||||
|
||||
res.json(result)
|
||||
})
|
||||
|
||||
router.post('/v2/populationstatistics/coursesbytag', async (req, res) => {
|
||||
try {
|
||||
const { tag } = req.body
|
||||
if (!tag) {
|
||||
res.status(400).json({ error: 'The body should have a tag defined' })
|
||||
return
|
||||
}
|
||||
let studentnumberlist
|
||||
const studentnumbers = await Student.findByTag(tag)
|
||||
const {
|
||||
decodedToken: { userId },
|
||||
roles,
|
||||
} = req
|
||||
if (roles && roles.includes('admin')) {
|
||||
studentnumberlist = studentnumbers
|
||||
} else {
|
||||
const unitsUserCanAccess = await UserService.getUnitsFromElementDetails(userId)
|
||||
const codes = unitsUserCanAccess.map(unit => unit.id)
|
||||
studentnumberlist = await Student.filterStudentnumbersByAccessrights(studentnumbers, codes)
|
||||
}
|
||||
const result = await Population.bottlenecksOf(
|
||||
{
|
||||
year: 1900,
|
||||
studyRights: [],
|
||||
semesters: ['FALL', 'SPRING'],
|
||||
months: 10000,
|
||||
tag,
|
||||
},
|
||||
studentnumberlist
|
||||
)
|
||||
|
||||
if (result.error) {
|
||||
console.log(result.error)
|
||||
res.status(400).end()
|
||||
return
|
||||
}
|
||||
|
||||
console.log(`request completed ${new Date()}`)
|
||||
res.json(result)
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
res.status(500).json({ error: e })
|
||||
const { tag } = req.body
|
||||
if (!tag) {
|
||||
res.status(400).json({ error: 'The body should have a tag defined' })
|
||||
return
|
||||
}
|
||||
let studentnumberlist
|
||||
const studentnumbers = await Student.findByTag(tag)
|
||||
const {
|
||||
decodedToken: { userId },
|
||||
roles,
|
||||
} = req
|
||||
if (roles && roles.includes('admin')) {
|
||||
studentnumberlist = studentnumbers
|
||||
} else {
|
||||
const unitsUserCanAccess = await UserService.getUnitsFromElementDetails(userId)
|
||||
const codes = unitsUserCanAccess.map(unit => unit.id)
|
||||
studentnumberlist = await Student.filterStudentnumbersByAccessrights(studentnumbers, codes)
|
||||
}
|
||||
const result = await Population.bottlenecksOf(
|
||||
{
|
||||
year: 1900,
|
||||
studyRights: [],
|
||||
semesters: ['FALL', 'SPRING'],
|
||||
months: 10000,
|
||||
tag,
|
||||
},
|
||||
studentnumberlist
|
||||
)
|
||||
|
||||
if (result.error) {
|
||||
Sentry.captureException(new Error(result.error))
|
||||
res.status(400).end()
|
||||
return
|
||||
}
|
||||
|
||||
res.json(result)
|
||||
})
|
||||
|
||||
router.post('/v2/populationstatistics/coursesbystudentnumberlist', async (req, res) => {
|
||||
try {
|
||||
if (!req.body.studentnumberlist) {
|
||||
res.status(400).json({ error: 'The body should have a studentnumberlist defined' })
|
||||
return
|
||||
}
|
||||
let studentnumberlist
|
||||
const {
|
||||
decodedToken: { userId },
|
||||
roles,
|
||||
} = req
|
||||
|
||||
if (roles && roles.includes('admin')) {
|
||||
studentnumberlist = req.body.studentnumberlist
|
||||
} else {
|
||||
const unitsUserCanAccess = await UserService.getUnitsFromElementDetails(userId)
|
||||
const codes = unitsUserCanAccess.map(unit => unit.id)
|
||||
studentnumberlist = await Student.filterStudentnumbersByAccessrights(req.body.studentnumberlist, codes)
|
||||
}
|
||||
|
||||
const result = await Population.bottlenecksOf(
|
||||
{
|
||||
year: 1900,
|
||||
studyRights: [],
|
||||
semesters: ['FALL', 'SPRING'],
|
||||
months: 10000,
|
||||
},
|
||||
studentnumberlist
|
||||
)
|
||||
if (result.error) {
|
||||
res.status(400).json(result)
|
||||
return
|
||||
}
|
||||
|
||||
res.json(result)
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
res.status(500).json({ error: err })
|
||||
if (!req.body.studentnumberlist) {
|
||||
res.status(400).json({ error: 'The body should have a studentnumberlist defined' })
|
||||
return
|
||||
}
|
||||
let studentnumberlist
|
||||
const {
|
||||
decodedToken: { userId, sisPersonId },
|
||||
roles,
|
||||
} = req
|
||||
|
||||
if (roles && roles.includes('admin')) {
|
||||
studentnumberlist = req.body.studentnumberlist
|
||||
} else if (req.body.usingStudyGuidanceGroups) {
|
||||
const studentsUserCanAccess = await getAllStudentsUserHasInGroups(sisPersonId)
|
||||
studentnumberlist = req.body.studentnumberlist.filter(s => studentsUserCanAccess.has(s))
|
||||
} else {
|
||||
const unitsUserCanAccess = await UserService.getUnitsFromElementDetails(userId)
|
||||
const codes = unitsUserCanAccess.map(unit => unit.id)
|
||||
studentnumberlist = await Student.filterStudentnumbersByAccessrights(req.body.studentnumberlist, codes)
|
||||
}
|
||||
|
||||
const result = await Population.bottlenecksOf(
|
||||
{
|
||||
year: 1900,
|
||||
studyRights: [],
|
||||
semesters: ['FALL', 'SPRING'],
|
||||
months: 10000,
|
||||
},
|
||||
studentnumberlist
|
||||
)
|
||||
if (result.error) {
|
||||
Sentry.captureException(new Error(result.error))
|
||||
res.status(400).json(result)
|
||||
return
|
||||
}
|
||||
|
||||
res.json(result)
|
||||
})
|
||||
|
||||
router.get('/v3/populationstatistics', async (req, res) => {
|
||||
const { year, semesters, studyRights: studyRightsJSON } = req.query
|
||||
const { decodedToken } = req
|
||||
|
||||
if (!year || !semesters || !studyRightsJSON) {
|
||||
res.status(400).json({ error: 'The query should have a year, semester and studyRights defined' })
|
||||
return
|
||||
}
|
||||
let studyRights = null
|
||||
try {
|
||||
if (!year || !semesters || !studyRightsJSON) {
|
||||
res.status(400).json({ error: 'The query should have a year, semester and studyRights defined' })
|
||||
return
|
||||
}
|
||||
let studyRights = null
|
||||
try {
|
||||
studyRights = JSON.parse(studyRightsJSON)
|
||||
const { rights, roles } = req
|
||||
studyRights = JSON.parse(studyRightsJSON)
|
||||
const { rights, roles } = req
|
||||
|
||||
if (!roles || !roles.includes('admin')) {
|
||||
if (!rights.includes(studyRights.programme)) {
|
||||
res.status(403).json([])
|
||||
return
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
res.status(400).json({ error: 'The query had invalid studyRights' })
|
||||
return
|
||||
}
|
||||
|
||||
if (req.query.months == null) {
|
||||
req.query.months = 12
|
||||
}
|
||||
|
||||
if (req.query.years) {
|
||||
const upperYearBound = new Date().getFullYear() + 1
|
||||
const multipopulationstudentPromises = Promise.all(
|
||||
req.query.years.map(year => {
|
||||
const newMonths = (upperYearBound - Number(year)) * 12
|
||||
const populationStudents = Population.optimizedStatisticsOf({
|
||||
...req.query,
|
||||
studyRights,
|
||||
year,
|
||||
months: newMonths,
|
||||
})
|
||||
return populationStudents
|
||||
})
|
||||
)
|
||||
const multipopulationstudents = await multipopulationstudentPromises
|
||||
|
||||
const result = StatMergeService.populationStudentsMerger(multipopulationstudents)
|
||||
|
||||
if (result.error) {
|
||||
console.log(result.error)
|
||||
res.status(400).end()
|
||||
if (!roles || !roles.includes('admin')) {
|
||||
if (!rights.includes(studyRights.programme)) {
|
||||
res.status(403).json([])
|
||||
return
|
||||
}
|
||||
|
||||
console.log(`request completed ${new Date()}`)
|
||||
res.json(filterPersonalTags(result, decodedToken.id))
|
||||
} else {
|
||||
const result = await Population.optimizedStatisticsOf({ ...req.query, studyRights })
|
||||
if (result.error) {
|
||||
console.log(result.error)
|
||||
res.status(400).end()
|
||||
return
|
||||
}
|
||||
|
||||
console.log(`request completed ${new Date()}`)
|
||||
res.json(filterPersonalTags(result, decodedToken.id))
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
res.status(500).json({ error: e })
|
||||
res.status(400).json({ error: 'The query had invalid studyRights' })
|
||||
return
|
||||
}
|
||||
|
||||
if (req.query.months == null) {
|
||||
req.query.months = 12
|
||||
}
|
||||
|
||||
if (req.query.years) {
|
||||
const upperYearBound = new Date().getFullYear() + 1
|
||||
const multipopulationstudentPromises = Promise.all(
|
||||
req.query.years.map(year => {
|
||||
const newMonths = (upperYearBound - Number(year)) * 12
|
||||
const populationStudents = Population.optimizedStatisticsOf({
|
||||
...req.query,
|
||||
studyRights,
|
||||
year,
|
||||
months: newMonths,
|
||||
})
|
||||
return populationStudents
|
||||
})
|
||||
)
|
||||
const multipopulationstudents = await multipopulationstudentPromises
|
||||
|
||||
const result = StatMergeService.populationStudentsMerger(multipopulationstudents)
|
||||
|
||||
if (result.error) {
|
||||
Sentry.captureException(new Error(result.error))
|
||||
res.status(400).end()
|
||||
return
|
||||
}
|
||||
|
||||
res.json(filterPersonalTags(result, decodedToken.id))
|
||||
} else {
|
||||
const result = await Population.optimizedStatisticsOf({ ...req.query, studyRights })
|
||||
if (result.error) {
|
||||
Sentry.captureException(new Error(result.error))
|
||||
res.status(400).end()
|
||||
return
|
||||
}
|
||||
|
||||
res.json(filterPersonalTags(result, decodedToken.id))
|
||||
}
|
||||
})
|
||||
|
||||
@@ -305,34 +280,28 @@ router.get('/v3/populationstatisticsbytag', async (req, res) => {
|
||||
const codes = unitsUserCanAccess.map(unit => unit.id)
|
||||
studentnumberlist = await Student.filterStudentnumbersByAccessrights(studentnumbers, codes)
|
||||
}
|
||||
try {
|
||||
const studyRights = JSON.parse(studyRightsJSON)
|
||||
const newStartYear = await Population.getEarliestYear(studentnumberlist, studyRights)
|
||||
const yearDifference = Number(year) - Number(newStartYear)
|
||||
const newMonths = Number(months) + 12 * yearDifference
|
||||
const result = await Population.optimizedStatisticsOf(
|
||||
{
|
||||
year: newStartYear,
|
||||
studyRights,
|
||||
semesters,
|
||||
months: newMonths,
|
||||
tag: foundTag,
|
||||
},
|
||||
studentnumberlist
|
||||
)
|
||||
const studyRights = JSON.parse(studyRightsJSON)
|
||||
const newStartYear = await Population.getEarliestYear(studentnumberlist, studyRights)
|
||||
const yearDifference = Number(year) - Number(newStartYear)
|
||||
const newMonths = Number(months) + 12 * yearDifference
|
||||
const result = await Population.optimizedStatisticsOf(
|
||||
{
|
||||
year: newStartYear,
|
||||
studyRights,
|
||||
semesters,
|
||||
months: newMonths,
|
||||
tag: foundTag,
|
||||
},
|
||||
studentnumberlist
|
||||
)
|
||||
|
||||
if (result.error) {
|
||||
console.log(result.error)
|
||||
res.status(400).end()
|
||||
return
|
||||
}
|
||||
|
||||
console.log(`request completed ${new Date()}`)
|
||||
res.json(filterPersonalTags(result, decodedToken.id))
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
res.status(500).json({ error: e })
|
||||
if (result.error) {
|
||||
Sentry.captureException(new Error(result.error))
|
||||
res.status(400).end()
|
||||
return
|
||||
}
|
||||
|
||||
res.json(filterPersonalTags(result, decodedToken.id))
|
||||
})
|
||||
|
||||
router.get('/v3/populationstatisticsbycourse', async (req, res) => {
|
||||
@@ -354,126 +323,108 @@ router.get('/v3/populationstatisticsbycourse', async (req, res) => {
|
||||
|
||||
const semesters = ['FALL', 'SPRING']
|
||||
const studentnumbers = await Student.findByCourseAndSemesters(JSON.parse(coursecodes), from, to, separate)
|
||||
try {
|
||||
const result = await Population.optimizedStatisticsOf(
|
||||
{
|
||||
// Useless, because studentnumbers are already filtered above by from & to.
|
||||
// We should probably refactor this to avoid more confusement.
|
||||
year: 1900,
|
||||
studyRights: [],
|
||||
semesters,
|
||||
months: 10000,
|
||||
},
|
||||
studentnumbers
|
||||
)
|
||||
const result = await Population.optimizedStatisticsOf(
|
||||
{
|
||||
// Useless, because studentnumbers are already filtered above by from & to.
|
||||
// We should probably refactor this to avoid more confusement.
|
||||
year: 1900,
|
||||
studyRights: [],
|
||||
semesters,
|
||||
months: 10000,
|
||||
},
|
||||
studentnumbers
|
||||
)
|
||||
|
||||
let studentsUserCanAccess
|
||||
if (roles && roles.includes('admin')) {
|
||||
studentsUserCanAccess = new Set(studentnumbers)
|
||||
} else {
|
||||
const unitsUserCanAccess = await UserService.getUnitsFromElementDetails(decodedToken.userId)
|
||||
const codes = unitsUserCanAccess.map(unit => unit.id)
|
||||
studentsUserCanAccess = new Set(await Student.filterStudentnumbersByAccessrights(studentnumbers, codes))
|
||||
}
|
||||
|
||||
const randomHash = crypto.randomBytes(12).toString('hex')
|
||||
const obfuscateStudent = ({ studyrights, studentNumber, courses, gender_code }) => ({
|
||||
courses,
|
||||
studyrights,
|
||||
gender_code,
|
||||
studentNumber: crypto.createHash('md5').update(`${studentNumber}${randomHash}`).digest('hex'),
|
||||
firstnames: '',
|
||||
lastname: '',
|
||||
name: '',
|
||||
email: '',
|
||||
started: null,
|
||||
credits: '',
|
||||
tags: [],
|
||||
obfuscated: true,
|
||||
})
|
||||
|
||||
result.students = result.students.map(s => (studentsUserCanAccess.has(s.studentNumber) ? s : obfuscateStudent(s)))
|
||||
|
||||
if (result.error) {
|
||||
console.log(result.error)
|
||||
res.status(400).end()
|
||||
return
|
||||
}
|
||||
|
||||
console.log(`request completed ${new Date()}`)
|
||||
res.json(filterPersonalTags(result, decodedToken.id))
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
res.status(500).json({ error: e })
|
||||
let studentsUserCanAccess
|
||||
if (roles && roles.includes('admin')) {
|
||||
studentsUserCanAccess = new Set(studentnumbers)
|
||||
} else {
|
||||
const unitsUserCanAccess = await UserService.getUnitsFromElementDetails(decodedToken.userId)
|
||||
const codes = unitsUserCanAccess.map(unit => unit.id)
|
||||
studentsUserCanAccess = new Set(await Student.filterStudentnumbersByAccessrights(studentnumbers, codes))
|
||||
}
|
||||
|
||||
const randomHash = crypto.randomBytes(12).toString('hex')
|
||||
const obfuscateStudent = ({ studyrights, studentNumber, courses, gender_code }) => ({
|
||||
courses,
|
||||
studyrights,
|
||||
gender_code,
|
||||
studentNumber: crypto.createHash('md5').update(`${studentNumber}${randomHash}`).digest('hex'),
|
||||
firstnames: '',
|
||||
lastname: '',
|
||||
name: '',
|
||||
email: '',
|
||||
started: null,
|
||||
credits: '',
|
||||
tags: [],
|
||||
obfuscated: true,
|
||||
})
|
||||
|
||||
result.students = result.students.map(s => (studentsUserCanAccess.has(s.studentNumber) ? s : obfuscateStudent(s)))
|
||||
|
||||
if (result.error) {
|
||||
Sentry.captureException(new Error(result.error))
|
||||
res.status(400).end()
|
||||
return
|
||||
}
|
||||
|
||||
res.json(filterPersonalTags(result, decodedToken.id))
|
||||
})
|
||||
|
||||
router.post('/v3/populationstatisticsbystudentnumbers', async (req, res) => {
|
||||
const { studentnumberlist } = req.body
|
||||
const { studentnumberlist, usingStudyGuidanceGroups } = req.body
|
||||
const { roles, decodedToken } = req
|
||||
|
||||
const studentsUserCanAccess = await UserService.getStudentsUserCanAccess(
|
||||
studentnumberlist,
|
||||
roles,
|
||||
decodedToken.userId
|
||||
)
|
||||
if (usingStudyGuidanceGroups) {
|
||||
if (!decodedToken.sisPersonId) {
|
||||
logger.error(
|
||||
`User ${decodedToken.userId} tried to get person groups but personId was ${decodedToken.sisPersonId} in header`
|
||||
)
|
||||
return res.status(400).json({ error: 'Not possible to get groups without personId' })
|
||||
}
|
||||
}
|
||||
|
||||
const studentsUserCanAccess = usingStudyGuidanceGroups
|
||||
? await getAllStudentsUserHasInGroups(decodedToken.sisPersonId)
|
||||
: await UserService.getStudentsUserCanAccess(studentnumberlist, roles, decodedToken.userId)
|
||||
const filteredStudentNumbers = studentnumberlist.filter(s => studentsUserCanAccess.has(s))
|
||||
|
||||
try {
|
||||
const result = await Population.optimizedStatisticsOf(
|
||||
{
|
||||
year: 1900,
|
||||
studyRights: [],
|
||||
semesters: ['FALL', 'SPRING'],
|
||||
months: 10000,
|
||||
},
|
||||
filteredStudentNumbers
|
||||
)
|
||||
if (result.error) {
|
||||
console.log(result.error)
|
||||
res.status(400).end()
|
||||
return
|
||||
}
|
||||
res.status(200).json(filterPersonalTags(result, decodedToken.id))
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
res.status(400).end()
|
||||
const result = await Population.optimizedStatisticsOf(
|
||||
{
|
||||
year: 1900,
|
||||
studyRights: [],
|
||||
semesters: ['FALL', 'SPRING'],
|
||||
months: 10000,
|
||||
},
|
||||
filteredStudentNumbers
|
||||
)
|
||||
if (result.error) {
|
||||
Sentry.captureException(new Error(result.error))
|
||||
return
|
||||
}
|
||||
res.status(200).json(filterPersonalTags(result, decodedToken.id))
|
||||
})
|
||||
|
||||
router.get('/v3/populationstatistics/studyprogrammes', async (req, res) => {
|
||||
try {
|
||||
const { rights, roles } = req
|
||||
if (roles && roles.includes('admin')) {
|
||||
const studyrights = await StudyrightService.getAssociations()
|
||||
res.json(studyrights)
|
||||
} else {
|
||||
const studyrights = await StudyrightService.getFilteredAssociations(rights)
|
||||
res.json(studyrights)
|
||||
}
|
||||
} catch (err) {
|
||||
res.status(500).json(err)
|
||||
const { rights, roles } = req
|
||||
if (roles && roles.includes('admin')) {
|
||||
const studyrights = await StudyrightService.getAssociations()
|
||||
res.json(studyrights)
|
||||
} else {
|
||||
const studyrights = await StudyrightService.getFilteredAssociations(rights)
|
||||
res.json(studyrights)
|
||||
}
|
||||
})
|
||||
|
||||
router.get('/v3/populationstatistics/studyprogrammes/unfiltered', async (req, res) => {
|
||||
try {
|
||||
const studyrights = await StudyrightService.getAssociations()
|
||||
res.json(studyrights)
|
||||
} catch (err) {
|
||||
res.status(500).json(err)
|
||||
}
|
||||
const studyrights = await StudyrightService.getAssociations()
|
||||
res.json(studyrights)
|
||||
})
|
||||
|
||||
router.get('/v3/populationstatistics/maxYearsToCreatePopulationFrom', async (req, res) => {
|
||||
try {
|
||||
const { courseCodes } = req.query
|
||||
const maxYearsToCreatePopulationFrom = await CourseService.maxYearsToCreatePopulationFrom(JSON.parse(courseCodes))
|
||||
return res.json(maxYearsToCreatePopulationFrom)
|
||||
} catch (err) {
|
||||
console.log('err', err)
|
||||
res.status(500).json(err)
|
||||
}
|
||||
const { courseCodes } = req.query
|
||||
const maxYearsToCreatePopulationFrom = await CourseService.maxYearsToCreatePopulationFrom(JSON.parse(courseCodes))
|
||||
return res.json(maxYearsToCreatePopulationFrom)
|
||||
})
|
||||
|
||||
router.use('*', (req, res, next) => next())
|
||||
|
||||
@@ -8,17 +8,20 @@ const {
|
||||
|
||||
router.get('/v3/programme_modules/:code', async (req, res) => {
|
||||
const { code } = req.params
|
||||
const module = await byProgrammeCode(code)
|
||||
if (!module) {
|
||||
res.status(400).end()
|
||||
return
|
||||
}
|
||||
res.json(module)
|
||||
const result = await byProgrammeCode(code)
|
||||
res.json(result)
|
||||
})
|
||||
|
||||
router.get('/v3/programme_modules/:code/modules', async (req, res) => {
|
||||
const { code } = req.params
|
||||
const result = await modulesByProgrammeCode(code)
|
||||
res.json(result)
|
||||
})
|
||||
|
||||
router.delete('/v3/programme_modules', async (req, res) => {
|
||||
const { programmecode, ids } = req.body
|
||||
await removeExcludedCourses(ids)
|
||||
const result = await byProgrammeCode(programmecode)
|
||||
if (!result) {
|
||||
res.status(400).end()
|
||||
return
|
||||
@@ -26,36 +29,15 @@ router.get('/v3/programme_modules/:code/modules', async (req, res) => {
|
||||
res.json(result)
|
||||
})
|
||||
|
||||
router.delete('/v3/programme_modules', async (req, res) => {
|
||||
const { programmecode, ids } = req.body
|
||||
try {
|
||||
await removeExcludedCourses(ids)
|
||||
const result = await byProgrammeCode(programmecode)
|
||||
if (!result) {
|
||||
res.status(400).end()
|
||||
return
|
||||
}
|
||||
res.json(result)
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
res.json(500).json(e)
|
||||
}
|
||||
})
|
||||
|
||||
router.post('/v3/programme_modules/:programmecode/', async (req, res) => {
|
||||
const { programmecode, coursecodes } = req.body
|
||||
try {
|
||||
await addExcludedCourses(programmecode, coursecodes)
|
||||
const result = await byProgrammeCode(programmecode)
|
||||
if (!result) {
|
||||
res.status(400).end()
|
||||
return
|
||||
}
|
||||
res.json(result)
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
res.json(500).json(e)
|
||||
await addExcludedCourses(programmecode, coursecodes)
|
||||
const result = await byProgrammeCode(programmecode)
|
||||
if (!result) {
|
||||
res.status(400).end()
|
||||
return
|
||||
}
|
||||
res.json(result)
|
||||
})
|
||||
|
||||
module.exports = router
|
||||
|
||||
@@ -1,21 +1,16 @@
|
||||
const router = require('express').Router()
|
||||
const { getImporterClient } = require('../util/importerClient')
|
||||
const { logger } = require('../util/logger')
|
||||
const { getAllGroupsAndStudents } = require('../services/studyGuidanceGroups')
|
||||
|
||||
router.get('/', async (req, res) => {
|
||||
const personId = req.headers.hypersonsisuid
|
||||
if (!personId) {
|
||||
const errorMessage = 'Not possible to get groups without personId header'
|
||||
console.log(errorMessage)
|
||||
return res.status(400).json({ error: errorMessage })
|
||||
}
|
||||
const importerClient = getImporterClient()
|
||||
try {
|
||||
const { data } = await importerClient.get(`/person-groups/person/${personId}`)
|
||||
return res.json(Object.values(data))
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
return res.status(500).json(e)
|
||||
const {
|
||||
decodedToken: { uid, sisPersonId },
|
||||
} = req
|
||||
if (!sisPersonId) {
|
||||
logger.error(`User ${uid} tried to get person groups but personId was ${sisPersonId} in header`)
|
||||
return res.status(400).json({ error: 'Not possible to get groups without personId' })
|
||||
}
|
||||
return res.json(await getAllGroupsAndStudents(sisPersonId))
|
||||
})
|
||||
|
||||
module.exports = router
|
||||
|
||||
@@ -2,24 +2,19 @@ const router = require('express').Router()
|
||||
const userService = require('../services/userService')
|
||||
|
||||
router.post('/:uid', async (req, res) => {
|
||||
try {
|
||||
const uid = req.headers['uid']
|
||||
if (req.headers['shib-session-id'] && uid) {
|
||||
const asUser = req.params.uid
|
||||
const token = await userService.superlogin(uid, asUser)
|
||||
res.status(200).json(token)
|
||||
} else {
|
||||
res
|
||||
.status(401)
|
||||
.json({
|
||||
message: `Not enough headers login, uid:
|
||||
const uid = req.headers['uid']
|
||||
if (req.headers['shib-session-id'] && uid) {
|
||||
const asUser = req.params.uid
|
||||
const token = await userService.superlogin(uid, asUser)
|
||||
res.status(200).json(token)
|
||||
} else {
|
||||
res
|
||||
.status(401)
|
||||
.json({
|
||||
message: `Not enough headers login, uid:
|
||||
${req.headers.uid} session-id ${req.headers['shib-session-id']}`,
|
||||
})
|
||||
.end()
|
||||
}
|
||||
} catch (err) {
|
||||
console.log(err.message)
|
||||
res.status(500).json({ message: 'problem with login' })
|
||||
})
|
||||
.end()
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -13,187 +13,132 @@ const filterRelevantStudentTags = (studentTags, userId) => {
|
||||
}
|
||||
|
||||
router.get('/tags', async (req, res) => {
|
||||
try {
|
||||
const tags = await Tags.findTags()
|
||||
res.status(200).json(tags)
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
res.status(500).json(err)
|
||||
}
|
||||
const tags = await Tags.findTags()
|
||||
res.status(200).json(tags)
|
||||
})
|
||||
|
||||
router.get('/tags/:studytrack', async (req, res) => {
|
||||
try {
|
||||
const { studytrack } = req.params
|
||||
const { rights, roles, decodedToken } = req
|
||||
const { studytrack } = req.params
|
||||
const { rights, roles, decodedToken } = req
|
||||
|
||||
if (!rights.includes(studytrack) && !(roles && roles.includes('admin'))) return res.status(403).end()
|
||||
if (!rights.includes(studytrack) && !(roles && roles.includes('admin'))) return res.status(403).end()
|
||||
|
||||
const tags = await Tags.findTagsByStudytrack(studytrack)
|
||||
res.status(200).json(filterRelevantTags(tags, decodedToken.id))
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
res.status(500).json(err)
|
||||
}
|
||||
const tags = await Tags.findTagsByStudytrack(studytrack)
|
||||
res.status(200).json(filterRelevantTags(tags, decodedToken.id))
|
||||
})
|
||||
|
||||
router.post('/tags', async (req, res) => {
|
||||
try {
|
||||
const {
|
||||
tag: { studytrack, tagname, year, personal_user_id },
|
||||
} = req.body
|
||||
const { rights, roles, decodedToken } = req
|
||||
const {
|
||||
tag: { studytrack, tagname, year, personal_user_id },
|
||||
} = req.body
|
||||
const { rights, roles, decodedToken } = req
|
||||
|
||||
if (!rights.includes(studytrack) && !(roles && roles.includes('admin'))) return res.status(403).end()
|
||||
if (!rights.includes(studytrack) && !(roles && roles.includes('admin'))) return res.status(403).end()
|
||||
|
||||
await Tags.createNewTag({ studytrack, tagname, year, personal_user_id })
|
||||
const tags = await Tags.findTagsByStudytrack(studytrack)
|
||||
res.status(200).json(filterRelevantTags(tags, decodedToken.id))
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
res.status(500).json(err.message)
|
||||
}
|
||||
await Tags.createNewTag({ studytrack, tagname, year, personal_user_id })
|
||||
const tags = await Tags.findTagsByStudytrack(studytrack)
|
||||
res.status(200).json(filterRelevantTags(tags, decodedToken.id))
|
||||
})
|
||||
|
||||
router.delete('/tags', async (req, res) => {
|
||||
try {
|
||||
const { tag } = req.body
|
||||
const { rights, roles, decodedToken } = req
|
||||
const { tag } = req.body
|
||||
const { rights, roles, decodedToken } = req
|
||||
|
||||
if (
|
||||
!rights.includes(tag.studytrack) &&
|
||||
!(roles && roles.includes('admin')) &&
|
||||
!(tag.personal_user_id === decodedToken.id)
|
||||
)
|
||||
return res.status(403).end()
|
||||
if (
|
||||
!rights.includes(tag.studytrack) &&
|
||||
!(roles && roles.includes('admin')) &&
|
||||
!(tag.personal_user_id === decodedToken.id)
|
||||
)
|
||||
return res.status(403).end()
|
||||
|
||||
await Tags.deleteTag(tag)
|
||||
const t = await Tags.findTagsByStudytrack(tag.studytrack)
|
||||
res.status(200).json(filterRelevantTags(t, decodedToken.id))
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
res.status(500).json(err)
|
||||
}
|
||||
await Tags.deleteTag(tag)
|
||||
const t = await Tags.findTagsByStudytrack(tag.studytrack)
|
||||
res.status(200).json(filterRelevantTags(t, decodedToken.id))
|
||||
})
|
||||
|
||||
router.get('/studenttags', async (req, res) => {
|
||||
try {
|
||||
const result = await TagStudent.getStudentTags()
|
||||
res.status(200).json(result)
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
res.status(500).json(err)
|
||||
}
|
||||
const result = await TagStudent.getStudentTags()
|
||||
res.status(200).json(result)
|
||||
})
|
||||
|
||||
router.get('/studenttags/:studytrack', async (req, res) => {
|
||||
try {
|
||||
const { studytrack } = req.params
|
||||
const { rights, roles, decodedToken } = req
|
||||
const { studytrack } = req.params
|
||||
const { rights, roles, decodedToken } = req
|
||||
|
||||
if (!rights.includes(studytrack) && !(roles && roles.includes('admin'))) return res.status(403).end()
|
||||
if (!rights.includes(studytrack) && !(roles && roles.includes('admin'))) return res.status(403).end()
|
||||
|
||||
const result = await TagStudent.getStudentTagsByStudytrack(studytrack)
|
||||
res.status(200).json(filterRelevantStudentTags(result, decodedToken.id))
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
res.status(500).json(err)
|
||||
}
|
||||
const result = await TagStudent.getStudentTagsByStudytrack(studytrack)
|
||||
res.status(200).json(filterRelevantStudentTags(result, decodedToken.id))
|
||||
})
|
||||
|
||||
router.get('/studenttags/:studentnumber', async (req, res) => {
|
||||
try {
|
||||
const { studentnumber } = req.params
|
||||
const { decodedToken } = req
|
||||
const result = await TagStudent.getStudentTagsByStudentnumber(studentnumber)
|
||||
res.status(200).json(filterRelevantStudentTags(result, decodedToken.id))
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
res.status(500).json(err)
|
||||
}
|
||||
const { studentnumber } = req.params
|
||||
const { decodedToken } = req
|
||||
const result = await TagStudent.getStudentTagsByStudentnumber(studentnumber)
|
||||
res.status(200).json(filterRelevantStudentTags(result, decodedToken.id))
|
||||
})
|
||||
|
||||
router.post('/studenttags/:studentnumber', async (req, res) => {
|
||||
try {
|
||||
const { tag, studytrack } = req.body
|
||||
const { rights, roles, decodedToken } = req
|
||||
const { tag, studytrack } = req.body
|
||||
const { rights, roles, decodedToken } = req
|
||||
|
||||
if (!rights.includes(studytrack) && !(roles && roles.includes('admin'))) return res.status(403).end()
|
||||
if (!rights.includes(studytrack) && !(roles && roles.includes('admin'))) return res.status(403).end()
|
||||
|
||||
await TagStudent.createStudentTag(tag)
|
||||
const result = await TagStudent.getStudentTagsByStudytrack(studytrack)
|
||||
res.status(200).json(filterRelevantStudentTags(result, decodedToken.id))
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
res.status(500).json(err)
|
||||
}
|
||||
await TagStudent.createStudentTag(tag)
|
||||
const result = await TagStudent.getStudentTagsByStudytrack(studytrack)
|
||||
res.status(200).json(filterRelevantStudentTags(result, decodedToken.id))
|
||||
})
|
||||
|
||||
router.post('/studenttags', async (req, res) => {
|
||||
try {
|
||||
const { tags, studytrack } = req.body
|
||||
const { rights, roles, decodedToken } = req
|
||||
const { tags, studytrack } = req.body
|
||||
const { rights, roles, decodedToken } = req
|
||||
|
||||
if (!rights.includes(studytrack) && !(roles && roles.includes('admin'))) return res.status(403).end()
|
||||
if (!rights.includes(studytrack) && !(roles && roles.includes('admin'))) return res.status(403).end()
|
||||
|
||||
const existingtags = await Tags.findTagsByStudytrack(studytrack)
|
||||
const existingTagids = existingtags.map(t => t.tag_id)
|
||||
const tagids = [...new Set(tags.map(t => t.tag_id))]
|
||||
if (!tagids.find(t => existingTagids.includes(t))) return res.status(400).json({ error: 'The tag does not exist' })
|
||||
const existingtags = await Tags.findTagsByStudytrack(studytrack)
|
||||
const existingTagids = existingtags.map(t => t.tag_id)
|
||||
const tagids = [...new Set(tags.map(t => t.tag_id))]
|
||||
if (!tagids.find(t => existingTagids.includes(t))) return res.status(400).json({ error: 'The tag does not exist' })
|
||||
|
||||
const studentnumbers = tags.map(t => t.studentnumber)
|
||||
const students = await Students.filterStudentnumbersByAccessrights(studentnumbers, [studytrack])
|
||||
const missingStudents = difference(studentnumbers, students)
|
||||
if (missingStudents.length !== 0)
|
||||
return res
|
||||
.status(400)
|
||||
.json({ error: `Could not find the following students from the programme: ${missingStudents.join(', ')}` })
|
||||
const studentnumbers = tags.map(t => t.studentnumber)
|
||||
const students = await Students.filterStudentnumbersByAccessrights(studentnumbers, [studytrack])
|
||||
const missingStudents = difference(studentnumbers, students)
|
||||
if (missingStudents.length !== 0)
|
||||
return res
|
||||
.status(400)
|
||||
.json({ error: `Could not find the following students from the programme: ${missingStudents.join(', ')}` })
|
||||
|
||||
await TagStudent.createMultipleStudentTags(tags)
|
||||
const result = await TagStudent.getStudentTagsByStudytrack(studytrack)
|
||||
res.status(200).json(filterRelevantStudentTags(result, decodedToken.id))
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
res.status(500).json(err)
|
||||
}
|
||||
await TagStudent.createMultipleStudentTags(tags)
|
||||
const result = await TagStudent.getStudentTagsByStudytrack(studytrack)
|
||||
res.status(200).json(filterRelevantStudentTags(result, decodedToken.id))
|
||||
})
|
||||
|
||||
router.delete('/studenttags/delete_one', async (req, res) => {
|
||||
try {
|
||||
const { tag_id, studentnumber, studytrack } = req.body
|
||||
const { rights, roles, decodedToken } = req
|
||||
const { tag_id, studentnumber, studytrack } = req.body
|
||||
const { rights, roles, decodedToken } = req
|
||||
|
||||
if (!rights.includes(studytrack) && !(roles && roles.includes('admin'))) return res.status(403).end()
|
||||
if (!rights.includes(studytrack) && !(roles && roles.includes('admin'))) return res.status(403).end()
|
||||
|
||||
const tags = await Tags.findTagsFromStudytrackById(studytrack, [tag_id])
|
||||
if (tags.length === 0) return res.status(403).json({ error: 'The tag does not exist' })
|
||||
const tags = await Tags.findTagsFromStudytrackById(studytrack, [tag_id])
|
||||
if (tags.length === 0) return res.status(403).json({ error: 'The tag does not exist' })
|
||||
|
||||
await TagStudent.deleteStudentTag(studentnumber, tag_id)
|
||||
const result = await TagStudent.getStudentTagsByStudytrack(studytrack)
|
||||
res.status(200).json(filterRelevantStudentTags(result, decodedToken.id))
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
res.status(500).json(err)
|
||||
}
|
||||
await TagStudent.deleteStudentTag(studentnumber, tag_id)
|
||||
const result = await TagStudent.getStudentTagsByStudytrack(studytrack)
|
||||
res.status(200).json(filterRelevantStudentTags(result, decodedToken.id))
|
||||
})
|
||||
|
||||
router.delete('/studenttags/delete_many', async (req, res) => {
|
||||
try {
|
||||
const { tagId, studentnumbers, studytrack } = req.body
|
||||
const { rights, roles, decodedToken } = req
|
||||
const { tagId, studentnumbers, studytrack } = req.body
|
||||
const { rights, roles, decodedToken } = req
|
||||
|
||||
if (!rights.includes(studytrack) && !(roles && roles.includes('admin'))) return res.status(403).end()
|
||||
if (!rights.includes(studytrack) && !(roles && roles.includes('admin'))) return res.status(403).end()
|
||||
|
||||
const tags = await Tags.findTagsFromStudytrackById(studytrack, [tagId])
|
||||
if (tags.length === 0) return res.status(403).json({ error: 'The tag does not exist' })
|
||||
const tags = await Tags.findTagsFromStudytrackById(studytrack, [tagId])
|
||||
if (tags.length === 0) return res.status(403).json({ error: 'The tag does not exist' })
|
||||
|
||||
await TagStudent.deleteMultipleStudentTags(tagId, studentnumbers)
|
||||
const result = await TagStudent.getStudentTagsByStudytrack(studytrack)
|
||||
res.status(200).json(filterRelevantStudentTags(result, decodedToken.id))
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
res.status(400).json(err)
|
||||
}
|
||||
await TagStudent.deleteMultipleStudentTags(tagId, studentnumbers)
|
||||
const result = await TagStudent.getStudentTagsByStudytrack(studytrack)
|
||||
res.status(200).json(filterRelevantStudentTags(result, decodedToken.id))
|
||||
})
|
||||
|
||||
router.use('*', (req, res, next) => next())
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
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')
|
||||
|
||||
@@ -24,6 +25,7 @@ router.get('/top', async (req, res) => {
|
||||
if (!yearcode) {
|
||||
return res.status(422).send('Missing required yearcode query param')
|
||||
}
|
||||
|
||||
const result = await topteachers.getTeacherStats(category, yearcode)
|
||||
res.json(result)
|
||||
})
|
||||
@@ -51,7 +53,19 @@ router.get('/stats', async (req, res) => {
|
||||
if (!(providers.every(p => providerRights.includes(p)) || roles.includes('admin'))) {
|
||||
return res.status(403).send('You do not have rights to see this data')
|
||||
}
|
||||
const result = await teachers.yearlyStatistics(providers, semesterStart, semesterEnd || semesterStart + 1)
|
||||
|
||||
// combine provider list that may include programmes and faculties
|
||||
const parsedProviders = [
|
||||
...new Set(
|
||||
await providers.reduce(
|
||||
async (acc, curr) =>
|
||||
isFaculty(curr) ? [...(await acc), ...(await providersOfFaculty(curr))] : [...(await acc), curr],
|
||||
[]
|
||||
)
|
||||
),
|
||||
]
|
||||
|
||||
const result = await teachers.yearlyStatistics(parsedProviders, semesterStart, semesterEnd || semesterStart + 1)
|
||||
res.json(result)
|
||||
})
|
||||
|
||||
|
||||
@@ -8,116 +8,63 @@ const {
|
||||
abort,
|
||||
updateCoursesByCourseCode,
|
||||
} = require('../services/sisUpdaterService')
|
||||
const { refreshStatistics } = require('../events')
|
||||
const { refreshStatistics, refreshTrends } = require('../events')
|
||||
|
||||
router.get('/update/v2/meta', async (req, res) => {
|
||||
const { roles } = req
|
||||
if (!roles.includes('dev')) return res.status(403).send('No rights, please stop')
|
||||
|
||||
try {
|
||||
const response = await updateSISMetadata(req)
|
||||
if (response) {
|
||||
res.status(200).json('Update SIS meta scheduled')
|
||||
}
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
res.status(500).json({ error: 'error' })
|
||||
const response = await updateSISMetadata(req)
|
||||
if (response) {
|
||||
res.status(200).json('Update SIS meta scheduled')
|
||||
}
|
||||
})
|
||||
|
||||
router.get('/update/v2/students', async (req, res) => {
|
||||
const { roles } = req
|
||||
if (!roles.includes('dev')) return res.status(403).send('No rights, please stop')
|
||||
|
||||
try {
|
||||
const response = await updateSISStudents()
|
||||
if (response) {
|
||||
res.status(200).json('Update SIS students scheduled')
|
||||
}
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
res.status(500).json({ error: 'error' })
|
||||
const response = await updateSISStudents()
|
||||
if (response) {
|
||||
res.status(200).json('Update SIS students scheduled')
|
||||
}
|
||||
})
|
||||
|
||||
router.post('/update/v2/students', async (req, res) => {
|
||||
const { roles } = req
|
||||
if (!roles.includes('dev')) return res.status(403).send('No rights, please stop')
|
||||
|
||||
try {
|
||||
const response = await updateStudentsByStudentNumber(req.body)
|
||||
if (response) {
|
||||
res.status(200).json('Update SIS students scheduled')
|
||||
}
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
res.status(500).json({ error: 'error' })
|
||||
const response = await updateStudentsByStudentNumber(req.body)
|
||||
if (response) {
|
||||
res.status(200).json('Update SIS students scheduled')
|
||||
}
|
||||
})
|
||||
|
||||
router.post('/update/v2/courses', async (req, res) => {
|
||||
const { roles } = req
|
||||
if (!roles.includes('dev')) return res.status(403).send('No rights, please stop')
|
||||
|
||||
try {
|
||||
const response = await updateCoursesByCourseCode(req.body)
|
||||
if (response) {
|
||||
res.status(200).json('Update SIS courses scheduled')
|
||||
}
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
res.status(500).json({ error: 'error' })
|
||||
const response = await updateCoursesByCourseCode(req.body)
|
||||
if (response) {
|
||||
res.status(200).json('Update SIS courses scheduled')
|
||||
}
|
||||
})
|
||||
|
||||
router.get('/update/v2/programmes', async (req, res) => {
|
||||
const { roles } = req
|
||||
if (!roles.includes('dev')) return res.status(403).send('No rights, please stop')
|
||||
|
||||
try {
|
||||
const response = await updateSISProgrammes()
|
||||
if (response) {
|
||||
res.status(200).json('Update SIS programmes scheduled')
|
||||
}
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
res.status(500).json({ error: 'error' })
|
||||
const response = await updateSISProgrammes()
|
||||
if (response) {
|
||||
res.status(200).json('Update SIS programmes scheduled')
|
||||
}
|
||||
})
|
||||
|
||||
router.get('/refresh_redis_cache', async (req, res) => {
|
||||
const { roles } = req
|
||||
if (!roles.includes('dev')) return res.status(403).send('No rights, please stop')
|
||||
|
||||
try {
|
||||
const response = await updateSISRedisCache()
|
||||
if (response) {
|
||||
res.status(200).json('Refreshing SIS redis cache scheduled')
|
||||
}
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
res.status(500).json({ error: 'error' })
|
||||
const response = await updateSISRedisCache()
|
||||
if (response) {
|
||||
res.status(200).json('Refreshing SIS redis cache scheduled')
|
||||
}
|
||||
})
|
||||
|
||||
router.post('/refresh_statistic_v2', async (req, res) => {
|
||||
try {
|
||||
refreshStatistics()
|
||||
res.status(200).json('Refreshing sis statistics')
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
res.status(500).json({ error: 'error' })
|
||||
}
|
||||
await refreshStatistics()
|
||||
res.status(200).json('Refreshing sis statistics')
|
||||
})
|
||||
|
||||
router.post('/refresh_trends', async (req, res) => {
|
||||
await refreshTrends()
|
||||
res.status(200).json('Refreshing trends')
|
||||
})
|
||||
|
||||
router.get('/abort', async (req, res) => {
|
||||
try {
|
||||
await abort()
|
||||
res.status(200).json()
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
res.status(500).json({ error: 'error' })
|
||||
}
|
||||
await abort()
|
||||
res.status(200).json()
|
||||
})
|
||||
|
||||
module.exports = router
|
||||
|
||||
@@ -3,6 +3,7 @@ const userService = require('../services/userService')
|
||||
const blacklist = require('../services/blacklist')
|
||||
const { userDataCache } = require('../services/cache')
|
||||
const { sendNotificationAboutAccessToUser, previewNotificationAboutAccessToUser } = require('../services/mailservice')
|
||||
const logger = require('../util/logger')
|
||||
|
||||
const addUserToBlacklist = async (user, decodedToken) => {
|
||||
if (user) {
|
||||
@@ -24,9 +25,11 @@ router.get('/access_groups', async (req, res) => {
|
||||
|
||||
router.post('/modifyaccess', async (req, res) => {
|
||||
try {
|
||||
const { uid } = req.body
|
||||
const { username } = req.body
|
||||
const result = await userService.modifyAccess(req.body)
|
||||
const user = await userService.byId(uid)
|
||||
const user = {
|
||||
username,
|
||||
}
|
||||
await addUserToBlacklist(user, req.decodedToken)
|
||||
res.status(200).json(result)
|
||||
} catch (e) {
|
||||
@@ -49,7 +52,7 @@ router.post('/email', async (req, res) => {
|
||||
if (result.error) {
|
||||
return res.status(400).json(result).end()
|
||||
}
|
||||
console.log('Message sent successfully')
|
||||
logger.info('Succesfully sent message about oodikone access to user')
|
||||
res.status(200).end()
|
||||
})
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@ const moment = require('moment')
|
||||
|
||||
const createRedisKeyForProductivity = id => `PRODUCTIVITY_${id}`
|
||||
const createRedisKeyForThroughput = id => `THROUGHPUT_${id}`
|
||||
const createRedisKeyForNonGraduatedStudents = id => `NONGRADUATEDSTUDENTS_${id}`
|
||||
|
||||
const getProductivity = async id => {
|
||||
const redisKey = createRedisKeyForProductivity(id)
|
||||
@@ -88,32 +87,6 @@ const patchThroughput = async data => {
|
||||
}
|
||||
}
|
||||
|
||||
const patchNonGraduatedStudents = async data => {
|
||||
const [id, dataToPatch] = Object.entries(data)[0]
|
||||
const redisKey = createRedisKeyForNonGraduatedStudents(id)
|
||||
const dataFromRedisRaw = await redisClient.getAsync(redisKey)
|
||||
const dataFromRedis = dataFromRedisRaw ? JSON.parse(dataFromRedisRaw) : { data: null }
|
||||
const patchedData = {
|
||||
data: {
|
||||
...dataFromRedis.data,
|
||||
...dataToPatch,
|
||||
},
|
||||
lastUpdated: moment().format(),
|
||||
}
|
||||
const setOperationStatus = await redisClient.setAsync(redisKey, JSON.stringify(patchedData))
|
||||
if (setOperationStatus !== 'OK') return null
|
||||
return {
|
||||
[id]: patchedData,
|
||||
}
|
||||
}
|
||||
|
||||
const getNonGraduatedStudents = async id => {
|
||||
const redisKey = createRedisKeyForNonGraduatedStudents(id)
|
||||
const dataFromRedis = await redisClient.getAsync(redisKey)
|
||||
if (!dataFromRedis) return null
|
||||
return JSON.parse(dataFromRedis)
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getProductivity,
|
||||
setProductivity,
|
||||
@@ -121,6 +94,4 @@ module.exports = {
|
||||
getThroughput,
|
||||
setThroughput,
|
||||
patchThroughput,
|
||||
patchNonGraduatedStudents,
|
||||
getNonGraduatedStudents,
|
||||
}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
const { redisClient } = require('./redis')
|
||||
const moment = require('moment')
|
||||
const logger = require('../util/logger')
|
||||
|
||||
const MAKE_REDIS_HASH_KEY = id => `BLACKLIST ${id}`
|
||||
|
||||
const addUserToBlacklist = async userId => {
|
||||
console.log('ADDED BLACKLIST', userId)
|
||||
logger.info(`user ${userId} added to token blacklist`)
|
||||
await redisClient.setAsync(MAKE_REDIS_HASH_KEY(userId), moment().toISOString(false))
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ class CourseStatsCounter {
|
||||
name,
|
||||
disciplines: {},
|
||||
coursetypes: {},
|
||||
substitutions: [],
|
||||
}
|
||||
this.students = {
|
||||
all: {},
|
||||
@@ -142,6 +143,10 @@ class CourseStatsCounter {
|
||||
this.course.coursetypes[typecode] = name
|
||||
}
|
||||
|
||||
addCourseSubstitutions(subs) {
|
||||
this.course.substitutions = subs
|
||||
}
|
||||
|
||||
addCourseDiscipline(id, name) {
|
||||
this.course.disciplines[id] = name
|
||||
}
|
||||
|
||||
@@ -172,7 +172,7 @@ const allCodeAltenatives = async code => {
|
||||
const temp = courses
|
||||
.map(c => c.code)
|
||||
.map(c => {
|
||||
if (c.match(/^A/)) return [c, 4] // open university codes come last
|
||||
if (c.match(/^AY?/)) return [c, 4] // open university codes come last
|
||||
if (c.match(/^\d/)) return [c, 2] // old numeric codes come second
|
||||
if (c.match(/^[A-Za-z]/)) return [c, 1] // new letter based codes come first
|
||||
return [c, 3] // unknown, comes before open uni?
|
||||
@@ -424,7 +424,6 @@ const byNameAndOrCodeLike = async (name, code) => {
|
||||
organizeSubgroups(course)
|
||||
}
|
||||
})
|
||||
console.log(courses)
|
||||
return { courses }
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
const axios = require('axios')
|
||||
const { isProduction, pateToken } = require('../conf-backend')
|
||||
const logger = require('../util/logger')
|
||||
|
||||
const pateClient = axios.create({
|
||||
baseURL: 'https://pate.toska.cs.helsinki.fi',
|
||||
@@ -21,13 +22,13 @@ const baseSettings = {
|
||||
const sendEmail = async (options = {}) => {
|
||||
if (!isProduction) {
|
||||
const errorMessage = 'Skipped sending email in non-production environment'
|
||||
console.log(errorMessage, options)
|
||||
logger.error({ message: errorMessage, meta: options })
|
||||
return { error: errorMessage }
|
||||
}
|
||||
|
||||
if (!pateToken) {
|
||||
const errorMessage = 'Skipped sending email since pate token was not available'
|
||||
console.log(errorMessage, options)
|
||||
logger.error({ message: errorMessage, meta: options })
|
||||
return { error: errorMessage }
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,155 @@
|
||||
const Sequelize = require('sequelize')
|
||||
const { Organization } = require('../models')
|
||||
const { Organization, ProgrammeModule } = 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_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', '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'],
|
||||
['H55', 'KH55_001'],
|
||||
['H55', 'MH55_001'],
|
||||
['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'],
|
||||
['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'],
|
||||
['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'],
|
||||
['H90', 'KH90_001'],
|
||||
['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 '],
|
||||
['H90', 'MH90_001'],
|
||||
]
|
||||
|
||||
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 = [
|
||||
@@ -33,6 +182,37 @@ const faculties = () => {
|
||||
})
|
||||
}
|
||||
|
||||
const degreeProgrammeCodesOfFaculty = async facultyCode =>
|
||||
(
|
||||
await ProgrammeModule.findAll({
|
||||
attributes: ['code'],
|
||||
include: {
|
||||
model: Organization,
|
||||
where: {
|
||||
code: facultyCode,
|
||||
},
|
||||
},
|
||||
})
|
||||
).map(({ code }) => code)
|
||||
|
||||
const providersOfFaculty = async facultyCode => {
|
||||
const [result] = await dbConnections.sequelize.query(
|
||||
`SELECT childOrg.code
|
||||
FROM organization parentOrg
|
||||
INNER JOIN organization childOrg
|
||||
ON childOrg.parent_id = parentOrg.id
|
||||
WHERE parentOrg.code = ?`,
|
||||
{ replacements: [facultyCode] }
|
||||
)
|
||||
return result.map(({ code }) => code)
|
||||
}
|
||||
|
||||
const isFaculty = facultyCode => facultiesInOodi.includes(facultyCode)
|
||||
|
||||
module.exports = {
|
||||
faculties,
|
||||
degreeProgrammeCodesOfFaculty,
|
||||
isFaculty,
|
||||
providersOfFaculty,
|
||||
facultiesAndProgrammesForTrends,
|
||||
}
|
||||
|
||||
@@ -801,7 +801,7 @@ const optimizedStatisticsOf = async (query, studentnumberlist) => {
|
||||
|
||||
const findCourses = async (studentnumbers, beforeDate) => {
|
||||
const res = await Course.findAll({
|
||||
attributes: ['code', 'name', 'coursetypecode'],
|
||||
attributes: ['code', 'name', 'coursetypecode', 'substitutions'],
|
||||
include: [
|
||||
{
|
||||
required: true,
|
||||
@@ -969,6 +969,7 @@ const bottlenecksOf = async (query, studentnumberlist) => {
|
||||
const coursestats = stats[maincourse.code]
|
||||
|
||||
coursestats.addCourseType(course_type.coursetypecode, course_type.name)
|
||||
coursestats.addCourseSubstitutions(course.substitutions)
|
||||
bottlenecks.coursetypes[course_type.coursetypecode] = course_type.name
|
||||
|
||||
course.credits.forEach(credit => {
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
const Sequelize = require('sequelize')
|
||||
const { ExcludedCourse } = require('../models/models_kone')
|
||||
const { Op } = Sequelize
|
||||
const logger = require('../util/logger')
|
||||
|
||||
const { dbConnections: sisConnections } = require('../database/connection')
|
||||
const { sequelizeKone } = require('../database/connection')
|
||||
const { dbConnections } = require('../database/connection')
|
||||
|
||||
const recursivelyGetModuleAndChildren = async (code, type) => {
|
||||
// TODO use only sis connection once sis is stable
|
||||
const connection = sisConnections.established ? sisConnections.sequelize : sequelizeKone
|
||||
const connection = dbConnections.sequelize
|
||||
try {
|
||||
const [result] = await connection.query(
|
||||
`WITH RECURSIVE children as (
|
||||
@@ -23,15 +22,14 @@ const recursivelyGetModuleAndChildren = async (code, type) => {
|
||||
)
|
||||
return result
|
||||
} catch (e) {
|
||||
console.error('Error when searching with code: ', code)
|
||||
console.error('Details: ', e)
|
||||
logger.error(`Error when searching modules and children with code: ${code}`)
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
const byProgrammeCode = async code => {
|
||||
const result = await recursivelyGetModuleAndChildren(code, 'course')
|
||||
|
||||
if (!result.length) return []
|
||||
const excluded = await ExcludedCourse.findAll({
|
||||
where: {
|
||||
programme_code: {
|
||||
|
||||
@@ -15,6 +15,7 @@ const {
|
||||
} = require('../models')
|
||||
const { TagStudent, Tag } = require('../models/models_kone')
|
||||
const Op = Sequelize.Op
|
||||
const logger = require('../util/logger')
|
||||
|
||||
const createStudent = student => Student.create(student)
|
||||
|
||||
@@ -177,9 +178,10 @@ const formatStudent = ({
|
||||
},
|
||||
date: attainment_date,
|
||||
course_code: course.code,
|
||||
passed: Credit.passed({ credittypecode }),
|
||||
passed: Credit.passed({ credittypecode }) || Credit.improved({ credittypecode }),
|
||||
grade,
|
||||
credits,
|
||||
credittypecode,
|
||||
isStudyModuleCredit: isStudyModule,
|
||||
}
|
||||
}
|
||||
@@ -232,7 +234,7 @@ const withId = async id => {
|
||||
const result = await byId(id)
|
||||
return formatStudent(result)
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
logger.error({ message: 'error when fetching single student', meta: e })
|
||||
return {
|
||||
error: e,
|
||||
}
|
||||
@@ -328,16 +330,6 @@ const filterStudentnumbersByAccessrights = async (studentnumbers, codes) => {
|
||||
return students.map(student => student.studentnumber)
|
||||
}
|
||||
|
||||
const hasEnrolledForSemester = async (studentnumber, semestercode) => {
|
||||
const enrollment = await SemesterEnrollment.findOne({
|
||||
where: {
|
||||
studentnumber,
|
||||
semestercode,
|
||||
},
|
||||
})
|
||||
return !!(enrollment && enrollment.enrollmenttype === 1)
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
withId,
|
||||
bySearchTerm,
|
||||
@@ -348,5 +340,4 @@ module.exports = {
|
||||
findByCourseAndSemesters,
|
||||
findByTag,
|
||||
splitByEmptySpace,
|
||||
hasEnrolledForSemester,
|
||||
}
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
const { getImporterClient } = require('../util/importerClient')
|
||||
const importerClient = getImporterClient()
|
||||
|
||||
const getAllStudentsUserHasInGroups = async sisPersonId => {
|
||||
if (!importerClient) {
|
||||
return new Set()
|
||||
}
|
||||
const { data } = await importerClient.get(`/person-groups/person/${sisPersonId}`)
|
||||
return new Set(
|
||||
Object.values(data)
|
||||
.map(group => group.members)
|
||||
.flat()
|
||||
.map(member => member.personStudentNumber)
|
||||
)
|
||||
}
|
||||
|
||||
const getAllGroupsAndStudents = async sisPersonId => {
|
||||
if (!importerClient) {
|
||||
return []
|
||||
}
|
||||
const { data } = await importerClient.get(`/person-groups/person/${sisPersonId}`)
|
||||
return Object.values(data)
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getAllStudentsUserHasInGroups,
|
||||
getAllGroupsAndStudents,
|
||||
}
|
||||
@@ -385,7 +385,9 @@ const graduationsFromClass = async (studentnumbers, studytrack) => {
|
||||
const genderCodeToValue = code => {
|
||||
if (code === '1') return 'Male'
|
||||
if (code === '2') return 'Female'
|
||||
return 'Other'
|
||||
if (code === '3') return 'Other'
|
||||
if (code === '0') return 'Unknown'
|
||||
return 'Unknown'
|
||||
}
|
||||
|
||||
const gendersFromClass = async studentnumbers => {
|
||||
|
||||
@@ -2,16 +2,11 @@ const {
|
||||
dbConnections: { sequelize },
|
||||
} = require('../database/connection')
|
||||
const { Studyright, StudyrightElement, ElementDetail } = require('../models')
|
||||
const { getUserElementDetails } = require('./userService')
|
||||
const { hasEnrolledForSemester } = require('./students')
|
||||
const { getCurrentSemester } = require('./semesters')
|
||||
const moment = require('moment')
|
||||
const { redisClient } = require('../services/redis')
|
||||
const _ = require('lodash')
|
||||
const { Op, col, where, fn } = require('sequelize')
|
||||
|
||||
const createStudyright = apiData => Studyright.create(apiData)
|
||||
|
||||
const REDIS_KEY = 'STUDYRIGHT_ASSOCIATIONS_V2'
|
||||
|
||||
const byStudent = studentNumber => {
|
||||
@@ -24,97 +19,6 @@ const byStudent = studentNumber => {
|
||||
})
|
||||
}
|
||||
|
||||
const getActiveStudyrightElementsOfCodeBeforeDate = async (code, date) =>
|
||||
await StudyrightElement.findAll({
|
||||
where: {
|
||||
code,
|
||||
enddate: {
|
||||
[Op.gte]: date,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const getActiveStudyrightsFromIdsBeforeDate = async (studyrightIds, date) =>
|
||||
await Studyright.findAll({
|
||||
where: {
|
||||
graduated: 0,
|
||||
studyrightid: {
|
||||
[Op.in]: studyrightIds,
|
||||
},
|
||||
canceldate: null,
|
||||
enddate: {
|
||||
[Op.gte]: date,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const nonGraduatedStudentsOfElementDetail = async code => {
|
||||
const today = new Date()
|
||||
const currentSemesterCode = (await getCurrentSemester()).semestercode
|
||||
|
||||
// Get studyrights of target code that haven't ended yet
|
||||
const studyrightElements = await getActiveStudyrightElementsOfCodeBeforeDate(code, today)
|
||||
const studentToDatesMap = {}
|
||||
|
||||
studyrightElements.forEach(({ startdate, enddate, studentnumber }) => {
|
||||
studentToDatesMap[studentnumber] = {
|
||||
startdate,
|
||||
enddate,
|
||||
}
|
||||
})
|
||||
|
||||
const studyrights = await getActiveStudyrightsFromIdsBeforeDate(
|
||||
studyrightElements.map(sE => sE.studyrightid),
|
||||
today
|
||||
)
|
||||
|
||||
// Filter out students that are in new master's programmes,
|
||||
// see if student is currently enrolled and format the result
|
||||
// to shape of { year1: [studentnumbers], ... }
|
||||
const result = {}
|
||||
const studentnumbers = new Set()
|
||||
const studentsToBeFiltered = new Set()
|
||||
await Promise.all(
|
||||
studyrights.map(
|
||||
({ studentStudentnumber: student_studentnumber, studyrightid }) =>
|
||||
new Promise(async res => {
|
||||
const [enrolled, studentElementDetails] = await Promise.all([
|
||||
await hasEnrolledForSemester(student_studentnumber, currentSemesterCode),
|
||||
await StudyrightElement.findAll({
|
||||
where: {
|
||||
studyrightid,
|
||||
},
|
||||
}),
|
||||
])
|
||||
|
||||
// If student is in new master's programme,
|
||||
// then don't include them in the result
|
||||
if (studentElementDetails.find(e => e.code.match(/^M[A-Z]*[0-9]*_[0-9]*$/))) {
|
||||
studentsToBeFiltered.add(student_studentnumber)
|
||||
return res()
|
||||
}
|
||||
|
||||
if (studentnumbers.has(student_studentnumber)) return res()
|
||||
const year = moment(studentToDatesMap[student_studentnumber].startdate).tz('Europe/Helsinki').year()
|
||||
if (!result[year]) result[year] = []
|
||||
studentnumbers.add(student_studentnumber)
|
||||
result[year].push({
|
||||
studentNumber: student_studentnumber,
|
||||
enrolled,
|
||||
})
|
||||
res()
|
||||
})
|
||||
)
|
||||
)
|
||||
|
||||
// Filter out some special cases
|
||||
Object.keys(result).forEach(year => {
|
||||
result[year] = result[year].filter(({ studentNumber }) => !studentsToBeFiltered.has(studentNumber))
|
||||
})
|
||||
|
||||
return [result, [...studentnumbers]]
|
||||
}
|
||||
|
||||
const studentNumbersWithAllStudyRightElements = async (codes, startedAfter, startedBefore) => {
|
||||
const studyrights = await StudyrightElement.findAll({
|
||||
attributes: ['studentnumber'],
|
||||
@@ -235,15 +139,6 @@ const getAllStudyrightElementsAndAssociations = async () => {
|
||||
return JSON.parse(studyright_elements)
|
||||
}
|
||||
|
||||
const getStudyrightElementsAndAssociationsForUser = async username => {
|
||||
const studyrightelementcodes = await getUserElementDetails(username)
|
||||
if (studyrightelementcodes.length === 0) {
|
||||
return []
|
||||
}
|
||||
const associations = await getAssociatedStudyrights(studyrightelementcodes)
|
||||
return formatStudyrightElements(studyrightelementcodes, associations)
|
||||
}
|
||||
|
||||
const getAllProgrammes = async () => {
|
||||
const elementDetails = ElementDetail.findAll({
|
||||
where: {
|
||||
@@ -402,7 +297,6 @@ const getAssociations = async (doRefresh = false) => {
|
||||
}
|
||||
|
||||
const getFilteredAssociations = async codes => {
|
||||
console.log(codes)
|
||||
const associations = await getAssociations()
|
||||
associations.programmes = _.pick(associations.programmes, codes)
|
||||
|
||||
@@ -420,24 +314,14 @@ const getFilteredAssociations = async codes => {
|
||||
return associations
|
||||
}
|
||||
|
||||
const getUserAssociations = async userid => {
|
||||
const codes = await getUserElementDetails(userid)
|
||||
const associations = await getFilteredAssociations(codes)
|
||||
return associations
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
byStudent,
|
||||
createStudyright,
|
||||
studentNumbersWithAllStudyRightElements,
|
||||
getAssociatedStudyrights,
|
||||
getAllStudyrightElementsAndAssociations,
|
||||
getStudyrightElementsAndAssociationsForUser,
|
||||
getAssociations,
|
||||
getFilteredAssociations,
|
||||
getUserAssociations,
|
||||
refreshAssociationsInRedis,
|
||||
getAllProgrammes,
|
||||
getAllElementDetails,
|
||||
nonGraduatedStudentsOfElementDetail,
|
||||
}
|
||||
|
||||
@@ -191,6 +191,11 @@ const activeTeachers = async (providers, semestercodeStart, semestercodeEnd) =>
|
||||
},
|
||||
],
|
||||
},
|
||||
where: {
|
||||
id: {
|
||||
[Op.notLike]: '%hy-hlo-org%',
|
||||
},
|
||||
},
|
||||
})
|
||||
return teachers.map(({ id }) => id)
|
||||
}
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
const { bottlenecksOf, optimizedStatisticsOf } = require('./populations')
|
||||
|
||||
const moment = require('moment')
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const run2 = async () => {
|
||||
const o = {
|
||||
semesters: ['FALL', 'SPRING'],
|
||||
year: 2017, // 2015
|
||||
studyRights: ['KH10_001'], // ['KH50_005'], // ['320001']
|
||||
months: 16, // 41
|
||||
}
|
||||
|
||||
const result = await bottlenecksOf(o) // eslint-disable-line
|
||||
|
||||
//console.log(JSON.stringify(result.coursestatistics[0].stats, null, 2))
|
||||
|
||||
//console.log(result.coursestatistics.length)
|
||||
|
||||
process.exit()
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const run1 = async () => {
|
||||
// eslint-disable-line
|
||||
const o = {
|
||||
semesters: ['FALL', 'SPRING'],
|
||||
year: 2018, // 2015
|
||||
studyRights: ['KH50_001'], // ['KH50_005'], // ['320001']
|
||||
months: 41, // 41
|
||||
}
|
||||
|
||||
const a = await optimizedStatisticsOf(o)
|
||||
|
||||
const start = moment('2018-07-31')
|
||||
|
||||
a.students.forEach(s1 => {
|
||||
// eslint-disable-next-line no-constant-condition
|
||||
if (true || s1.studentNumber === '014612688') {
|
||||
const studyright = s1.studyrights.find(s =>
|
||||
s.studyright_elements.map(d => d.element_detail.code).includes('KH50_001')
|
||||
)
|
||||
|
||||
console.log(studyright.startdate, start.isAfter(moment(studyright.startdate)), s1.changedStudyright)
|
||||
|
||||
//console.log(JSON.stringify(studyright, null, 2))
|
||||
}
|
||||
})
|
||||
|
||||
process.exit()
|
||||
}
|
||||
|
||||
const run3 = async () => {
|
||||
const userStatistics = require('./userService')
|
||||
const resp = await userStatistics.ping()
|
||||
console.log(resp)
|
||||
}
|
||||
|
||||
run3()
|
||||
@@ -1,7 +1,8 @@
|
||||
const { redisClient } = require('../services/redis')
|
||||
const { getSemestersAndYears, getMaxYearcode } = require('./semesters')
|
||||
const { getSemestersAndYears, getCurrentSemester } = require('./semesters')
|
||||
const { Teacher, Semester, Credit, Course } = require('../models')
|
||||
const { Op } = require('sequelize')
|
||||
const logger = require('../util/logger')
|
||||
|
||||
const category = (name, rediskey) => ({ name, rediskey })
|
||||
|
||||
@@ -128,11 +129,13 @@ const findTopTeachers = async yearcode => {
|
||||
}
|
||||
}
|
||||
|
||||
const findAndSaveTeachers = async (startcode = 50, to) => {
|
||||
const endcode = to ? to : await getMaxYearcode()
|
||||
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 => {
|
||||
|
||||
@@ -14,7 +14,6 @@ const CURRENT_DATE = new Date()
|
||||
|
||||
const withErr = handler => (req, res, next) =>
|
||||
handler(req, res, next).catch(e => {
|
||||
console.error(e)
|
||||
res.status(500).json({ error: { message: e.message, stack: e.stack } })
|
||||
})
|
||||
|
||||
|
||||
@@ -1,15 +1,8 @@
|
||||
const axios = require('axios')
|
||||
const { redisClient } = require('../../services/redis')
|
||||
const { USERSERVICE_URL } = require('../../conf-backend')
|
||||
const {
|
||||
dbConnections: { sequelize },
|
||||
} = require('../../database/connection')
|
||||
|
||||
const userServiceClient = axios.create({
|
||||
baseURL: USERSERVICE_URL,
|
||||
headers: { secret: process.env.USERSERVICE_SECRET },
|
||||
})
|
||||
|
||||
const getRedisCDS = async REDIS_KEY => {
|
||||
const raw = await redisClient.getAsync(REDIS_KEY)
|
||||
return raw && JSON.parse(raw)
|
||||
@@ -177,6 +170,5 @@ const getTargetStudentCounts = async ({ codes, includeOldAttainments, excludeNon
|
||||
module.exports = {
|
||||
getRedisCDS,
|
||||
saveToRedis,
|
||||
userServiceClient,
|
||||
getTargetStudentCounts,
|
||||
}
|
||||
|
||||
@@ -4,9 +4,10 @@ const {
|
||||
} = require('../../database/connection')
|
||||
const { mapToProviders } = require('../../util/utils')
|
||||
const { ElementDetail, Organization } = require('../../models')
|
||||
const { getRedisCDS, saveToRedis, userServiceClient } = require('./shared')
|
||||
const { getRedisCDS, saveToRedis } = require('./shared')
|
||||
const { facultiesAndProgrammesForTrends } = require('../../services/organisations')
|
||||
|
||||
const REDIS_KEY_STATUS = 'STATUS_DATA_V2'
|
||||
const REDIS_KEY_STATUS = 'STATUS_DATA_V5'
|
||||
|
||||
const getCurrentStudyYearStartDate = _.memoize(
|
||||
async unixMillis =>
|
||||
@@ -29,6 +30,140 @@ const getCurrentYearStartDate = () => {
|
||||
return new Date(new Date().getFullYear(), 0, 1)
|
||||
}
|
||||
|
||||
const getOrganizationStudents = async (start, end) => {
|
||||
const rows = await sequelize.query(
|
||||
`
|
||||
SELECT
|
||||
sre.code AS code,
|
||||
ARRAY_AGG(DISTINCT(sr.student_studentnumber)) AS students
|
||||
FROM studyright sr
|
||||
INNER JOIN studyright_elements sre ON sre.studyrightid = sr.studyrightid
|
||||
INNER JOIN element_details ed ON ed.code = sre.code
|
||||
WHERE ed.type = 20 AND (:start, :end) OVERLAPS (sr.startdate, sr.enddate)
|
||||
GROUP BY sre.code
|
||||
`,
|
||||
{
|
||||
type: sequelize.QueryTypes.SELECT,
|
||||
replacements: { start, end },
|
||||
}
|
||||
)
|
||||
|
||||
const codes = mapToProviders(rows.map(({ code }) => code))
|
||||
|
||||
const orgIdRows = await sequelize.query(
|
||||
`
|
||||
SELECT o.code, o.id FROM organization o WHERE o.code IN (:codes)
|
||||
`,
|
||||
{
|
||||
type: sequelize.QueryTypes.SELECT,
|
||||
replacements: { codes },
|
||||
}
|
||||
)
|
||||
|
||||
const orgIds = Object.fromEntries(orgIdRows.map(({ code, id }) => [code, id]))
|
||||
|
||||
return rows.reduce((acc, { code, students }) => {
|
||||
const [orgCode] = mapToProviders([code])
|
||||
const orgId = orgIds[orgCode]
|
||||
acc[orgId] = students
|
||||
return acc
|
||||
}, {})
|
||||
}
|
||||
|
||||
const getOrganizationCredits = async (start, end) => {
|
||||
const rows = await sequelize.query(
|
||||
`
|
||||
SELECT
|
||||
cp.organizationcode AS code,
|
||||
-- ARRAY_AGG(DISTINCT(cr.student_studentnumber)) AS students,
|
||||
SUM(cr.credits) AS credits
|
||||
FROM credit cr
|
||||
INNER JOIN course co ON co.code = cr.course_code
|
||||
INNER JOIN course_providers cp ON cp.coursecode = co.id
|
||||
WHERE
|
||||
cr.attainment_date BETWEEN :start AND :end AND
|
||||
cr.credittypecode IN (4, 9) AND
|
||||
(cr."isStudyModule" = FALSE OR cr."isStudyModule" IS NULL)
|
||||
GROUP BY cp.organizationcode
|
||||
`,
|
||||
{
|
||||
type: sequelize.QueryTypes.SELECT,
|
||||
replacements: { start, end },
|
||||
}
|
||||
)
|
||||
|
||||
return rows.reduce((acc, { code, credits }) => {
|
||||
acc[code] = credits
|
||||
return acc
|
||||
}, {})
|
||||
}
|
||||
|
||||
const getOrganizationStatistics = async (organizations, start, end) => {
|
||||
const orgCredits = await getOrganizationCredits(start, end)
|
||||
const orgStudents = await getOrganizationStudents(start, end)
|
||||
|
||||
const organizationStats = {}
|
||||
|
||||
let stack_pointer = 0
|
||||
const stack = ['hy-university-root-id']
|
||||
|
||||
while (stack_pointer < stack.length) {
|
||||
const orgId = stack[stack_pointer]
|
||||
stack_pointer += 1
|
||||
|
||||
organizations.filter(({ parent_id }) => parent_id === orgId).forEach(({ id }) => stack.push(id))
|
||||
}
|
||||
|
||||
const updateOrgStats = (orgId, stats, isDirect) => {
|
||||
if (organizationStats[orgId] === undefined) {
|
||||
organizationStats[orgId] = {
|
||||
direct: { credits: 0, students: new Set() },
|
||||
aggregated: { credits: 0, students: new Set() },
|
||||
}
|
||||
}
|
||||
|
||||
const orgStats = organizationStats[orgId]
|
||||
|
||||
const statsRefs = isDirect ? [orgStats.direct, orgStats.aggregated] : [orgStats.aggregated]
|
||||
|
||||
statsRefs.forEach(statsRef => {
|
||||
statsRef.credits += stats.credits
|
||||
stats.students.forEach(s => statsRef.students.add(s))
|
||||
})
|
||||
}
|
||||
|
||||
while (stack.length > 0) {
|
||||
const orgId = stack.pop()
|
||||
const credits = orgCredits[orgId] ?? 0
|
||||
const students = orgStudents[orgId] ?? []
|
||||
|
||||
const org = organizations.find(({ id }) => id === orgId)
|
||||
const parentOrgId = org.parent_id
|
||||
|
||||
updateOrgStats(orgId, { credits, students }, true)
|
||||
|
||||
if (parentOrgId) {
|
||||
updateOrgStats(parentOrgId, organizationStats[orgId].aggregated, false)
|
||||
}
|
||||
}
|
||||
|
||||
return Object.entries(organizationStats).reduce((acc, [id, { direct, aggregated }]) => {
|
||||
acc[id] = {
|
||||
id,
|
||||
direct: {
|
||||
students: direct.students.size,
|
||||
credits: direct.credits,
|
||||
},
|
||||
aggregated: {
|
||||
students: aggregated.students.size,
|
||||
credits: aggregated.credits,
|
||||
},
|
||||
}
|
||||
|
||||
return acc
|
||||
}, {})
|
||||
}
|
||||
|
||||
const getTotalCreditsOfCoursesBetween = async (a, b, alias = 'sum', alias2 = 'sum2') => {
|
||||
return sequelize.query(
|
||||
`
|
||||
@@ -54,18 +189,6 @@ const getTotalCreditsOfCoursesBetween = async (a, b, alias = 'sum', alias2 = 'su
|
||||
)
|
||||
}
|
||||
|
||||
const sumMerge = (a, b) => {
|
||||
if (!a) return b
|
||||
if (!b) return a
|
||||
return a + b
|
||||
}
|
||||
|
||||
const mergele = (a, b) => {
|
||||
if (!a) return _.clone(b)
|
||||
if (!b) return _.clone(a)
|
||||
return _.mergeWith(a, b, sumMerge)
|
||||
}
|
||||
|
||||
const makeYearlyCreditsPromises = (currentYear, years, getRange, alias = 'sum', alias2 = 'sum2') => {
|
||||
return years.map(
|
||||
year =>
|
||||
@@ -83,6 +206,18 @@ const makeYearlyCreditsPromises = (currentYear, years, getRange, alias = 'sum',
|
||||
)
|
||||
}
|
||||
|
||||
const getOrgStats = (stats, orgId) =>
|
||||
stats[orgId] ?? {
|
||||
accumulated: {
|
||||
aggregated: { students: 0, credits: 0 },
|
||||
direct: { students: 0, credits: 0 },
|
||||
},
|
||||
total: {
|
||||
aggregated: { students: 0, credits: 0 },
|
||||
direct: { students: 0, credits: 0 },
|
||||
},
|
||||
}
|
||||
|
||||
const calculateStatusStatistics = async (unixMillis, showByYear) => {
|
||||
const YEAR_TO_MILLISECONDS = 31556952000
|
||||
/* Memoize parses booleans into strings... */
|
||||
@@ -116,14 +251,64 @@ const calculateStatusStatistics = async (unixMillis, showByYear) => {
|
||||
)
|
||||
|
||||
/* Gather all required data */
|
||||
const [yearlyAccCredits, yearlyTotalCredits, elementDetails, faculties, { data: facultyProgrammes }] =
|
||||
await Promise.all([
|
||||
Promise.all(yearlyAccCreditsPromises),
|
||||
Promise.all(yearlyTotalCreditsPromises),
|
||||
ElementDetail.findAll(),
|
||||
Organization.findAll(),
|
||||
userServiceClient.get('/faculty_programmes'),
|
||||
])
|
||||
const [yearlyAccCredits, yearlyTotalCredits, elementDetails, faculties] = await Promise.all([
|
||||
Promise.all(yearlyAccCreditsPromises),
|
||||
Promise.all(yearlyTotalCreditsPromises),
|
||||
ElementDetail.findAll(),
|
||||
Organization.findAll(),
|
||||
])
|
||||
const facultyProgrammes = facultiesAndProgrammesForTrends
|
||||
|
||||
const yearlyOrgStatPromises = yearRange.map(async year => {
|
||||
const accumulatedStats = await getOrganizationStatistics(
|
||||
faculties,
|
||||
new Date(startTime - (startYear - year) * YEAR_TO_MILLISECONDS),
|
||||
showByYear === 'true'
|
||||
? new Date(startTime - (startYear - year - 1) * YEAR_TO_MILLISECONDS)
|
||||
: new Date(unixMillis - (startYear - year) * YEAR_TO_MILLISECONDS)
|
||||
)
|
||||
|
||||
const totalStats =
|
||||
year !== startYear &&
|
||||
(await getOrganizationStatistics(
|
||||
faculties,
|
||||
new Date(startTime - (startYear - year) * YEAR_TO_MILLISECONDS),
|
||||
new Date(startTime - (startYear - year - 1) * YEAR_TO_MILLISECONDS)
|
||||
))
|
||||
|
||||
return [
|
||||
year,
|
||||
_.merge(
|
||||
_.mapValues(totalStats, total => ({ total })),
|
||||
_.mapValues(accumulatedStats, accumulated => ({ accumulated }))
|
||||
),
|
||||
]
|
||||
})
|
||||
|
||||
const orgStats = await Promise.all(yearlyOrgStatPromises)
|
||||
|
||||
const yearlyOrgStats = Object.fromEntries(orgStats)
|
||||
const [[, currentOrgStats], [, prevOrgStats]] = orgStats.reverse()
|
||||
|
||||
const defaultYearStats = _.chain(_.range(2017, startYear + 1))
|
||||
.map(year => [year, { acc: 0, accStudents: 0 }])
|
||||
.fromPairs()
|
||||
.value()
|
||||
|
||||
const getOrgYearlyStats = orgId =>
|
||||
_.chain(yearlyOrgStats)
|
||||
.mapValues(stats => {
|
||||
const orgStats = getOrgStats(stats, orgId)
|
||||
|
||||
return {
|
||||
acc: orgStats.accumulated.aggregated.credits,
|
||||
accStudents: orgStats.accumulated.aggregated.students,
|
||||
total: orgStats.total?.aggregated?.credits,
|
||||
totalStudents: orgStats.total?.aggregated?.students,
|
||||
}
|
||||
})
|
||||
.defaults(defaultYearStats)
|
||||
.value()
|
||||
|
||||
/* Construct some helper maps */
|
||||
const facultyCodeToFaculty = faculties.reduce((res, curr) => {
|
||||
@@ -146,14 +331,26 @@ const calculateStatusStatistics = async (unixMillis, showByYear) => {
|
||||
return res
|
||||
}, {})
|
||||
|
||||
const organizationCodeToOrganization = faculties.reduce((acc, org) => {
|
||||
acc[org.code] = org
|
||||
return acc
|
||||
}, {})
|
||||
|
||||
/* Calculate course level stats and group by providers */
|
||||
const coursesGroupedByProvider = Object.entries(
|
||||
_.groupBy([..._.flatten(yearlyAccCredits), ..._.flatten(yearlyTotalCredits)], 'organizationcode')
|
||||
).reduce((acc, [organizationcode, courseCredits]) => {
|
||||
acc[organizationcode] = Object.entries(_.groupBy(courseCredits, 'code')).reduce(
|
||||
(acc, [courseCode, yearlyInstances]) => {
|
||||
acc[courseCode] = { yearly: {}, name: yearlyInstances[0].name }
|
||||
const array = [2020, 2019, 2018, 2017]
|
||||
acc[courseCode] = {
|
||||
yearly: {},
|
||||
name: yearlyInstances[0].name,
|
||||
type: 'course',
|
||||
code: courseCode,
|
||||
}
|
||||
|
||||
const array = _.range(2017, startYear + 1)
|
||||
|
||||
array.forEach(year => {
|
||||
acc[courseCode]['yearly'][year] = {}
|
||||
acc[courseCode]['yearly'][year]['acc'] = 0
|
||||
@@ -161,6 +358,7 @@ const calculateStatusStatistics = async (unixMillis, showByYear) => {
|
||||
acc[courseCode]['yearly'][year]['total'] = 0
|
||||
acc[courseCode]['yearly'][year]['totalStudents'] = 0
|
||||
})
|
||||
|
||||
yearlyInstances.forEach(instance => {
|
||||
if (!acc[courseCode]['yearly'][instance.year]) acc[courseCode]['yearly'][instance.year] = {}
|
||||
|
||||
@@ -186,17 +384,28 @@ const calculateStatusStatistics = async (unixMillis, showByYear) => {
|
||||
/* Map providers into proper programmes and calculate programme level stats */
|
||||
const groupedByProgramme = Object.entries(coursesGroupedByProvider).reduce((acc, [organizationcode, courses]) => {
|
||||
const programme = providerToProgramme[organizationcode]
|
||||
const courseValues = Object.values(courses)
|
||||
const yearlyValues = courseValues.map(c => c.yearly)
|
||||
|
||||
if (programme && programme.code) {
|
||||
const orgId = organizationCodeToOrganization[organizationcode]?.id
|
||||
|
||||
const { students: currentStudents, credits: current } = getOrgStats(currentOrgStats, orgId).accumulated.aggregated
|
||||
const { students: previousStudents, credits: previous } = getOrgStats(prevOrgStats, orgId).accumulated.aggregated
|
||||
|
||||
const yearly = getOrgYearlyStats(orgId)
|
||||
|
||||
acc[programme.code] = {
|
||||
type: 'programme',
|
||||
code: programme.code,
|
||||
name: programme.name,
|
||||
drill: courses,
|
||||
yearly: _.mergeWith({}, ...yearlyValues, mergele),
|
||||
current: _.sumBy(courseValues, 'current'),
|
||||
previous: _.sumBy(courseValues, 'previous'),
|
||||
yearly,
|
||||
current,
|
||||
previous,
|
||||
currentStudents,
|
||||
previousStudents,
|
||||
}
|
||||
}
|
||||
|
||||
return acc
|
||||
}, {})
|
||||
|
||||
@@ -207,18 +416,29 @@ const calculateStatusStatistics = async (unixMillis, showByYear) => {
|
||||
facultyCodes.forEach(facultyCode => {
|
||||
if (!facultyCode) return
|
||||
if (!acc[facultyCode]) {
|
||||
const orgId = organizationCodeToOrganization[facultyCode].id
|
||||
|
||||
const { students: currentStudents, credits: current } = getOrgStats(currentOrgStats, orgId).accumulated
|
||||
.aggregated
|
||||
const { students: previousStudents, credits: previous } = getOrgStats(prevOrgStats, orgId).accumulated
|
||||
.aggregated
|
||||
|
||||
const yearly = getOrgYearlyStats(orgId)
|
||||
|
||||
acc[facultyCode] = {
|
||||
type: 'faculty',
|
||||
code: facultyCode,
|
||||
drill: {},
|
||||
name: facultyCodeToFaculty[facultyCode] ? facultyCodeToFaculty[facultyCode].name : null,
|
||||
yearly: {},
|
||||
current: 0,
|
||||
previous: 0,
|
||||
yearly,
|
||||
current,
|
||||
previous,
|
||||
currentStudents,
|
||||
previousStudents,
|
||||
}
|
||||
}
|
||||
|
||||
acc[facultyCode]['drill'][programmeCode] = programmeStats
|
||||
acc[facultyCode]['yearly'] = _.mergeWith(acc[facultyCode]['yearly'], programmeStats.yearly, mergele)
|
||||
acc[facultyCode]['current'] += programmeStats.current
|
||||
acc[facultyCode]['previous'] += programmeStats.previous
|
||||
})
|
||||
return acc
|
||||
}, {})
|
||||
|
||||
@@ -1,117 +1,67 @@
|
||||
const axios = require('axios')
|
||||
const _ = require('lodash')
|
||||
const { USERSERVICE_URL } = require('../conf-backend')
|
||||
const UnitService = require('./units')
|
||||
const elementDetailService = require('./elementdetails')
|
||||
const { filterStudentnumbersByAccessrights } = require('./students')
|
||||
const { userDataCache } = require('./cache')
|
||||
const { getImporterClient } = require('../util/importerClient')
|
||||
const logger = require('../util/logger')
|
||||
const { facultiesAndProgrammesForTrends } = require('../services/organisations')
|
||||
|
||||
const client = axios.create({ baseURL: USERSERVICE_URL, headers: { secret: process.env.USERSERVICE_SECRET } })
|
||||
|
||||
const ping = async () => {
|
||||
const url = '/ping'
|
||||
const response = await axios.get(url)
|
||||
return response.data
|
||||
// Private helpers
|
||||
const enrichProgrammesFromFaculties = faculties =>
|
||||
facultiesAndProgrammesForTrends.filter(f => faculties.includes(f.faculty_code)).map(f => f.programme_code)
|
||||
|
||||
const enrichWithProgrammes = toEnrich => {
|
||||
if (Array.isArray(toEnrich)) {
|
||||
return toEnrich.map(obj => enrichWithProgrammes(obj))
|
||||
}
|
||||
if (toEnrich.rights) {
|
||||
return {
|
||||
...toEnrich,
|
||||
rights: _.union(toEnrich.rights, enrichProgrammesFromFaculties(toEnrich.faculties)),
|
||||
}
|
||||
}
|
||||
|
||||
if (toEnrich.elementdetails) {
|
||||
return {
|
||||
...toEnrich,
|
||||
elementdetails: _.union(
|
||||
toEnrich.elementdetails,
|
||||
enrichProgrammesFromFaculties(toEnrich.faculty.map(f => f.faculty_code))
|
||||
),
|
||||
}
|
||||
}
|
||||
return toEnrich
|
||||
}
|
||||
const checkStudyGuidanceGroupsAccess = async hyPersonSisuId => {
|
||||
if (!hyPersonSisuId) {
|
||||
logger.error('Not possible to get groups without personId header')
|
||||
return false
|
||||
}
|
||||
const importerClient = getImporterClient()
|
||||
if (!importerClient) return false
|
||||
const { data } = await importerClient.get(`/person-groups/person/${hyPersonSisuId}`)
|
||||
return data && Object.values(data).length > 0
|
||||
}
|
||||
|
||||
const findAll = async () => {
|
||||
const response = await client.get('/findall')
|
||||
return response.data
|
||||
}
|
||||
|
||||
const login = async (uid, full_name, hyGroups, affiliations, email, hyPersonSisuId) => {
|
||||
const response = await client.post('/login', {
|
||||
uid,
|
||||
full_name,
|
||||
hyGroups,
|
||||
affiliations,
|
||||
email,
|
||||
hyPersonSisuId,
|
||||
})
|
||||
return response.data
|
||||
}
|
||||
|
||||
const superlogin = async (uid, asUser) => {
|
||||
const response = await client.post('/superlogin', {
|
||||
uid,
|
||||
asUser,
|
||||
})
|
||||
return response.data
|
||||
}
|
||||
|
||||
const byUsername = async uid => {
|
||||
const url = `/user/${uid}`
|
||||
const byUsernameData = async username => {
|
||||
// OK, enriched
|
||||
const url = `/user/${username}/user_data`
|
||||
const response = await client.get(url)
|
||||
return response.data
|
||||
return enrichWithProgrammes(response.data)
|
||||
}
|
||||
|
||||
const byUsernameData = async uid => {
|
||||
const url = `/user/${uid}/user_data`
|
||||
const response = await client.get(url)
|
||||
return response.data
|
||||
}
|
||||
|
||||
const getUserElementDetails = async username => {
|
||||
const url = `/user/elementdetails/${username}`
|
||||
const response = await client.get(url)
|
||||
const elementdetailcodes = response.data
|
||||
return elementDetailService.byCodes(elementdetailcodes)
|
||||
}
|
||||
|
||||
const byId = async id => {
|
||||
const url = `/user/id/${id}`
|
||||
const response = await client.get(url)
|
||||
return response.data
|
||||
}
|
||||
|
||||
const updateUser = async (uid, fields) => {
|
||||
userDataCache.del(uid)
|
||||
const url = `/user/${uid}`
|
||||
const response = await client.put(url, fields)
|
||||
return response.data
|
||||
}
|
||||
|
||||
const getRolesFor = async user => {
|
||||
console.log('roles for', user)
|
||||
const url = `/get_roles/${user}`
|
||||
const response = await client.get(url)
|
||||
console.log(response.data)
|
||||
return response.data
|
||||
}
|
||||
|
||||
const enableElementDetails = async (uid, codes) => {
|
||||
const response = await client.post('/add_rights', { uid, codes })
|
||||
return response.data.user
|
||||
}
|
||||
|
||||
const removeElementDetails = async (uid, codes) => {
|
||||
const response = await client.post('/remove_rights', { uid, codes })
|
||||
return response.data.user
|
||||
}
|
||||
|
||||
const setFaculties = async (uid, faculties) => {
|
||||
const response = await client.post('/set_faculties', { uid, faculties })
|
||||
return response.data.user
|
||||
}
|
||||
|
||||
const getUnitsFromElementDetails = async username => {
|
||||
const elementDetails = await getUserElementDetails(username)
|
||||
return elementDetails.map(element => UnitService.parseUnitFromElement(element))
|
||||
}
|
||||
|
||||
const modifyAccess = async body => {
|
||||
const response = await client.post('/modifyaccess', body)
|
||||
return response.data
|
||||
}
|
||||
|
||||
const getAccessGroups = async () => {
|
||||
const response = await client.get('/access_groups')
|
||||
return response.data
|
||||
}
|
||||
|
||||
const getUserDataFor = async uid => {
|
||||
let userData = userDataCache.get(uid)
|
||||
// Exported helpers
|
||||
const getUserDataFor = async username => {
|
||||
// no need to enrich
|
||||
let userData = userDataCache.get(username)
|
||||
if (!userData) {
|
||||
userData = await byUsernameData(uid)
|
||||
userDataCache.set(uid, userData)
|
||||
userData = await byUsernameData(username)
|
||||
userDataCache.set(username, userData)
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -120,9 +70,21 @@ const getUserDataFor = async uid => {
|
||||
}
|
||||
}
|
||||
|
||||
const getUnitsFromElementDetails = async username => {
|
||||
// No need to enrich
|
||||
let userData = userDataCache.get(username)
|
||||
if (!userData) {
|
||||
userData = await byUsernameData(username)
|
||||
userDataCache.set(username, userData)
|
||||
}
|
||||
const elementDetails = await elementDetailService.byCodes(userData.rights)
|
||||
return elementDetails.map(element => UnitService.parseUnitFromElement(element))
|
||||
}
|
||||
|
||||
const getStudentsUserCanAccess = async (studentnumbers, roles, userId) => {
|
||||
// No need to enrich
|
||||
let studentsUserCanAccess
|
||||
if (roles && roles.includes('admin')) {
|
||||
if (roles?.includes('admin')) {
|
||||
studentsUserCanAccess = new Set(studentnumbers)
|
||||
} else {
|
||||
const unitsUserCanAccess = await getUnitsFromElementDetails(userId)
|
||||
@@ -132,12 +94,52 @@ const getStudentsUserCanAccess = async (studentnumbers, roles, userId) => {
|
||||
return studentsUserCanAccess
|
||||
}
|
||||
|
||||
// FOR ROUTES 1-to-1 mapping
|
||||
const modifyAccess = async body => enrichWithProgrammes((await client.post('/modifyaccess', body)).data)
|
||||
|
||||
const getAccessGroups = async () => enrichWithProgrammes((await client.get('/access_groups')).data)
|
||||
|
||||
const enableElementDetails = async (uid, codes) =>
|
||||
enrichWithProgrammes((await client.post('/add_rights', { uid, codes })).data.user)
|
||||
|
||||
const findAll = async () => enrichWithProgrammes((await client.get('/findall')).data)
|
||||
|
||||
const setFaculties = async (uid, faculties) =>
|
||||
enrichWithProgrammes((await client.post('/set_faculties', { uid, faculties })).data.user)
|
||||
|
||||
const removeElementDetails = async (uid, codes) =>
|
||||
enrichWithProgrammes((await client.post('/remove_rights', { uid, codes })).data.user)
|
||||
|
||||
const updateUser = async (username, fields) => {
|
||||
userDataCache.del(username)
|
||||
return enrichWithProgrammes((await client.put(`/user/${username}`, fields)).data)
|
||||
}
|
||||
|
||||
const login = async (uid, full_name, hyGroups, affiliations, email, hyPersonSisuId) => {
|
||||
const hasStudyGuidanceGroupAccess = await checkStudyGuidanceGroupsAccess(hyPersonSisuId)
|
||||
return (
|
||||
await client.post('/login', {
|
||||
uid,
|
||||
full_name,
|
||||
hyGroups,
|
||||
affiliations,
|
||||
email,
|
||||
hyPersonSisuId,
|
||||
hasStudyGuidanceGroupAccess,
|
||||
})
|
||||
).data
|
||||
}
|
||||
|
||||
const superlogin = async (uid, asUser) =>
|
||||
(
|
||||
await client.post('/superlogin', {
|
||||
uid,
|
||||
asUser,
|
||||
})
|
||||
).data
|
||||
|
||||
module.exports = {
|
||||
ping,
|
||||
byUsername,
|
||||
updateUser,
|
||||
byId,
|
||||
getUserElementDetails,
|
||||
login,
|
||||
superlogin,
|
||||
enableElementDetails,
|
||||
@@ -146,7 +148,6 @@ module.exports = {
|
||||
modifyAccess,
|
||||
getAccessGroups,
|
||||
getUnitsFromElementDetails,
|
||||
getRolesFor,
|
||||
setFaculties,
|
||||
getUserDataFor,
|
||||
getStudentsUserCanAccess,
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
class ApplicationError extends Error {
|
||||
constructor(message, status, extra) {
|
||||
super()
|
||||
|
||||
Error.captureStackTrace(this, this.constructor)
|
||||
|
||||
this.name = this.constructor.name
|
||||
|
||||
this.message = message || 'Something went wrong. Please try again.'
|
||||
|
||||
this.status = status || 500
|
||||
|
||||
this.extra = extra || {}
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
return {
|
||||
error: this.message,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
ApplicationError,
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
const axios = require('axios')
|
||||
|
||||
const { importerToken, importerUrl } = require('../conf-backend')
|
||||
const logger = require('./logger')
|
||||
|
||||
const importerClient = axios.create({
|
||||
headers: {
|
||||
@@ -10,10 +10,8 @@ const importerClient = axios.create({
|
||||
})
|
||||
|
||||
const getImporterClient = () => {
|
||||
console.log('importerToken', importerToken)
|
||||
console.log('importerUrl', importerUrl)
|
||||
if (!importerToken || !importerUrl) {
|
||||
console.log("Importer token or importer url not set, can't return client!")
|
||||
logger.error("Importer token or importer url not set, can't return client!")
|
||||
return null
|
||||
}
|
||||
return importerClient
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
const validateParamLength = (param, minLength) => param && param.trim().length >= minLength
|
||||
|
||||
const isNewHYStudyProgramme = code => !!(code && code.match(/^[A-Z]*[0-9]*_[0-9]*$/))
|
||||
|
||||
module.exports = {
|
||||
validateParamLength,
|
||||
isNewHYStudyProgramme,
|
||||
}
|
||||
|
||||
@@ -1,26 +1,47 @@
|
||||
const winston = require('winston')
|
||||
const Log2gelf = require('winston-log2gelf')
|
||||
const path = require('path')
|
||||
const { isProduction, PWD } = require('../conf-backend')
|
||||
|
||||
const { combine, timestamp, printf, splat } = winston.format
|
||||
|
||||
const transports = []
|
||||
// Write logfile to docker containers pwd
|
||||
const filename = path.join(PWD, 'debug.log')
|
||||
transports.push(new winston.transports.File({ filename, level: 'debug' }))
|
||||
|
||||
if (!isProduction) {
|
||||
const devFormat = printf(
|
||||
({ level, message, timestamp, ...rest }) => `${timestamp} ${level}: ${message} ${JSON.stringify(rest)}`
|
||||
)
|
||||
|
||||
if (process.env.LOG_PORT && process.env.LOG_HOST) {
|
||||
transports.push(
|
||||
new Log2gelf({
|
||||
hostname: process.env.LOG_HOSTNAME || 'oodikone-backend',
|
||||
host: process.env.LOG_HOST,
|
||||
port: process.env.LOG_PORT,
|
||||
protocol: process.env.LOG_PROTOCOL || 'https',
|
||||
environment: 'production',
|
||||
protocolOptions: {
|
||||
path: process.env.LOG_PATH || '/gelf',
|
||||
},
|
||||
new winston.transports.Console({
|
||||
level: 'debug',
|
||||
format: combine(splat(), timestamp(), devFormat),
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
transports.push(new winston.transports.File({ filename: 'debug.log' }))
|
||||
if (isProduction) {
|
||||
const levels = {
|
||||
error: 0,
|
||||
warn: 1,
|
||||
info: 2,
|
||||
http: 3,
|
||||
verbose: 4,
|
||||
debug: 5,
|
||||
silly: 6,
|
||||
}
|
||||
|
||||
transports.push(new winston.transports.Console({ level: 'debug' }))
|
||||
const prodFormat = winston.format.printf(({ level, ...rest }) =>
|
||||
JSON.stringify({
|
||||
level: levels[level],
|
||||
...rest,
|
||||
})
|
||||
)
|
||||
|
||||
transports.push(new winston.transports.Console({ format: prodFormat, level: 'info' }))
|
||||
}
|
||||
|
||||
const logger = winston.createLogger({ transports })
|
||||
|
||||
|
||||
@@ -7,10 +7,7 @@
|
||||
"parserOptions": {
|
||||
"requireConfigFile": false
|
||||
},
|
||||
"extends": [
|
||||
"airbnb",
|
||||
"plugin:prettier/recommended"
|
||||
],
|
||||
"extends": ["airbnb", "plugin:prettier/recommended"],
|
||||
"rules": {
|
||||
"react/no-did-mount-set-state": "off",
|
||||
"react/destructuring-assignment": "off",
|
||||
@@ -31,6 +28,7 @@
|
||||
"react/static-property-placement": "off",
|
||||
"react/no-deprecated": "off",
|
||||
"react/no-unescaped-entities": "off",
|
||||
"react/prop-types": "off",
|
||||
"react/prefer-stateless-function": "error",
|
||||
"import/no-unresolved": "error",
|
||||
"react/jsx-no-bind": "error",
|
||||
@@ -38,6 +36,7 @@
|
||||
"babel/no-invalid-this": "error",
|
||||
"react-hooks/rules-of-hooks": "error",
|
||||
"prettier/prettier": "error",
|
||||
"no-console": "error",
|
||||
"no-param-reassign": [
|
||||
"error",
|
||||
{
|
||||
@@ -45,11 +44,5 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"plugins": [
|
||||
"react",
|
||||
"import",
|
||||
"prefer-object-spread",
|
||||
"babel",
|
||||
"react-hooks"
|
||||
]
|
||||
"plugins": ["react", "import", "prefer-object-spread", "babel", "react-hooks"]
|
||||
}
|
||||
|
||||
Generated
+22519
-28
File diff suppressed because it is too large
Load Diff
@@ -12,7 +12,7 @@
|
||||
"highcharts": "^9.1.2",
|
||||
"highcharts-custom-events": "^3.0.10",
|
||||
"history": "^4.10.1",
|
||||
"jStat": "^1.8.6",
|
||||
"jstat": "^1.9.5",
|
||||
"jwt-decode": "^3.1.2",
|
||||
"lodash": "^4.17.21",
|
||||
"moment": "^2.29.1",
|
||||
|
||||
@@ -1,63 +1,90 @@
|
||||
export default {
|
||||
protoC: `Graafi ottaa huomioon vain opiskelijat joiden opinto-oikeus on alkanut 1.8 joka vuodelta vuoden 2017 jälkeen.
|
||||
Opiskelijat jotka ovat vaihtaneet ohjelmaa tiedekunnan sisällä suodatetaan pois.
|
||||
<br> <br>
|
||||
Opiskelijoiden tavoiteaika lasketaan seuraavasti: **3 vuoden tahdissa** olevat opiskelijat 60op × ((tämä päivä - aloituspäivä) / 365) ≤ opiskelijan opintopisteet,
|
||||
**4 vuoden tahdissa** olevat opiskelijat 45op × ((tämä päivä - aloituspäivä) / 365) ≤ opiskelijan opintopisteet,
|
||||
**peruutettu** jos optinto-oikeus merkattu perutuksi,
|
||||
**ei tahdissa** = (kaikki opiskelijat - 3v - 4v - peruutettu).
|
||||
<br> <br>
|
||||
Vakiona opintopisteet ennen opinto-oikeutta suodatetaan pois. **Include old attainments** laskee mukaan opiskelijan ennen opinto-oikeuden alkua saadut opintopisteet.
|
||||
<br> <br>
|
||||
Vakiona opiskelijat jotka eivät ole ilmoittautunut millekkään lukuvuodelle (läsnä tai poissa olevaksi) on sisällytetty laskuihin. **Include only at least once enrolled students**
|
||||
suodattaa pois opiskelijat jotka eivät ole ollenkaan ilmoittautunut
|
||||
<br> <br>
|
||||
Graafissa pystyy pisteitä klikkaamalla porautumaan ensin ohjelmatasolle ja edelleen opintosuuntatasolle.
|
||||
`,
|
||||
Opiskelijat jotka ovat vaihtaneet ohjelmaa tiedekunnan sisällä suodatetaan pois.
|
||||
|
||||
**3 vuoden tahdissa** olevat opiskelijat 60op × ((tämä päivä - aloituspäivä) / 365) ≤ opiskelijan opintopisteet,
|
||||
|
||||
**4 vuoden tahdissa** olevat opiskelijat 45op × ((tämä päivä - aloituspäivä) / 365) ≤ opiskelijan opintopisteet,
|
||||
|
||||
**ei tahdissa** = (kaikki opiskelijat - 3v - 4v - peruutettu).
|
||||
|
||||
**peruutettu**, jos opinto-oikeus merkattu perutuksi
|
||||
|
||||
**Include only at least once enrolled students** suodattaa pois opiskelijat jotka eivät ole ollenkaan ilmoittautunut.
|
||||
Vakiona opiskelijat jotka eivät ole ilmoittautunut millekkään lukuvuodelle (läsnä tai poissa olevaksi)
|
||||
on sisällytetty laskuihin.
|
||||
|
||||
**Include attainments attained before the studyright start** laskee mukaan opiskelijan ennen opinto-oikeuden alkua saadut opintopisteet.
|
||||
Vakiona opintopisteet ennen opinto-oikeutta suodatetaan pois.
|
||||
`,
|
||||
protoC2: `Graafi ottaa huomioon vain opiskelijat joiden opinto-oikeus on alkanut 1.8 joka vuodelta vuoden 2017 jälkeen.
|
||||
Opiskelijat jotka ovat vaihtaneet ohjelmaa tiedekunnan sisällä suodatetaan pois.
|
||||
<br> <br>
|
||||
Opiskelijoiden tavoiteaika lasketaan seuraavasti: **3 vuoden tahdissa** olevat opiskelijat 60op × ((tämä päivä - aloituspäivä) / 365) ≤ opiskelijan opintopisteet,
|
||||
|
||||
**3 vuoden tahdissa** olevat opiskelijat 60op × ((tämä päivä - aloituspäivä) / 365) ≤ opiskelijan opintopisteet,
|
||||
|
||||
**4 vuoden tahdissa** olevat opiskelijat 45op × ((tämä päivä - aloituspäivä) / 365) ≤ opiskelijan opintopisteet,
|
||||
**peruutettu** jos optinto-oikeus merkattu perutuksi,
|
||||
|
||||
**ei tahdissa** = (kaikki opiskelijat - 3v - 4v - peruutettu).
|
||||
<br> <br>
|
||||
Vakiona opintopisteet ennen opinto-oikeutta suodatetaan pois. **Include old attainments** laskee mukaan opiskelijan ennen opinto-oikeuden alkua saadut opintopisteet.
|
||||
<br> <br>
|
||||
Vakiona opiskelijat jotka eivät ole ilmoittautunut millekkään lukuvuodelle (läsnä tai poissa olevaksi) on sisällytetty laskuihin. **Include only at least once enrolled students**
|
||||
suodattaa pois opiskelijat jotka eivät ole ollenkaan ilmoittautunut
|
||||
<br> <br>
|
||||
Graafissa pystyy palkkeja klikkaamalla porautumaan kunkin tiedekunnan ohjelmatasolle. Painamalla ohjelmatasolla jotakin palkkia uudelleen pääsee takaisin tiedekuntatasolle.
|
||||
|
||||
**peruutettu**, jos opinto-oikeus merkattu perutuksi
|
||||
|
||||
**Include only at least once enrolled students** suodattaa pois opiskelijat,
|
||||
jotka eivät ole ollenkaan ilmoittautunut. Vakiona opiskelijat, jotka eivät ole ilmoittautunut millekkään lukuvuodelle (läsnä tai poissa olevaksi) on sisällytetty laskuihin.
|
||||
|
||||
**Include attainments attained before the studyright start** laskee mukaan opiskelijan ennen opinto-oikeuden alkua saadut opintopisteet.
|
||||
Vakiona opintopisteet ennen opinto-oikeutta suodatetaan pois.
|
||||
`,
|
||||
protoG: `Graafi ottaa huomioon vain opiskelijat joiden opinto-oikeus on alkanut 1.8 joka vuodelta vuoden 2017 jälkeen.
|
||||
Opiskelijat jotka ovat vaihtaneet ohjelmaa tiedekunnan sisällä suodatetaan pois.
|
||||
<br> <br>
|
||||
Opiskelijoiden tavoiteaika lasketaan seuraavasti: **3 vuoden tahdissa** olevat opiskelijat 60op × ((tarkastuspäivä - aloituspäivä) / 365) ≤ opiskelijan opintopisteet,
|
||||
|
||||
**3 vuoden tahdissa** olevat opiskelijat 60op × ((tarkastuspäivä - aloituspäivä) / 365) ≤ opiskelijan opintopisteet,
|
||||
|
||||
**4 vuoden tahdissa** olevat opiskelijat 45op × ((tarkastuspäivä - aloituspäivä) / 365) ≤ opiskelijan opintopisteet,
|
||||
**peruutettu** jos optinto-oikeus merkattu perutuksi,
|
||||
**ei tahdissa** = (kaikki opiskelijat - 3v - 4v - peruutettu). **Tarkastuspäivät** ovat 31.7, 30.11 ja 1.4 joka vuodelle.
|
||||
<br> <br>
|
||||
Vakiona opintopisteet ennen opinto-oikeutta suodatetaan pois. **Include old attainments** laskee mukaan opiskelijan ennen opinto-oikeuden alkua saadut opintopisteet.`,
|
||||
|
||||
**ei tahdissa** = (kaikki opiskelijat - 3v - 4v - peruutettu).
|
||||
|
||||
**Tarkastuspäivät** ovat 31.7, 30.11 ja 1.4 joka vuodelle.
|
||||
|
||||
**peruutettu**, jos opinto-oikeus merkattu perutuksi
|
||||
|
||||
**Include attainments attained before the studyright start** laskee mukaan opiskelijan ennen opinto-oikeuden alkua saadut opintopisteet.
|
||||
Vakiona opintopisteet ennen opinto-oikeutta suodatetaan pois.
|
||||
`,
|
||||
status: `
|
||||
**Tilannekuva** näyttää "reaaliaikaisesti" eri tiedekuntien, ohjelmien ja kurssien tuottamat opintopisteet nykyisen lukuvuoden aikana
|
||||
verrattuna edelliseen lukuvuoteen. **Nuoli** kuvaa muutosta, paljonko opintopisteitä on kertynyt tämänhetkisen lukuvuoden alusta tarkastelupäivään mennessä
|
||||
Tilannekuva näyttää "reaaliaikaisesti" eri tiedekuntien, ohjelmien ja kurssien tuottamat opintopisteet nykyisen lukuvuoden aikana
|
||||
verrattuna edelliseen lukuvuoteen.
|
||||
|
||||
Nuoli kuvaa muutosta, paljonko opintopisteitä on kertynyt tämänhetkisen lukuvuoden alusta tarkastelupäivään mennessä
|
||||
suhteessa edellisen lukuvuoden alusta vastaavaan tarkastelupäivään viime vuonna. Vain suoritetut ja hyväksiluetut opintopisteet huomioidaan.
|
||||
<br> <br>
|
||||
Tilannekuvassa voi **porautua** kortteja klikkaamalla ensin ohjelmatasolle ja edelleen kurssitasolle. **Asetukset**-nappi avaa valikon, josta voi valita missä muodossa data halutaan nähdä. Valinta **Näytä edelliset vuodet** näyttää
|
||||
vuosittaisen opintopiste-kertymän tähän päivään mennessä vuonna X sekä koko lukuvuoden X opintopisteet muodossa "\`kerääntymä vuonna X / koko lukuvuoden X opintopisteet\`". Valinta **Näytä vuositasolla** laskee kertymän kalenterivuoden alusta,
|
||||
eikä lukuvuoden alusta. Kohdasta **Näytä päivänä** voi valita tarkastelupäivän, joka on oletusarvoisesti tämänhetkinen päivä.
|
||||
<br> <br>
|
||||
|
||||
Tilannekuvassa voi porautua kortteja klikkaamalla ensin ohjelmatasolle ja edelleen kurssitasolle. Asetukset-nappi avaa valikon, josta voi valita missä muodossa data halutaan nähdä.
|
||||
|
||||
Valinta "Näytä edelliset vuodet" näyttää vuosittaisen opintopiste-kertymän tähän päivään
|
||||
mennessä vuonna X sekä koko lukuvuoden X opintopisteet muodossa "kerääntymä vuonna X / koko lukuvuoden X opintopisteet".
|
||||
|
||||
Valinta "Näytä kalenterivuosittain" laskee kertymän kalenterivuoden alusta,
|
||||
eikä lukuvuoden alusta.
|
||||
|
||||
Valinta "Näytä suhteutettuna opiskelijoiden määrään" näyttää opintopistemäärät suhteutettuna kyseisen ajanjakson aikana kyseisen organisaation alaisuuteen kuuluvia kursseja suorittaineiden opiskelijoiden määrään. Tämä luku sisältää myös avoimen yliopiston opiskelijat.
|
||||
|
||||
Kohdasta "Näytä päivänä" voi valita tarkastelupäivän, joka on oletusarvoisesti tämänhetkinen päivä.
|
||||
|
||||
Avoimen yliopiston suorituksia ei vielä lasketa mukaan.
|
||||
`,
|
||||
statusGraduated: `
|
||||
**Tilannekuva** näyttää "reaaliaikaisesti" eri tiedekuntien ja ohjelmien valmistumiset nykyisen lukuvuoden aikana
|
||||
verrattuna edelliseen lukuvuoteen. **Nuoli** kuvaa muutosta, paljonko valmistuneita on kertynyt tämänhetkisen lukuvuoden alusta tarkastelupäivään mennessä
|
||||
Tilannekuva näyttää "reaaliaikaisesti" eri tiedekuntien ja ohjelmien valmistumiset nykyisen lukuvuoden aikana
|
||||
verrattuna edelliseen lukuvuoteen.
|
||||
|
||||
Nuoli kuvaa muutosta, paljonko valmistuneita on kertynyt tämänhetkisen lukuvuoden alusta tarkastelupäivään mennessä
|
||||
suhteessa edellisen lukuvuoden alusta vastaavaan tarkastelupäivään viime vuonna.
|
||||
<br> <br>
|
||||
Tilannekuvassa voi **porautua** kortteja klikkaamalla ohjelmatasolle. **Asetukset**-nappi avaa valikon, josta voi valita missä muodossa data halutaan nähdä. Valinta **Näytä edelliset vuodet** näyttää
|
||||
vuosittaisen valmistumis-kertymän tähän päivään mennessä vuonna X sekä koko lukuvuoden X valmistuneet muodossa "\`kerääntymä vuonna X / koko lukuvuoden X valmistuneet\`". Valinta **Näytä vuositasolla**
|
||||
laskee kertymän kalenterivuoden alusta,
|
||||
eikä lukuvuoden alusta. Kohdasta **Näytä päivänä** voi valita tarkastelupäivän, joka on oletusarvoisesti tämänhetkinen päivä.
|
||||
<br> <br>
|
||||
|
||||
Tilannekuvassa voi porautua kortteja klikkaamalla ohjelmatasolle.
|
||||
Asetukset-nappi avaa valikon, josta voi valita missä muodossa data halutaan nähdä.
|
||||
|
||||
Valinta "Näytä edelliset vuodet" näyttää vuosittaisen valmistumiskertymän tähän päivään mennessä vuonna X sekä koko lukuvuoden X valmistuneet muodossa "kerääntymä vuonna X / koko lukuvuoden X valmistuneet".
|
||||
|
||||
Valinta "Näytä vuositasolla" laskee kertymän kalenterivuoden alusta, eikä lukuvuoden alusta.
|
||||
|
||||
Kohdasta "Näytä päivänä" voi valita tarkastelupäivän, joka on oletusarvoisesti tämänhetkinen päivä.
|
||||
`,
|
||||
}
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
export default {
|
||||
Students: `Students
|
||||
|
||||
Harmaata _Show_-painiketta klikkaamalla saa listan kaikista populaation opiskelijoista. (Huom. suodattimet vaikuttavat tähän listaan!) Taulukko näyttää oletusarvoisesti vain opiskelijanumerot, mutta harmaata liukukytkintä klikkaamalla saa näkyviin opiskelijoiden nimet.
|
||||
|
||||
- **Student Number:** Klikkaamalla opiskelijanumeron vieressä olevaa sinistä nuolta, siirtyy "student statistic" -näkymään valitun opiskelijan osalta.
|
||||
- **All Credits:** listaa opiskelijan kaikki opintopisteet (myös aiemmat suoritukset, eli ne jotka tehty ennen valittuun tarkastelujaksoon ja populaatioon kuulumista)
|
||||
- **Tags:** Opiskelijoille voi luoda tageja ryhmittelyä varten kohdassa _Study Program > Overview > haluttu koulutusohjelma_.
|
||||
- **Study Programme:** Opiskelijan koulutusohjelma.
|
||||
- **Start Year at Uni:** Aloitusvuosi yliopistolla.
|
||||
`,
|
||||
}
|
||||
@@ -2,10 +2,12 @@ import PopulationStatistics from './populationStatistics'
|
||||
import PopulationOverview from './populationOverview'
|
||||
import CoolDataScience from './coolDataScience'
|
||||
import CourseStatistics from './courseStatistics'
|
||||
import CoursePopulation from './coursePopulation'
|
||||
|
||||
export default {
|
||||
PopulationStatistics,
|
||||
PopulationOverview,
|
||||
CoolDataScience,
|
||||
CourseStatistics,
|
||||
CoursePopulation,
|
||||
}
|
||||
|
||||
@@ -2,6 +2,89 @@ export default {
|
||||
Advanced: `**Semesters:** Jos haluaa tarkastella vain syyslukukaudella tai kevätlukukaudella ilmoittautuneita, haluttu ajanjakso valitaan tästä.
|
||||
|
||||
**Include:** Koulutusohjelmapopulaatiosta on oletusarvoisesti suodatettu pois vaihto-opiskelijat, tutkinnonsuoritusoikeudesta luopuneet ja erillisopinto-oikeudella opiskelevat. Näitä ryhmiä voi sisällyttää mukaan lisäämällä ruksin haluamaansa kohtaan.`,
|
||||
QueryCard: `**Valitun koulutusohjelmavuosikurssin perustiedot.**
|
||||
|
||||
**Sample size** luku sisältää läsnä- ja poissaolevaksi ilmoittautuneet opiskelijat, muttei "excludes"-ryhmiä, jotka voi määritellä "advanced settings" -kohdasta.
|
||||
|
||||
**Fall** = 1.8. - 31.12. ilmoittautuneet ja **Spring** = 1.1. - 31.7. ilmoittautuneet.
|
||||
|
||||
**Showing XX months**: Dataa näytetään ohjelman vuosikurssin alkamispäivästä lähtien haluttuun kuukauteen asti (oletusarvoisesti "tähän päivään").
|
||||
|
||||
**Updated** kertoo milloin data on päivitetty.`,
|
||||
CreditAccumulation: `**Credit Accumulation**
|
||||
|
||||
Opiskelijoiden kumulatiiviset opintopistekertymät opiskelijakohtaisina kuvaajina.
|
||||
Kun hiiren vie kuvaajien päälle, saa näkyviin jokaista kuvaajaa vastaavan opiskelijanumeron.
|
||||
Opiskelijanumeron yhteydessä olevaa ympyrää klikkaamalla siirtyy ”student statistics” puolelle ja näkee valitun opiskelijan opintotiedot.
|
||||
|
||||
X-akselin skaalaa voi säätää harmaista painikkeista ”1m”, ”3m”, ”6m”, ”YTD”, ”1y” ja ”ALL” tai kuvaajan alla olevan sinisen näkymän harmaita ”kahvoja” siirtämällä.
|
||||
Y-akselin skaalaa voi säätää harmaista painikkeista ”small” ”medium” ja "large”`,
|
||||
CreditStatistics: `**Opintopistekertymä**
|
||||
|
||||
Taulukon rivit skaalautuvat valitun tarkastelujakson mukaan.
|
||||
Jos tarkastelujaksoksi valitsee 12 kuukautta, on ylin kategoria 55 opintopistettä tai yli, eli opiskelija etenee tavoiteajassa.
|
||||
Taulukosta näkee silmäyksellä tavoiteajassa etenevät (ylin kategoria) ja esim. nollasuorittajat.
|
||||
|
||||
**Huom! nollasuorittajissa on mukana myös poissaolevaksi ilmoittautuneet, ellei heitä ole erikseen suodatettu populaatiosta pois.**
|
||||
|
||||
Kategorioita voi myös itsessään käyttää suodattimina. Klikkaamalla rivin vasemmassa reunassa olevaa suodatinta jäävät näkyviin vain sen kategorian opiskelijat.
|
||||
Suodatin aukeaa samalla ikkunan vasempaan laitaan, josta sitä voi käyttää vapaavalintaisilla arvoilla.
|
||||
`,
|
||||
CoursesOfPopulation: `**Courses of population**
|
||||
|
||||
Tämä taulukko sisältää näkyvissä olevien opiskelijoiden kaikki kurssisuoritukset.
|
||||
|
||||
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.
|
||||
|
||||
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- ja koodisarakkeissa on hakukentät *("filter here")* suodattamista varten.
|
||||
Suodattimet saa tyhjennettyä painamalla kentän vieressä olevaa ruksia.
|
||||
|
||||
**Huom! Nämä suodattimet vaikuttavat vain tähän taulukkoon, eivät koko populatioon.**
|
||||
|
||||
**Taulukot**
|
||||
|
||||
- **pass/fail** - oletusnäkymä, joka sisältää kurssisuoritukset, hylätyt sekä yritykset. Taulukon tiedot voi järjestää sarakkeita (n, after retry, percentage, many times, per student, passed ja attempted) klikkaamalla.
|
||||
- **grades** - arvosanajakauma.
|
||||
- **when passed** - kurssisuorituksen ajankohta. Huom.! *Passed*-sarakkeen kokonaissumma ei välttämättä täsmää puolivuosittaisten lukumäärien summan kanssa johtuen tuplakirjauksista Oodissa.
|
||||
`,
|
||||
Search: `Tässä osiossa voi tarkastella koulutusohjelmakohtaisia populaatioita sisäänotto-lukuvuoden mukaisesti,
|
||||
vuosikurssi kerrallaan. Uudet koulutusohjelmat ovat pääsääntöisesti alkaneet 1.8.2017,
|
||||
joten oletusvalinnat näyttävät tiedot tästä päivämäärästä lähtien.
|
||||
|
||||
- **Class of:** lukuvuosi, jolloin opiskelija on ilmoittautunut ensimmäisen kerran (läsnä- tai poissaolevaksi) ohjelmaan. Ilmoittautumisen opinto-oikeus voi olla ensi- tai toissijainen.
|
||||
- **Study programme:** haluttu koulutusohjelma.
|
||||
- **Select tag (Optional):** itsetehty tai valmis tägi.
|
||||
- **Study track (Optional):** opintosuunta.`,
|
||||
Students: `**Students**
|
||||
|
||||
Taulukko näyttää oletusarvoisesti vain opiskelijanumerot, mutta harmaata liukukytkintä klikkaamalla saa näkyviin opiskelijoiden nimet.
|
||||
|
||||
**General**
|
||||
|
||||
Klikkaamalla opiskelijanumeron vieressä olevaa sinistä nuolta, siirtyy "student statistic" näkymään valitun opiskelijan osalta.
|
||||
|
||||
- **credits since start of studyright:** listaa opiskelijan suoritetut opintopisteet valitun tarkastelujakson / populaation ajalta.
|
||||
- **all credits:** listaa opiskelijan kaikki opintopisteet (myös aiemmat suoritukset, eli ne jotka tehty ennen valittuun tarkastelujaksoon ja populaatioon kuulumista)
|
||||
- **transferred from:** opiskelijan vanha koulutusohjelma, josta opiskelija on siirtynyt uuteen.
|
||||
- **studytracks:** opintosuunta
|
||||
|
||||
**Mandatory Courses**
|
||||
|
||||
Tällä välilehdellä näkyy, mitä pakollisiksi määriteltyjä kursseja opiskelija on suorittanut.
|
||||
Kurssit voi määrittää kohdassa "Study program" > "Overview" > haluttu koulutusohjelma. Scrollatessa kurssitaulukkoa alaspäin, kurssin nimen saa näkyviin viemällä kursorin halutun solun kohdalle.
|
||||
|
||||
**Tags**
|
||||
|
||||
Opiskelijoille voi luoda tag:eja ryhmittelyä varten kohdassa "Study Program" > "Overview" > haluttu koulutusohjelma.
|
||||
|
||||
**Download**
|
||||
|
||||
Klikkaamalla tätä harmaata kuvaketta, saa ladattua .xlsx tiedoston, joka sisältää kaikki "General" ja "Mandatory Courses" välilehtien tiedot.
|
||||
`,
|
||||
Filters: {
|
||||
Add: `Harmaata ”add” painiketta klikkaamalla aukeaa filters- eli suodatinvalikko,
|
||||
josta voi lisätä haluamiaan suodattimia, jotka rajaavat valittua populaatiota.`,
|
||||
|
||||
@@ -2,6 +2,8 @@ import React, { useCallback, useState, useEffect, useRef } from 'react'
|
||||
import { chunk, isEqual } from 'lodash'
|
||||
import qs from 'query-string'
|
||||
import { useSelector } from 'react-redux'
|
||||
import { useHistory } from 'react-router-dom'
|
||||
import { setMocking } from './index'
|
||||
|
||||
import { SEARCH_HISTORY_VERSION } from '../constants'
|
||||
import TSA from './tsa'
|
||||
@@ -276,3 +278,24 @@ export const useIsAdmin = () => {
|
||||
if (!roles) return false
|
||||
return roles.some(role => role.group_code === 'admin')
|
||||
}
|
||||
|
||||
// From: https://www.joshwcomeau.com/snippets/react-hooks/use-toggle/
|
||||
export const useToggle = (initialValue = false) => {
|
||||
const [value, setValue] = useState(initialValue)
|
||||
const toggle = useCallback(() => {
|
||||
setValue(v => !v)
|
||||
}, [])
|
||||
return [value, toggle]
|
||||
}
|
||||
|
||||
export const useShowAsUser = () => {
|
||||
const history = useHistory()
|
||||
|
||||
const showAsUser = uid => {
|
||||
setMocking(uid)
|
||||
history.push('/')
|
||||
window.location.reload()
|
||||
}
|
||||
|
||||
return showAsUser
|
||||
}
|
||||
|
||||
@@ -12,11 +12,6 @@ export const setMocking = userid =>
|
||||
userid ? localStorage.setItem(MOCK_USERID, userid) : localStorage.removeItem(MOCK_USERID)
|
||||
export const getMocked = () => localStorage.getItem(MOCK_USERID)
|
||||
|
||||
export const SIS_WARNING_FLAG = 'SIS_WARNING_FLAG'
|
||||
export const setHideSisWarningFlag = booleanFlag =>
|
||||
booleanFlag ? localStorage.setItem(SIS_WARNING_FLAG, booleanFlag) : localStorage.removeItem(SIS_WARNING_FLAG)
|
||||
export const getHideSisWarningFlag = () => localStorage.getItem(SIS_WARNING_FLAG)
|
||||
|
||||
export const textAndDescriptionSearch = (dropDownOptions, param) =>
|
||||
filter(dropDownOptions, option =>
|
||||
option.text
|
||||
@@ -228,3 +223,36 @@ export const validateInputLength = (input, minLength) => input && input.trim().l
|
||||
export const splitByEmptySpace = str => str.replace(/\s\s+/g, ' ').split(' ')
|
||||
|
||||
export const isNewHYStudyProgramme = code => !!(code && code.match(/^[A-Z]*[0-9]*_[0-9]*$/))
|
||||
|
||||
export const bachelorHonoursProgrammes = [
|
||||
'KH50_001',
|
||||
'KH50_002',
|
||||
'KH50_003',
|
||||
'KH50_004',
|
||||
'KH50_005',
|
||||
'KH50_006',
|
||||
'KH50_007',
|
||||
'KH50_008',
|
||||
]
|
||||
|
||||
export const bachelorHonoursBasicModules = {
|
||||
KH50_001: ['MAT110', 'MAT120', 'MAT130', 'MAT120', 'TKT1'],
|
||||
KH50_002: ['FYS1100', 'FYS1200'],
|
||||
KH50_003: ['KEK100'],
|
||||
KH50_004: ['MAT110', 'FYS1100', 'FYS1200', 'KEK100'],
|
||||
KH50_005: ['TKT1'],
|
||||
KH50_006: ['GEOK_100'],
|
||||
KH50_007: ['MAA-100'],
|
||||
KH50_008: ['BSCH1000', 'BSCS1000', 'BSMA1000', 'BSMS1000', 'BSPH1000'],
|
||||
}
|
||||
|
||||
export const bachelorHonoursIntermediateModules = {
|
||||
KH50_001: ['mat217', 'MAT210', 'MAT220', 'MAT240', 'MAT213', 'MAT230', 'MAT218', 'MAT011'],
|
||||
KH50_002: ['FYS2100', 'FYS2200', 'FYS2300', 'FYS2400', 'FYS2500', 'FYS2600'],
|
||||
KH50_003: ['KEK200'],
|
||||
KH50_004: ['MFK-M200', 'MFK-F200', 'MFK-K200'],
|
||||
KH50_005: ['TKT2'],
|
||||
KH50_006: ['GEOK_200'],
|
||||
KH50_007: ['MAA-200'],
|
||||
KH50_008: ['BSCH2000', 'BSCS2000', 'BSMA2000', 'BSMS2000', 'BSPH2000'],
|
||||
}
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
# Students
|
||||
|
||||
Harmaata _Show_-painiketta klikkaamalla saa listan kaikista populaation opiskelijoista. (Huom. suodattimet vaikuttavat tähän listaan!) Taulukko näyttää oletusarvoisesti vain opiskelijanumerot, mutta harmaata liukukytkintä klikkaamalla saa näkyviin opiskelijoiden nimet.
|
||||
|
||||
- **Student Number:** Klikkaamalla opiskelijanumeron vieressä olevaa sinistä nuolta, siirtyy "student statistic" -näkymään valitun opiskelijan osalta.
|
||||
- **All Credits:** listaa opiskelijan kaikki opintopisteet (myös aiemmat suoritukset, eli ne jotka tehty ennen valittuun tarkastelujaksoon ja populaatioon kuulumista)
|
||||
- **Tags:** Opiskelijoille voi luoda tageja ryhmittelyä varten kohdassa _Study Program > Overview > haluttu koulutusohjelma_.
|
||||
- **Study Programme:** Opiskelijan koulutusohjelma.
|
||||
- **Start Year at Uni:** Aloitusvuosi yliopistolla.
|
||||
-15
@@ -1,15 +0,0 @@
|
||||
# Courses of population
|
||||
|
||||
Tämä taulukko sisältää näkyvissä olevien opiskelijoiden kaikki kurssisuoritukset.
|
||||
|
||||
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.
|
||||
|
||||
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- ja koodisarakkeissa on hakukentät *("filter here")* suodattamista varten. Suodattimet saa tyhjennettyä painamalla kentän vieressä olevaa ruksia. *Huom! Nämä suodattimet vaikuttavat vain tähän taulukkoon, eivät koko populatioon.*
|
||||
|
||||
## Taulukot:
|
||||
|
||||
- **pass/fail** - oletusnäkymä, joka sisältää kurssisuoritukset, hylätyt sekä yritykset. Taulukon tiedot voi järjestää sarakkeita (n, after retry, percentage, many times, per student, passed ja attempted) klikkaamalla.
|
||||
- **grades** - arvosanajakauma.
|
||||
- **when passed** - kurssisuorituksen ajankohta. Huom.! *Passed*-sarakkeen kokonaissumma ei välttämättä täsmää puolivuosittaisten lukumäärien summan kanssa johtuen tuplakirjauksista Oodissa.
|
||||
@@ -1,5 +0,0 @@
|
||||
# Credit Accumulation
|
||||
|
||||
Opiskelijoiden kumulatiiviset opintopistekertymät opiskelijakohtaisina kuvaajina. Kun hiiren vie kuvaajien päälle, saa näkyviin jokaista kuvaajaa vastaavan opiskelijanumeron. Opiskelijanumeron yhteydessä olevaa ympyrää klikkaamalla siirtyy ”student statistics” puolelle ja näkee valitun opiskelijan opintotiedot.
|
||||
|
||||
X-akselin skaalaa voi säätää harmaista painikkeista ”1m”, ”3m”, ”6m”, ”YTD”, ”1y” ja ”ALL” tai kuvaajan alla olevan sinisen näkymän harmaita ”kahvoja” siirtämällä. Y-akselin skaalaa voi säätää harmaista painikkeista ”small” ”medium” ja "large”
|
||||
@@ -1,5 +0,0 @@
|
||||
# Opintopistekertymä
|
||||
|
||||
Taulukon rivit skaalautuvat valitun tarkastelujakson mukaan. Jos tarkastelujaksoksi valitsee 12 kuukautta, on ylin kategoria 55 opintopistettä tai yli, eli opiskelija etenee tavoiteajassa. Taulukosta näkee silmäyksellä tavoiteajassa etenevät (ylin kategoria) ja esim. nollasuorittajat. *Huom! nollasuorittajissa on mukana myös poissaolevaksi ilmoittautuneet, ellei heitä ole erikseen suodatettu populaatiosta pois.*
|
||||
|
||||
Kategorioita voi myös itsessään käyttää suodattimina. Klikkaamalla rivin vasemmassa reunassa olevaa suodatinta jäävät näkyviin vain sen kategorian opiskelijat. Suodatin aukeaa samalla ikkunan vasempaan laitaan, josta sitä voi käyttää vapaavalintaisilla arvoilla.
|
||||
@@ -1,12 +0,0 @@
|
||||
Valitun koulutusohjelmavuosikurssin perustiedot.
|
||||
<br>
|
||||
<br>
|
||||
**Sample size** luku sisältää läsnä- ja poissaolevaksi ilmoittautuneet opiskelijat, muttei "excludes"-ryhmiä, jotka voi määritellä "advanced settings" -kohdasta.
|
||||
<br>
|
||||
**Fall** = 1.8. - 31.12. ilmoittautuneet ja **Spring** = 1.1. - 31.7. ilmoittautuneet.
|
||||
<br>
|
||||
**showing XX months**: Dataa näytetään ohjelman vuosikurssin alkamispäivästä lähtien haluttuun kuukauteen asti (oletusarvoisesti "tähän päivään").
|
||||
<br>
|
||||
**Updated** kertoo milloin data on päivitetty
|
||||
<br>
|
||||
Mustasta rastista voi sulkea valitun koulutusohjelmavuosikurssin.
|
||||
@@ -1,6 +0,0 @@
|
||||
Tässä osiossa voi tarkastella koulutusohjelmakohtaisia populaatioita sisäänotto-lukuvuoden mukaisesti, vuosikurssi kerrallaan. Uudet koulutusohjelmat ovat pääsääntöisesti alkaneet 1.8.2017 joten oletusvalinnat näyttävät tiedot tästä päivämäärästä lähtien.
|
||||
|
||||
- **Class of:** lukuvuosi, jolloin opiskelija on ilmoittautunut ensimmäisen kerran (läsnä- tai poissaolevaksi) ohjelmaan. Ilmoittautumisen opinto-oikeus voi olla ensi- tai toissijainen.
|
||||
- **Study programme:** haluttu koulutusohjelma.
|
||||
- **Select tag (Optional):** itsetehty tai valmis tägi.
|
||||
- **Study track (Optional):** opintosuunta.
|
||||
@@ -1,24 +0,0 @@
|
||||
# Students
|
||||
|
||||
Taulukko näyttää oletusarvoisesti vain opiskelijanumerot, mutta harmaata liukukytkintä klikkaamalla saa näkyviin opiskelijoiden nimet.
|
||||
|
||||
## General
|
||||
|
||||
Klikkaamalla opiskelijanumeron vieressä olevaa sinistä nuolta, siirtyy "student statistic" näkymään valitun opiskelijan osalta.
|
||||
|
||||
- **credits since start of studyright:** listaa opiskelijan suoritetut opintopisteet valitun tarkastelujakson / populaation ajalta.
|
||||
- **all credits:** listaa opiskelijan kaikki opintopisteet (myös aiemmat suoritukset, eli ne jotka tehty ennen valittuun tarkastelujaksoon ja populaatioon kuulumista)
|
||||
- **transferred from:** opiskelijan vanha koulutusohjelma, josta opiskelija on siirtynyt uuteen.
|
||||
- **studytracks:** opintosuunta
|
||||
|
||||
## Mandatory Courses
|
||||
|
||||
Tällä välilehdellä näkyy, mitä pakollisiksi määriteltyjä kursseja opiskelija on suorittanut. Kurssit voi määrittää kohdassa "Study program" > "Overview" > haluttu koulutusohjelma. Scrollatessa kurssitaulukkoa alaspäin, kurssin nimen saa näkyviin viemällä kursorin halutun solun kohdalle.
|
||||
|
||||
## Tags
|
||||
|
||||
Opiskelijoille voi luoda tag:eja ryhmittelyä varten kohdassa "Study Program" > "Overview" > haluttu koulutusohjelma.
|
||||
|
||||
## Download
|
||||
|
||||
Klikkaamalla tätä harmaata kuvaketta, saa ladattua .xlsx tiedoston, joka sisältää kaikki "General" ja "Mandatory Courses" välilehtien tiedot.
|
||||
@@ -1,11 +1,10 @@
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { Transition, Dimmer, Header, Image, Container, Button } from 'semantic-ui-react'
|
||||
import { bool, func } from 'prop-types'
|
||||
import Highcharts from 'highcharts'
|
||||
import ReactHighchart from 'react-highcharts'
|
||||
import { connect } from 'react-redux'
|
||||
import { useDispatch } from 'react-redux'
|
||||
import { random } from 'lodash'
|
||||
import { logout as logoutAction } from '../../redux/auth'
|
||||
import { logout } from '../../redux/auth'
|
||||
import { images } from '../../common'
|
||||
import MulticolorBarChart from './MulticolorBarChart'
|
||||
|
||||
@@ -28,6 +27,7 @@ const names = [
|
||||
'otahontas',
|
||||
'saarasat',
|
||||
'popalmu',
|
||||
'ollikehy',
|
||||
]
|
||||
|
||||
const dummyData = names.map(name => ({
|
||||
@@ -38,7 +38,7 @@ const dummyData = names.map(name => ({
|
||||
}, []),
|
||||
}))
|
||||
|
||||
const AccessDenied = ({ notEnabled, logout }) => {
|
||||
const AccessDenied = ({ notEnabled }) => {
|
||||
const header = notEnabled ? 'Welcome to Oodikone!' : 'Something broke'
|
||||
const subheader = notEnabled
|
||||
? `You're currently not allowed to enter
|
||||
@@ -47,6 +47,7 @@ const AccessDenied = ({ notEnabled, logout }) => {
|
||||
pressing log out or contacting grp-toska@helsinki.fi`
|
||||
|
||||
const [easterEgg, setEasterEgg] = useState(false)
|
||||
const dispatch = useDispatch()
|
||||
|
||||
useEffect(() => {
|
||||
if (notEnabled) {
|
||||
@@ -95,7 +96,7 @@ const AccessDenied = ({ notEnabled, logout }) => {
|
||||
<p>{header}</p>
|
||||
<Header.Subheader>{subheader}</Header.Subheader>
|
||||
<br />
|
||||
<Button onClick={logout} color="pink">
|
||||
<Button onClick={() => dispatch(logout())} color="pink">
|
||||
{' '}
|
||||
Log out{' '}
|
||||
</Button>
|
||||
@@ -114,13 +115,4 @@ const AccessDenied = ({ notEnabled, logout }) => {
|
||||
)
|
||||
}
|
||||
|
||||
AccessDenied.propTypes = {
|
||||
notEnabled: bool.isRequired,
|
||||
logout: func.isRequired,
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
logout: logoutAction,
|
||||
}
|
||||
|
||||
export default connect(null, mapDispatchToProps)(AccessDenied)
|
||||
export default AccessDenied
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { Segment } from 'semantic-ui-react'
|
||||
import { Switch, Route, Link, useRouteMatch } from 'react-router-dom'
|
||||
|
||||
@@ -32,20 +31,10 @@ const NavTile = ({ to, title, imageSrc }) => {
|
||||
)
|
||||
}
|
||||
|
||||
NavTile.propTypes = {
|
||||
to: PropTypes.string.isRequired,
|
||||
title: PropTypes.string.isRequired,
|
||||
imageSrc: PropTypes.string.isRequired,
|
||||
}
|
||||
|
||||
const NavTiles = ({ children }) => {
|
||||
return <div className="cool-ds-nav-tiles">{children}</div>
|
||||
}
|
||||
|
||||
NavTiles.propTypes = {
|
||||
children: PropTypes.node.isRequired,
|
||||
}
|
||||
|
||||
const baseURI = '/cool-data-science'
|
||||
|
||||
const CoolDataScience = () => {
|
||||
|
||||
@@ -5,12 +5,12 @@ import Highcharts from 'highcharts'
|
||||
import ReactHighcharts from 'react-highcharts'
|
||||
import { Segment, Loader, Dimmer, Checkbox, Button, Message, Icon } from 'semantic-ui-react'
|
||||
import _ from 'lodash'
|
||||
import ReactMarkdown from 'react-markdown'
|
||||
import HighchartsCustomEvents from 'highcharts-custom-events'
|
||||
|
||||
import TSA from '../../common/tsa'
|
||||
import InfoToolTips from '../../common/InfoToolTips'
|
||||
import { getProtoC, getProtoCProgramme } from '../../redux/coolDataScience'
|
||||
import ReactMarkdown from 'react-markdown'
|
||||
|
||||
HighchartsCustomEvents(Highcharts)
|
||||
|
||||
@@ -578,7 +578,7 @@ const ProtoC = ({
|
||||
})
|
||||
|
||||
const SorterButtons = () => (
|
||||
<div align="center" style={{ marginTop: '10px' }}>
|
||||
<div align="center" style={{ marginTop: '15px' }}>
|
||||
<Button.Group>
|
||||
<Button style={{ cursor: 'default' }} active color="black">
|
||||
Sort by:
|
||||
@@ -622,14 +622,11 @@ const ProtoC = ({
|
||||
checked={includeOldAttainments}
|
||||
/>
|
||||
</div>
|
||||
<Message>
|
||||
<ReactMarkdown source={CoolDataScience.protoC} escapeHtml={false} />
|
||||
</Message>
|
||||
</>
|
||||
)
|
||||
|
||||
const DrilldownMessage = () => (
|
||||
<Message color='blue'
|
||||
<Message style={{ margin: '2em' }} color='blue'
|
||||
content="Graafissa pystyy pisteitä klikkaamalla porautumaan ensin ohjelmatasolle ja edelleen opintosuuntatasolle.
|
||||
Uusi graafi avautuu nykyisen alle."
|
||||
/>
|
||||
@@ -675,6 +672,9 @@ const ProtoC = ({
|
||||
)}
|
||||
</Segment>
|
||||
<RenderBelowGraph />
|
||||
<Message>
|
||||
<ReactMarkdown children={CoolDataScience.protoC} escapeHtml={false} />
|
||||
</Message>
|
||||
</Segment>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -466,7 +466,10 @@ const ProtoC = ({
|
||||
/>
|
||||
</div>
|
||||
<Message>
|
||||
<ReactMarkdown source={CoolDataScience.protoC2} escapeHtml={false} />
|
||||
{
|
||||
// eslint-disable-next-line react/no-children-prop
|
||||
<ReactMarkdown children={CoolDataScience.protoC2} escapeHtml={false} />
|
||||
}
|
||||
</Message>
|
||||
</Segment>
|
||||
</Segment>
|
||||
@@ -529,7 +532,10 @@ const ProtoC = ({
|
||||
/>
|
||||
</div>
|
||||
<Message>
|
||||
<ReactMarkdown source={CoolDataScience.protoC2} escapeHtml={false} />
|
||||
{
|
||||
// eslint-disable-next-line react/no-children-prop
|
||||
<ReactMarkdown children={CoolDataScience.protoC2} escapeHtml={false} />
|
||||
}
|
||||
</Message>
|
||||
</Segment>
|
||||
</Segment>
|
||||
|
||||
@@ -291,7 +291,9 @@ const ProtoG = ({ uberdata, years, isLoading, loadingYears, getUberDispatch, get
|
||||
</Table>
|
||||
)}
|
||||
<Message>
|
||||
<ReactMarkdown source={CoolDataScience.protoG} escapeHtml={false} />
|
||||
{//eslint-disable-next-line react/no-children-prop
|
||||
<ReactMarkdown children={CoolDataScience.protoC} escapeHtml={false} />
|
||||
}
|
||||
</Message>
|
||||
</Segment>
|
||||
</Segment>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import PropTypes, { shape, bool, func } from 'prop-types'
|
||||
import { connect } from 'react-redux'
|
||||
import { Segment, Loader, Dimmer, Icon, Accordion, Checkbox, Message, Form } from 'semantic-ui-react'
|
||||
import { Segment, Popup, Loader, Dimmer, Icon, Accordion, Checkbox, Message, Form } from 'semantic-ui-react'
|
||||
import _ from 'lodash'
|
||||
import moment from 'moment'
|
||||
import ReactMarkdown from 'react-markdown'
|
||||
@@ -28,31 +27,58 @@ const mapValueToRange = (x, min1, max1, min2, max2) => {
|
||||
return ((x - min1) * (max2 - min2)) / (max1 - min1) + min2
|
||||
}
|
||||
|
||||
const StatusContainer = ({
|
||||
title,
|
||||
current,
|
||||
previous,
|
||||
clickable,
|
||||
handleClick,
|
||||
min1,
|
||||
max1,
|
||||
showYearlyValues,
|
||||
yearlyValues,
|
||||
showByYear,
|
||||
}) => {
|
||||
const diff = Math.round(current - previous)
|
||||
const StatusContainer = ({ stats, handleClick, min1, max1, showYearlyValues, showRelativeValues, showByYear }) => {
|
||||
const title = getTextIn(stats.name)
|
||||
const clickable = !!stats.drill
|
||||
|
||||
let current
|
||||
let previous
|
||||
|
||||
if (!stats.drill) {
|
||||
current = stats.currentStudents
|
||||
previous = stats.previousStudents
|
||||
} else {
|
||||
current = stats.current
|
||||
previous = stats.previous
|
||||
|
||||
if (showRelativeValues) {
|
||||
current /= stats.currentStudents
|
||||
previous /= stats.previousStudents
|
||||
}
|
||||
}
|
||||
|
||||
const diff = current - previous
|
||||
const p = getP(current, previous)
|
||||
const change = Math.round((p - 1) * 1000) / 10
|
||||
const change = p - 1
|
||||
|
||||
const getColor = v => {
|
||||
if (v > 2.5) return '#6ab04c'
|
||||
if (v < -2.5) return '#ff7979'
|
||||
if (v > 0.025) return '#6ab04c'
|
||||
if (v < -0.025) return '#ff7979'
|
||||
return '#7B9FCF'
|
||||
}
|
||||
|
||||
const plussify = x => {
|
||||
if (x > 0) return `+${x.toLocaleString('fi')}`
|
||||
return x.toLocaleString('fi')
|
||||
const plussify = (x, decimals = 0) =>
|
||||
x.toLocaleString('fi', {
|
||||
minimumFractionDigits: decimals,
|
||||
maximumFractionDigits: decimals,
|
||||
signDisplay: 'always',
|
||||
})
|
||||
|
||||
const getDisplayValue = (value, denominator) => {
|
||||
let displayValue
|
||||
|
||||
if (denominator === 0) {
|
||||
displayValue = value === 0 ? 0 : Infinity
|
||||
} else if (showRelativeValues) {
|
||||
displayValue = value / denominator
|
||||
} else {
|
||||
displayValue = value
|
||||
}
|
||||
|
||||
return displayValue.toLocaleString('fi', {
|
||||
minimumFractionDigits: showRelativeValues ? 2 : 0,
|
||||
maximumFractionDigits: showRelativeValues ? 2 : 0,
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -80,90 +106,82 @@ const StatusContainer = ({
|
||||
overflowWrap: 'break-word',
|
||||
}}
|
||||
>
|
||||
<span className="status-title">{title}</span>
|
||||
<Popup position="bottom center" size="tiny" trigger={<span className="status-title">{title}</span>}>
|
||||
{stats.code}
|
||||
</Popup>
|
||||
<div style={{ margin: '10px 0' }}>
|
||||
<Icon
|
||||
style={{
|
||||
color: getColor(change),
|
||||
transform: `rotateZ(${-mapValueToRange(change, min1, max1, -45, 45)}deg)`,
|
||||
transform: `rotateZ(${-mapValueToRange(change * 100, min1, max1, -45, 45)}deg)`,
|
||||
}}
|
||||
size="huge"
|
||||
name="arrow right"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<span style={{ fontSize: 20, fontWeight: 'bold', color: getColor(change) }}>{plussify(diff)}</span>
|
||||
<span style={{ fontSize: 20, fontWeight: 'bold', color: getColor(change) }}>
|
||||
{plussify(diff, showRelativeValues ? 2 : 0)}
|
||||
</span>
|
||||
<br />
|
||||
<span style={{ fontWeight: 'bold', color: getColor(change) }}>({plussify(change * 100, 1)}%)</span>
|
||||
<br />
|
||||
<span style={{ fontWeight: 'bold', color: getColor(change) }}>({plussify(change)}%)</span>
|
||||
</div>
|
||||
</div>
|
||||
{showYearlyValues && (
|
||||
<div style={{ marginTop: '10px', textAlign: 'start' }}>
|
||||
{_.orderBy(Object.entries(yearlyValues), ([y]) => y, ['desc']).map(
|
||||
([year, { acc, total, accStudents, totalStudents }]) => {
|
||||
return (
|
||||
<div style={{ margin: '5px 0' }} key={`${title}-${year}`}>
|
||||
<span>
|
||||
<b>
|
||||
{year}
|
||||
{!showByYear && `-${`${Number(year) + 1}`.slice(-2)}`}:
|
||||
</b>{' '}
|
||||
{/* render num of students instead of credits if no credits are given from the course */}
|
||||
{accStudents <= acc ? Math.round(acc).toLocaleString('fi') : accStudents}
|
||||
{/* if no total students add students after accStudents */}
|
||||
{acc <= accStudents && totalStudents < 1 && ' students'}
|
||||
{/* render total if there are credits or students for either */}
|
||||
{(total > 0 || totalStudents > 0) &&
|
||||
` / ${
|
||||
// same logic as before to check if render num of students or credits
|
||||
totalStudents <= total && total > 0
|
||||
? Math.round(total).toLocaleString('fi')
|
||||
: `${totalStudents} students`
|
||||
}`}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
)}
|
||||
<div className={`years item-type-${stats.type}`}>
|
||||
{_.orderBy(Object.entries(stats.yearly), ([y]) => y, ['desc']).map(([year, yearStats]) => {
|
||||
return (
|
||||
<span style={{ display: 'contents' }} className="year-row">
|
||||
<b className="year-label">
|
||||
{year}
|
||||
{!showByYear && `-${`${Number(year) + 1}`.slice(-2)}`}:
|
||||
</b>
|
||||
{stats.type === 'course' ? (
|
||||
<>
|
||||
<span className="year-value">{yearStats.accStudents}</span>
|
||||
<span className="unit">students</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<span className="year-value accumulated">
|
||||
{getDisplayValue(yearStats.acc, yearStats.accStudents)}
|
||||
</span>
|
||||
<span style={{ color: '#AAA', margin: '0 0.3em' }}>{yearStats.total !== undefined && '/'}</span>
|
||||
<span className="year-value total">
|
||||
{yearStats.total !== undefined && getDisplayValue(yearStats.total, yearStats.totalStudents)}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</span>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</Segment>
|
||||
)
|
||||
}
|
||||
|
||||
StatusContainer.propTypes = {
|
||||
title: PropTypes.string.isRequired,
|
||||
current: PropTypes.number.isRequired,
|
||||
previous: PropTypes.number.isRequired,
|
||||
clickable: PropTypes.bool.isRequired,
|
||||
handleClick: PropTypes.func.isRequired,
|
||||
min1: PropTypes.number.isRequired,
|
||||
max1: PropTypes.number.isRequired,
|
||||
showYearlyValues: PropTypes.bool.isRequired,
|
||||
yearlyValues: PropTypes.shape({}).isRequired,
|
||||
showByYear: PropTypes.bool.isRequired,
|
||||
}
|
||||
|
||||
const VerticalLine = () => <div style={{ margin: '0 10px', fontSize: '20px' }}>|</div>
|
||||
|
||||
const Status = ({ getStatusDispatch, data, loading }) => {
|
||||
const DATE_FORMAT = 'DD.MM.YYYY'
|
||||
const [showYearlyValues, setShowYearlyValues] = useLocalStorage('showYearlyValues', true)
|
||||
const [showByYear, setShowByYear] = useLocalStorage('showByYear', false)
|
||||
const [showRelativeValues, setShowRelativeValues] = useLocalStorage('showRelativeValues', false)
|
||||
const [drillStack, setDrillStack] = useState([])
|
||||
const [showSettings, setShowSettings] = useState(true)
|
||||
const [selectedDate, setSelectedDate] = useState(moment())
|
||||
const [codes, setCodes] = useState([])
|
||||
const [showByStudents, setShowByStudents] = useState(false)
|
||||
const { CoolDataScience } = InfoToolTips
|
||||
|
||||
const isValidDate = d => moment.isMoment(d) && moment().diff(d) > 0
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedDate && isValidDate(selectedDate)) {
|
||||
getStatusDispatch({ date: selectedDate.valueOf(), showByYear, showByStudents })
|
||||
getStatusDispatch({ date: selectedDate.valueOf(), showByYear })
|
||||
}
|
||||
}, [selectedDate, showByYear, showByStudents])
|
||||
}, [selectedDate, showByYear])
|
||||
|
||||
useEffect(() => {
|
||||
if (codes.length > 0) {
|
||||
@@ -192,10 +210,9 @@ const Status = ({ getStatusDispatch, data, loading }) => {
|
||||
sendAnalytics(`S Show by year toggle ${!byYear ? 'on' : 'off'}`, 'Status')
|
||||
}
|
||||
|
||||
const handleShowByStudentsToggled = () => {
|
||||
const byStudents = showByStudents
|
||||
setShowByStudents(!showByStudents)
|
||||
sendAnalytics(`S Show by students toggle ${!byStudents ? 'on' : 'off'}`, 'Status')
|
||||
const handleShowRelativeValuesChanged = () => {
|
||||
sendAnalytics(`S Show relative values toggle ${!showRelativeValues ? 'on' : 'off'}`)
|
||||
setShowRelativeValues(!showRelativeValues)
|
||||
}
|
||||
|
||||
const pushToDrillStack = (values, code) => {
|
||||
@@ -256,9 +273,9 @@ const Status = ({ getStatusDispatch, data, loading }) => {
|
||||
<VerticalLine />
|
||||
<Checkbox
|
||||
style={{ fontSize: '0.9em', fontWeight: 'normal' }}
|
||||
label="Näytä suhteessa opiskelijamäärään"
|
||||
onChange={handleShowByStudentsToggled}
|
||||
checked={showByStudents}
|
||||
label="Näytä suhteutettuna opiskelijoiden määrään"
|
||||
onChange={handleShowRelativeValuesChanged}
|
||||
checked={showRelativeValues}
|
||||
/>
|
||||
<VerticalLine />
|
||||
<Form>
|
||||
@@ -299,14 +316,25 @@ const Status = ({ getStatusDispatch, data, loading }) => {
|
||||
)
|
||||
|
||||
const orderedAbsDiffs = _.orderBy(
|
||||
Object.values(_.last(drillStack) || data).map(({ current, previous }) => {
|
||||
return Math.abs(Math.floor((getP(current, previous) - 1) * 1000) / 10)
|
||||
Object.values(_.last(drillStack) || data).map(({ current, currentStudents, previous, previousStudents }) => {
|
||||
return Math.abs(
|
||||
Math.floor(
|
||||
(getP(
|
||||
showRelativeValues ? current / currentStudents : current,
|
||||
showRelativeValues ? previous / previousStudents : previous
|
||||
) -
|
||||
1) *
|
||||
1000
|
||||
) / 10
|
||||
)
|
||||
})
|
||||
)
|
||||
|
||||
const medianDiff = orderedAbsDiffs[Math.round((orderedAbsDiffs.length - 1) / 2)]
|
||||
|
||||
return (
|
||||
<>
|
||||
<h2>Uusien ohjelmien tuottamat opintopisteet</h2>
|
||||
<h2>Koulutusohjelmien tuottamat opintopisteet</h2>
|
||||
<DrilldownMessage />
|
||||
<div style={{ display: 'flex', marginBottom: '20px', marginRight: '40px' }}>
|
||||
{drillStack.length > 0 && (
|
||||
@@ -329,23 +357,16 @@ const Status = ({ getStatusDispatch, data, loading }) => {
|
||||
['desc']
|
||||
).map(([code, stats]) => {
|
||||
const handleClick = () => pushToDrillStack(stats.drill, code)
|
||||
// check if the course has credits or not (if credits is zero but there are students who have completed it)
|
||||
const current =
|
||||
!stats.drill && stats.current === 0 && stats.currentStudents > 1 ? stats.currentStudents : stats.current
|
||||
const previous =
|
||||
!stats.drill && stats.previous === 0 && stats.previousStudents > 1 ? stats.previousStudents : stats.previous
|
||||
|
||||
return (
|
||||
<StatusContainer
|
||||
key={code}
|
||||
clickable={!!stats.drill}
|
||||
stats={stats}
|
||||
handleClick={handleClick}
|
||||
title={getTextIn(stats.name)}
|
||||
current={current}
|
||||
previous={previous}
|
||||
showRelativeValues={showRelativeValues}
|
||||
showYearlyValues={showYearlyValues}
|
||||
min1={-medianDiff * 2}
|
||||
max1={medianDiff * 2}
|
||||
yearlyValues={stats.yearly}
|
||||
showByYear={showByYear}
|
||||
/>
|
||||
)
|
||||
@@ -353,18 +374,15 @@ const Status = ({ getStatusDispatch, data, loading }) => {
|
||||
</div>
|
||||
{renderSettings()}
|
||||
<Message>
|
||||
<ReactMarkdown source={CoolDataScience.status} escapeHtml={false} />
|
||||
{
|
||||
// eslint-disable-next-line react/no-children-prop
|
||||
<ReactMarkdown children={CoolDataScience.status} escapeHtml={false} />
|
||||
}
|
||||
</Message>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
Status.propTypes = {
|
||||
data: shape({}).isRequired,
|
||||
loading: bool.isRequired,
|
||||
getStatusDispatch: func.isRequired,
|
||||
}
|
||||
|
||||
const mapStateToProps = ({ coolDataScience }) => ({
|
||||
data: coolDataScience.data.status,
|
||||
loading: coolDataScience.pending.status,
|
||||
|
||||
@@ -309,7 +309,7 @@ const Status = ({ getStatusGraduatedDispatch, data, loading }) => {
|
||||
const medianDiff = orderedAbsDiffs[Math.round((orderedAbsDiffs.length - 1) / 2)]
|
||||
return (
|
||||
<>
|
||||
<h2>Uusista ohjelmista valmistuneet</h2>
|
||||
<h2>Koulutusohjelmista valmistuneet</h2>
|
||||
<DrilldownMessage />
|
||||
<div style={{ display: 'flex', marginBottom: '20px', marginRight: '40px' }}>
|
||||
{drillStack.length > 0 && (
|
||||
@@ -351,7 +351,10 @@ const Status = ({ getStatusGraduatedDispatch, data, loading }) => {
|
||||
</div>
|
||||
{renderSettings()}
|
||||
<Message>
|
||||
<ReactMarkdown source={CoolDataScience.statusGraduated} escapeHtml={false} />
|
||||
{
|
||||
// eslint-disable-next-line react/no-children-prop
|
||||
<ReactMarkdown children={CoolDataScience.statusGraduated} escapeHtml={false} />
|
||||
}
|
||||
</Message>
|
||||
</>
|
||||
)
|
||||
|
||||
@@ -10,6 +10,42 @@
|
||||
box-shadow: 0 3px 6px rgba(0, 0, 0, 0.16), 0 3px 6px rgba(0, 0, 0, 0.23);
|
||||
}
|
||||
|
||||
.years {
|
||||
margin-top: 10px;
|
||||
text-align: start;
|
||||
font-variant-numeric: tabular-nums;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, fit-content(0));
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
color: #888;
|
||||
}
|
||||
|
||||
.unit {
|
||||
margin-left: 0.3em;
|
||||
}
|
||||
|
||||
.years.item-type-course {
|
||||
grid-template-columns: repeat(2, fit-content(0)) auto;
|
||||
}
|
||||
|
||||
.year-label {
|
||||
white-space: nowrap;
|
||||
padding-right: 0.5em;
|
||||
}
|
||||
|
||||
.year-value {
|
||||
text-align: right;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.year-row {
|
||||
position: relative;
|
||||
padding-left: 0.5em;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.status-date-time-input {
|
||||
padding-right: 0;
|
||||
margin-left: 5px;
|
||||
|
||||
@@ -21,6 +21,7 @@ import infotooltips from '../../common/InfoToolTips'
|
||||
import InfoBox from '../InfoBox'
|
||||
import FilterTray from '../FilterTray'
|
||||
import useFilters from '../FilterTray/useFilters'
|
||||
import useProgrammeFilter from '../FilterTray/filters/Programmes/useProgrammeFilter'
|
||||
import { CoursePopulationFilters } from '../FilterTray/FilterSets'
|
||||
import useLanguage from '../LanguagePicker/useLanguage'
|
||||
import { queryParamsFromUrl } from '../../common/query'
|
||||
@@ -39,6 +40,7 @@ const CoursePopulation = ({
|
||||
}) => {
|
||||
const { language } = useLanguage()
|
||||
const { setAllStudents, filteredStudents } = useFilters()
|
||||
const { setCourseCodes } = useProgrammeFilter()
|
||||
const selectedStudents = filteredStudents.map(stu => stu.studentNumber)
|
||||
|
||||
const [codes, setCodes] = useState([])
|
||||
@@ -106,6 +108,7 @@ const CoursePopulation = ({
|
||||
separate,
|
||||
})
|
||||
setCodes(parsedCourseCodes)
|
||||
setCourseCodes(parsedCourseCodes)
|
||||
if (years) {
|
||||
setYears(years)
|
||||
} else {
|
||||
@@ -121,7 +124,6 @@ const CoursePopulation = ({
|
||||
}, [semesters])
|
||||
|
||||
const header = courseData ? `${getTextIn(courseData.name, language)} ${headerYears}` : null
|
||||
const coursecode = courseData ? courseData.alternatives : []
|
||||
|
||||
const subHeader = codes.join(', ')
|
||||
|
||||
@@ -260,7 +262,7 @@ const CoursePopulation = ({
|
||||
filteredStudents={studentData.students.filter(stu => selectedStudents.includes(stu.studentNumber))}
|
||||
coursePopulation
|
||||
language={language}
|
||||
coursecode={coursecode}
|
||||
coursecode={codes}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
|
||||
+1
-1
@@ -13,7 +13,7 @@ const countTotalStats = (formattedStats, userHasAccessToAllStats) => {
|
||||
if (!cgrades[grade]) cgrades[grade] = 0
|
||||
cgrades[grade] += curr.attempts.grades[grade]
|
||||
|
||||
if (['Eisa', 'Hyl.', '0', 'Luop'].includes(grade)) {
|
||||
if (['EISA', 'Hyl.', 'HYL', '0', 'LUOP'].includes(grade)) {
|
||||
failed += curr.attempts.grades[grade]
|
||||
} else {
|
||||
passed += curr.attempts.grades[grade]
|
||||
|
||||
@@ -27,36 +27,29 @@ class CreditAccumulationGraphHighCharts extends Component {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.state = {
|
||||
studentCreditLines: [],
|
||||
options: [],
|
||||
updateGraph: false,
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const { students } = this.props
|
||||
this.getMoreCreditLines(students)
|
||||
const { students, selectedStudents, currentGraphSize, render } = this.props
|
||||
|
||||
this.createGraphOptions(students, selectedStudents, currentGraphSize, render)
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (nextProps.students) {
|
||||
const nextStudents = nextProps.students.map(student => student.studentNumber)
|
||||
const oldStudents = this.props.students.map(student => student.studentNumber)
|
||||
const nextMinDate = nextProps.students.minDate
|
||||
const oldMinDate = this.props.students.minDate
|
||||
const clear = nextMinDate !== oldMinDate
|
||||
const changedStudentAmount = nextProps.selectedStudents.length !== this.props.selectedStudents.length
|
||||
const changedGraphSize = nextProps.currentGraphSize !== this.props.currentGraphSize
|
||||
|
||||
const changed =
|
||||
nextStudents.some(student => !oldStudents.includes(student)) ||
|
||||
oldStudents.some(student => !nextStudents.includes(student)) ||
|
||||
clear
|
||||
oldStudents.some(student => !nextStudents.includes(student))
|
||||
|
||||
if (changed) {
|
||||
const { students } = nextProps
|
||||
this.getMoreCreditLines(students, clear)
|
||||
} else if (changedStudentAmount || changedGraphSize) {
|
||||
if (changed || changedStudentAmount || changedGraphSize) {
|
||||
this.createGraphOptions(
|
||||
nextProps.students,
|
||||
nextProps.selectedStudents,
|
||||
@@ -88,19 +81,6 @@ class CreditAccumulationGraphHighCharts extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
getMoreCreditLines = (students, clear) => {
|
||||
if (clear) {
|
||||
// eslint-disable-next-line react/no-access-state-in-setstate
|
||||
const removed = this.state.studentCreditLines.splice(0, 0)
|
||||
const studentCreditLines = removed.concat(this.createStudentCreditLines(students))
|
||||
this.setState({ studentCreditLines, updateGraph: true })
|
||||
} else {
|
||||
// eslint-disable-next-line react/no-access-state-in-setstate
|
||||
const studentCreditLines = this.state.studentCreditLines.concat(this.createStudentCreditLines(students))
|
||||
this.setState({ studentCreditLines, updateGraph: true })
|
||||
}
|
||||
}
|
||||
|
||||
getXAxisMonth = (date, startDate) =>
|
||||
Math.max(moment(date, API_DATE_FORMAT).diff(moment(startDate, API_DATE_FORMAT), 'days') / 30, 0)
|
||||
|
||||
@@ -134,7 +114,10 @@ class CreditAccumulationGraphHighCharts extends Component {
|
||||
}
|
||||
|
||||
createGraphOptions = (students, selectedStudents, graphSize, render) => {
|
||||
const dataOfSelected = this.state.studentCreditLines.filter(line => selectedStudents.includes(line.name))
|
||||
const studentCreditLines = this.createStudentCreditLines(students)
|
||||
|
||||
const dataOfSelected = studentCreditLines.filter(line => selectedStudents.includes(line.name))
|
||||
|
||||
let lastCredits = null
|
||||
if (this.isSingleStudentGraph() && render) {
|
||||
const started = moment(students.minDate)
|
||||
|
||||
+27
-4
@@ -5,6 +5,8 @@ import { arrayOf, string, shape } from 'prop-types'
|
||||
import SearchResultTable from '../../SearchResultTable'
|
||||
import { getNewestProgramme, getTextIn } from '../../../common'
|
||||
import useLanguage from '../../LanguagePicker/useLanguage'
|
||||
import useProgrammeFilter from '../../FilterTray/filters/Programmes/useProgrammeFilter'
|
||||
import FilterToggleIcon from '../../FilterToggleIcon'
|
||||
|
||||
const CustomPopulationProgrammeDist = ({
|
||||
samples,
|
||||
@@ -13,7 +15,9 @@ const CustomPopulationProgrammeDist = ({
|
||||
populationStatistics,
|
||||
}) => {
|
||||
const { language } = useLanguage()
|
||||
const { selectedProgrammes, toggleFilterProgramme } = useProgrammeFilter()
|
||||
const [tableRows, setRows] = useState([])
|
||||
|
||||
useEffect(() => {
|
||||
if (Object.keys(populationStatistics).length > 0) {
|
||||
const allProgrammes = {}
|
||||
@@ -41,7 +45,8 @@ const CustomPopulationProgrammeDist = ({
|
||||
}
|
||||
})
|
||||
const rows = Object.entries(allProgrammes).map(([code, { programme, students }]) => [
|
||||
`${getTextIn(programme.name, language)}, ${code}`,
|
||||
getTextIn(programme.name, language),
|
||||
code,
|
||||
students.length,
|
||||
<Progress
|
||||
style={{ margin: '0px' }}
|
||||
@@ -49,14 +54,32 @@ const CustomPopulationProgrammeDist = ({
|
||||
progress
|
||||
/>,
|
||||
])
|
||||
const sortedRows = rows.sort((a, b) => b[1] - a[1])
|
||||
const sortedRows = rows.sort((a, b) => b[2] - a[2])
|
||||
setRows(sortedRows)
|
||||
}
|
||||
}, [selectedStudents])
|
||||
|
||||
const headers = ['Programmes', `Students (all=${selectedStudents.length})`, 'Percentage of population']
|
||||
const headers = ['Programme', 'Code', `Students (all=${selectedStudents.length})`, 'Percentage of population']
|
||||
|
||||
return <SearchResultTable headers={headers} rows={tableRows} selectable noResultText="placeholder" />
|
||||
const handleFilterToggle = code => {
|
||||
toggleFilterProgramme(code)
|
||||
}
|
||||
|
||||
const isProgrammeFilterActive = code => selectedProgrammes.find(p => p.code === code) !== undefined
|
||||
|
||||
return (
|
||||
<SearchResultTable
|
||||
headers={headers}
|
||||
rows={tableRows}
|
||||
selectable
|
||||
noResultText="placeholder"
|
||||
actionTrigger={row => (
|
||||
<span style={{ display: 'inline-block', marginRight: '0.3em' }}>
|
||||
<FilterToggleIcon onClick={() => handleFilterToggle(row[1])} isActive={isProgrammeFilterActive(row[1])} />
|
||||
</span>
|
||||
)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
CustomPopulationProgrammeDist.defaultProps = {
|
||||
|
||||
@@ -1,148 +0,0 @@
|
||||
import React, { Component } from 'react'
|
||||
import { withRouter } from 'react-router-dom'
|
||||
import { Radio, Icon, Header, Segment, Loader, Popup } from 'semantic-ui-react'
|
||||
import { connect } from 'react-redux'
|
||||
import { func, shape, string, bool, arrayOf, number } from 'prop-types'
|
||||
import { getUsers } from '../../redux/users'
|
||||
import { getUnits } from '../../redux/units'
|
||||
import { getElementDetails } from '../../redux/elementdetails'
|
||||
import { makeSortUsers } from '../../selectors/users'
|
||||
import { copyToClipboard } from '../../common'
|
||||
import UserPageNew from '../UserPage'
|
||||
import UserSearchList from './UserSearchList'
|
||||
|
||||
class EnableUsers extends Component {
|
||||
state = {
|
||||
enabledOnly: true,
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
if (this.props.elementdetails.length === 0) this.props.getElementDetails()
|
||||
if (this.props.units.length === 0) this.props.getUnits()
|
||||
if (this.props.users.length === 0) this.props.getUsers()
|
||||
document.title = 'Users - Oodikone'
|
||||
}
|
||||
|
||||
componentWillUnmount = () => {
|
||||
clearTimeout(this.popupTimeout)
|
||||
}
|
||||
|
||||
toggleEnabledOnly() {
|
||||
if (this.props.enabledOnly) this.props.getUsers()
|
||||
const { enabledOnly } = this.state
|
||||
this.setState({ enabledOnly: !enabledOnly })
|
||||
}
|
||||
|
||||
openUsersPage = () => {
|
||||
const { history } = this.props
|
||||
history.push('/users')
|
||||
}
|
||||
|
||||
copyEmailsToClippoard = () => {
|
||||
const clipboardString = this.props.users
|
||||
.filter(u => u.is_enabled && u.email)
|
||||
.map(u => u.email)
|
||||
.join('; ')
|
||||
copyToClipboard(clipboardString)
|
||||
}
|
||||
|
||||
renderUserPage = userid => {
|
||||
const { users } = this.props
|
||||
const user = users.find(u => u.id === userid)
|
||||
return !user ? <Loader active /> : <UserPageNew userid={userid} user={user} goBack={this.openUsersPage} />
|
||||
}
|
||||
|
||||
handlePopupOpen = () => {
|
||||
this.setState({ popupOpen: true })
|
||||
this.popupTimeout = setTimeout(() => {
|
||||
this.setState({ popupOpen: false })
|
||||
}, 1500)
|
||||
}
|
||||
|
||||
handlePopupClose = () => {
|
||||
this.popupTimeout = null
|
||||
}
|
||||
|
||||
render() {
|
||||
const { match, pending, users, error, elementdetails } = this.props
|
||||
const { enabledOnly } = this.state
|
||||
const { userid } = match.params
|
||||
return (
|
||||
<div style={{ marginBottom: '10px' }} className="segmentContainer">
|
||||
<Header className="segmentTitle" size="large">
|
||||
Oodikone users
|
||||
</Header>
|
||||
<Radio
|
||||
label={`Showing ${enabledOnly ? 'only enabled' : 'all'} users`}
|
||||
toggle
|
||||
onClick={() => this.toggleEnabledOnly()}
|
||||
/>
|
||||
<Segment loading={pending} className="contentSegment">
|
||||
{!userid ? (
|
||||
<UserSearchList enabledOnly={enabledOnly} users={users} error={error} elementdetails={elementdetails} />
|
||||
) : (
|
||||
this.renderUserPage(userid)
|
||||
)}
|
||||
</Segment>
|
||||
<Popup
|
||||
trigger={<Icon link name="envelope" onClick={this.copyEmailsToClippoard} style={{ float: 'right' }} />}
|
||||
content="Copied email(s)!"
|
||||
on="click"
|
||||
onOpen={this.handlePopupOpen}
|
||||
onClose={this.handlePopupClose}
|
||||
open={this.state.popupOpen}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
EnableUsers.propTypes = {
|
||||
match: shape({
|
||||
params: shape({
|
||||
studentNumber: string,
|
||||
}),
|
||||
}).isRequired,
|
||||
getUsers: func.isRequired,
|
||||
pending: bool.isRequired,
|
||||
getUnits: func.isRequired,
|
||||
enabledOnly: bool.isRequired,
|
||||
users: arrayOf(
|
||||
shape({
|
||||
id: string,
|
||||
full_name: string,
|
||||
is_enabled: bool,
|
||||
username: string,
|
||||
units: arrayOf(
|
||||
shape({
|
||||
id: string,
|
||||
name: shape({}).isRequired,
|
||||
})
|
||||
),
|
||||
})
|
||||
).isRequired,
|
||||
error: bool.isRequired,
|
||||
units: arrayOf(shape({})).isRequired,
|
||||
elementdetails: arrayOf(shape({ code: string, type: number, name: shape({}) })).isRequired,
|
||||
getElementDetails: func.isRequired,
|
||||
history: shape({}).isRequired,
|
||||
}
|
||||
|
||||
const sortUsers = makeSortUsers()
|
||||
|
||||
const mapStateToProps = ({ users, units, elementdetails }) => ({
|
||||
units: units.data,
|
||||
elementdetails: elementdetails.data,
|
||||
enabledOnly: users.enabledOnly,
|
||||
users: sortUsers(users),
|
||||
pending: typeof users.pending === 'boolean' ? users.pending : true,
|
||||
error: users.error || false,
|
||||
})
|
||||
|
||||
export default withRouter(
|
||||
connect(mapStateToProps, {
|
||||
getUsers,
|
||||
getUnits,
|
||||
getElementDetails,
|
||||
})(EnableUsers)
|
||||
)
|
||||
@@ -1,5 +1,5 @@
|
||||
.flexIcon {
|
||||
display: flex;
|
||||
display: inline-flex;
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
|
||||
@@ -4,25 +4,24 @@ import { CourseFilterProvider } from './filters/Courses/useCourseFilter'
|
||||
import { CreditFilterProvider } from './filters/CreditsEarned/useCreditFilter'
|
||||
import { AgeFilterProvider } from './filters/Age/useAgeFilter'
|
||||
import { FilterTrayProvider } from './useFilterTray'
|
||||
import { ProgrammeFilterProvider } from './filters/Programmes/useProgrammeFilter'
|
||||
import { FilterProvider } from './useFilters'
|
||||
import { FilterAnalyticsProvider } from './useAnalytics'
|
||||
import { GradeFilterProvider } from './filters/Grade/useGradeFilter'
|
||||
|
||||
const FilterContextProvider = ({ children }) => (
|
||||
<FilterAnalyticsProvider>
|
||||
<FilterTrayProvider>
|
||||
<FilterProvider>
|
||||
<CourseFilterProvider>
|
||||
<CreditFilterProvider>
|
||||
<AgeFilterProvider>
|
||||
<GradeFilterProvider>{children}</GradeFilterProvider>
|
||||
</AgeFilterProvider>
|
||||
</CreditFilterProvider>
|
||||
</CourseFilterProvider>
|
||||
</FilterProvider>
|
||||
</FilterTrayProvider>
|
||||
</FilterAnalyticsProvider>
|
||||
)
|
||||
const filterProviders = [
|
||||
FilterAnalyticsProvider,
|
||||
FilterTrayProvider,
|
||||
FilterProvider,
|
||||
CourseFilterProvider,
|
||||
CreditFilterProvider,
|
||||
AgeFilterProvider,
|
||||
GradeFilterProvider,
|
||||
ProgrammeFilterProvider,
|
||||
]
|
||||
|
||||
const FilterContextProvider = ({ children }) =>
|
||||
filterProviders.reduceRight((children, Provider) => <Provider>{children}</Provider>, children)
|
||||
|
||||
FilterContextProvider.propTypes = {
|
||||
children: PropTypes.node.isRequired,
|
||||
|
||||
@@ -6,6 +6,7 @@ import CreditsEarned from './filters/CreditsEarned'
|
||||
import Gender from './filters/Gender'
|
||||
import StartYearAtUni from './filters/StartYearAtUni'
|
||||
import Courses from './filters/Courses'
|
||||
import Programmes from './filters/Programmes'
|
||||
import useAnalytics from './useAnalytics'
|
||||
import Grade from './filters/Grade'
|
||||
import Date from './filters/Date'
|
||||
@@ -13,9 +14,8 @@ import Age from './filters/Age'
|
||||
import AdmissionType from './filters/AdmissionType'
|
||||
import Tags from './filters/Tags'
|
||||
|
||||
export const PopulationStatisticsFilters = () => {
|
||||
export const PopulationStatisticsFilters = ({ query }) => {
|
||||
const analytics = useAnalytics()
|
||||
|
||||
useEffect(() => {
|
||||
analytics.setTarget('Population Statistics')
|
||||
})
|
||||
@@ -29,7 +29,7 @@ export const PopulationStatisticsFilters = () => {
|
||||
<Age />
|
||||
<Gender />
|
||||
<StartYearAtUni />
|
||||
<AdmissionType />
|
||||
{parseInt(query?.year, 10) >= 2020 ? <AdmissionType /> : null}
|
||||
<Courses />
|
||||
<Tags />
|
||||
</>
|
||||
@@ -49,6 +49,8 @@ export const CoursePopulationFilters = () => {
|
||||
<Age />
|
||||
<Gender />
|
||||
<StartYearAtUni />
|
||||
<Courses />
|
||||
<Programmes />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
import React, { useEffect, useMemo } from 'react'
|
||||
import { Dropdown } from 'semantic-ui-react'
|
||||
import FilterCard from '../common/FilterCard'
|
||||
import { getTextIn } from '../../../../common'
|
||||
import useLanguage from '../../../LanguagePicker/useLanguage'
|
||||
import useProgrammeFilter from './useProgrammeFilter'
|
||||
import useFilters from '../../useFilters'
|
||||
|
||||
const Programme = () => {
|
||||
const { programmes, selectedProgrammes, studentToProgrammeMap, selectFilterProgramme, removeFilterProgramme } =
|
||||
useProgrammeFilter()
|
||||
const { removeFilter, addFilter } = useFilters()
|
||||
const { language } = useLanguage()
|
||||
const name = 'programmeFilter'
|
||||
|
||||
const options = useMemo(
|
||||
() =>
|
||||
programmes
|
||||
.map(program => ({
|
||||
key: `programme-filter-value-${program.code}`,
|
||||
text: getTextIn(program.name, language),
|
||||
value: program.code,
|
||||
}))
|
||||
.sort((a, b) => a.text.localeCompare(b.text)),
|
||||
[programmes, language]
|
||||
)
|
||||
|
||||
const handleChange = (_, { value }) => {
|
||||
value.filter(code => selectedProgrammes.findIndex(p => p.code === code) === -1).forEach(selectFilterProgramme)
|
||||
|
||||
selectedProgrammes.filter(({ code }) => value.indexOf(code) === -1).forEach(removeFilterProgramme)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedProgrammes.length === 0) {
|
||||
removeFilter(name)
|
||||
} else {
|
||||
addFilter(name, ({ studentNumber }) => {
|
||||
return selectedProgrammes.findIndex(p => p.code === studentToProgrammeMap[studentNumber]) !== -1
|
||||
})
|
||||
}
|
||||
}, [selectedProgrammes, studentToProgrammeMap])
|
||||
|
||||
return (
|
||||
<FilterCard title="Programme" active={selectedProgrammes.length > 0} className="programmes-filter" name={name}>
|
||||
<Dropdown
|
||||
options={options}
|
||||
placeholder="Select Programme"
|
||||
onChange={handleChange}
|
||||
button
|
||||
value={selectedProgrammes.map(p => p.code)}
|
||||
name={name}
|
||||
closeOnChange
|
||||
multiple
|
||||
/>
|
||||
</FilterCard>
|
||||
)
|
||||
}
|
||||
|
||||
export default Programme
|
||||
+119
@@ -0,0 +1,119 @@
|
||||
import React, { useState, createContext, useContext, useMemo } from 'react'
|
||||
import { useSelector } from 'react-redux'
|
||||
import { getStudentToTargetCourseDateMap, getNewestProgramme } from '../../../../common'
|
||||
import useFilters from '../../useFilters'
|
||||
|
||||
const ProgrammeFilterContext = createContext([[], () => {}])
|
||||
ProgrammeFilterContext.displayName = 'Programme Filter'
|
||||
|
||||
const defaultState = {
|
||||
selectedProgrammes: [],
|
||||
}
|
||||
|
||||
export const ProgrammeFilterProvider = ({ children }) => {
|
||||
const [state, setState] = useState(defaultState)
|
||||
const [courseCodes, setCourseCodes] = useState([])
|
||||
|
||||
const { selectedProgrammes } = state
|
||||
|
||||
const setSelectedProgrammes = programmes => {
|
||||
setState({
|
||||
selectedProgrammes: programmes,
|
||||
})
|
||||
}
|
||||
|
||||
const context = {
|
||||
selectedProgrammes,
|
||||
setSelectedProgrammes,
|
||||
courseCodes,
|
||||
setCourseCodes,
|
||||
}
|
||||
|
||||
return <ProgrammeFilterContext.Provider value={context}>{children}</ProgrammeFilterContext.Provider>
|
||||
}
|
||||
|
||||
export default () => {
|
||||
const { selectedProgrammes, courseCodes, setCourseCodes, setSelectedProgrammes } = useContext(ProgrammeFilterContext)
|
||||
|
||||
const { allStudents } = useFilters()
|
||||
|
||||
const studentToTargetCourseDateMap = useMemo(
|
||||
() => getStudentToTargetCourseDateMap(allStudents, courseCodes),
|
||||
[allStudents]
|
||||
)
|
||||
|
||||
const elementDetails = useSelector(state => state?.populations?.data?.elementdetails?.data)
|
||||
|
||||
const { programmes, studentToProgrammeMap } = useMemo(() => {
|
||||
if (!elementDetails) {
|
||||
return {
|
||||
programmes: [],
|
||||
studentToProgrammeMap: {},
|
||||
}
|
||||
}
|
||||
|
||||
const programmeMap = {}
|
||||
const studentToProgrammeMap = {}
|
||||
|
||||
allStudents
|
||||
.map(student => [
|
||||
student,
|
||||
getNewestProgramme(student.studyrights, student.studentNumber, studentToTargetCourseDateMap, elementDetails),
|
||||
])
|
||||
.forEach(([{ studentNumber }, programme]) => {
|
||||
programmeMap[programme.code] = programme
|
||||
studentToProgrammeMap[studentNumber] = programme.code
|
||||
})
|
||||
|
||||
return {
|
||||
programmes: Object.values(programmeMap),
|
||||
studentToProgrammeMap,
|
||||
}
|
||||
}, [allStudents, elementDetails])
|
||||
|
||||
const selectFilterProgramme = code => {
|
||||
const index = selectedProgrammes.findIndex(p => p.code === code)
|
||||
|
||||
if (index !== -1) {
|
||||
return
|
||||
}
|
||||
|
||||
const programme = programmes.find(p => p.code === code)
|
||||
|
||||
if (programme === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
setSelectedProgrammes([...selectedProgrammes, programme])
|
||||
}
|
||||
|
||||
const removeFilterProgramme = code => {
|
||||
const index = selectedProgrammes.findIndex(p => p.code === code)
|
||||
const newSelection = [...selectedProgrammes]
|
||||
newSelection.splice(index, 1)
|
||||
setSelectedProgrammes(newSelection)
|
||||
}
|
||||
|
||||
const toggleFilterProgramme = code => {
|
||||
const index = selectedProgrammes.findIndex(p => p.code === code)
|
||||
|
||||
if (index === -1) {
|
||||
const programme = programmes.find(p => p.code === code)
|
||||
setSelectedProgrammes([...selectedProgrammes, programme])
|
||||
} else {
|
||||
const newSelection = [...selectedProgrammes]
|
||||
newSelection.splice(index, 1)
|
||||
setSelectedProgrammes(newSelection)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
programmes,
|
||||
selectedProgrammes,
|
||||
toggleFilterProgramme,
|
||||
selectFilterProgramme,
|
||||
removeFilterProgramme,
|
||||
setCourseCodes,
|
||||
studentToProgrammeMap,
|
||||
}
|
||||
}
|
||||
@@ -18,7 +18,7 @@ const FilterCard = ({ title, children, contextKey, name, active }) => {
|
||||
{title}
|
||||
</Header>
|
||||
</div>
|
||||
{open && children}
|
||||
{(active || open) && children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ const FilterTray = ({ children, filterSet, visible }) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<div style={{ display: 'inline-flex', alignItems: 'flex-start' }}>
|
||||
<div style={{ display: 'inline-flex', alignItems: 'flex-start', width: '100%' }}>
|
||||
<Segment style={{ marginLeft: '0.5rem', width: '12rem', position: 'sticky', top: '10px' }}>
|
||||
<Header size="small" style={{ textAlign: 'center' }}>
|
||||
Filter students
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user