mirror of
https://github.com/UniversityOfHelsinkiCS/oodikone.git
synced 2026-10-13 03:13:40 +03:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1626f76e3a | ||
|
|
d597b9e67f | ||
|
|
e1dbcd0e7e | ||
|
|
70b33a3ae4 | ||
|
|
4834515b4c | ||
|
|
fa0bf87307 | ||
|
|
4b9fe9d6a0 | ||
|
|
a674c4257d | ||
|
|
992d1c13dc | ||
|
|
7024345088 | ||
|
|
9560f84395 | ||
|
|
9f9ab3ce09 | ||
|
|
4ac29fcfe8 | ||
|
|
174a3069fe | ||
|
|
fc6b77e32b | ||
|
|
6cfb89336f | ||
|
|
635409442c | ||
|
|
2ffd2db995 | ||
|
|
431c2292cb | ||
|
|
1a4a1a1f1c | ||
|
|
fe1b5ac4c6 | ||
|
|
0e9253f698 | ||
|
|
11f9b935b4 | ||
|
|
a16896e74d | ||
|
|
814599d6ea | ||
|
|
8f2dcac0f9 | ||
|
|
240659afc9 | ||
|
|
b9059e2a52 | ||
|
|
a1688f7cdd | ||
|
|
2c378bfca2 | ||
|
|
aec495b161 | ||
|
|
486bc1a194 | ||
|
|
18bcb91d09 | ||
|
|
5c4d73f555 | ||
|
|
27e8450789 | ||
|
|
56733f2445 | ||
|
|
089b00fcd6 | ||
|
|
a709fe70ba | ||
|
|
f26e4b4859 | ||
|
|
18a382167b | ||
|
|
b4be0a3e6b | ||
|
|
596ac08ad4 | ||
|
|
9c6ad0027a | ||
|
|
27ce374f35 | ||
|
|
7e6f311b62 | ||
|
|
dd77fdff9e | ||
|
|
0b7adf4888 | ||
|
|
9deee8a384 | ||
|
|
81791381a5 | ||
|
|
9806639c1b | ||
|
|
5e9a3c4b99 | ||
|
|
12f73f4dee | ||
|
|
9d27dfebe5 | ||
|
|
7bebd4c845 | ||
|
|
43d2e84b32 | ||
|
|
ee1cb395a2 | ||
|
|
4e221d8867 | ||
|
|
3aab7b74a3 | ||
|
|
92651b8bc7 | ||
|
|
08b53c34f2 | ||
|
|
8d6a311936 | ||
|
|
79b8b92b66 | ||
|
|
fa8a07481a | ||
|
|
f79d6ef0fb | ||
|
|
bb25541202 | ||
|
|
3530723bd5 | ||
|
|
f2ffaacc6c | ||
|
|
14e95fe4df | ||
|
|
f69b12a1f9 | ||
|
|
e4e527eaeb | ||
|
|
c4c8f6123b | ||
|
|
bdb71242a7 | ||
|
|
a3a36fcef1 | ||
|
|
adbcb1bb88 | ||
|
|
37504f9e97 | ||
|
|
1722bcd7a3 | ||
|
|
a770873da5 | ||
|
|
a4c3385956 | ||
|
|
9fe491e7cc | ||
|
|
42213948b7 | ||
|
|
1014348e5b | ||
|
|
dcecd66e49 | ||
|
|
4509e51f66 | ||
|
|
90841369bb | ||
|
|
c3e41c8a60 | ||
|
|
81ffbf653e | ||
|
|
b46e30c8fa | ||
|
|
3619698744 | ||
|
|
81ee6b8146 | ||
|
|
c9b27fdc64 | ||
|
|
6b50c07ed7 | ||
|
|
d2670e180c | ||
|
|
580e1b4ed0 | ||
|
|
e84e487307 | ||
|
|
d0a54c95a6 | ||
|
|
18ac10f399 | ||
|
|
263e070b13 | ||
|
|
8cf9ceac6e | ||
|
|
32005ce23f | ||
|
|
111a397811 | ||
|
|
454032368a | ||
|
|
5509cd4b9a | ||
|
|
346c9f9803 | ||
|
|
0a738df3d0 | ||
|
|
b67da1c2c4 | ||
|
|
e700586588 | ||
|
|
9bb7c1c6f1 | ||
|
|
7f70968531 | ||
|
|
9ae35e46db | ||
|
|
3d667fe1c6 | ||
|
|
62e07ce571 | ||
|
|
38cd587369 | ||
|
|
77c9801e4c | ||
|
|
d3f1e101ce | ||
|
|
10a2d2743e | ||
|
|
4d26f42f25 | ||
|
|
e8f3d9f601 | ||
|
|
6f3af6f135 | ||
|
|
3e08836d4e | ||
|
|
efea7cdc9e | ||
|
|
c6232e617f | ||
|
|
f360716b8e | ||
|
|
55bf6b0457 | ||
|
|
3067b37298 | ||
|
|
a19a343feb | ||
|
|
edb5ca5422 | ||
|
|
2eb5f10eee | ||
|
|
7ce269d8be | ||
|
|
42554bba7a | ||
|
|
be77832f72 | ||
|
|
f32d29ca34 | ||
|
|
bb3652f9c4 | ||
|
|
a6272bff4f | ||
|
|
29f8cf110b | ||
|
|
05ccbd6b16 | ||
|
|
608f2a921e | ||
|
|
7bd40f7030 | ||
|
|
0075a5f994 | ||
|
|
06414288f8 | ||
|
|
ac9b32408c | ||
|
|
4ad1e4237c | ||
|
|
2f0ebd04d0 | ||
|
|
40264f28d1 | ||
|
|
95eaef7f8d | ||
|
|
7a1fa68c94 | ||
|
|
1d86557ed4 | ||
|
|
d013809645 | ||
|
|
32b860bf0b | ||
|
|
02ea189fcb | ||
|
|
d38b647fc3 | ||
|
|
0eb31cc8ea | ||
|
|
7aef916e70 | ||
|
|
fc84ef78fd | ||
|
|
7c31425114 | ||
|
|
c88ad2201d | ||
|
|
e936510754 | ||
|
|
825e549b49 | ||
|
|
b8b0ffe34b | ||
|
|
e2018d3e4c | ||
|
|
c3dbc9682e | ||
|
|
54195f0a85 | ||
|
|
600795ca48 | ||
|
|
3b2b04b10d | ||
|
|
57ddbcaa36 | ||
|
|
1185793e4d | ||
|
|
d8a5989ba1 | ||
|
|
7a8ba85071 | ||
|
|
378b886947 | ||
|
|
167fa3b979 | ||
|
|
7ddcf94b57 | ||
|
|
f71c406180 | ||
|
|
c1a94acb92 | ||
|
|
1264d4f3b9 | ||
|
|
3ab87a18e3 | ||
|
|
2a68a9cc9b | ||
|
|
15f86a85d4 | ||
|
|
16952cece6 | ||
|
|
4c71092e7f | ||
|
|
d2558a40d8 | ||
|
|
c918c5ffc9 | ||
|
|
54ecddc46e | ||
|
|
f2e655bb05 | ||
|
|
5ee3e7310e | ||
|
|
8bdb30bca5 | ||
|
|
bc06295f97 | ||
|
|
522e9f851e | ||
|
|
28e798ff28 | ||
|
|
ca4576e155 | ||
|
|
90309143fd | ||
|
|
8f4fcb4364 | ||
|
|
facd2c15b9 | ||
|
|
756cadd0e6 | ||
|
|
842a561f94 | ||
|
|
9346b1e245 | ||
|
|
b93b430554 | ||
|
|
a90dd779d5 | ||
|
|
dafec6bd6b | ||
|
|
b27e23e7fb | ||
|
|
6fff1f15fb | ||
|
|
85bfc6041e | ||
|
|
eb1a92e96a | ||
|
|
8c7a94b476 | ||
|
|
f683d33769 | ||
|
|
0c84cbf08f | ||
|
|
358f629285 | ||
|
|
af77bc500e | ||
|
|
7608f62869 | ||
|
|
03c3b99f3d | ||
|
|
af08a0b681 | ||
|
|
44ee47c34d | ||
|
|
f79aafa6cf | ||
|
|
0b5b4b5ebd | ||
|
|
e61a4cf09c | ||
|
|
11d58a9fc4 |
+2
-1
@@ -14,6 +14,7 @@
|
||||
"no-console": "error",
|
||||
"react/no-unescaped-entities": "off",
|
||||
"no-async-promise-executor": "off",
|
||||
"prettier/prettier": "error"
|
||||
"prettier/prettier": "error",
|
||||
"cypress/no-assigning-return-values": "off"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
name: 'Run tests'
|
||||
description: 'Runs Cypress tests for individual spec'
|
||||
|
||||
inputs:
|
||||
username:
|
||||
required: true
|
||||
description: 'Toska registry username'
|
||||
password:
|
||||
required: true
|
||||
description: 'Toska registry password'
|
||||
spec:
|
||||
required: true
|
||||
description: 'Path to Cypress file'
|
||||
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- name: 'Download images'
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
path: /tmp/
|
||||
- name: 'Load built images'
|
||||
shell: bash
|
||||
run: |
|
||||
docker load --input /tmp/artifact/backend.tar
|
||||
docker load --input /tmp/artifact/frontend.tar
|
||||
- name: 'Login to toska docker registry'
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
registry: registry.toska.cs.helsinki.fi
|
||||
username: ${{ inputs.username }}
|
||||
password: ${{ inputs.password }}
|
||||
- name: 'Run services, pull rest of the needed images'
|
||||
shell: bash
|
||||
run: docker-compose --file docker-compose.ci.yml up --detach --no-recreate
|
||||
- name: 'Run cypress'
|
||||
uses: cypress-io/github-action@v4
|
||||
with:
|
||||
browser: chrome
|
||||
spec: ${{ inputs.spec }}
|
||||
env:
|
||||
CYPRESS_baseUrl: http://localhost:1337
|
||||
- name: 'Upload video if tests fail'
|
||||
uses: actions/upload-artifact@v3
|
||||
if: failure()
|
||||
with:
|
||||
name: Cypress videos
|
||||
path: ./cypress/videos/*
|
||||
- name: 'Print logs on failure'
|
||||
shell: bash
|
||||
if: failure()
|
||||
run: docker-compose --file docker-compose.ci.yml logs
|
||||
@@ -8,6 +8,10 @@ updates:
|
||||
labels:
|
||||
- 'dependencies'
|
||||
- 'infra'
|
||||
ignore:
|
||||
- dependency-name: '*'
|
||||
update-types: ['version-update:semver-major']
|
||||
|
||||
- package-ecosystem: npm
|
||||
directory: '/updater/sis-updater-scheduler'
|
||||
schedule:
|
||||
@@ -16,6 +20,10 @@ updates:
|
||||
labels:
|
||||
- 'dependencies'
|
||||
- 'updater'
|
||||
ignore:
|
||||
- dependency-name: '*'
|
||||
update-types: ['version-update:semver-major']
|
||||
|
||||
- package-ecosystem: npm
|
||||
directory: '/updater/sis-updater-worker'
|
||||
schedule:
|
||||
@@ -24,6 +32,10 @@ updates:
|
||||
labels:
|
||||
- 'dependencies'
|
||||
- 'updater'
|
||||
ignore:
|
||||
- dependency-name: '*'
|
||||
update-types: ['version-update:semver-major']
|
||||
|
||||
- package-ecosystem: npm
|
||||
directory: '/services/backend'
|
||||
schedule:
|
||||
@@ -32,6 +44,10 @@ updates:
|
||||
labels:
|
||||
- 'dependencies'
|
||||
- 'back'
|
||||
ignore:
|
||||
- dependency-name: '*'
|
||||
update-types: ['version-update:semver-major']
|
||||
|
||||
- package-ecosystem: npm
|
||||
directory: '/services/frontend'
|
||||
schedule:
|
||||
@@ -40,8 +56,13 @@ updates:
|
||||
labels:
|
||||
- 'dependencies'
|
||||
- 'front'
|
||||
ignore:
|
||||
- dependency-name: '*'
|
||||
update-types: ['version-update:semver-major']
|
||||
|
||||
- package-ecosystem: 'github-actions'
|
||||
directory: '/'
|
||||
schedule:
|
||||
interval: weekly
|
||||
time: '03:00'
|
||||
open-pull-requests-limit: 10
|
||||
|
||||
@@ -9,10 +9,10 @@ jobs:
|
||||
name: 'Build and release backend'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: docker/setup-buildx-action@v1
|
||||
- uses: actions/checkout@v3
|
||||
- uses: docker/setup-buildx-action@v2
|
||||
- name: 'Login to DockerHub'
|
||||
uses: docker/login-action@v1
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
@@ -23,7 +23,7 @@ jobs:
|
||||
id: extract_version
|
||||
run: echo ::set-output name=VERSION::"${GITHUB_REF#refs/tags/}"
|
||||
- name: 'Build and push'
|
||||
uses: docker/build-push-action@v2
|
||||
uses: docker/build-push-action@v3
|
||||
with:
|
||||
build-args: |
|
||||
SENTRY_RELEASE=${{ steps.extract_version.outputs.VERSION }}
|
||||
@@ -41,10 +41,10 @@ jobs:
|
||||
name: 'Build and release frontend'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: docker/setup-buildx-action@v1
|
||||
- uses: actions/checkout@v3
|
||||
- uses: docker/setup-buildx-action@v2
|
||||
- name: 'Login to DockerHub'
|
||||
uses: docker/login-action@v1
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
@@ -58,7 +58,7 @@ jobs:
|
||||
id: extract_version
|
||||
run: echo ::set-output name=VERSION::"${GITHUB_REF#refs/tags/}"
|
||||
- name: 'Build and push'
|
||||
uses: docker/build-push-action@v2
|
||||
uses: docker/build-push-action@v3
|
||||
with:
|
||||
build-args: |
|
||||
SENTRY_RELEASE=${{ steps.extract_version.outputs.VERSION }}
|
||||
|
||||
@@ -14,10 +14,10 @@ jobs:
|
||||
name: 'Build sis-updater-worker'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: docker/setup-buildx-action@v1
|
||||
- uses: actions/checkout@v3
|
||||
- uses: docker/setup-buildx-action@v2
|
||||
- name: 'Build the image'
|
||||
uses: docker/build-push-action@v2
|
||||
uses: docker/build-push-action@v3
|
||||
with:
|
||||
build-args: |
|
||||
SENTRY_RELEASE=${{ github.sha }}
|
||||
@@ -30,7 +30,7 @@ jobs:
|
||||
cache-from: type=registry,ref=toska/sis-updater-worker:latest
|
||||
cache-to: type=inline
|
||||
- name: 'Store built image'
|
||||
uses: actions/upload-artifact@v2
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
path: /tmp/worker.tar
|
||||
|
||||
@@ -38,10 +38,10 @@ jobs:
|
||||
name: 'Build sis-updater-scheduler'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: docker/setup-buildx-action@v1
|
||||
- uses: actions/checkout@v3
|
||||
- uses: docker/setup-buildx-action@v2
|
||||
- name: 'Build the image'
|
||||
uses: docker/build-push-action@v2
|
||||
uses: docker/build-push-action@v3
|
||||
with:
|
||||
build-args: |
|
||||
SENTRY_RELEASE=${{ github.sha }}
|
||||
@@ -54,7 +54,7 @@ jobs:
|
||||
cache-from: type=registry,ref=toska/sis-updater-scheduler:latest
|
||||
cache-to: type=inline
|
||||
- name: 'Store built image'
|
||||
uses: actions/upload-artifact@v2
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
path: /tmp/scheduler.tar
|
||||
|
||||
@@ -65,15 +65,15 @@ jobs:
|
||||
- build_worker
|
||||
- build_scheduler
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
- name: 'Login to toska docker registry'
|
||||
uses: docker/login-action@v1
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
registry: registry.toska.cs.helsinki.fi
|
||||
username: ${{ secrets.TOSKAREGISTRY_USERNAME }}
|
||||
password: ${{ secrets.TOSKAREGISTRY_PASSWORD }}
|
||||
- name: 'Download built images'
|
||||
uses: actions/download-artifact@v2
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
path: /tmp/
|
||||
- name: 'Load worker image'
|
||||
@@ -114,15 +114,15 @@ jobs:
|
||||
needs:
|
||||
- test_updater
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: docker/setup-buildx-action@v1
|
||||
- uses: actions/checkout@v3
|
||||
- uses: docker/setup-buildx-action@v2
|
||||
- name: 'Login to DockerHub'
|
||||
uses: docker/login-action@v1
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
- name: 'Build and push scheduler'
|
||||
uses: docker/build-push-action@v2
|
||||
uses: docker/build-push-action@v3
|
||||
with:
|
||||
cache-from: type=registry,ref=toska/sis-updater-scheduler:latest
|
||||
cache-to: type=inline
|
||||
@@ -131,7 +131,7 @@ jobs:
|
||||
tags: toska/sis-updater-scheduler:latest
|
||||
target: production
|
||||
- name: 'Build and push worker'
|
||||
uses: docker/build-push-action@v2
|
||||
uses: docker/build-push-action@v3
|
||||
with:
|
||||
cache-from: type=registry,ref=toska/sis-updater-worker:latest
|
||||
cache-to: type=inline
|
||||
|
||||
@@ -6,14 +6,16 @@ on:
|
||||
- master
|
||||
paths:
|
||||
- '.github/workflows/test-oodikone.yaml'
|
||||
- '.github/actions/**'
|
||||
- 'cypress/**'
|
||||
- 'cypress.json'
|
||||
- 'cypress.config.json'
|
||||
- 'services/**'
|
||||
- '!services/backend/src/rapodiff.js'
|
||||
- 'docker-compose.ci.yml'
|
||||
- 'docker-compose.test.yml'
|
||||
- 'nginx.conf'
|
||||
- 'package*.json'
|
||||
- 'updater/sis-updater-worker/src/db/migrations/**'
|
||||
|
||||
jobs:
|
||||
# === Static tests / linting ===
|
||||
@@ -22,8 +24,8 @@ jobs:
|
||||
name: 'Lint backend'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v2
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3.4.1
|
||||
with:
|
||||
node-version: 14
|
||||
- name: 'Install dependencies'
|
||||
@@ -35,8 +37,8 @@ jobs:
|
||||
name: 'Lint frontend'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v2
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3.4.1
|
||||
with:
|
||||
node-version: 14
|
||||
- name: 'Install dependencies'
|
||||
@@ -51,10 +53,10 @@ jobs:
|
||||
name: 'Build backend'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: docker/setup-buildx-action@v1
|
||||
- uses: actions/checkout@v3
|
||||
- uses: docker/setup-buildx-action@v2
|
||||
- name: 'Build image and export it'
|
||||
uses: docker/build-push-action@v2
|
||||
uses: docker/build-push-action@v3
|
||||
with:
|
||||
build-args: |
|
||||
SENTRY_RELEASE=${{ github.sha }}
|
||||
@@ -69,7 +71,7 @@ jobs:
|
||||
target: production
|
||||
outputs: type=docker,dest=/tmp/backend.tar
|
||||
- name: 'Upload image for other jobs'
|
||||
uses: actions/upload-artifact@v2
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
path: /tmp/backend.tar
|
||||
|
||||
@@ -77,13 +79,13 @@ jobs:
|
||||
name: 'Build frontend'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: docker/setup-buildx-action@v1
|
||||
- uses: actions/checkout@v3
|
||||
- uses: docker/setup-buildx-action@v2
|
||||
- name: 'Get current time'
|
||||
id: get_current_time
|
||||
run: echo ::set-output name=NOW::"$(date --iso-8601=seconds)"
|
||||
- name: 'Build image and export it'
|
||||
uses: docker/build-push-action@v2
|
||||
uses: docker/build-push-action@v3
|
||||
with:
|
||||
build-args: |
|
||||
SENTRY_RELEASE=${{ github.sha }}
|
||||
@@ -99,7 +101,7 @@ jobs:
|
||||
target: production
|
||||
outputs: type=docker,dest=/tmp/frontend.tar
|
||||
- name: 'Upload image for other jobs'
|
||||
uses: actions/upload-artifact@v2
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
path: /tmp/frontend.tar
|
||||
|
||||
@@ -107,7 +109,7 @@ jobs:
|
||||
name: 'Build anon sis-db and push to toska hub if new migrations have been added'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
- uses: dorny/paths-filter@v2
|
||||
id: check_migrations
|
||||
with:
|
||||
@@ -116,7 +118,7 @@ jobs:
|
||||
- 'updater/sis-updater-worker/src/db/migrations/**'
|
||||
- '.github/workflows/test-oodikone.yaml'
|
||||
- name: 'Login to toska docker registry'
|
||||
uses: docker/login-action@v1
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
registry: registry.toska.cs.helsinki.fi
|
||||
username: ${{ secrets.TOSKAREGISTRY_USERNAME }}
|
||||
@@ -134,7 +136,7 @@ jobs:
|
||||
- build_backend
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
- uses: dorny/paths-filter@v2
|
||||
id: check_migrations
|
||||
with:
|
||||
@@ -143,13 +145,13 @@ jobs:
|
||||
- 'services/backend/src/database/migrations_kone/**'
|
||||
- '.github/workflows/test-oodikone.yaml'
|
||||
- name: 'Download images'
|
||||
uses: actions/download-artifact@v2
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
path: /tmp/
|
||||
- name: 'Load built backend image'
|
||||
run: docker load --input /tmp/artifact/backend.tar
|
||||
- name: 'Login to toska docker registry'
|
||||
uses: docker/login-action@v1
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
registry: registry.toska.cs.helsinki.fi
|
||||
username: ${{ secrets.TOSKAREGISTRY_USERNAME }}
|
||||
@@ -167,7 +169,7 @@ jobs:
|
||||
- build_backend
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
- uses: dorny/paths-filter@v2
|
||||
id: check_migrations
|
||||
with:
|
||||
@@ -176,13 +178,13 @@ jobs:
|
||||
- 'services/backend/src/database/migrations_user/**'
|
||||
- '.github/workflows/test-oodikone.yaml'
|
||||
- name: 'Download images'
|
||||
uses: actions/download-artifact@v2
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
path: /tmp/
|
||||
- name: 'Load built backend image'
|
||||
run: docker load --input /tmp/artifact/backend.tar
|
||||
- name: 'Login to toska docker registry'
|
||||
uses: docker/login-action@v1
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
registry: registry.toska.cs.helsinki.fi
|
||||
username: ${{ secrets.TOSKAREGISTRY_USERNAME }}
|
||||
@@ -205,39 +207,13 @@ jobs:
|
||||
- build_and_push_anon_user_db
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: 'Download images'
|
||||
uses: actions/download-artifact@v2
|
||||
- uses: actions/checkout@v3
|
||||
- name: 'Run tests'
|
||||
uses: ./.github/actions/run-tests
|
||||
with:
|
||||
path: /tmp/
|
||||
- name: 'Load built images'
|
||||
run: |
|
||||
docker load --input /tmp/artifact/backend.tar
|
||||
docker load --input /tmp/artifact/frontend.tar
|
||||
- name: 'Login to toska docker registry'
|
||||
uses: docker/login-action@v1
|
||||
with:
|
||||
registry: registry.toska.cs.helsinki.fi
|
||||
username: ${{ secrets.TOSKAREGISTRY_USERNAME }}
|
||||
password: ${{ secrets.TOSKAREGISTRY_PASSWORD }}
|
||||
- name: 'Run services, pull rest of the needed images'
|
||||
run: docker-compose --file docker-compose.ci.yml up --detach --no-recreate
|
||||
- name: 'Run cypress'
|
||||
uses: cypress-io/github-action@v2
|
||||
with:
|
||||
browser: chrome
|
||||
spec: cypress/integration/Course_statistics.js
|
||||
env:
|
||||
CYPRESS_baseUrl: http://localhost:1337
|
||||
- name: 'Upload video if tests fail'
|
||||
uses: actions/upload-artifact@v2
|
||||
if: failure()
|
||||
with:
|
||||
name: Cypress videos
|
||||
path: ./cypress/videos/*
|
||||
- name: 'Print logs on failure'
|
||||
if: failure()
|
||||
run: docker-compose --file docker-compose.ci.yml logs
|
||||
spec: cypress/e2e/Course_statistics.js
|
||||
|
||||
test_custom_population:
|
||||
name: 'Test custom population'
|
||||
@@ -249,39 +225,13 @@ jobs:
|
||||
- build_and_push_anon_user_db
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: 'Download images'
|
||||
uses: actions/download-artifact@v2
|
||||
- uses: actions/checkout@v3
|
||||
- name: 'Run tests'
|
||||
uses: ./.github/actions/run-tests
|
||||
with:
|
||||
path: /tmp/
|
||||
- name: 'Load built images'
|
||||
run: |
|
||||
docker load --input /tmp/artifact/backend.tar
|
||||
docker load --input /tmp/artifact/frontend.tar
|
||||
- name: 'Login to toska docker registry'
|
||||
uses: docker/login-action@v1
|
||||
with:
|
||||
registry: registry.toska.cs.helsinki.fi
|
||||
username: ${{ secrets.TOSKAREGISTRY_USERNAME }}
|
||||
password: ${{ secrets.TOSKAREGISTRY_PASSWORD }}
|
||||
- name: 'Run services, pull rest of the needed images'
|
||||
run: docker-compose --file docker-compose.ci.yml up --detach --no-recreate
|
||||
- name: 'Run cypress'
|
||||
uses: cypress-io/github-action@v2
|
||||
with:
|
||||
browser: chrome
|
||||
spec: cypress/integration/Custom_population.js
|
||||
env:
|
||||
CYPRESS_baseUrl: http://localhost:1337
|
||||
- name: 'Upload video if tests fail'
|
||||
uses: actions/upload-artifact@v2
|
||||
if: failure()
|
||||
with:
|
||||
name: Cypress videos
|
||||
path: ./cypress/videos/*
|
||||
- name: 'Print logs on failure'
|
||||
if: failure()
|
||||
run: docker-compose --file docker-compose.ci.yml logs
|
||||
spec: cypress/e2e/Custom_population.js
|
||||
|
||||
test_population_statistics:
|
||||
name: 'Test population statistics'
|
||||
@@ -293,39 +243,13 @@ jobs:
|
||||
- build_and_push_anon_user_db
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: 'Download images'
|
||||
uses: actions/download-artifact@v2
|
||||
- uses: actions/checkout@v3
|
||||
- name: 'Run tests'
|
||||
uses: ./.github/actions/run-tests
|
||||
with:
|
||||
path: /tmp/
|
||||
- name: 'Load built images'
|
||||
run: |
|
||||
docker load --input /tmp/artifact/backend.tar
|
||||
docker load --input /tmp/artifact/frontend.tar
|
||||
- name: 'Login to toska docker registry'
|
||||
uses: docker/login-action@v1
|
||||
with:
|
||||
registry: registry.toska.cs.helsinki.fi
|
||||
username: ${{ secrets.TOSKAREGISTRY_USERNAME }}
|
||||
password: ${{ secrets.TOSKAREGISTRY_PASSWORD }}
|
||||
- name: 'Run services, pull rest of the needed images'
|
||||
run: docker-compose --file docker-compose.ci.yml up --detach --no-recreate
|
||||
- name: 'Run cypress'
|
||||
uses: cypress-io/github-action@v2
|
||||
with:
|
||||
browser: chrome
|
||||
spec: cypress/integration/Population_statistics.js
|
||||
env:
|
||||
CYPRESS_baseUrl: http://localhost:1337
|
||||
- name: 'Upload video if tests fail'
|
||||
uses: actions/upload-artifact@v2
|
||||
if: failure()
|
||||
with:
|
||||
name: Cypress videos
|
||||
path: ./cypress/videos/*
|
||||
- name: 'Print logs on failure'
|
||||
if: failure()
|
||||
run: docker-compose --file docker-compose.ci.yml logs
|
||||
spec: cypress/e2e/Population_statistics.js
|
||||
|
||||
test_filters:
|
||||
name: 'Test filters'
|
||||
@@ -337,39 +261,13 @@ jobs:
|
||||
- build_and_push_anon_user_db
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: 'Download images'
|
||||
uses: actions/download-artifact@v2
|
||||
- uses: actions/checkout@v3
|
||||
- name: 'Run tests'
|
||||
uses: ./.github/actions/run-tests
|
||||
with:
|
||||
path: /tmp/
|
||||
- name: 'Load built images'
|
||||
run: |
|
||||
docker load --input /tmp/artifact/backend.tar
|
||||
docker load --input /tmp/artifact/frontend.tar
|
||||
- name: 'Login to toska docker registry'
|
||||
uses: docker/login-action@v1
|
||||
with:
|
||||
registry: registry.toska.cs.helsinki.fi
|
||||
username: ${{ secrets.TOSKAREGISTRY_USERNAME }}
|
||||
password: ${{ secrets.TOSKAREGISTRY_PASSWORD }}
|
||||
- name: 'Run services, pull rest of the needed images'
|
||||
run: docker-compose --file docker-compose.ci.yml up --detach --no-recreate
|
||||
- name: 'Run cypress'
|
||||
uses: cypress-io/github-action@v2
|
||||
with:
|
||||
browser: chrome
|
||||
spec: cypress/integration/Filters.js
|
||||
env:
|
||||
CYPRESS_baseUrl: http://localhost:1337
|
||||
- name: 'Upload video if tests fail'
|
||||
uses: actions/upload-artifact@v2
|
||||
if: failure()
|
||||
with:
|
||||
name: Cypress videos
|
||||
path: ./cypress/videos/*
|
||||
- name: 'Print logs on failure'
|
||||
if: failure()
|
||||
run: docker-compose --file docker-compose.ci.yml logs
|
||||
spec: cypress/e2e/Filters.js
|
||||
|
||||
test_student_statistics:
|
||||
name: 'Test student statistics'
|
||||
@@ -381,39 +279,13 @@ jobs:
|
||||
- build_and_push_anon_user_db
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: 'Download images'
|
||||
uses: actions/download-artifact@v2
|
||||
- uses: actions/checkout@v3
|
||||
- name: 'Run tests'
|
||||
uses: ./.github/actions/run-tests
|
||||
with:
|
||||
path: /tmp/
|
||||
- name: 'Load built images'
|
||||
run: |
|
||||
docker load --input /tmp/artifact/backend.tar
|
||||
docker load --input /tmp/artifact/frontend.tar
|
||||
- name: 'Login to toska docker registry'
|
||||
uses: docker/login-action@v1
|
||||
with:
|
||||
registry: registry.toska.cs.helsinki.fi
|
||||
username: ${{ secrets.TOSKAREGISTRY_USERNAME }}
|
||||
password: ${{ secrets.TOSKAREGISTRY_PASSWORD }}
|
||||
- name: 'Run services, pull rest of the needed images'
|
||||
run: docker-compose --file docker-compose.ci.yml up --detach --no-recreate
|
||||
- name: 'Run cypress'
|
||||
uses: cypress-io/github-action@v2
|
||||
with:
|
||||
browser: chrome
|
||||
spec: cypress/integration/Student_statistics.js
|
||||
env:
|
||||
CYPRESS_baseUrl: http://localhost:1337
|
||||
- name: 'Upload video if tests fail'
|
||||
uses: actions/upload-artifact@v2
|
||||
if: failure()
|
||||
with:
|
||||
name: Cypress videos
|
||||
path: ./cypress/videos/*
|
||||
- name: 'Print logs on failure'
|
||||
if: failure()
|
||||
run: docker-compose --file docker-compose.ci.yml logs
|
||||
spec: cypress/e2e/Student_statistics.js
|
||||
|
||||
test_studyprogramme_overview:
|
||||
name: 'Test studyprogramme overview'
|
||||
@@ -425,39 +297,13 @@ jobs:
|
||||
- build_and_push_anon_user_db
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: 'Download images'
|
||||
uses: actions/download-artifact@v2
|
||||
- uses: actions/checkout@v3
|
||||
- name: 'Run tests'
|
||||
uses: ./.github/actions/run-tests
|
||||
with:
|
||||
path: /tmp/
|
||||
- name: 'Load built images'
|
||||
run: |
|
||||
docker load --input /tmp/artifact/backend.tar
|
||||
docker load --input /tmp/artifact/frontend.tar
|
||||
- name: 'Login to toska docker registry'
|
||||
uses: docker/login-action@v1
|
||||
with:
|
||||
registry: registry.toska.cs.helsinki.fi
|
||||
username: ${{ secrets.TOSKAREGISTRY_USERNAME }}
|
||||
password: ${{ secrets.TOSKAREGISTRY_PASSWORD }}
|
||||
- name: 'Run services, pull rest of the needed images'
|
||||
run: docker-compose --file docker-compose.ci.yml up --detach --no-recreate
|
||||
- name: 'Run cypress'
|
||||
uses: cypress-io/github-action@v2
|
||||
with:
|
||||
browser: chrome
|
||||
spec: cypress/integration/Studyprogramme_overview.js
|
||||
env:
|
||||
CYPRESS_baseUrl: http://localhost:1337
|
||||
- name: 'Upload video if tests fail'
|
||||
uses: actions/upload-artifact@v2
|
||||
if: failure()
|
||||
with:
|
||||
name: Cypress videos
|
||||
path: ./cypress/videos/*
|
||||
- name: 'Print logs on failure'
|
||||
if: failure()
|
||||
run: docker-compose --file docker-compose.ci.yml logs
|
||||
spec: cypress/e2e/Studyprogramme_overview.js
|
||||
|
||||
test_teachers_and_users:
|
||||
name: 'Test teachers and users'
|
||||
@@ -469,41 +315,15 @@ jobs:
|
||||
- build_and_push_anon_user_db
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: 'Download images'
|
||||
uses: actions/download-artifact@v2
|
||||
- uses: actions/checkout@v3
|
||||
- name: 'Run tests'
|
||||
uses: ./.github/actions/run-tests
|
||||
with:
|
||||
path: /tmp/
|
||||
- name: 'Load built images'
|
||||
run: |
|
||||
docker load --input /tmp/artifact/backend.tar
|
||||
docker load --input /tmp/artifact/frontend.tar
|
||||
- name: 'Login to toska docker registry'
|
||||
uses: docker/login-action@v1
|
||||
with:
|
||||
registry: registry.toska.cs.helsinki.fi
|
||||
username: ${{ secrets.TOSKAREGISTRY_USERNAME }}
|
||||
password: ${{ secrets.TOSKAREGISTRY_PASSWORD }}
|
||||
- name: 'Run services, pull rest of the needed images'
|
||||
run: docker-compose --file docker-compose.ci.yml up --detach --no-recreate
|
||||
- name: 'Run cypress'
|
||||
uses: cypress-io/github-action@v2
|
||||
with:
|
||||
browser: chrome
|
||||
spec: |
|
||||
cypress/integration/Teachers.js
|
||||
cypress/integration/Users.js
|
||||
env:
|
||||
CYPRESS_baseUrl: http://localhost:1337
|
||||
- name: 'Upload video if tests fail'
|
||||
uses: actions/upload-artifact@v2
|
||||
if: failure()
|
||||
with:
|
||||
name: Cypress videos
|
||||
path: ./cypress/videos/*
|
||||
- name: 'Print logs on failure'
|
||||
if: failure()
|
||||
run: docker-compose --file docker-compose.ci.yml logs
|
||||
cypress/e2e/Teachers.js
|
||||
cypress/e2e/Users.js
|
||||
|
||||
test_faculty_statistics:
|
||||
name: 'Test faculty statistics'
|
||||
@@ -515,39 +335,13 @@ jobs:
|
||||
- build_and_push_anon_user_db
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: 'Download images'
|
||||
uses: actions/download-artifact@v2
|
||||
- uses: actions/checkout@v3
|
||||
- name: 'Run tests'
|
||||
uses: ./.github/actions/run-tests
|
||||
with:
|
||||
path: /tmp/
|
||||
- name: 'Load built images'
|
||||
run: |
|
||||
docker load --input /tmp/artifact/backend.tar
|
||||
docker load --input /tmp/artifact/frontend.tar
|
||||
- name: 'Login to toska docker registry'
|
||||
uses: docker/login-action@v1
|
||||
with:
|
||||
registry: registry.toska.cs.helsinki.fi
|
||||
username: ${{ secrets.TOSKAREGISTRY_USERNAME }}
|
||||
password: ${{ secrets.TOSKAREGISTRY_PASSWORD }}
|
||||
- name: 'Run services, pull rest of the needed images'
|
||||
run: docker-compose --file docker-compose.ci.yml up --detach --no-recreate
|
||||
- name: 'Run cypress'
|
||||
uses: cypress-io/github-action@v2
|
||||
with:
|
||||
browser: chrome
|
||||
spec: cypress/integration/Faculty_statistics.js
|
||||
env:
|
||||
CYPRESS_baseUrl: http://localhost:1337
|
||||
- name: 'Upload video if tests fail'
|
||||
uses: actions/upload-artifact@v2
|
||||
if: failure()
|
||||
with:
|
||||
name: Cypress videos
|
||||
path: ./cypress/videos/*
|
||||
- name: 'Print logs on failure'
|
||||
if: failure()
|
||||
run: docker-compose --file docker-compose.ci.yml logs
|
||||
spec: cypress/e2e/Faculty_statistics.js
|
||||
|
||||
# === Other tests ===
|
||||
|
||||
@@ -561,9 +355,9 @@ jobs:
|
||||
- build_and_push_anon_user_db
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
- name: 'Download images'
|
||||
uses: actions/download-artifact@v2
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
path: /tmp/
|
||||
- name: 'Load built images'
|
||||
@@ -571,7 +365,7 @@ jobs:
|
||||
docker load --input /tmp/artifact/backend.tar
|
||||
docker load --input /tmp/artifact/frontend.tar
|
||||
- name: 'Login to toska docker registry'
|
||||
uses: docker/login-action@v1
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
registry: registry.toska.cs.helsinki.fi
|
||||
username: ${{ secrets.TOSKAREGISTRY_USERNAME }}
|
||||
@@ -607,9 +401,9 @@ jobs:
|
||||
- test_faculty_statistics
|
||||
- test_service_exit_statuses
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
- name: 'Download images'
|
||||
uses: actions/download-artifact@v2
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
path: /tmp/
|
||||
- name: 'Load built images'
|
||||
@@ -617,7 +411,7 @@ jobs:
|
||||
docker load --input /tmp/artifact/backend.tar
|
||||
docker load --input /tmp/artifact/frontend.tar
|
||||
- name: 'Login to DockerHub'
|
||||
uses: docker/login-action@v1
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
@@ -675,7 +469,7 @@ jobs:
|
||||
- test_service_exit_statuses
|
||||
if: failure()
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
- name: 'Send notification to slack'
|
||||
uses: UniversityOfHelsinkiCS/slackbot-action@v1.4.2
|
||||
with:
|
||||
|
||||
+1
-1
@@ -9,7 +9,7 @@ module.exports = {
|
||||
'*.css': files => `stylelint --fix ${files.join(' ')}`,
|
||||
Dockerfile: files => `${dockerCmdBase} hadolint/hadolint:v2.7.0 hadolint ${relativeFilePaths(files)}`,
|
||||
'*.sh': files => `${dockerCmdBase} koalaman/shellcheck:v0.7.2 ${relativeFilePaths(files)} -x`,
|
||||
'.github/workflows/*': files => `${dockerCmdBase} rhysd/actionlint:1.6.2 ${relativeFilePaths(files)}`,
|
||||
'.github/{workflows,actions}/*': files => `npm run actionlint ${relativeFilePaths(files)}`,
|
||||
'docker-compose*': files => {
|
||||
const composeFiles = file => {
|
||||
if (['docker-compose.ci.yml', 'docker-compose.test.yml'].some(f => file.includes(f))) {
|
||||
|
||||
+1
-1
@@ -5,5 +5,5 @@
|
||||
"printWidth": 120,
|
||||
"tabWidth": 2,
|
||||
"useTabs": false,
|
||||
"jsxSingleQuote": false,
|
||||
"jsxSingleQuote": false
|
||||
}
|
||||
+5
-1
@@ -13,6 +13,10 @@
|
||||
"ignorePseudoClasses": ["global"]
|
||||
}
|
||||
],
|
||||
"prettier/prettier": true
|
||||
"prettier/prettier": true,
|
||||
"selector-class-pattern": null,
|
||||
"custom-property-pattern": null,
|
||||
"value-no-vendor-prefix": null,
|
||||
"property-no-vendor-prefix": null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -108,7 +108,7 @@ There are three types of tests in this project: static tests with eslint/prettie
|
||||
|
||||
- Cypress
|
||||
- can be launched in interactive mode with `npm run cypress open`. `package.json` defines entrypoint `npm run cypress` so you can basically run cypress with any arguments you want
|
||||
- are defined in cypress -folder and cypress.json
|
||||
- are defined in cypress -folder and cypress.config.json
|
||||
- whole cypress test stack takes about 15 to 20 mins to run. Since tests are ran in our Github actions CI pipe, you're encouraged to take advantage of this instead of running all tests locally.
|
||||
- There are some different user types and cypress commands defined for testing. Take a look at these when debugging tests.
|
||||
- Jest
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
const { defineConfig } = require('cypress')
|
||||
|
||||
module.exports = defineConfig({
|
||||
projectId: 'c3jsph',
|
||||
defaultCommandTimeout: 30000,
|
||||
requestTimeout: 30000,
|
||||
videoCompression: false,
|
||||
viewportWidth: 1800,
|
||||
viewportHeight: 1200,
|
||||
e2e: {
|
||||
// We've imported your old cypress plugins here.
|
||||
// You may want to clean this up later by importing these.
|
||||
setupNodeEvents(on, config) {
|
||||
return require('./cypress/plugins/index.js')(on, config)
|
||||
},
|
||||
baseUrl: 'http://localhost:3000',
|
||||
specPattern: 'cypress/e2e/**/*.{js,jsx,ts,tsx}',
|
||||
},
|
||||
})
|
||||
@@ -1,7 +0,0 @@
|
||||
{
|
||||
"projectId": "c3jsph",
|
||||
"baseUrl": "http://localhost:3000",
|
||||
"defaultCommandTimeout": 30000,
|
||||
"requestTimeout": 30000,
|
||||
"videoCompression": false
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
/// <reference types="Cypress" />
|
||||
|
||||
// Change admin to regular user when feature is ready for general use
|
||||
|
||||
describe('Faculty overview', () => {
|
||||
describe('Faculty can be selected', () => {
|
||||
it('Faculties are listed and one can be chosen', () => {
|
||||
cy.init('/faculties', 'admin')
|
||||
cy.get('[data-cy=select-faculty]').contains('td', 'H10')
|
||||
cy.contains('td', 'H99').should('not.exist')
|
||||
cy.contains('td', 'H90').click()
|
||||
cy.contains('.header', 'Eläinlääketieteellinen tiedekunta')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Faculty basic information: admin user', () => {
|
||||
beforeEach(() => {
|
||||
cy.init('/faculties', 'admin')
|
||||
cy.contains('td', 'H90').click()
|
||||
})
|
||||
it('Credits produced by faculty are shown', () => {
|
||||
cy.get('[data-cy="Section-CreditsProducedByTheFaculty"]').should('be.visible')
|
||||
cy.get('[data-cy="Graph-CreditsProducedByTheFaculty"]').should('be.visible')
|
||||
})
|
||||
|
||||
it('Update statistics tab is shown', () => {
|
||||
cy.get('[data-cy="FacultySegmentContainer"]').should('contain', 'Update statistics')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Faculty basic information: basic user', () => {
|
||||
beforeEach(() => {
|
||||
cy.init('/faculties')
|
||||
cy.contains('td', 'H80').click()
|
||||
})
|
||||
it('Basic information tab show all graphs and tables', () => {
|
||||
cy.get('[data-cy="Table-CreditsProducedByTheFaculty"]').should('be.visible')
|
||||
cy.get('[data-cy="Graph-CreditsProducedByTheFaculty"]').should('be.visible')
|
||||
cy.get('[data-cy="Table-ThesisWritersOfTheFaculty"]').should('be.visible')
|
||||
cy.get('[data-cy="Graph-ThesisWritersOfTheFaculty"]').should('be.visible')
|
||||
cy.get('[data-cy="Table-StudentsOfTheFaculty"]').should('be.visible')
|
||||
cy.get('[data-cy="Graph-StudentsOfTheFaculty"]').should('be.visible')
|
||||
cy.get('[data-cy="Table-GraduatedOfTheFaculty"]').should('be.visible')
|
||||
cy.get('[data-cy="Graph-GraduatedOfTheFaculty"]').should('be.visible')
|
||||
})
|
||||
|
||||
it('Correct tabs are shown', () => {
|
||||
cy.get('[data-cy="FacultySegmentContainer"]').should('contain', 'Basic information')
|
||||
// Fix these when access is gained for facultyStatistics users
|
||||
cy.get('[data-cy="FacultySegmentContainer"]').should('not.contain', 'Programmes and student populations')
|
||||
cy.get('[data-cy="FacultySegmentContainer"]').should('not.contain', 'Graduation times')
|
||||
cy.get('[data-cy="FacultySegmentContainer"]').should('not.contain', 'Update statistics')
|
||||
})
|
||||
|
||||
it('Toggle years works', () => {
|
||||
cy.get('[data-cy="Table-CreditsProducedByTheFaculty"]').should('contain', '2022')
|
||||
cy.get('[data-cy="Table-ThesisWritersOfTheFaculty"]').should('contain', '2022')
|
||||
cy.get('[data-cy="Table-StudentsOfTheFaculty"]').should('contain', '2022')
|
||||
cy.get('[data-cy="Table-GraduatedOfTheFaculty"]').should('contain', '2022')
|
||||
cy.get('[data-cy="YearToggle"]').click()
|
||||
cy.get('[data-cy="Table-CreditsProducedByTheFaculty"]').should('contain', '2022 - 2023')
|
||||
cy.get('[data-cy="Table-ThesisWritersOfTheFaculty"]').should('contain', '2022 - 2023')
|
||||
cy.get('[data-cy="Table-StudentsOfTheFaculty"]').should('contain', '2022 - 2023')
|
||||
cy.get('[data-cy="Table-GraduatedOfTheFaculty"]').should('contain', '2022 - 2023')
|
||||
})
|
||||
|
||||
it('Toggle programmes works', () => {
|
||||
cy.get('[data-cy="FacultyProgrammesShownInfo"]').should('not.exist')
|
||||
cy.get('[data-cy="ProgrammeToggle"]').click()
|
||||
cy.get('[data-cy="FacultyProgrammesShownInfo"]').should('be.visible')
|
||||
})
|
||||
|
||||
it('Students of the faculty infobox works', () => {
|
||||
cy.get('[data-cy="StudentsOfTheFaculty-info-content"]').should('not.exist')
|
||||
cy.get('[data-cy="StudentsOfTheFaculty-open-info"]').click()
|
||||
cy.get('[data-cy="StudentsOfTheFaculty-info-content"]').should('be.visible')
|
||||
cy.get('[data-cy="StudentsOfTheFaculty-close-info"]').click()
|
||||
cy.get('[data-cy="StudentsOfTheFaculty-info-content"]').should('not.exist')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Study programme information: users with access', () => {
|
||||
beforeEach(() => {
|
||||
cy.init('/faculties')
|
||||
cy.contains('td', 'H80').click()
|
||||
})
|
||||
|
||||
it('Study programme credit information is not visible in the beginning', () => {
|
||||
cy.get('table[data-cy="Table-CreditsProducedByTheFaculty"]').should('be.visible')
|
||||
|
||||
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-0"]').should('not.be.visible')
|
||||
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-1"]').should('not.be.visible')
|
||||
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-2"]').should('not.be.visible')
|
||||
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-3"]').should('not.be.visible')
|
||||
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-4"]').should('not.be.visible')
|
||||
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-5"]').should('not.be.visible')
|
||||
})
|
||||
|
||||
it('Study programme credit information can be toggled', () => {
|
||||
cy.get('[data-cy="Table-CreditsProducedByTheFaculty"]').should('be.visible')
|
||||
|
||||
// I tried to fix this without cy.wait() method without effort. Sorting the programmes
|
||||
// or something else causes rerendering of the view and yield to failing tests.
|
||||
// eslint-disable-next-line cypress/no-unnecessary-waiting
|
||||
cy.wait(10000)
|
||||
cy.get('[data-cy="Button-Show-CreditsProducedByTheFaculty-0"]').click()
|
||||
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-0"]').should('be.visible')
|
||||
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-1"]').should('not.be.visible')
|
||||
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-2"]').should('not.be.visible')
|
||||
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-3"]').should('not.be.visible')
|
||||
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-4"]').should('not.be.visible')
|
||||
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-5"]').should('not.be.visible')
|
||||
cy.get('[data-cy="Button-Show-CreditsProducedByTheFaculty-3"]').click()
|
||||
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-0"]').should('be.visible')
|
||||
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-1"]').should('not.be.visible')
|
||||
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-2"]').should('not.be.visible')
|
||||
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-3"]').should('be.visible')
|
||||
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-4"]').should('not.be.visible')
|
||||
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-5"]').should('not.be.visible')
|
||||
cy.get('[data-cy="Button-Hide-CreditsProducedByTheFaculty-0"]').click()
|
||||
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-0"]').should('not.be.visible')
|
||||
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-1"]').should('not.be.visible')
|
||||
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-2"]').should('not.be.visible')
|
||||
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-3"]').should('be.visible')
|
||||
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-4"]').should('not.be.visible')
|
||||
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-5"]').should('not.be.visible')
|
||||
})
|
||||
|
||||
it('Graph stays open when sorted', () => {
|
||||
cy.get('[data-cy="Table-CreditsProducedByTheFaculty"]').should('be.visible')
|
||||
// eslint-disable-next-line cypress/no-unnecessary-waiting
|
||||
cy.wait(10000)
|
||||
cy.get('[data-cy="Button-Show-CreditsProducedByTheFaculty-0"]').click()
|
||||
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-0"]').should('be.visible')
|
||||
cy.get('[data-cy="Menu-CreditsProducedByTheFaculty-Total"]').click()
|
||||
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-0"]').should('be.visible')
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -217,4 +217,30 @@ describe('Population Statistics tests', () => {
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('when using IAM user', () => {
|
||||
beforeEach(() => {
|
||||
cy.init(pathToCSBach2017, 'onlyiamrights')
|
||||
})
|
||||
|
||||
it('Population statistics is visible', () => {
|
||||
cy.reload()
|
||||
|
||||
cy.get('.card').within(() => {
|
||||
cy.contains('Tietojenkäsittelytieteen kandiohjelma')
|
||||
cy.contains('Sample size: 170 students')
|
||||
cy.contains('Excludes exchange students')
|
||||
cy.contains('Excludes students with non-degree study right')
|
||||
cy.contains('Excludes students who have transferred out of this programme')
|
||||
})
|
||||
|
||||
cy.contains('Credit accumulation (for 170 students)')
|
||||
cy.contains('Credit statistics')
|
||||
})
|
||||
|
||||
it('Students tab is not available', () => {
|
||||
cy.contains('Students (170)').should('not.exist')
|
||||
// TODO: somehow check that obfuscation works
|
||||
})
|
||||
})
|
||||
})
|
||||
+54
-16
@@ -249,8 +249,8 @@ describe('Studyprogramme overview', () => {
|
||||
'0 %',
|
||||
0,
|
||||
'0 %',
|
||||
0,
|
||||
'0 %',
|
||||
1,
|
||||
'1.2 %',
|
||||
1,
|
||||
'1.2 %',
|
||||
65,
|
||||
@@ -291,8 +291,8 @@ describe('Studyprogramme overview', () => {
|
||||
'0 %',
|
||||
0,
|
||||
'0 %',
|
||||
3,
|
||||
'1.8 %',
|
||||
4,
|
||||
'2.3 %',
|
||||
42,
|
||||
'24.6 %',
|
||||
135,
|
||||
@@ -312,8 +312,8 @@ describe('Studyprogramme overview', () => {
|
||||
'0 %',
|
||||
0,
|
||||
'0 %',
|
||||
3,
|
||||
'0.7 %',
|
||||
5,
|
||||
'1.2 %',
|
||||
54,
|
||||
'12.5 %',
|
||||
328,
|
||||
@@ -332,11 +332,11 @@ describe('Studyprogramme overview', () => {
|
||||
cy.contains('a', 'Tietojenkäsittelytieteen kandiohjelma').click()
|
||||
cy.get('.attached').contains('Studytracks and student populations').click()
|
||||
const tableContents = [
|
||||
['2020 - 2021', 12, 5, 2, 2, 0, 2, 0, 1],
|
||||
['2019 - 2020', 86, 13, 15, 32, 11, 2, 0, 13],
|
||||
['2018 - 2019', 162, 7, 16, 35, 33, 27, 18, 26],
|
||||
['2017 - 2018', 171, 10, 22, 18, 22, 24, 27, 48],
|
||||
['Total', 431, 35, 55, 87, 66, 55, 45, 88],
|
||||
['2020 - 2021', 12, 12, 0, 0, 0, 0, 0, 0, 0],
|
||||
['2019 - 2020', 86, 10, 16, 34, 21, 5, 0, 0, 0],
|
||||
['2018 - 2019', 162, 2, 8, 27, 47, 36, 26, 12, 4],
|
||||
['2017 - 2018', 171, 5, 15, 21, 26, 22, 22, 33, 27],
|
||||
['Total', 431, 14, 37, 64, 97, 79, 55, 51, 34],
|
||||
]
|
||||
|
||||
cy.checkTableStats(tableContents, 'StudytrackProgress')
|
||||
@@ -347,12 +347,12 @@ describe('Studyprogramme overview', () => {
|
||||
cy.get('.attached').contains('Studytracks and student populations').click()
|
||||
|
||||
cy.get('[data-cy=Graph-StudytrackProgress]')
|
||||
.should('contain', 'Less than 30 credits')
|
||||
.should('contain', 'Less than 15 credits')
|
||||
.should('contain', '30-59 credits')
|
||||
.should('contain', 'More than 180 credits')
|
||||
.should('contain', '5.8%') // The percentage for less than 30 credits in 2017-2018, to check that the graph renders
|
||||
.should('contain', '4.3%') // The percentage for less than 30 credits in 2018-2019
|
||||
.should('contain', '15.1%') // The percentage for less than 30 credits in 2019-2020
|
||||
.should('contain', '2.9%') // The percentage for less than 15 credits in 2017-2018, to check that the graph renders
|
||||
.should('contain', '4.9%') // The percentage for 15-29 credits in 2018-2019
|
||||
.should('contain', '11.6%') // The percentage for less than 15 credits in 2019-2020
|
||||
|
||||
cy.get('[data-cy=Graph-2018-AverageGraduationTimes]')
|
||||
.should('contain', '2018 - 2019')
|
||||
@@ -411,7 +411,7 @@ describe('Studyprogramme overview', () => {
|
||||
cy.get('[data-cy=CoursesSortableTable]').within(() => {
|
||||
// Test sorting by different columns
|
||||
cy.get('th').eq(0).click()
|
||||
cy.get('tr').eq(1).contains('Tietojenkäsittelytieteen kisälliopetus: Java-ohjelmointi')
|
||||
cy.get('tr').eq(2).contains('Tietojenkäsittelytieteen kisälliopetus: Java-ohjelmointi')
|
||||
cy.get('th').eq(1).click()
|
||||
cy.get('tr').eq(1).contains('Äidinkielinen viestintä')
|
||||
cy.get('th').eq(2).click()
|
||||
@@ -499,4 +499,42 @@ describe('Studyprogramme overview', () => {
|
||||
cy.contains(name).should('not.exist')
|
||||
})
|
||||
})
|
||||
|
||||
describe('IAM user', () => {
|
||||
beforeEach(() => {
|
||||
cy.init('/study-programme', 'onlyiamrights')
|
||||
})
|
||||
|
||||
it('can access programme and correct tabs are visible', () => {
|
||||
cy.reload()
|
||||
|
||||
cy.contains('a', 'Tietojenkäsittelytieteen kandiohjelma').click()
|
||||
|
||||
cy.contains('Basic information')
|
||||
cy.contains('Studytracks and student populations')
|
||||
|
||||
cy.contains('Update statistics').should('not.exist')
|
||||
cy.contains('Degree Courses').should('not.exist')
|
||||
})
|
||||
|
||||
it('can access basic information', () => {
|
||||
cy.contains('a', 'Tietojenkäsittelytieteen kandiohjelma').click()
|
||||
cy.contains('Basic information').click()
|
||||
|
||||
cy.get('[data-cy=Section-StudentsOfTheStudyprogramme]')
|
||||
cy.get('[data-cy=Section-CreditsProducedByTheStudyprogramme]')
|
||||
cy.get('[data-cy=Section-GraduatedAndThesisWritersOfTheProgramme]')
|
||||
cy.get('[data-cy=Section-ProgrammesBeforeOrAfter]')
|
||||
cy.get('[data-cy=Section-AverageGraduationTimes]')
|
||||
})
|
||||
|
||||
it('can access studytracks', () => {
|
||||
cy.contains('a', 'Tietojenkäsittelytieteen kandiohjelma').click()
|
||||
cy.get('.attached').contains('Studytracks and student populations').click()
|
||||
|
||||
cy.get('[data-cy=Section-StudytrackOverview]')
|
||||
cy.get('[data-cy=Section-StudytrackProgress]')
|
||||
cy.get('[data-cy=Section-AverageGraduationTimes]')
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,26 +0,0 @@
|
||||
/// <reference types="Cypress" />
|
||||
|
||||
// Change admin to regular user when feature is ready for general use
|
||||
|
||||
describe('Faculty overview', () => {
|
||||
describe('Faculty can be selected', () => {
|
||||
it('Faculties are listed and one can be chosen', () => {
|
||||
cy.init('/faculties', 'admin')
|
||||
cy.get('[data-cy=select-faculty]').contains('td', 'H10')
|
||||
cy.contains('td', 'H99').should('not.exist')
|
||||
cy.contains('td', 'H90').click()
|
||||
cy.contains('.header', 'Eläinlääketieteellinen tiedekunta')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Faculty basic information', () => {
|
||||
beforeEach(() => {
|
||||
cy.init('/faculties', 'admin')
|
||||
cy.contains('td', 'H90').click()
|
||||
})
|
||||
it('Credits produced by faculty are shown', () => {
|
||||
cy.get(['data-cy=Section-CreditsProducedByTheFaculty'])
|
||||
cy.get(['data-cy=Graph-CreditsProducedByTheFaculty'])
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -43,7 +43,23 @@ const norightsUserHeaders = {
|
||||
hypersonsisuid: 'hy-hlo-0000000',
|
||||
}
|
||||
|
||||
const userHeaders = [adminUserHeaders, basicUserHeaders, onlycoursestatisticsUserHeaders, norightsUserHeaders]
|
||||
const onlyIamRightsUserHeaders = {
|
||||
uid: 'onlyiamrights',
|
||||
displayname: 'Only IAM rights user',
|
||||
'shib-session-id': 'mock-cypress-session',
|
||||
hygroupcn: 'hy-employees;hy-mltdk-tkt-jory',
|
||||
edupersonaffiliation: 'member',
|
||||
mail: 'grp-toska+mockonlyiamrightsuser@helsinki.fi',
|
||||
hypersonsisuid: 'hy-hlo-1111111',
|
||||
}
|
||||
|
||||
const userHeaders = [
|
||||
adminUserHeaders,
|
||||
basicUserHeaders,
|
||||
onlycoursestatisticsUserHeaders,
|
||||
norightsUserHeaders,
|
||||
onlyIamRightsUserHeaders,
|
||||
]
|
||||
|
||||
/**
|
||||
Set up headers to login, set up correct user (admin / basic / etc.) and open given path.
|
||||
|
||||
Generated
+9128
-2710
File diff suppressed because it is too large
Load Diff
+16
-13
@@ -6,6 +6,7 @@
|
||||
"concurrently": "concurrently",
|
||||
"eslint": "eslint",
|
||||
"stylelint": "stylelint",
|
||||
"actionlint": "node-actionlint",
|
||||
"prettier": "prettier",
|
||||
"cli": "./cli.sh",
|
||||
"oodikone": "./run.sh oodikone anon up --detach",
|
||||
@@ -28,31 +29,33 @@
|
||||
},
|
||||
"homepage": "https://github.com/UniversityOfHelsinkiCS/oodikone#readme",
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.18.13",
|
||||
"@babel/eslint-parser": "^7.17.0",
|
||||
"@babel/plugin-syntax-jsx": "^7.16.7",
|
||||
"concurrently": "^6.5.1",
|
||||
"cypress": "^8.7.0",
|
||||
"eslint": "^7.32.0",
|
||||
"eslint-config-airbnb": "^18.2.1",
|
||||
"concurrently": "^7.3.0",
|
||||
"cypress": "^10.6.0",
|
||||
"eslint": "^8.23.0",
|
||||
"eslint-config-airbnb": "^19.0.4",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
"eslint-plugin-babel": "^5.3.1",
|
||||
"eslint-plugin-cypress": "^2.12.1",
|
||||
"eslint-plugin-import": "^2.25.4",
|
||||
"eslint-plugin-jsx-a11y": "^6.5.1",
|
||||
"eslint-plugin-prefer-object-spread": "^1.2.1",
|
||||
"eslint-plugin-prettier": "^3.4.1",
|
||||
"eslint-plugin-react": "^7.29.3",
|
||||
"eslint-plugin-prettier": "^4.2.1",
|
||||
"eslint-plugin-react": "^7.31.1",
|
||||
"eslint-plugin-react-hooks": "^4.3.0",
|
||||
"husky": "^7.0.4",
|
||||
"lint-staged": "^11.2.6",
|
||||
"husky": "^8.0.1",
|
||||
"lint-staged": "^13.0.3",
|
||||
"node-actionlint": "^1.2.2",
|
||||
"prettier": "^2.5.1",
|
||||
"stylelint": "^13.13.1",
|
||||
"stylelint-config-prettier": "^8.0.2",
|
||||
"stylelint-config-standard": "^22.0.0",
|
||||
"stylelint-prettier": "^1.2.0"
|
||||
"stylelint": "^14.11.0",
|
||||
"stylelint-config-prettier": "^9.0.3",
|
||||
"stylelint-config-standard": "^26.0.0",
|
||||
"stylelint-prettier": "^2.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"lodash": "^4.17.21",
|
||||
"moment": "^2.29.1"
|
||||
"moment": "^2.29.4"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -393,7 +393,7 @@ const iamToOrganisationCode = iam => {
|
||||
|
||||
const kosuIamToFaculties = iam => {
|
||||
const faculties = kosuFacultyMap[iam]
|
||||
if (!faculties?.length > 0) return []
|
||||
if (!Array.isArray(faculties) || !faculties.length) return []
|
||||
|
||||
return faculties.map(f => facultyMap[f])
|
||||
}
|
||||
|
||||
Generated
+5303
-1349
File diff suppressed because it is too large
Load Diff
@@ -20,11 +20,11 @@
|
||||
"lodash": "^4.17.21",
|
||||
"lru-cache": "^5.1.1",
|
||||
"moment": "^2.29.1",
|
||||
"moment-timezone": "^0.5.34",
|
||||
"moment-timezone": "^0.5.37",
|
||||
"morgan": "^1.10.0",
|
||||
"pg": "^8.7.3",
|
||||
"pg": "^8.8.0",
|
||||
"redis": "^3.1.2",
|
||||
"sequelize": "^6.17.0",
|
||||
"sequelize": "^6.21.4",
|
||||
"umzug": "^2.3.0",
|
||||
"unfuck-utf8-headers-middleware": "^1.0.1",
|
||||
"uuid": "^3.4.0",
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
module.exports = {
|
||||
up: queryInterface => {
|
||||
return queryInterface.bulkInsert('access_groups', [
|
||||
{
|
||||
group_code: 'facultyStatistics',
|
||||
group_info: 'grants access to faculty statistics',
|
||||
},
|
||||
])
|
||||
},
|
||||
down: queryInterface => {
|
||||
return queryInterface.bulkDelete('access_groups', [
|
||||
{
|
||||
group_code: 'facultyStatistics',
|
||||
},
|
||||
])
|
||||
},
|
||||
}
|
||||
@@ -5,6 +5,8 @@ const { refreshAssociationsInRedis } = require('./services/studyrights')
|
||||
const { getAllProgrammes } = require('./services/studyrights')
|
||||
const { updateBasicView, updateStudytrackView } = require('./services/studyprogrammeUpdates')
|
||||
const { findAndSaveTeachers } = require('./services/topteachers')
|
||||
const { faculties } = require('./services/organisations')
|
||||
const { updateFacultyOverview } = require('./services/faculty/facultyUpdates')
|
||||
const { isProduction } = require('./conf-backend')
|
||||
const { getCurrentSemester } = require('./services/semesters')
|
||||
const logger = require('./util/logger')
|
||||
@@ -16,6 +18,23 @@ const refreshStudyrightAssociations = async () => {
|
||||
logger.info('Studyright associations refreshed!')
|
||||
}
|
||||
|
||||
const refreshFaculties = async () => {
|
||||
logger.info('Refreshing faculty overview statistics for all faculties')
|
||||
const facultyList = (await faculties()).filter(f => !['Y', 'H99', 'Y01', 'H92', 'H930'].includes(f.code))
|
||||
let ready = 0
|
||||
|
||||
for (const faculty of facultyList) {
|
||||
try {
|
||||
await updateFacultyOverview(faculty.code, 'ALL')
|
||||
ready += 1
|
||||
} catch (e) {
|
||||
logger.error({ message: `Failed to update stats for faculty ${faculty?.code} with type ALL`, meta: e })
|
||||
}
|
||||
logger.info(`${ready}/${facultyList.length} faculties done`)
|
||||
}
|
||||
logger.info('Faculty stats refreshed!')
|
||||
}
|
||||
|
||||
const refreshNewOverviews = async () => {
|
||||
logger.info('Refreshing studyprogramme and studytrack overview statistics for all programmes')
|
||||
const codes = (await getAllProgrammes())
|
||||
@@ -100,7 +119,7 @@ const startCron = () => {
|
||||
logger.info('Cronjob for refreshing stats started: runs at 3am every day.')
|
||||
// refresh 3am every day
|
||||
schedule('0 3 * * *', async () => {
|
||||
for (const func of [refreshStatistics, refreshTrends, refreshNewOverviews]) {
|
||||
for (const func of [refreshStatistics, refreshTrends, refreshNewOverviews, refreshFaculties]) {
|
||||
await func()
|
||||
}
|
||||
})
|
||||
@@ -111,4 +130,5 @@ module.exports = {
|
||||
startCron,
|
||||
refreshStatistics,
|
||||
refreshTrends,
|
||||
refreshFaculties,
|
||||
}
|
||||
|
||||
@@ -27,11 +27,16 @@ const accessLogger = morgan((tokens, req, res) => {
|
||||
'ms',
|
||||
].join(' ')
|
||||
|
||||
const usingIamRights = user.iamRights.some(programmeCode => tokens['url'](req, res).includes(programmeCode))
|
||||
const onlyIamRights = !user.isAdmin && user.rights.length === 0
|
||||
|
||||
logger.info(message, {
|
||||
// don't log student list which might be huge
|
||||
..._.omit(meta, ['studentsUserCanAccess']),
|
||||
// pass this as a custom field so we can filter by it in graylog
|
||||
isUsageStats: true,
|
||||
// needed for Grafana IAM users panel
|
||||
isIamRights: onlyIamRights || usingIamRights,
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -4,10 +4,12 @@ const { getUser, getMockedUser } = require('../services/userService')
|
||||
const { requiredGroup } = require('../conf-backend')
|
||||
const _ = require('lodash')
|
||||
const { relevantIAMs } = require('../../config/IAMConfig')
|
||||
const { getOrganizationAccess } = require('../util/organizationAccess')
|
||||
|
||||
const parseIamGroups = iamGroups => iamGroups?.split(';').filter(Boolean) ?? []
|
||||
|
||||
const hasRequiredIamGroup = iamGroups => _.intersection(iamGroups, requiredGroup).length > 0
|
||||
const hasRequiredIamGroup = (iamGroups, iamRights) =>
|
||||
_.intersection(iamGroups, requiredGroup).length > 0 || iamRights.length > 0
|
||||
|
||||
const currentUserMiddleware = async (req, _res, next) => {
|
||||
const {
|
||||
@@ -27,7 +29,9 @@ const currentUserMiddleware = async (req, _res, next) => {
|
||||
|
||||
const iamGroups = parseIamGroups(hygroupcn).filter(iam => relevantIAMs.includes(iam))
|
||||
|
||||
if (!hasRequiredIamGroup(iamGroups)) {
|
||||
const iamRights = Object.keys(await getOrganizationAccess({ iamGroups }))
|
||||
|
||||
if (!hasRequiredIamGroup(iamGroups, iamRights)) {
|
||||
throw new ApplicationError('User does not have required iam group', 403, { logoutUrl })
|
||||
}
|
||||
|
||||
|
||||
@@ -57,7 +57,7 @@ module.exports = (app, url) => {
|
||||
app.use(url, tags)
|
||||
app.use(url, programmeModules)
|
||||
app.use(url, studyProgramme)
|
||||
app.use(url, faculties)
|
||||
app.use(`${url}/faculties`, auth.roles(['facultyStatistics']), faculties)
|
||||
app.use(`${url}/updater`, auth.roles(['admin']), updater)
|
||||
app.use(`${url}/teachers`, auth.roles(['teachers']), teachers)
|
||||
app.use(`${url}/users`, auth.roles(['admin']), users)
|
||||
|
||||
@@ -1,101 +1,167 @@
|
||||
const router = require('express').Router()
|
||||
const { faculties, degreeProgrammeCodesOfFaculty } = require('../services/organisations')
|
||||
const { combineFacultyBasics } = require('../services/facultyBasics')
|
||||
const { combineFacultyCredits } = require('../services/facultyCredits')
|
||||
const { faculties } = require('../services/organisations')
|
||||
const { combineFacultyBasics } = require('../services/faculty/facultyBasics')
|
||||
const { combineFacultyCredits } = require('../services/faculty/facultyCredits')
|
||||
const { findFacultyProgrammeCodes } = require('../services/faculty/faculty')
|
||||
const { combineFacultyThesisWriters } = require('../services/faculty/facultyThesisWriters')
|
||||
const { countGraduationTimes } = require('../services/faculty/facultyGraduationTimes')
|
||||
const { updateFacultyOverview } = require('../services/faculty/facultyUpdates')
|
||||
const { getFacultyStudentProgress } = require('../services/faculty/facultyStudentProgress')
|
||||
//const { getFacultyStudents } = require('../services/faculty/facultyStudents')
|
||||
|
||||
const {
|
||||
getFacultyProgrammes,
|
||||
setFacultyProgrammes,
|
||||
getBasicStats,
|
||||
setBasicStats,
|
||||
getCreditStats,
|
||||
setCreditStats,
|
||||
getThesisWritersStats,
|
||||
setThesisWritersStats,
|
||||
} = require('../services/faculty/facultyService')
|
||||
const logger = require('../util/logger')
|
||||
|
||||
// Faculty uses a lot of tools designed for Study programme.
|
||||
// Some of them have been copied here and slightly edited for faculty purpose.
|
||||
|
||||
router.get('/faculties', async (req, res) => {
|
||||
const facultyList = await faculties()
|
||||
const getProgrammes = async (code, programmeFilter) => {
|
||||
const programmes = await getFacultyProgrammes(code, programmeFilter)
|
||||
if (programmes) return programmes
|
||||
let updatedProgrammes = await findFacultyProgrammeCodes(code, programmeFilter)
|
||||
if (updatedProgrammes) updatedProgrammes = await setFacultyProgrammes(code, updatedProgrammes, programmeFilter)
|
||||
|
||||
return updatedProgrammes
|
||||
}
|
||||
|
||||
router.get('/', async (req, res) => {
|
||||
const ignore = ['Y', 'H99', 'Y01', 'H92', 'H930']
|
||||
const facultyList = (await faculties()).filter(f => !ignore.includes(f.code))
|
||||
facultyList.sort((a, b) => (a.name.fi > b.name.fi ? 1 : -1))
|
||||
res.json(facultyList)
|
||||
})
|
||||
|
||||
router.get('/faculties/:id/basicstats', async (req, res) => {
|
||||
router.get('/:id/basicstats', async (req, res) => {
|
||||
const code = req.params.id
|
||||
const yearType = req.query?.year_type
|
||||
const programmeFilter = req.query?.programme_filter
|
||||
const specialGroups = req.query?.special_groups
|
||||
|
||||
if (!code) return res.status(422).end()
|
||||
|
||||
let allBasics = {
|
||||
id: code,
|
||||
years: [],
|
||||
tableStats: [],
|
||||
graphStats: [],
|
||||
titles: [
|
||||
'',
|
||||
'Started studying',
|
||||
'All graduated',
|
||||
'Graduated bachelors',
|
||||
'Graduated masters',
|
||||
'Graduated doctors',
|
||||
'Other graduations',
|
||||
'Transferred inside',
|
||||
'Transferred away',
|
||||
'Transferred to',
|
||||
],
|
||||
status: 'DONE',
|
||||
lastUpdated: '',
|
||||
const data = await getBasicStats(code, yearType, programmeFilter, specialGroups)
|
||||
if (data) return res.json(data)
|
||||
|
||||
const wantedProgrammes = await getProgrammes(code, programmeFilter)
|
||||
if (!wantedProgrammes) return res.status(422).end()
|
||||
|
||||
//all programmes are required for correct sorting of transfers
|
||||
let allProgrammeCodes = []
|
||||
if (programmeFilter === 'NEW_STUDY_PROGRAMMES') {
|
||||
const allProgs = await getProgrammes(code, 'ALL_PROGRAMMES')
|
||||
allProgs?.data.forEach(prog => allProgrammeCodes.push(prog.code))
|
||||
} else {
|
||||
wantedProgrammes?.data.forEach(prog => allProgrammeCodes.push(prog.code))
|
||||
}
|
||||
|
||||
const programmes = await degreeProgrammeCodesOfFaculty(code)
|
||||
if (programmes) {
|
||||
await combineFacultyBasics(allBasics, code, programmes, yearType)
|
||||
let updatedStats = await combineFacultyBasics(
|
||||
code,
|
||||
wantedProgrammes.data,
|
||||
yearType,
|
||||
allProgrammeCodes,
|
||||
programmeFilter,
|
||||
specialGroups
|
||||
)
|
||||
if (updatedStats) {
|
||||
updatedStats = await setBasicStats(updatedStats, yearType, programmeFilter, specialGroups)
|
||||
}
|
||||
|
||||
return res.json(allBasics)
|
||||
return res.json(updatedStats)
|
||||
})
|
||||
|
||||
router.get('/faculties/:id/creditstats', async (req, res) => {
|
||||
router.get('/:id/creditstats', async (req, res) => {
|
||||
const code = req.params.id
|
||||
const yearType = req.query?.year_type
|
||||
const specialGroups = 'SPECIAL_INCLUDED' // req.query?.special_groups
|
||||
const specialGroups = req.query?.special_groups
|
||||
const programmeFilter = req.query?.programme_filter
|
||||
|
||||
if (!code) return res.status(422).end()
|
||||
|
||||
let counts = {}
|
||||
let years = []
|
||||
let allCredits = {
|
||||
id: code,
|
||||
years: [],
|
||||
tableStats: [],
|
||||
graphStats: [],
|
||||
titles: [
|
||||
'',
|
||||
'Total',
|
||||
'Major students credits',
|
||||
'Non-major faculty students credits',
|
||||
'Non-major other faculty students credits',
|
||||
'Non-degree student credits',
|
||||
],
|
||||
status: 'DONE',
|
||||
lastUpdated: '',
|
||||
const data = await getCreditStats(code, yearType, programmeFilter, specialGroups)
|
||||
if (data) return res.json(data)
|
||||
|
||||
const programmes = await getProgrammes(code, programmeFilter)
|
||||
if (!programmes) return res.status(422).end()
|
||||
|
||||
let updatedStats = await combineFacultyCredits(code, programmes.data, yearType, specialGroups)
|
||||
if (updatedStats) {
|
||||
updatedStats = await setCreditStats(updatedStats, yearType, programmeFilter, specialGroups)
|
||||
}
|
||||
|
||||
const programmes = await degreeProgrammeCodesOfFaculty(code)
|
||||
|
||||
if (programmes) {
|
||||
await combineFacultyCredits(allCredits, programmes, yearType, specialGroups, counts, years)
|
||||
}
|
||||
let majors = []
|
||||
let facultyNonMajor = []
|
||||
let otherNonMajor = []
|
||||
let nonDegree = []
|
||||
|
||||
years.forEach(year => {
|
||||
majors.push(counts[year][1])
|
||||
facultyNonMajor.push(counts[year][2])
|
||||
otherNonMajor.push(counts[year][3])
|
||||
nonDegree.push(counts[year][4])
|
||||
})
|
||||
|
||||
allCredits.graphStats = [
|
||||
{ name: 'Major students credits', data: majors },
|
||||
{ name: 'Non-major faculty students credits', data: facultyNonMajor },
|
||||
{ name: 'Non-major other faculty students credits', data: otherNonMajor },
|
||||
{ name: 'Non-degree credits', data: nonDegree },
|
||||
]
|
||||
return res.json(allCredits)
|
||||
return res.json(updatedStats)
|
||||
})
|
||||
|
||||
router.get('/:id/thesisstats', async (req, res) => {
|
||||
const code = req.params.id
|
||||
const yearType = req.query?.year_type
|
||||
const specialGroups = req.query?.special_groups
|
||||
const programmeFilter = req.query?.programme_filter
|
||||
|
||||
if (!code) return res.status(422).end()
|
||||
const data = await getThesisWritersStats(code, yearType, programmeFilter, specialGroups)
|
||||
if (data) return res.json(data)
|
||||
|
||||
const programmes = await getProgrammes(code, programmeFilter)
|
||||
if (!programmes) return res.status(422).end()
|
||||
let updateStats = await combineFacultyThesisWriters(code, programmes.data, yearType, specialGroups)
|
||||
if (updateStats) {
|
||||
updateStats = await setThesisWritersStats(updateStats, yearType, programmeFilter, specialGroups)
|
||||
}
|
||||
return res.json(updateStats)
|
||||
})
|
||||
|
||||
router.get('/:id/graduationtimes', async (req, res) => {
|
||||
const code = req.params.id
|
||||
const programmeFilter = req.query?.programme_filter
|
||||
|
||||
if (!code) return res.status(422).end()
|
||||
|
||||
// const programmes = await getProgrammes(code, programmeFilter)
|
||||
const result = await countGraduationTimes(code, programmeFilter)
|
||||
// currently counts all programmes
|
||||
return res.json(result)
|
||||
})
|
||||
|
||||
router.get('/:id/progressstats', async (req, res) => {
|
||||
const faculty = req.params.id
|
||||
const programmeFilter = req.query.programme_filter
|
||||
|
||||
if (!faculty) return res.status(422).end()
|
||||
const programmes = await getProgrammes(faculty, programmeFilter)
|
||||
const progressStats = await getFacultyStudentProgress(faculty, programmes)
|
||||
return res.json(progressStats)
|
||||
})
|
||||
|
||||
// router.get('/:id/studentstats', async (req, res) => {
|
||||
// const code = req.params.id
|
||||
// const programmeFilter = req.query.programme_filter
|
||||
|
||||
// if (!code) return res.status(422).end()
|
||||
// const programmes = await getProgrammes(code, programmeFilter)
|
||||
// const studentStats = await getFacultyStudents(code, programmes)
|
||||
// return res.json(studentStats)
|
||||
// })
|
||||
|
||||
router.get('/:id/update_basicview', async (req, res) => {
|
||||
const code = req.params.id
|
||||
const statsType = req.query?.stats_type
|
||||
if (code) {
|
||||
let result = null
|
||||
try {
|
||||
result = await updateFacultyOverview(code, statsType)
|
||||
} catch (e) {
|
||||
logger.error(`Failed to update faculty ${code} basic tab stats for ${statsType}: ${e}`)
|
||||
}
|
||||
return res.json(result)
|
||||
}
|
||||
return res.status(422).end()
|
||||
})
|
||||
module.exports = router
|
||||
|
||||
@@ -9,6 +9,7 @@ const TagService = require('../services/tags')
|
||||
const CourseService = require('../services/courses')
|
||||
const StatMergeService = require('../services/statMerger')
|
||||
const { mapToProviders } = require('../util/utils')
|
||||
const { encrypt, decrypt } = require('../services/encrypt')
|
||||
|
||||
const { ApplicationError } = require('../util/customErrors')
|
||||
|
||||
@@ -38,16 +39,26 @@ router.post('/v2/populationstatistics/courses', async (req, res) => {
|
||||
req.body.months = 12
|
||||
}
|
||||
|
||||
const encrypted = req.body.selectedStudents[0]?.encryptedData
|
||||
|
||||
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]
|
||||
req.body.selectedStudents = encrypted
|
||||
? req.body.selectedStudents
|
||||
.filter(
|
||||
({ encryptedData }) =>
|
||||
req.body.selectedStudentsByYear[year] &&
|
||||
req.body.selectedStudentsByYear[year].includes(encryptedData)
|
||||
)
|
||||
.map(decrypt)
|
||||
: req.body.selectedStudentsByYear[year]
|
||||
}
|
||||
const newMonths = (upperYearBound - Number(year)) * 12
|
||||
const query = { ...req.body, year, months: newMonths }
|
||||
const coursestatistics = Population.bottlenecksOf(query)
|
||||
const coursestatistics = Population.bottlenecksOf(query, null, encrypted)
|
||||
return coursestatistics
|
||||
})
|
||||
)
|
||||
@@ -60,7 +71,9 @@ router.post('/v2/populationstatistics/courses', async (req, res) => {
|
||||
|
||||
res.json(result)
|
||||
} else {
|
||||
const result = await Population.bottlenecksOf(req.body)
|
||||
if (encrypted) req.body.selectedStudents = req.body.selectedStudents.map(decrypt)
|
||||
|
||||
const result = await Population.bottlenecksOf(req.body, null, encrypted)
|
||||
|
||||
if (result.error) {
|
||||
Sentry.captureException(new Error(result.error))
|
||||
@@ -68,6 +81,20 @@ router.post('/v2/populationstatistics/courses', async (req, res) => {
|
||||
return
|
||||
}
|
||||
|
||||
//These cause problems in front (where they were originally calculated)
|
||||
// Do them here for the moment
|
||||
for (const course of result.coursestatistics) {
|
||||
// course.students.notParticipated = {}
|
||||
course.students.didNotPass = {}
|
||||
|
||||
for (const student of Object.keys(course.students.all)) {
|
||||
if (
|
||||
!Object.keys(course.students.passed).includes(student) &&
|
||||
!Object.keys(course.students.retryPassed).includes(student)
|
||||
)
|
||||
course.students.didNotPass[student] = true
|
||||
}
|
||||
}
|
||||
res.json(result)
|
||||
}
|
||||
})
|
||||
@@ -180,13 +207,13 @@ router.get('/v3/populationstatistics', async (req, res) => {
|
||||
return
|
||||
}
|
||||
let studyRights = null
|
||||
try {
|
||||
studyRights = JSON.parse(studyRightsJSON)
|
||||
const {
|
||||
user: { rights, isAdmin },
|
||||
} = req
|
||||
studyRights = JSON.parse(studyRightsJSON)
|
||||
const {
|
||||
user: { rights, iamRights, isAdmin },
|
||||
} = req
|
||||
|
||||
if (!isAdmin && !rights.includes(studyRights.programme)) {
|
||||
try {
|
||||
if (!isAdmin && !rights.includes(studyRights.programme) && !iamRights.includes(studyRights.programme)) {
|
||||
res.status(403).json([])
|
||||
return
|
||||
}
|
||||
@@ -232,6 +259,27 @@ router.get('/v3/populationstatistics', async (req, res) => {
|
||||
return
|
||||
}
|
||||
|
||||
// Obfuscate if user has only iam rights
|
||||
if (!isAdmin && !rights.includes(studyRights.programme)) {
|
||||
result.students = result.students.map(student => {
|
||||
const { iv, encryptedData } = encrypt(student.studentNumber)
|
||||
const obfuscatedBirthDate = new Date(new Date(student.birthdate).setMonth(0, 0)) // only birth year for age distribution
|
||||
return {
|
||||
...student,
|
||||
firstnames: '',
|
||||
lastname: '',
|
||||
started: null,
|
||||
iv,
|
||||
studentNumber: encryptedData,
|
||||
name: '',
|
||||
email: '',
|
||||
tags: [],
|
||||
birthdate: obfuscatedBirthDate,
|
||||
obfuscated: true,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
res.json(filterPersonalTags(result, user.id))
|
||||
}
|
||||
})
|
||||
|
||||
@@ -449,7 +449,8 @@ const codeLikeTerm = code =>
|
||||
? undefined
|
||||
: {
|
||||
code: {
|
||||
[Op.iLike]: `%${code.trim()}%`,
|
||||
// Starts with code or has AY/A in front of the code
|
||||
[Op.iRegexp]: `^(AY|ay|A|a)?${code}`,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
const { randomBytes, createCipheriv, createDecipheriv } = require('crypto')
|
||||
|
||||
const algorithm = 'aes-256-cbc'
|
||||
const key = randomBytes(32)
|
||||
const iv = randomBytes(16)
|
||||
|
||||
const encrypt = text => {
|
||||
const cipher = createCipheriv(algorithm, Buffer.from(key), iv)
|
||||
let encrypted = cipher.update(text)
|
||||
encrypted = Buffer.concat([encrypted, cipher.final()])
|
||||
|
||||
return {
|
||||
iv: iv.toString('hex'),
|
||||
encryptedData: encrypted.toString('hex'),
|
||||
}
|
||||
}
|
||||
|
||||
const decrypt = text => {
|
||||
const iv = Buffer.from(text.iv, 'hex')
|
||||
const encryptedText = Buffer.from(text.encryptedData, 'hex')
|
||||
const decipher = createDecipheriv(algorithm, Buffer.from(key), iv)
|
||||
let decrypted = decipher.update(encryptedText)
|
||||
decrypted = Buffer.concat([decrypted, decipher.final()])
|
||||
|
||||
return decrypted.toString()
|
||||
}
|
||||
|
||||
module.exports = { encrypt, decrypt }
|
||||
@@ -1,90 +0,0 @@
|
||||
const Sequelize = require('sequelize')
|
||||
const { Op } = Sequelize
|
||||
const { Studyright, Student, Transfer } = require('../models')
|
||||
const { formatStudyright, formatTransfer } = require('./studyprogrammeHelpers')
|
||||
|
||||
const startedStudyrights = async (faculty, since) =>
|
||||
(
|
||||
await Studyright.findAll({
|
||||
include: [
|
||||
{
|
||||
model: Student,
|
||||
attributes: ['studentnumber'],
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
where: {
|
||||
faculty_code: faculty,
|
||||
studystartdate: {
|
||||
[Op.gte]: since,
|
||||
},
|
||||
student_studentnumber: { [Op.not]: null },
|
||||
},
|
||||
})
|
||||
).map(formatStudyright)
|
||||
|
||||
const graduatedStudyrights = async (faculty, since) =>
|
||||
(
|
||||
await Studyright.findAll({
|
||||
include: [
|
||||
{
|
||||
model: Student,
|
||||
attributes: ['studentnumber'],
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
where: {
|
||||
faculty_code: faculty,
|
||||
enddate: {
|
||||
[Op.gte]: since,
|
||||
},
|
||||
graduated: 1,
|
||||
student_studentnumber: { [Op.not]: null },
|
||||
},
|
||||
})
|
||||
).map(formatStudyright)
|
||||
|
||||
const transferredInsideFaculty = async (programmes, since) =>
|
||||
(
|
||||
await Transfer.findAll({
|
||||
where: {
|
||||
transferdate: {
|
||||
[Op.gte]: since,
|
||||
},
|
||||
sourcecode: programmes,
|
||||
targetcode: programmes,
|
||||
},
|
||||
})
|
||||
).map(formatTransfer)
|
||||
|
||||
const transferredAway = async (programmes, since) =>
|
||||
(
|
||||
await Transfer.findAll({
|
||||
where: {
|
||||
transferdate: {
|
||||
[Op.gte]: since,
|
||||
},
|
||||
sourcecode: programmes,
|
||||
targetcode: {
|
||||
[Op.notIn]: programmes,
|
||||
},
|
||||
},
|
||||
})
|
||||
).map(formatTransfer)
|
||||
|
||||
const transferredTo = async (programmes, since) =>
|
||||
(
|
||||
await Transfer.findAll({
|
||||
where: {
|
||||
transferdate: {
|
||||
[Op.gte]: since,
|
||||
},
|
||||
sourcecode: {
|
||||
[Op.notIn]: programmes,
|
||||
},
|
||||
targetcode: programmes,
|
||||
},
|
||||
})
|
||||
).map(formatTransfer)
|
||||
|
||||
module.exports = { startedStudyrights, graduatedStudyrights, transferredInsideFaculty, transferredAway, transferredTo }
|
||||
@@ -0,0 +1,338 @@
|
||||
const Sequelize = require('sequelize')
|
||||
const { Op } = Sequelize
|
||||
const {
|
||||
ElementDetail,
|
||||
Organization,
|
||||
ProgrammeModule,
|
||||
Semester,
|
||||
SemesterEnrollment,
|
||||
Studyright,
|
||||
StudyrightElement,
|
||||
Student,
|
||||
Transfer,
|
||||
Credit,
|
||||
Course,
|
||||
} = require('../../models')
|
||||
const {
|
||||
facultyFormatStudyright,
|
||||
facultyFormatProgramme,
|
||||
formatFacultyTransfer,
|
||||
formatFacultyThesisWriter,
|
||||
formatOrganization,
|
||||
formatAbsence,
|
||||
isNewProgramme,
|
||||
} = require('./facultyHelpers')
|
||||
|
||||
const getTransferredToAndAway = async (programmeCodes, allProgrammeCodes, since) => {
|
||||
const awayTransfers = (await transferredAway(programmeCodes, allProgrammeCodes, since)).map(t => t.studyrightid)
|
||||
const toTransfers = (await transferredTo(programmeCodes, allProgrammeCodes, since)).map(t => t.studyrightid)
|
||||
const transferredToOrAway = [...toTransfers, ...awayTransfers]
|
||||
return transferredToOrAway
|
||||
}
|
||||
|
||||
const getTransferredInside = async (programmeCodes, allProgrammeCodes, since) => {
|
||||
const transfersInside = (await transferredInsideFaculty(programmeCodes, allProgrammeCodes, since)).map(
|
||||
t => t.studyrightid
|
||||
)
|
||||
return transfersInside
|
||||
}
|
||||
const startedStudyrights = async (faculty, since, studyRightWhere) =>
|
||||
(
|
||||
await Studyright.findAll({
|
||||
include: [
|
||||
{
|
||||
model: Student,
|
||||
attributes: ['studentnumber'],
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
model: StudyrightElement,
|
||||
required: true,
|
||||
include: {
|
||||
model: ElementDetail,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
where: {
|
||||
faculty_code: faculty,
|
||||
studystartdate: {
|
||||
[Op.gte]: since,
|
||||
},
|
||||
student_studentnumber: { [Op.not]: null },
|
||||
...studyRightWhere,
|
||||
},
|
||||
})
|
||||
).map(facultyFormatStudyright)
|
||||
|
||||
const graduatedStudyrights = async (faculty, since, studyrightWhere) =>
|
||||
(
|
||||
await Studyright.findAll({
|
||||
include: [
|
||||
{
|
||||
model: Student,
|
||||
attributes: ['studentnumber'],
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
model: StudyrightElement,
|
||||
required: true,
|
||||
include: {
|
||||
model: ElementDetail,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
where: {
|
||||
faculty_code: faculty,
|
||||
enddate: {
|
||||
[Op.gte]: since,
|
||||
},
|
||||
graduated: 1,
|
||||
student_studentnumber: { [Op.not]: null },
|
||||
...studyrightWhere,
|
||||
},
|
||||
})
|
||||
).map(facultyFormatStudyright)
|
||||
|
||||
const graduatedStudyrightsByStartYear = async (faculty, since) =>
|
||||
(
|
||||
await Studyright.findAll({
|
||||
include: [
|
||||
{
|
||||
model: Student,
|
||||
attributes: ['studentnumber'],
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
model: StudyrightElement,
|
||||
required: true,
|
||||
include: {
|
||||
model: ElementDetail,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
where: {
|
||||
faculty_code: faculty,
|
||||
startdate: {
|
||||
[Op.gte]: since,
|
||||
},
|
||||
graduated: 1,
|
||||
student_studentnumber: { [Op.not]: null },
|
||||
},
|
||||
})
|
||||
).map(facultyFormatStudyright)
|
||||
|
||||
const bachelorStudyright = async id => {
|
||||
return await Studyright.findOne({
|
||||
attributes: ['studystartdate'],
|
||||
where: {
|
||||
studyrightid: id,
|
||||
extentcode: 1,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const transferredInsideFaculty = async (programmes, allProgrammeCodes, since) =>
|
||||
(
|
||||
await Transfer.findAll({
|
||||
where: {
|
||||
transferdate: {
|
||||
[Op.gte]: since,
|
||||
},
|
||||
sourcecode: allProgrammeCodes,
|
||||
targetcode: programmes,
|
||||
},
|
||||
})
|
||||
).map(formatFacultyTransfer)
|
||||
|
||||
const transferredAway = async (programmes, allProgrammeCodes, since) =>
|
||||
(
|
||||
await Transfer.findAll({
|
||||
where: {
|
||||
transferdate: {
|
||||
[Op.gte]: since,
|
||||
},
|
||||
sourcecode: programmes,
|
||||
targetcode: {
|
||||
[Op.notIn]: allProgrammeCodes,
|
||||
},
|
||||
},
|
||||
})
|
||||
).map(formatFacultyTransfer)
|
||||
|
||||
const transferredTo = async (programmes, allProgrammeCodes, since) =>
|
||||
(
|
||||
await Transfer.findAll({
|
||||
where: {
|
||||
transferdate: {
|
||||
[Op.gte]: since,
|
||||
},
|
||||
sourcecode: {
|
||||
[Op.notIn]: allProgrammeCodes,
|
||||
},
|
||||
targetcode: programmes,
|
||||
},
|
||||
})
|
||||
).map(formatFacultyTransfer)
|
||||
|
||||
const degreeProgrammesOfFaculty = async facultyCode =>
|
||||
(
|
||||
await ProgrammeModule.findAll({
|
||||
attributes: ['code', 'name'],
|
||||
include: {
|
||||
model: Organization,
|
||||
where: {
|
||||
code: {
|
||||
[Op.startsWith]: facultyCode,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
).map(facultyFormatProgramme)
|
||||
|
||||
const facultyOgranizationId = async faculty => {
|
||||
return await Organization.findOne({
|
||||
attributes: ['id'],
|
||||
where: {
|
||||
code: faculty,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const getChildOrganizations = async facultyId =>
|
||||
(
|
||||
await Organization.findAll({
|
||||
attributes: ['id', 'name', 'code', 'parent_id'],
|
||||
where: {
|
||||
parent_id: facultyId,
|
||||
},
|
||||
})
|
||||
).map(formatOrganization)
|
||||
|
||||
const thesisWriters = async (provider, since, thesisTypes, students) =>
|
||||
(
|
||||
await Credit.findAll({
|
||||
attributes: ['id', 'course_code', 'attainment_date', 'student_studentnumber'],
|
||||
include: {
|
||||
model: Course,
|
||||
attributes: ['course_unit_type'],
|
||||
required: true,
|
||||
where: {
|
||||
course_unit_type: {
|
||||
[Op.in]: thesisTypes,
|
||||
},
|
||||
},
|
||||
include: {
|
||||
model: Organization,
|
||||
attributes: [],
|
||||
required: true,
|
||||
where: {
|
||||
code: provider,
|
||||
},
|
||||
},
|
||||
},
|
||||
where: {
|
||||
credittypecode: 4,
|
||||
isStudyModule: {
|
||||
[Op.not]: true,
|
||||
},
|
||||
attainment_date: {
|
||||
[Op.gte]: since,
|
||||
},
|
||||
student_studentnumber: students.length > 0 ? students : { [Op.not]: null },
|
||||
},
|
||||
})
|
||||
).map(formatFacultyThesisWriter)
|
||||
|
||||
const statutoryAbsences = async (studentnumber, startdate, enddate) =>
|
||||
(
|
||||
await SemesterEnrollment.findAll({
|
||||
attributes: ['semestercode'],
|
||||
include: {
|
||||
model: Semester,
|
||||
attributes: ['startdate', 'enddate'],
|
||||
where: {
|
||||
startdate: {
|
||||
[Op.gte]: startdate,
|
||||
},
|
||||
enddate: {
|
||||
[Op.lte]: enddate,
|
||||
},
|
||||
},
|
||||
},
|
||||
where: {
|
||||
studentnumber: studentnumber,
|
||||
statutory_absence: true,
|
||||
},
|
||||
})
|
||||
).map(formatAbsence)
|
||||
|
||||
// Some programme modules are not directly associated to a faculty (organization).
|
||||
// Some have intermediate organizations, such as department, so the connection must be digged up
|
||||
const findFacultyProgrammeCodes = async (faculty, programmeFilter) => {
|
||||
let allProgrammes = []
|
||||
let allProgrammeCodes = []
|
||||
|
||||
const directAssociationProgrammes = await degreeProgrammesOfFaculty(faculty)
|
||||
|
||||
allProgrammes = allProgrammes.concat(directAssociationProgrammes)
|
||||
allProgrammeCodes = allProgrammeCodes.concat(directAssociationProgrammes.map(prog => prog.code))
|
||||
|
||||
// find faculty's organization id and its direct child organizations
|
||||
const { id } = await facultyOgranizationId(faculty)
|
||||
const facultyChildOrganizations = await getChildOrganizations(id)
|
||||
|
||||
// get programme modules that have a faculty child as organization(_id)
|
||||
for (const org of facultyChildOrganizations) {
|
||||
const childAssociationProgrammes = await degreeProgrammesOfFaculty(org.code)
|
||||
if (childAssociationProgrammes.length > 0) {
|
||||
childAssociationProgrammes.forEach(prog => {
|
||||
if (!(prog.code in allProgrammeCodes)) {
|
||||
allProgrammes = allProgrammes.concat([prog])
|
||||
allProgrammeCodes = allProgrammeCodes.concat([prog.code])
|
||||
}
|
||||
})
|
||||
} else {
|
||||
// dig deeper
|
||||
const grandChildren = await getChildOrganizations(org.id)
|
||||
if (grandChildren.length > 0) {
|
||||
for (const gcOrg of grandChildren) {
|
||||
const associatedProgrammes = await degreeProgrammesOfFaculty(gcOrg.code)
|
||||
if (associatedProgrammes.length > 0) {
|
||||
associatedProgrammes.forEach(prog => {
|
||||
if (!(prog.code in allProgrammeCodes)) {
|
||||
allProgrammes = allProgrammes.concat([prog])
|
||||
allProgrammeCodes = allProgrammeCodes.concat([prog.code])
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (programmeFilter === 'NEW_STUDY_PROGRAMMES') {
|
||||
allProgrammes = allProgrammes.filter(prog => isNewProgramme(prog.code))
|
||||
}
|
||||
|
||||
return allProgrammes
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
startedStudyrights,
|
||||
graduatedStudyrights,
|
||||
graduatedStudyrightsByStartYear,
|
||||
bachelorStudyright,
|
||||
transferredInsideFaculty,
|
||||
transferredAway,
|
||||
transferredTo,
|
||||
degreeProgrammesOfFaculty,
|
||||
thesisWriters,
|
||||
findFacultyProgrammeCodes,
|
||||
facultyOgranizationId,
|
||||
statutoryAbsences,
|
||||
getTransferredToAndAway,
|
||||
getTransferredInside,
|
||||
}
|
||||
@@ -0,0 +1,432 @@
|
||||
const { indexOf } = require('lodash')
|
||||
const {
|
||||
startedStudyrights,
|
||||
graduatedStudyrights,
|
||||
transferredInsideFaculty,
|
||||
transferredAway,
|
||||
transferredTo,
|
||||
getTransferredToAndAway,
|
||||
getTransferredInside,
|
||||
} = require('./faculty')
|
||||
const { getStatsBasis, getYearsArray, defineYear } = require('../studyprogrammeHelpers')
|
||||
const { findRightProgramme, isNewProgramme, checkTransfers, getExtentFilter } = require('./facultyHelpers')
|
||||
|
||||
const filterDuplicateStudyrights = studyrights => {
|
||||
// bachelor+master students have two studyrights (separated by two last digits in studyrightid)
|
||||
// choose only the earlier started one, so we don't count start of masters as starting in faculty
|
||||
let rightsToCount = {}
|
||||
|
||||
studyrights.forEach(right => {
|
||||
const id = right.studyrightid.slice(0, -2)
|
||||
const start = new Date(right.studystartdate)
|
||||
if (id in rightsToCount) {
|
||||
if (new Date(rightsToCount[id].studystartdate) > start) {
|
||||
rightsToCount[id] = right
|
||||
}
|
||||
} else {
|
||||
rightsToCount[id] = right
|
||||
}
|
||||
})
|
||||
return Object.values(rightsToCount)
|
||||
}
|
||||
|
||||
const addProgramme = async (
|
||||
stats,
|
||||
programme,
|
||||
tableStats,
|
||||
transferYear,
|
||||
index,
|
||||
allBasics,
|
||||
facultyProgrammes,
|
||||
includeAllSpecials
|
||||
) => {
|
||||
if (!(programme in stats)) {
|
||||
stats[programme] = {}
|
||||
if (includeAllSpecials) {
|
||||
Object.keys(tableStats).forEach(year => (stats[programme][year] = [0, 0, 0]))
|
||||
} else {
|
||||
Object.keys(tableStats).forEach(year => (stats[programme][year] = [0]))
|
||||
}
|
||||
}
|
||||
stats[programme][transferYear][index] += 1
|
||||
|
||||
if (!(programme in allBasics.programmeNames)) {
|
||||
const name = facultyProgrammes.filter(prog => prog.code === programme)[0]?.name
|
||||
allBasics.programmeNames[programme] = name
|
||||
}
|
||||
}
|
||||
|
||||
const getFacultyStarters = async (
|
||||
faculty,
|
||||
transfersToOrAwayStudyrights,
|
||||
insideTransfersStudyrights,
|
||||
since,
|
||||
isAcademicYear,
|
||||
yearsArray,
|
||||
graphStats,
|
||||
tableStats,
|
||||
counts,
|
||||
allBasics,
|
||||
programmeData,
|
||||
programmeFilter,
|
||||
includeAllSpecials
|
||||
) => {
|
||||
const startedGraphStats = [...graphStats]
|
||||
const startedTableStats = { ...tableStats }
|
||||
const studyrightWhere = getExtentFilter(includeAllSpecials)
|
||||
const studyrights = await startedStudyrights(faculty, since, studyrightWhere)
|
||||
let filteredStudyrights = filterDuplicateStudyrights(studyrights)
|
||||
const programmeTableStats = {}
|
||||
if (!includeAllSpecials) {
|
||||
filteredStudyrights = filteredStudyrights.filter(
|
||||
s => !checkTransfers(s, insideTransfersStudyrights, transfersToOrAwayStudyrights)
|
||||
)
|
||||
}
|
||||
|
||||
filteredStudyrights.forEach(({ studystartdate, studyrightElements }) => {
|
||||
const { programme, programmeName } = findRightProgramme(studyrightElements, 'started')
|
||||
|
||||
if (programmeFilter === 'ALL_PROGRAMMES' || isNewProgramme(programme)) {
|
||||
const startYear = defineYear(studystartdate, isAcademicYear)
|
||||
|
||||
startedGraphStats[indexOf(yearsArray, startYear)] += 1
|
||||
startedTableStats[startYear] += 1
|
||||
|
||||
if (!(programme in programmeTableStats)) {
|
||||
programmeTableStats[programme] = { ...tableStats }
|
||||
}
|
||||
programmeTableStats[programme][startYear] += 1
|
||||
|
||||
if (!(programme in allBasics.programmeNames)) {
|
||||
allBasics.programmeNames[programme] = programmeName
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
allBasics.studentInfo.graphStats.push({ name: 'Started studying', data: startedGraphStats })
|
||||
programmeData['started'] = programmeTableStats
|
||||
|
||||
Object.keys(startedTableStats).forEach(year => {
|
||||
counts[year] = [startedTableStats[year]]
|
||||
})
|
||||
}
|
||||
|
||||
const getFacultyGraduates = async (
|
||||
faculty,
|
||||
transfersToOrAwayStudyrights,
|
||||
insideTransfersStudyrights,
|
||||
since,
|
||||
isAcademicYear,
|
||||
yearsArray,
|
||||
graphStats,
|
||||
tableStats,
|
||||
allBasics,
|
||||
counts,
|
||||
countsGraduations,
|
||||
programmeData,
|
||||
programmeFilter,
|
||||
includeAllSpecials
|
||||
) => {
|
||||
const graduatedGraphStats = [[...graphStats], [...graphStats], [...graphStats], [...graphStats], [...graphStats]]
|
||||
const graduatedTableStats = {}
|
||||
|
||||
let studyrightWhere = getExtentFilter(includeAllSpecials)
|
||||
let graduatedRights = await graduatedStudyrights(faculty, since, studyrightWhere)
|
||||
const programmeTableStats = {}
|
||||
|
||||
Object.keys(tableStats).forEach(year => (graduatedTableStats[year] = [0, 0, 0, 0, 0]))
|
||||
if (!includeAllSpecials) {
|
||||
graduatedRights = graduatedRights.filter(
|
||||
s => !checkTransfers(s, insideTransfersStudyrights, transfersToOrAwayStudyrights)
|
||||
)
|
||||
}
|
||||
|
||||
graduatedRights.forEach(({ enddate, extentcode, studyrightElements }) => {
|
||||
const { programme, programmeName } = findRightProgramme(studyrightElements, 'graduated')
|
||||
if (programmeFilter === 'ALL_PROGRAMMES' || isNewProgramme(programme)) {
|
||||
const endYear = defineYear(enddate, isAcademicYear)
|
||||
graduatedGraphStats[0][indexOf(yearsArray, endYear)] += 1
|
||||
graduatedTableStats[endYear][0] += 1
|
||||
|
||||
if (!(programme in programmeTableStats)) {
|
||||
programmeTableStats[programme] = {}
|
||||
Object.keys(tableStats).forEach(year => (programmeTableStats[programme][year] = [0, 0, 0, 0, 0]))
|
||||
}
|
||||
programmeTableStats[programme][endYear][0] += 1
|
||||
|
||||
if (extentcode === 1) {
|
||||
graduatedGraphStats[1][indexOf(yearsArray, endYear)] += 1
|
||||
graduatedTableStats[endYear][1] += 1
|
||||
programmeTableStats[programme][endYear][1] += 1
|
||||
} else if (extentcode === 2) {
|
||||
graduatedGraphStats[2][indexOf(yearsArray, endYear)] += 1
|
||||
graduatedTableStats[endYear][2] += 1
|
||||
programmeTableStats[programme][endYear][2] += 1
|
||||
} else if (extentcode === 4) {
|
||||
graduatedGraphStats[3][indexOf(yearsArray, endYear)] += 1
|
||||
graduatedTableStats[endYear][3] += 1
|
||||
programmeTableStats[programme][endYear][3] += 1
|
||||
} else {
|
||||
graduatedGraphStats[4][indexOf(yearsArray, endYear)] += 1
|
||||
graduatedTableStats[endYear][4] += 1
|
||||
programmeTableStats[programme][endYear][4] += 1
|
||||
}
|
||||
|
||||
if (!(programme in allBasics.programmeNames)) {
|
||||
allBasics.programmeNames[programme] = programmeName
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
allBasics.studentInfo.graphStats.push({ name: 'Graduated', data: graduatedGraphStats[0] })
|
||||
|
||||
allBasics.graduationInfo.graphStats.push({ name: 'Bachelors', data: graduatedGraphStats[1] })
|
||||
allBasics.graduationInfo.graphStats.push({ name: 'Masters', data: graduatedGraphStats[2] })
|
||||
allBasics.graduationInfo.graphStats.push({ name: 'Doctors', data: graduatedGraphStats[3] })
|
||||
allBasics.graduationInfo.graphStats.push({ name: 'Others', data: graduatedGraphStats[4] })
|
||||
|
||||
programmeData['graduated'] = programmeTableStats
|
||||
|
||||
Object.keys(graduatedTableStats).forEach(year => {
|
||||
counts[year] = counts[year].concat(graduatedTableStats[year][0])
|
||||
countsGraduations[year] = graduatedTableStats[year]
|
||||
})
|
||||
}
|
||||
|
||||
const getFacultyTransfers = async (
|
||||
programmes,
|
||||
programmeCodes,
|
||||
allProgrammeCodes,
|
||||
since,
|
||||
isAcademicYear,
|
||||
yearsArray,
|
||||
graphStats,
|
||||
tableStats,
|
||||
allBasics,
|
||||
counts,
|
||||
programmeData,
|
||||
includeAllSpecials
|
||||
) => {
|
||||
const insideTransfers = await transferredInsideFaculty(programmeCodes, allProgrammeCodes, since)
|
||||
// Filter still away those that have started studying before 1.8.2017, when include all specials false.
|
||||
let transferGraphStats = []
|
||||
let transferTableStats = {}
|
||||
if (includeAllSpecials) {
|
||||
transferGraphStats = [[...graphStats], [...graphStats], [...graphStats]]
|
||||
Object.keys(tableStats).forEach(year => (transferTableStats[year] = [0, 0, 0]))
|
||||
} else {
|
||||
transferGraphStats = [[...graphStats]]
|
||||
Object.keys(tableStats).forEach(year => (transferTableStats[year] = [0]))
|
||||
}
|
||||
|
||||
const programmeTableStats = {}
|
||||
// inside transfer is counted towards target
|
||||
for (const { transferdate, targetcode } of insideTransfers) {
|
||||
const transferYear = defineYear(transferdate, isAcademicYear)
|
||||
transferGraphStats[0][indexOf(yearsArray, transferYear)] += 1
|
||||
transferTableStats[transferYear][0] += 1
|
||||
await addProgramme(
|
||||
programmeTableStats,
|
||||
targetcode,
|
||||
tableStats,
|
||||
transferYear,
|
||||
0,
|
||||
allBasics,
|
||||
programmes,
|
||||
includeAllSpecials
|
||||
)
|
||||
}
|
||||
if (includeAllSpecials) {
|
||||
const awayTransfers = await transferredAway(programmeCodes, allProgrammeCodes, since)
|
||||
const toTransfers = await transferredTo(programmeCodes, allProgrammeCodes, since)
|
||||
|
||||
for (const { transferdate, sourcecode } of awayTransfers) {
|
||||
const transferYear = defineYear(transferdate, isAcademicYear)
|
||||
transferGraphStats[1][indexOf(yearsArray, transferYear)] += 1
|
||||
transferTableStats[transferYear][1] += 1
|
||||
await addProgramme(
|
||||
programmeTableStats,
|
||||
sourcecode,
|
||||
tableStats,
|
||||
transferYear,
|
||||
1,
|
||||
allBasics,
|
||||
programmes,
|
||||
includeAllSpecials
|
||||
)
|
||||
}
|
||||
|
||||
for (const { transferdate, targetcode } of toTransfers) {
|
||||
const transferYear = defineYear(transferdate, isAcademicYear)
|
||||
transferGraphStats[2][indexOf(yearsArray, transferYear)] += 1
|
||||
transferTableStats[transferYear][2] += 1
|
||||
await addProgramme(
|
||||
programmeTableStats,
|
||||
targetcode,
|
||||
tableStats,
|
||||
transferYear,
|
||||
2,
|
||||
allBasics,
|
||||
programmes,
|
||||
includeAllSpecials
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
allBasics.studentInfo.graphStats.push({ name: 'Transferred inside', data: transferGraphStats[0] })
|
||||
if (includeAllSpecials) {
|
||||
allBasics.studentInfo.graphStats.push({ name: 'Transferred away', data: transferGraphStats[1] })
|
||||
allBasics.studentInfo.graphStats.push({ name: 'Transferred to', data: transferGraphStats[2] })
|
||||
}
|
||||
programmeData['transferred'] = programmeTableStats
|
||||
|
||||
Object.keys(transferTableStats).forEach(year => {
|
||||
counts[year] = counts[year].concat(transferTableStats[year])
|
||||
})
|
||||
}
|
||||
|
||||
const combineFacultyBasics = async (faculty, programmes, yearType, allProgrammeCodes, programmeFilter, special) => {
|
||||
let counts = {}
|
||||
let countsGraduations = {}
|
||||
let years = []
|
||||
let programmeData = {}
|
||||
const isAcademicYear = yearType === 'ACADEMIC_YEAR'
|
||||
const includeAllSpecials = special === 'SPECIAL_INCLUDED'
|
||||
const since = isAcademicYear ? new Date('2017-08-01') : new Date('2017-01-01')
|
||||
const yearsArray = getYearsArray(since.getFullYear(), isAcademicYear)
|
||||
const { graphStats, tableStats } = getStatsBasis(yearsArray)
|
||||
Object.keys(tableStats).forEach(year => years.push(year))
|
||||
const wantedProgrammeCodes = programmes.map(prog => prog.code)
|
||||
|
||||
let allBasics = {
|
||||
id: faculty,
|
||||
years: [],
|
||||
programmeNames: {},
|
||||
studentInfo: {
|
||||
tableStats: [],
|
||||
graphStats: [],
|
||||
titles: includeAllSpecials
|
||||
? ['', 'Started studying', 'Graduated', 'Transferred inside', 'Transferred away', 'Transferred to']
|
||||
: ['', 'Started studying', 'Graduated', 'Transferred inside'],
|
||||
programmeTableStats: {},
|
||||
},
|
||||
graduationInfo: {
|
||||
tableStats: [],
|
||||
graphStats: [],
|
||||
titles: ['', 'All', 'Bachelors', 'Masters', 'Doctors', 'Others'],
|
||||
programmeTableStats: {},
|
||||
},
|
||||
}
|
||||
const transfersToAwayStudyrights = await getTransferredToAndAway(wantedProgrammeCodes, allProgrammeCodes, since)
|
||||
const transferInsideStudyrights = await getTransferredInside(wantedProgrammeCodes, allProgrammeCodes, since)
|
||||
// Started studying in faculty
|
||||
await getFacultyStarters(
|
||||
faculty,
|
||||
transfersToAwayStudyrights,
|
||||
transferInsideStudyrights,
|
||||
since,
|
||||
isAcademicYear,
|
||||
yearsArray,
|
||||
graphStats,
|
||||
tableStats,
|
||||
counts,
|
||||
allBasics,
|
||||
programmeData,
|
||||
programmeFilter,
|
||||
includeAllSpecials
|
||||
)
|
||||
|
||||
// Graduated in faculty
|
||||
await getFacultyGraduates(
|
||||
faculty,
|
||||
transfersToAwayStudyrights,
|
||||
transferInsideStudyrights,
|
||||
since,
|
||||
isAcademicYear,
|
||||
yearsArray,
|
||||
graphStats,
|
||||
tableStats,
|
||||
allBasics,
|
||||
counts,
|
||||
countsGraduations,
|
||||
programmeData,
|
||||
programmeFilter,
|
||||
includeAllSpecials
|
||||
)
|
||||
|
||||
// Transfers
|
||||
await getFacultyTransfers(
|
||||
programmes,
|
||||
wantedProgrammeCodes,
|
||||
allProgrammeCodes,
|
||||
since,
|
||||
isAcademicYear,
|
||||
yearsArray,
|
||||
graphStats,
|
||||
tableStats,
|
||||
allBasics,
|
||||
counts,
|
||||
programmeData,
|
||||
includeAllSpecials
|
||||
)
|
||||
|
||||
// combine tableStats from all categories
|
||||
allBasics.years = years
|
||||
years.forEach(year => {
|
||||
allBasics.studentInfo.tableStats.push([year, ...counts[year]])
|
||||
allBasics.graduationInfo.tableStats.push([year, ...countsGraduations[year]])
|
||||
})
|
||||
allBasics.studentInfo.tableStats.reverse()
|
||||
allBasics.graduationInfo.tableStats.reverse()
|
||||
|
||||
//combine programme level tablestats
|
||||
// all programmes are not present in all data types, check all found programmes and patch missing details with 0
|
||||
|
||||
let allCodes = Object.keys(programmeData.started).concat(
|
||||
Object.keys(programmeData.graduated),
|
||||
Object.keys(programmeData.transferred)
|
||||
)
|
||||
allCodes = [...new Set(allCodes)]
|
||||
|
||||
let studentInfo = {}
|
||||
let graduationInfo = {}
|
||||
|
||||
allCodes.forEach(code => {
|
||||
if (!(code in studentInfo)) studentInfo[code] = {}
|
||||
|
||||
for (const year of years) {
|
||||
// started
|
||||
if (code in programmeData.started) {
|
||||
studentInfo[code][year] = [year, programmeData.started[code][year]]
|
||||
} else {
|
||||
studentInfo[code][year] = [year, 0]
|
||||
}
|
||||
// graduated total
|
||||
if (code in programmeData.graduated) {
|
||||
studentInfo[code][year] = studentInfo[code][year].concat([programmeData.graduated[code][year][0]])
|
||||
if (!(code in graduationInfo)) graduationInfo[code] = {}
|
||||
graduationInfo[code][year] = [year, ...programmeData.graduated[code][year]]
|
||||
} else {
|
||||
studentInfo[code][year] = studentInfo[code][year].concat([0])
|
||||
}
|
||||
// trasferred
|
||||
if (code in programmeData.transferred) {
|
||||
studentInfo[code][year] = studentInfo[code][year].concat(programmeData.transferred[code][year])
|
||||
} else {
|
||||
studentInfo[code][year] = studentInfo[code][year].concat([0, 0, 0])
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
Object.keys(studentInfo).forEach(prog => {
|
||||
allBasics.studentInfo.programmeTableStats[prog] = [...Object.values(studentInfo[prog])].reverse()
|
||||
})
|
||||
|
||||
Object.keys(graduationInfo).forEach(prog => {
|
||||
allBasics.graduationInfo.programmeTableStats[prog] = Object.values(graduationInfo[prog]).reverse()
|
||||
})
|
||||
|
||||
return allBasics
|
||||
}
|
||||
|
||||
module.exports = { combineFacultyBasics }
|
||||
+85
-42
@@ -1,5 +1,5 @@
|
||||
const { indexOf } = require('lodash')
|
||||
const { mapToProviders } = require('../util/utils')
|
||||
const { mapToProviders } = require('../../util/utils')
|
||||
const {
|
||||
getStatsBasis,
|
||||
defineYear,
|
||||
@@ -8,14 +8,13 @@ const {
|
||||
getYearsArray,
|
||||
getStartDate,
|
||||
isSpecialGroupCredit,
|
||||
tableTitles,
|
||||
} = require('./studyprogrammeHelpers')
|
||||
} = require('../studyprogrammeHelpers')
|
||||
const {
|
||||
getStudyRights,
|
||||
getCreditsForStudyProgramme,
|
||||
allTransfers,
|
||||
getCourseCodesForStudyProgramme,
|
||||
} = require('./studyprogramme')
|
||||
} = require('../studyprogramme')
|
||||
|
||||
const isFacultyNonMajorCredit = (studyrights, attainment_date, facultyProgrammes) => {
|
||||
let right = ''
|
||||
@@ -56,8 +55,8 @@ const getFacultyRegularCreditStats = async ({
|
||||
const credits = await getCreditsForStudyProgramme(courses, since)
|
||||
const students = [...new Set(credits.map(({ student_studentnumber }) => student_studentnumber))]
|
||||
|
||||
let studyrights = await getStudyRights(students, since)
|
||||
const transfers = (await allTransfers(studyprogramme, since)).map(t => t.studyrightid)
|
||||
let studyrights = await getStudyRights(students)
|
||||
const transfers = (await allTransfers(studyprogramme, since)).map(t => t.studyrightid).filter(s => s !== 'undefined')
|
||||
if (!includeAllSpecials) {
|
||||
studyrights = studyrights.filter(s => !transfers.includes(s.studyrightid))
|
||||
}
|
||||
@@ -96,7 +95,6 @@ const getFacultyRegularCreditStats = async ({
|
||||
nonDegree.tableStats[attainmentYear] += credits || 0
|
||||
}
|
||||
})
|
||||
|
||||
if (
|
||||
majors.graphStats.every(year => year === 0) &&
|
||||
facultyNonMajors.graphStats.every(year => year === 0) &&
|
||||
@@ -116,7 +114,7 @@ const getFacultyRegularCreditStats = async ({
|
||||
|
||||
// Fetches all credits for the studytrack and combines them into statistics
|
||||
// These are for the faculty view which differs from study programme overview
|
||||
// (no tranfers, non majors split to in- and outside-faculty)
|
||||
// (no transfers, non majors split to in- and outside-faculty)
|
||||
const getFacultyCreditStatsForStudytrack = async ({ studyprogramme, facultyProgrammes, settings }) => {
|
||||
const { isAcademicYear, includeAllSpecials } = settings
|
||||
const since = getStartDate(studyprogramme, isAcademicYear)
|
||||
@@ -126,7 +124,6 @@ const getFacultyCreditStatsForStudytrack = async ({ studyprogramme, facultyProgr
|
||||
const { majors, facultyNonMajors, otherNonMajors, nonDegree } = await getFacultyRegularCreditStats(queryParameters)
|
||||
|
||||
const reversedYears = getYearsArray(since.getFullYear(), isAcademicYear).reverse()
|
||||
const titles = tableTitles['credits'][includeAllSpecials ? 'SPECIAL_INCLUDED' : 'SPECIAL_EXCLUDED']
|
||||
const dataFound = [majors, facultyNonMajors, otherNonMajors].some(d => d.graphStats.length) // , transferred
|
||||
|
||||
if (!dataFound) return null
|
||||
@@ -144,48 +141,44 @@ const getFacultyCreditStatsForStudytrack = async ({ studyprogramme, facultyProgr
|
||||
otherNonMajors.tableStats[year],
|
||||
nonDegree.tableStats[year],
|
||||
]
|
||||
: [
|
||||
year,
|
||||
majors.tableStats[year] +
|
||||
facultyNonMajors.tableStats[year] +
|
||||
otherNonMajors.tableStats[year] +
|
||||
nonDegree.tableStats[year],
|
||||
majors.tableStats[year],
|
||||
]
|
||||
: [year, majors.tableStats[year], majors.tableStats[year]]
|
||||
)
|
||||
|
||||
const graphStats = [
|
||||
{
|
||||
name: 'Major students credits',
|
||||
data: majors.graphStats,
|
||||
},
|
||||
{
|
||||
name: 'Non-major faculty students credits',
|
||||
data: facultyNonMajors.graphStats,
|
||||
},
|
||||
{
|
||||
name: 'Non-major other faculty students credits 2',
|
||||
data: otherNonMajors.graphStats,
|
||||
},
|
||||
{
|
||||
name: 'Non-degree credits',
|
||||
data: nonDegree.graphStats,
|
||||
},
|
||||
]
|
||||
const graphStats = includeAllSpecials
|
||||
? [
|
||||
{
|
||||
name: 'Major students credits',
|
||||
data: majors.graphStats,
|
||||
},
|
||||
{
|
||||
name: 'Non-major faculty students credits',
|
||||
data: facultyNonMajors.graphStats,
|
||||
},
|
||||
{
|
||||
name: 'Non-major other faculty students credits 2',
|
||||
data: otherNonMajors.graphStats,
|
||||
},
|
||||
{
|
||||
name: 'Non-degree credits',
|
||||
data: nonDegree.graphStats,
|
||||
},
|
||||
]
|
||||
: [
|
||||
{
|
||||
name: 'Major students credits',
|
||||
data: majors.graphStats,
|
||||
},
|
||||
]
|
||||
|
||||
return {
|
||||
id: studyprogramme,
|
||||
years,
|
||||
tableStats,
|
||||
graphStats,
|
||||
titles,
|
||||
}
|
||||
}
|
||||
|
||||
const getProgrammeCredits = async (code, yearType, specialGroups, facultyProgrammes) => {
|
||||
// redis for faculty? Create faculty versions for out-commented functions
|
||||
// const data = await getCreditStats(code, yearType, specialGroups)
|
||||
// if (data) return data
|
||||
const updatedStats = await getFacultyCreditStatsForStudytrack({
|
||||
studyprogramme: code,
|
||||
facultyProgrammes,
|
||||
@@ -195,14 +188,40 @@ const getProgrammeCredits = async (code, yearType, specialGroups, facultyProgram
|
||||
},
|
||||
})
|
||||
|
||||
// if (updatedStats) await setCreditStats(updatedStats, yearType, specialGroups)
|
||||
return updatedStats
|
||||
}
|
||||
|
||||
const combineFacultyCredits = async (allCredits, programmes, yearType, specialGroups, counts, years) => {
|
||||
const combineFacultyCredits = async (faculty, programmes, yearType, specialGroups) => {
|
||||
let counts = {}
|
||||
let years = []
|
||||
const titles =
|
||||
specialGroups === 'SPECIAL_INCLUDED'
|
||||
? [
|
||||
'',
|
||||
'Total',
|
||||
'Major students credits',
|
||||
'Non-major faculty students credits',
|
||||
'Non-major other faculty students credits',
|
||||
'Non-degree student credits',
|
||||
]
|
||||
: ['', 'Total', 'Major students credits']
|
||||
|
||||
let allCredits = {
|
||||
id: faculty,
|
||||
years: [],
|
||||
tableStats: [],
|
||||
graphStats: [],
|
||||
programmeTableStats: {},
|
||||
programmeNames: {},
|
||||
titles,
|
||||
}
|
||||
|
||||
const progCodes = programmes.map(p => p.code)
|
||||
for (const prog of programmes) {
|
||||
const data = await getProgrammeCredits(prog, yearType, specialGroups, programmes)
|
||||
const data = await getProgrammeCredits(prog.code, yearType, specialGroups, progCodes)
|
||||
if (data) {
|
||||
allCredits.programmeNames[prog.code] = prog.name
|
||||
allCredits.programmeTableStats[prog.code] = data.tableStats
|
||||
if (!allCredits.lastUpdated || new Date(data.lastUpdated) > new Date(allCredits.lastUpdated))
|
||||
allCredits.lastUpdated = data.lastUpdated
|
||||
|
||||
@@ -225,6 +244,30 @@ const combineFacultyCredits = async (allCredits, programmes, yearType, specialGr
|
||||
|
||||
years.sort()
|
||||
allCredits.years = years
|
||||
|
||||
let majors = []
|
||||
let facultyNonMajor = []
|
||||
let otherNonMajor = []
|
||||
let nonDegree = []
|
||||
|
||||
years.forEach(year => {
|
||||
majors.push(counts[year][1])
|
||||
facultyNonMajor.push(counts[year][2])
|
||||
otherNonMajor.push(counts[year][3])
|
||||
nonDegree.push(counts[year][4])
|
||||
})
|
||||
|
||||
allCredits.graphStats =
|
||||
specialGroups === 'SPECIAL_INCLUDED'
|
||||
? [
|
||||
{ name: 'Major students credits', data: majors },
|
||||
{ name: 'Non-major faculty students credits', data: facultyNonMajor },
|
||||
{ name: 'Non-major other faculty students credits', data: otherNonMajor },
|
||||
{ name: 'Non-degree credits', data: nonDegree },
|
||||
]
|
||||
: [{ name: 'Major students credits', data: majors }]
|
||||
|
||||
return allCredits
|
||||
}
|
||||
|
||||
module.exports = { combineFacultyCredits, getFacultyCreditStatsForStudytrack }
|
||||
@@ -0,0 +1,338 @@
|
||||
const moment = require('moment')
|
||||
const {
|
||||
graduatedStudyrights,
|
||||
graduatedStudyrightsByStartYear,
|
||||
bachelorStudyright,
|
||||
statutoryAbsences,
|
||||
} = require('./faculty')
|
||||
const { findRightProgramme, isNewProgramme } = require('./facultyHelpers')
|
||||
const { getYearsArray, getYearsObject, getMean, getMedian, defineYear } = require('../studyprogrammeHelpers')
|
||||
|
||||
const findBachelorStartdate = async id => {
|
||||
const studyright = await bachelorStudyright(id)
|
||||
if (studyright) return studyright.studystartdate
|
||||
return null
|
||||
}
|
||||
|
||||
const getProgrammeObjectBasis = (years, levels) => {
|
||||
return levels.reduce(
|
||||
(acc, level) => ({ ...acc, [level]: years.reduce((acc, year) => ({ ...acc, [year]: {} }), {}) }),
|
||||
{}
|
||||
)
|
||||
}
|
||||
|
||||
const getStatutoryAbsences = async (studentnumber, startdate, enddate) => {
|
||||
const absences = await statutoryAbsences(studentnumber, startdate, enddate)
|
||||
if (absences.length) {
|
||||
const absentMonths = absences.reduce((sum, ab) => sum + moment(ab.end).diff(moment(ab.start), 'months'), 0)
|
||||
return absentMonths
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
const countTimeCategories = (times, goal) => {
|
||||
const statistics = { onTime: 0, yearOver: 0, wayOver: 0 }
|
||||
|
||||
times.forEach(time => {
|
||||
if (time <= goal) statistics.onTime += 1
|
||||
else if (time <= goal + 12) statistics.yearOver += 1
|
||||
else statistics.wayOver += 1
|
||||
})
|
||||
return statistics
|
||||
}
|
||||
|
||||
const addGraduation = async (
|
||||
extentcode,
|
||||
startdate,
|
||||
studyrightid,
|
||||
enddate,
|
||||
studentnumber,
|
||||
graduationAmounts,
|
||||
graduationTimes,
|
||||
year,
|
||||
programmes,
|
||||
programme
|
||||
) => {
|
||||
let actualStartdate = startdate
|
||||
let level = null
|
||||
|
||||
if (extentcode == 1) {
|
||||
level = 'bachelor'
|
||||
} else if (extentcode === 2) {
|
||||
if (studyrightid.slice(-2) === '-2') {
|
||||
level = 'bcMsCombo'
|
||||
actualStartdate = await findBachelorStartdate(studyrightid.replace(/-2$/, '-1'))
|
||||
} else {
|
||||
level = 'master'
|
||||
}
|
||||
} else if (extentcode === 4) {
|
||||
level = 'doctor'
|
||||
} else if (extentcode === 3) {
|
||||
level = 'licentiate'
|
||||
} else {
|
||||
return
|
||||
}
|
||||
|
||||
const absoluteTimeToGraduation = moment(enddate).diff(moment(actualStartdate), 'months')
|
||||
const statutoryAbsences = await getStatutoryAbsences(studentnumber, actualStartdate, enddate)
|
||||
const timeToGraduation = absoluteTimeToGraduation - statutoryAbsences
|
||||
|
||||
graduationAmounts[level][year] += 1
|
||||
graduationTimes[level][year] = [...graduationTimes[level][year], timeToGraduation]
|
||||
|
||||
if (!(programme in programmes[level][year]))
|
||||
programmes[level][year][programme] = { graduationTimes: [], graduationAmounts: 0 }
|
||||
|
||||
programmes[level][year][programme].graduationAmounts += 1
|
||||
programmes[level][year][programme].graduationTimes = [
|
||||
...programmes[level][year][programme].graduationTimes,
|
||||
timeToGraduation,
|
||||
]
|
||||
}
|
||||
|
||||
const countByStartYear = async (faculty, since, years, yearsList, levels, programmeFilter, programmeNames, goals) => {
|
||||
let graduationAmounts = {}
|
||||
let graduationTimes = {}
|
||||
let programmes = getProgrammeObjectBasis(yearsList, levels)
|
||||
let byStartYear = {
|
||||
means: {},
|
||||
medians: {},
|
||||
programmes: {
|
||||
medians: getProgrammeObjectBasis(yearsList, levels),
|
||||
means: getProgrammeObjectBasis(yearsList, levels),
|
||||
},
|
||||
}
|
||||
levels.forEach(level => {
|
||||
graduationAmounts[level] = getYearsObject({ years: yearsList })
|
||||
graduationTimes[level] = getYearsObject({ years: yearsList, emptyArrays: true })
|
||||
byStartYear.medians[level] = []
|
||||
byStartYear.means[level] = []
|
||||
})
|
||||
|
||||
const graduatedRights = await graduatedStudyrightsByStartYear(faculty, since)
|
||||
for (const right of graduatedRights) {
|
||||
const { enddate, startdate, studyrightid, extentcode, studyrightElements, studentnumber } = right
|
||||
const { programme, programmeName } = findRightProgramme(studyrightElements, 'graduated')
|
||||
if (programmeFilter === 'NEW_STUDY_PROGRAMMES' && !isNewProgramme(programme)) continue
|
||||
|
||||
if (!(programme in programmeNames)) {
|
||||
programmeNames[programme] = programmeName
|
||||
}
|
||||
|
||||
const startYear = defineYear(startdate, false)
|
||||
await addGraduation(
|
||||
extentcode,
|
||||
startdate,
|
||||
studyrightid,
|
||||
enddate,
|
||||
studentnumber,
|
||||
graduationAmounts,
|
||||
graduationTimes,
|
||||
startYear,
|
||||
programmes,
|
||||
programme
|
||||
)
|
||||
}
|
||||
|
||||
years.forEach(year => {
|
||||
levels.forEach(level => {
|
||||
const median = getMedian(graduationTimes[level][year])
|
||||
const mean = getMean(graduationTimes[level][year])
|
||||
const statistics = countTimeCategories(graduationTimes[level][year], goals[level])
|
||||
byStartYear.medians[level] = [
|
||||
...byStartYear.medians[level],
|
||||
{ y: median, amount: graduationAmounts[level][year], statistics },
|
||||
]
|
||||
byStartYear.means[level] = [
|
||||
...byStartYear.means[level],
|
||||
{ y: mean, amount: graduationAmounts[level][year], statistics },
|
||||
]
|
||||
|
||||
// Programme level breakdown
|
||||
byStartYear.programmes.medians[level][year] = { programmes: [], data: [] }
|
||||
byStartYear.programmes.means[level][year] = { programmes: [], data: [] }
|
||||
const programmeCodes = Object.keys(programmes[level][year])
|
||||
programmeCodes.sort()
|
||||
|
||||
if (programmeCodes.length > 0) {
|
||||
for (const prog of programmeCodes) {
|
||||
const progMedian = getMedian(programmes[level][year][prog].graduationTimes)
|
||||
const progMean = getMean(programmes[level][year][prog].graduationTimes)
|
||||
const progStatistics = countTimeCategories(programmes[level][year][prog].graduationTimes, goals[level])
|
||||
|
||||
byStartYear.programmes.medians[level][year].programmes = [
|
||||
...byStartYear.programmes.medians[level][year].programmes,
|
||||
prog,
|
||||
]
|
||||
byStartYear.programmes.medians[level][year].data = [
|
||||
...byStartYear.programmes.medians[level][year].data,
|
||||
{ y: progMedian, amount: programmes[level][year][prog].graduationAmounts, statistics: progStatistics },
|
||||
]
|
||||
|
||||
byStartYear.programmes.means[level][year].programmes = [
|
||||
...byStartYear.programmes.means[level][year].programmes,
|
||||
prog,
|
||||
]
|
||||
byStartYear.programmes.means[level][year].data = [
|
||||
...byStartYear.programmes.means[level][year].data,
|
||||
{ y: progMean, amount: programmes[level][year][prog].graduationAmounts, statistics: progStatistics },
|
||||
]
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
return byStartYear
|
||||
}
|
||||
|
||||
const countByGraduationYear = async (
|
||||
faculty,
|
||||
since,
|
||||
years,
|
||||
yearsList,
|
||||
levels,
|
||||
programmeFilter,
|
||||
programmeNames,
|
||||
goals
|
||||
) => {
|
||||
let graduationAmounts = {}
|
||||
let graduationTimes = {}
|
||||
let programmes = getProgrammeObjectBasis(yearsList, levels)
|
||||
let byGradYear = {
|
||||
means: {},
|
||||
medians: {},
|
||||
programmes: {
|
||||
medians: getProgrammeObjectBasis(yearsList, levels),
|
||||
means: getProgrammeObjectBasis(yearsList, levels),
|
||||
},
|
||||
}
|
||||
|
||||
levels.forEach(level => {
|
||||
graduationAmounts[level] = getYearsObject({ years: yearsList })
|
||||
graduationTimes[level] = getYearsObject({ years: yearsList, emptyArrays: true })
|
||||
byGradYear.medians[level] = []
|
||||
byGradYear.means[level] = []
|
||||
})
|
||||
|
||||
const graduatedRights = await graduatedStudyrights(faculty, since)
|
||||
|
||||
for (const right of graduatedRights) {
|
||||
const { enddate, startdate, studyrightid, extentcode, studyrightElements, studentnumber } = right
|
||||
const { programme, programmeName } = findRightProgramme(studyrightElements, 'graduated')
|
||||
if (programmeFilter === 'NEW_STUDY_PROGRAMMES' && !isNewProgramme(programme)) continue
|
||||
|
||||
if (!(programme in programmeNames)) {
|
||||
programmeNames[programme] = programmeName
|
||||
}
|
||||
|
||||
const graduationYear = defineYear(enddate, false)
|
||||
await addGraduation(
|
||||
extentcode,
|
||||
startdate,
|
||||
studyrightid,
|
||||
enddate,
|
||||
studentnumber,
|
||||
graduationAmounts,
|
||||
graduationTimes,
|
||||
graduationYear,
|
||||
programmes,
|
||||
programme
|
||||
)
|
||||
}
|
||||
|
||||
years.forEach(year => {
|
||||
levels.forEach(level => {
|
||||
const median = getMedian(graduationTimes[level][year])
|
||||
const mean = getMean(graduationTimes[level][year])
|
||||
const statistics = countTimeCategories(graduationTimes[level][year], goals[level])
|
||||
byGradYear.medians[level] = [
|
||||
...byGradYear.medians[level],
|
||||
{ y: median, amount: graduationAmounts[level][year], statistics },
|
||||
]
|
||||
byGradYear.means[level] = [
|
||||
...byGradYear.means[level],
|
||||
{ y: mean, amount: graduationAmounts[level][year], statistics },
|
||||
]
|
||||
|
||||
// Programme level breakdown
|
||||
byGradYear.programmes.medians[level][year] = { programmes: [], data: [] }
|
||||
byGradYear.programmes.means[level][year] = { programmes: [], data: [] }
|
||||
const programmeCodes = Object.keys(programmes[level][year])
|
||||
programmeCodes.sort()
|
||||
|
||||
if (programmeCodes.length > 0) {
|
||||
for (const prog of programmeCodes) {
|
||||
const progMedian = getMedian(programmes[level][year][prog].graduationTimes)
|
||||
const progMean = getMean(programmes[level][year][prog].graduationTimes)
|
||||
const progStatistics = countTimeCategories(programmes[level][year][prog].graduationTimes, goals[level])
|
||||
|
||||
byGradYear.programmes.medians[level][year].programmes = [
|
||||
...byGradYear.programmes.medians[level][year].programmes,
|
||||
prog,
|
||||
]
|
||||
byGradYear.programmes.medians[level][year].data = [
|
||||
...byGradYear.programmes.medians[level][year].data,
|
||||
{ y: progMedian, amount: programmes[level][year][prog].graduationAmounts, statistics: progStatistics },
|
||||
]
|
||||
|
||||
byGradYear.programmes.means[level][year].programmes = [
|
||||
...byGradYear.programmes.means[level][year].programmes,
|
||||
prog,
|
||||
]
|
||||
byGradYear.programmes.means[level][year].data = [
|
||||
...byGradYear.programmes.means[level][year].data,
|
||||
{ y: progMean, amount: programmes[level][year][prog].graduationAmounts, statistics: progStatistics },
|
||||
]
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
return byGradYear
|
||||
}
|
||||
|
||||
const countGraduationTimes = async (faculty, programmeFilter) => {
|
||||
const isAcademicYear = false // = yearType === 'ACADEMIC_YEAR'
|
||||
const since = isAcademicYear ? new Date('2017-08-01') : new Date('2017-01-01')
|
||||
const yearsList = getYearsArray(since.getFullYear(), isAcademicYear)
|
||||
const levels = ['bachelor', 'bcMsCombo', 'master', 'doctor', 'licentiate']
|
||||
const programmeNames = {}
|
||||
|
||||
const years = [...yearsList].reverse()
|
||||
const goals = {
|
||||
bachelor: 36,
|
||||
bcMsCombo: 60,
|
||||
master: 24,
|
||||
doctor: 48,
|
||||
// Fix differrent cases?
|
||||
licentiate: 78,
|
||||
}
|
||||
|
||||
// We count studyrights (vs. studyright_elements)
|
||||
// This way we get the whole time for a degree, even if the student was transferred to a new programme
|
||||
// E.g. started 8/2016 in old Bc, transferred to new 10/2020, graduated from new 1/2021 --> total 53 months (not 3)
|
||||
const byGradYear = await countByGraduationYear(
|
||||
faculty,
|
||||
since,
|
||||
years,
|
||||
yearsList,
|
||||
levels,
|
||||
programmeFilter,
|
||||
programmeNames,
|
||||
goals
|
||||
)
|
||||
const byStartYear = await countByStartYear(
|
||||
faculty,
|
||||
since,
|
||||
years,
|
||||
yearsList,
|
||||
levels,
|
||||
programmeFilter,
|
||||
programmeNames,
|
||||
goals
|
||||
)
|
||||
|
||||
return { years, goals, byGradYear, byStartYear, programmeNames }
|
||||
}
|
||||
|
||||
module.exports = { countGraduationTimes }
|
||||
@@ -0,0 +1,137 @@
|
||||
const Sequelize = require('sequelize')
|
||||
const { Op } = Sequelize
|
||||
|
||||
const findRightProgramme = (studyrightElements, mode) => {
|
||||
let programme = ''
|
||||
let programmeName = ''
|
||||
let studyRightElement = null
|
||||
|
||||
if (studyrightElements) {
|
||||
if (mode === 'started') {
|
||||
studyRightElement = studyrightElements
|
||||
.filter(sre => sre.element_detail.type === 20)
|
||||
.sort((a, b) => new Date(a.startdate) - new Date(b.startdate))[0]
|
||||
// this way round counts to old programmes, other way around to new programmes
|
||||
} else {
|
||||
// graduated
|
||||
studyRightElement = studyrightElements
|
||||
.filter(sre => sre.element_detail.type === 20)
|
||||
.sort((a, b) => new Date(b.startdate) - new Date(a.startdate))[0]
|
||||
}
|
||||
if (studyRightElement) {
|
||||
programme = studyRightElement.code
|
||||
programmeName = studyRightElement.element_detail.name
|
||||
}
|
||||
}
|
||||
return { programme, programmeName }
|
||||
}
|
||||
|
||||
const facultyFormatStudyright = studyright => {
|
||||
const {
|
||||
studyrightid,
|
||||
studystartdate,
|
||||
enddate,
|
||||
givendate,
|
||||
graduated,
|
||||
active,
|
||||
prioritycode,
|
||||
extentcode,
|
||||
student,
|
||||
studyright_elements,
|
||||
startdate,
|
||||
} = studyright
|
||||
|
||||
return {
|
||||
studyrightid,
|
||||
studystartdate,
|
||||
startdate,
|
||||
enddate,
|
||||
givendate,
|
||||
graduated,
|
||||
active,
|
||||
prioritycode,
|
||||
extentcode,
|
||||
studentnumber: student.studentnumber,
|
||||
studyrightElements: studyright_elements,
|
||||
}
|
||||
}
|
||||
|
||||
const formatFacultyTransfer = transfer => {
|
||||
const { sourcecode, targetcode, transferdate, studyrightid } = transfer
|
||||
return {
|
||||
sourcecode,
|
||||
targetcode,
|
||||
transferdate,
|
||||
studyrightid,
|
||||
}
|
||||
}
|
||||
|
||||
const facultyFormatProgramme = programme => {
|
||||
const { code, name } = programme
|
||||
return { code, name }
|
||||
}
|
||||
|
||||
const formatFacultyThesisWriter = credit => {
|
||||
const { course_code, credits, attainment_date, student_studentnumber, course } = credit
|
||||
return {
|
||||
course_code,
|
||||
credits,
|
||||
attainment_date,
|
||||
student_studentnumber,
|
||||
courseUnitType: course.course_unit_type,
|
||||
}
|
||||
}
|
||||
|
||||
const formatOrganization = org => {
|
||||
const { id, name, code, parent_id } = org
|
||||
return { id, name, code, parentId: parent_id }
|
||||
}
|
||||
|
||||
const formatAbsence = absence => {
|
||||
const { semestercode, semester } = absence
|
||||
return { semestercode, start: semester.startdate, end: semester.enddate }
|
||||
}
|
||||
|
||||
const newProgrammes = [/^KH/, /^MH/, /^T/, /^LI/, /^K-/, /^FI/, /^00901$/, /^00910$/]
|
||||
|
||||
const isNewProgramme = code => {
|
||||
for (let i = 0; i < newProgrammes.length; i++) {
|
||||
if (newProgrammes[i].test(code)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
const checkTransfers = (s, insideTransfersStudyrights, transfersToOrAwayStudyrights) => {
|
||||
if (transfersToOrAwayStudyrights.includes(s.studyrightid)) return true
|
||||
if (insideTransfersStudyrights.includes(s.studyrightid) && new Date(s.studystartdate) < new Date('2017-08-01'))
|
||||
return true
|
||||
return false
|
||||
}
|
||||
|
||||
const getExtentFilter = includeAllSpecials => {
|
||||
const filteredExtents = [16] // always filter out secondary subject students
|
||||
if (!includeAllSpecials) {
|
||||
filteredExtents.push(7, 34, 22, 99, 14, 13, 9)
|
||||
}
|
||||
let studyrightWhere = {
|
||||
extentcode: {
|
||||
[Op.notIn]: filteredExtents,
|
||||
},
|
||||
}
|
||||
return studyrightWhere
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
findRightProgramme,
|
||||
facultyFormatStudyright,
|
||||
facultyFormatProgramme,
|
||||
formatFacultyTransfer,
|
||||
formatFacultyThesisWriter,
|
||||
formatOrganization,
|
||||
formatAbsence,
|
||||
isNewProgramme,
|
||||
checkTransfers,
|
||||
getExtentFilter,
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
const { redisClient } = require('../redis')
|
||||
const moment = require('moment')
|
||||
|
||||
const createRedisKeyForFacultyProgrammes = (id, programmeFilter) => `FACULTY_PROGRAMMES_${id}_${programmeFilter}`
|
||||
const createRedisKeyForBasicStats = (id, yearType, programmeFilter, specialGroups) =>
|
||||
`FACULTY_BASIC_STATS_${id}_${yearType}_${programmeFilter}_${specialGroups}`
|
||||
const createRedisKeyForCreditStats = (id, yearType, programmeFilter, specialGroups) =>
|
||||
`FACULTY_CREDIT_STATS_${id}_${yearType}_${programmeFilter}_${specialGroups}`
|
||||
// const createRedisKeyForGraduationTimeStats = (id, mode, programmeFilter) =>
|
||||
// `FACULTY_GRADUATION_TIME_STATS_${id}_${mode}__${programmeFilter}`
|
||||
const createRedisKeyForThesiswriters = (id, yearType, programmeFilter, specialGroups) =>
|
||||
`FACULTY_THESIS_WRITERS_STATS_${id}_${yearType}_${programmeFilter}_${specialGroups}`
|
||||
|
||||
const setFacultyProgrammes = async (id, data, programmeFilter) => {
|
||||
const redisKey = createRedisKeyForFacultyProgrammes(id, programmeFilter)
|
||||
const dataToRedis = {
|
||||
data: data,
|
||||
status: 'DONE',
|
||||
lastUpdated: moment().format(),
|
||||
}
|
||||
const setOperationStatus = await redisClient.setAsync(redisKey, JSON.stringify(dataToRedis))
|
||||
if (setOperationStatus !== 'OK') return null
|
||||
return dataToRedis
|
||||
}
|
||||
|
||||
const getFacultyProgrammes = async (id, programmeFilter) => {
|
||||
const redisKey = createRedisKeyForFacultyProgrammes(id, programmeFilter)
|
||||
const dataFromRedis = await redisClient.getAsync(redisKey)
|
||||
if (!dataFromRedis) return null
|
||||
return JSON.parse(dataFromRedis)
|
||||
}
|
||||
|
||||
const setBasicStats = async (data, yearType, programmeFilter, specialGroups) => {
|
||||
const { id } = data
|
||||
const redisKey = createRedisKeyForBasicStats(id, yearType, programmeFilter, specialGroups)
|
||||
const dataToRedis = {
|
||||
...data,
|
||||
status: 'DONE',
|
||||
lastUpdated: moment().format(),
|
||||
}
|
||||
const setOperationStatus = await redisClient.setAsync(redisKey, JSON.stringify(dataToRedis))
|
||||
if (setOperationStatus !== 'OK') return null
|
||||
return dataToRedis
|
||||
}
|
||||
|
||||
const getBasicStats = async (id, yearType, programmeFilter, specialGroups) => {
|
||||
const redisKey = createRedisKeyForBasicStats(id, yearType, programmeFilter, specialGroups)
|
||||
const dataFromRedis = await redisClient.getAsync(redisKey)
|
||||
if (!dataFromRedis) return null
|
||||
return JSON.parse(dataFromRedis)
|
||||
}
|
||||
|
||||
const setCreditStats = async (data, yearType, programmeFilter, specialGroups) => {
|
||||
const { id } = data
|
||||
const redisKey = createRedisKeyForCreditStats(id, yearType, programmeFilter, specialGroups)
|
||||
const dataToRedis = {
|
||||
...data,
|
||||
status: 'DONE',
|
||||
lastUpdated: moment().format(),
|
||||
}
|
||||
const setOperationStatus = await redisClient.setAsync(redisKey, JSON.stringify(dataToRedis))
|
||||
if (setOperationStatus !== 'OK') return null
|
||||
return dataToRedis
|
||||
}
|
||||
|
||||
const getCreditStats = async (id, yearType, programmeFilter, specialGroups) => {
|
||||
const redisKey = createRedisKeyForCreditStats(id, yearType, programmeFilter, specialGroups)
|
||||
const dataFromRedis = await redisClient.getAsync(redisKey)
|
||||
if (!dataFromRedis) return null
|
||||
return JSON.parse(dataFromRedis)
|
||||
}
|
||||
|
||||
const setThesisWritersStats = async (data, yearType, programmeFilter, specialGroups) => {
|
||||
const { id } = data
|
||||
const redisKey = createRedisKeyForThesiswriters(id, yearType, programmeFilter, specialGroups)
|
||||
const dataToRedis = {
|
||||
...data,
|
||||
status: 'DONE',
|
||||
lastUpdated: moment().format(),
|
||||
}
|
||||
const setOperationStatus = await redisClient.setAsync(redisKey, JSON.stringify(dataToRedis))
|
||||
if (setOperationStatus !== 'OK') return null
|
||||
return dataToRedis
|
||||
}
|
||||
const getThesisWritersStats = async (id, yearType, programmeFilter, specialGroups) => {
|
||||
const redisKey = createRedisKeyForThesiswriters(id, yearType, programmeFilter, specialGroups)
|
||||
const dataFromRedis = await redisClient.getAsync(redisKey)
|
||||
if (!dataFromRedis) return null
|
||||
return JSON.parse(dataFromRedis)
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
setFacultyProgrammes,
|
||||
getFacultyProgrammes,
|
||||
setBasicStats,
|
||||
getBasicStats,
|
||||
setCreditStats,
|
||||
getCreditStats,
|
||||
setThesisWritersStats,
|
||||
getThesisWritersStats,
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
const { indexOf } = require('lodash')
|
||||
const moment = require('moment')
|
||||
const { getAcademicYearDates } = require('../../util/semester')
|
||||
const {
|
||||
getCreditThresholds,
|
||||
getBachelorCreditGraphStats,
|
||||
getMasterCreditGraphStats,
|
||||
getDoctoralCreditGraphStats,
|
||||
getYearsArray,
|
||||
getCorrectStudentnumbers,
|
||||
tableTitles,
|
||||
} = require('../studyprogrammeHelpers')
|
||||
const { studytrackStudents, allStudyrights } = require('../studyprogramme')
|
||||
|
||||
const getStudentData = (startDate, students, thresholdKeys, thresholdAmounts, studentStats = false) => {
|
||||
let data = {}
|
||||
if (studentStats) {
|
||||
data = { female: 0, male: 0, finnish: 0 }
|
||||
}
|
||||
thresholdKeys.forEach(t => (data[t] = 0))
|
||||
|
||||
students.forEach(({ gender_code, home_country_en, credits }) => {
|
||||
const creditcount = credits
|
||||
.filter(credit => moment(credit.attainment_date).isAfter(startDate))
|
||||
.reduce((prev, curr) => (prev += curr.credits), 0)
|
||||
|
||||
if (studentStats) {
|
||||
data.male += gender_code === '1' ? 1 : 0
|
||||
data.female += gender_code === '2' ? 1 : 0
|
||||
data.finnish += home_country_en === 'Finland' ? 1 : 0
|
||||
}
|
||||
data[thresholdKeys[0]] += creditcount < thresholdAmounts[0] ? 1 : 0
|
||||
data[thresholdKeys[1]] += creditcount >= thresholdAmounts[0] && creditcount < thresholdAmounts[1] ? 1 : 0
|
||||
data[thresholdKeys[2]] += creditcount >= thresholdAmounts[1] && creditcount < thresholdAmounts[2] ? 1 : 0
|
||||
data[thresholdKeys[3]] += creditcount >= thresholdAmounts[2] && creditcount < thresholdAmounts[3] ? 1 : 0
|
||||
data[thresholdKeys[4]] += creditcount >= thresholdAmounts[3] && creditcount < thresholdAmounts[4] ? 1 : 0
|
||||
data[thresholdKeys[5]] += creditcount >= thresholdAmounts[4] && creditcount < thresholdAmounts[5] ? 1 : 0
|
||||
if (thresholdKeys.length > 7) {
|
||||
data[thresholdKeys[6]] += creditcount >= thresholdAmounts[5] && creditcount < thresholdAmounts[6] ? 1 : 0
|
||||
data[thresholdKeys[7]] += creditcount >= thresholdAmounts[6] ? 1 : 0
|
||||
} else {
|
||||
data[thresholdKeys[6]] += creditcount >= thresholdAmounts[5] ? 1 : 0
|
||||
}
|
||||
})
|
||||
return data
|
||||
}
|
||||
|
||||
const getFacultyStudentProgress = async (faculty, programmes) => {
|
||||
const since = new Date('2017-08-01')
|
||||
const isAcademicYear = true
|
||||
const includeYearsCombined = true
|
||||
const yearsArray = getYearsArray(since.getFullYear(), isAcademicYear, includeYearsCombined)
|
||||
const facultyProgressStats = {
|
||||
id: faculty,
|
||||
years: yearsArray,
|
||||
bachelorsTableStats: [],
|
||||
mastersTableStats: [],
|
||||
doctoralTableStats: [],
|
||||
bachelorsGraphStats: {},
|
||||
mastersGraphStats: {},
|
||||
doctoralGraphStats: {},
|
||||
bachelorTitles: tableTitles.creditProgress.bachelor,
|
||||
mastersTitles: tableTitles.creditProgress.master,
|
||||
doctoralTitles: tableTitles.creditProgress.doctoral,
|
||||
programmeNames: programmes.data.reduce((obj, dataItem) => ({ ...obj, [dataItem.code]: dataItem.name }), {}),
|
||||
bachelorsProgrammeStats: {},
|
||||
mastersProgrammeStats: {},
|
||||
doctoralProgrammeStats: {},
|
||||
}
|
||||
|
||||
let allBachelorGraphStats = getBachelorCreditGraphStats(yearsArray)
|
||||
let allMastersGraphStats = getMasterCreditGraphStats(yearsArray)
|
||||
let allDoctoralGraphStats = getDoctoralCreditGraphStats(yearsArray)
|
||||
|
||||
let bachelorsProgrammeStats = {}
|
||||
let mastersProgrammeStats = {}
|
||||
let doctoralProgrammeStats = {}
|
||||
|
||||
let bachelorsTableStats = new Array(yearsArray.length)
|
||||
let mastersTableStats = new Array(yearsArray.length)
|
||||
let doctoralTableStats = new Array(yearsArray.length)
|
||||
|
||||
const reversedYears = [...yearsArray].reverse()
|
||||
reversedYears.forEach(year => {
|
||||
bachelorsTableStats[indexOf(reversedYears, year)] = [year, 0, 0, 0, 0, 0, 0, 0, 0, 0]
|
||||
mastersTableStats[indexOf(reversedYears, year)] = [year, 0, 0, 0, 0, 0, 0, 0, 0]
|
||||
doctoralTableStats[indexOf(reversedYears, year)] = [year, 0, 0, 0, 0, 0, 0, 0, 0]
|
||||
})
|
||||
|
||||
for (const programme of programmes.data) {
|
||||
let { creditThresholdKeys, creditThresholdAmounts } = getCreditThresholds(programme.code)
|
||||
if (!creditThresholdKeys) return
|
||||
|
||||
for (const year of reversedYears) {
|
||||
const { startDate, endDate } = getAcademicYearDates(year, since)
|
||||
const specialGroups = true
|
||||
const includeGraduated = true
|
||||
const studentnumbers = await getCorrectStudentnumbers({
|
||||
codes: [programme.code],
|
||||
startDate,
|
||||
endDate,
|
||||
specialGroups,
|
||||
includeGraduated,
|
||||
})
|
||||
|
||||
const all = await allStudyrights(programme.code, studentnumbers)
|
||||
const students = await studytrackStudents(studentnumbers)
|
||||
const studentData = getStudentData(startDate, students, creditThresholdKeys, creditThresholdAmounts)
|
||||
|
||||
if (programme.code.includes('KH')) {
|
||||
if (!(programme.code in bachelorsProgrammeStats)) {
|
||||
bachelorsProgrammeStats[programme.code] = new Array(reversedYears.length - 1)
|
||||
}
|
||||
|
||||
bachelorsTableStats[indexOf(reversedYears, year)][1] += all.length || 0
|
||||
|
||||
if (year !== 'Total') {
|
||||
bachelorsProgrammeStats[programme.code][indexOf(reversedYears, year)] = creditThresholdKeys.map(
|
||||
key => studentData[key]
|
||||
)
|
||||
}
|
||||
for (const key of Object.keys(studentData)) {
|
||||
bachelorsTableStats[indexOf(reversedYears, year)][indexOf(creditThresholdKeys, key) + 2] +=
|
||||
studentData[key] || 0
|
||||
allBachelorGraphStats[key].data[indexOf(yearsArray, year)] += studentData[key] || 0
|
||||
}
|
||||
} else if (programme.code.includes('MH')) {
|
||||
mastersTableStats[indexOf(reversedYears, year)][1] += all.length || 0
|
||||
if (!(programme.code in mastersProgrammeStats)) {
|
||||
mastersProgrammeStats[programme.code] = new Array(reversedYears.length - 1)
|
||||
}
|
||||
if (year !== 'Total') {
|
||||
mastersProgrammeStats[programme.code][indexOf(reversedYears, year)] = creditThresholdKeys.map(
|
||||
key => studentData[key]
|
||||
)
|
||||
}
|
||||
for (const key of Object.keys(studentData)) {
|
||||
mastersTableStats[indexOf(reversedYears, year)][indexOf(creditThresholdKeys, key) + 2] +=
|
||||
studentData[key] || 0
|
||||
allMastersGraphStats[key].data[indexOf(yearsArray, year)] += studentData[key] || 0
|
||||
}
|
||||
} else {
|
||||
doctoralTableStats[indexOf(reversedYears, year)][1] += all.length || 0
|
||||
if (!(programme.code in doctoralProgrammeStats)) {
|
||||
doctoralProgrammeStats[programme.code] = new Array(reversedYears.length - 1)
|
||||
}
|
||||
if (year !== 'Total') {
|
||||
doctoralProgrammeStats[programme.code][indexOf(reversedYears, year)] = creditThresholdKeys.map(
|
||||
key => studentData[key]
|
||||
)
|
||||
}
|
||||
for (const key of Object.keys(studentData)) {
|
||||
doctoralTableStats[indexOf(reversedYears, year)][indexOf(creditThresholdKeys, key) + 2] +=
|
||||
studentData[key] || 0
|
||||
allDoctoralGraphStats[key].data[indexOf(yearsArray, year)] += studentData[key] || 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
facultyProgressStats.bachelorsTableStats = bachelorsTableStats
|
||||
facultyProgressStats.mastersTableStats = mastersTableStats
|
||||
facultyProgressStats.doctoralTableStats = doctoralTableStats
|
||||
|
||||
facultyProgressStats.doctoralProgrammeStats = doctoralProgrammeStats
|
||||
facultyProgressStats.mastersProgrammeStats = mastersProgrammeStats
|
||||
facultyProgressStats.bachelorsProgrammeStats = bachelorsProgrammeStats
|
||||
|
||||
facultyProgressStats.bachelorsGraphStats = allBachelorGraphStats
|
||||
facultyProgressStats.mastersGraphStats = allMastersGraphStats
|
||||
facultyProgressStats.doctoralGraphStats = allDoctoralGraphStats
|
||||
|
||||
return facultyProgressStats
|
||||
}
|
||||
module.exports = { getFacultyStudentProgress }
|
||||
@@ -0,0 +1,134 @@
|
||||
const { indexOf } = require('lodash')
|
||||
const { mapToProviders } = require('../../util/utils')
|
||||
const { thesisWriters } = require('./faculty')
|
||||
const {
|
||||
getStatsBasis,
|
||||
getYearsArray,
|
||||
defineYear,
|
||||
getCorrectStudentnumbers,
|
||||
alltimeEndDate,
|
||||
alltimeStartDate,
|
||||
} = require('../studyprogrammeHelpers')
|
||||
|
||||
const getFacultyThesisWriters = async ({ since, years, isAcademicYear, facultyProgrammes, includeAllSpecials }) => {
|
||||
const thesisTypes = [
|
||||
'urn:code:course-unit-type:bachelors-thesis',
|
||||
'urn:code:course-unit-type:masters-thesis',
|
||||
'urn:code:course-unit-type:doctors-thesis',
|
||||
'urn:code:course-unit-type:licentiate-thesis',
|
||||
]
|
||||
|
||||
let bachelors = getStatsBasis(years)
|
||||
let masters = getStatsBasis(years)
|
||||
let doctors = getStatsBasis(years)
|
||||
let licentiates = getStatsBasis(years)
|
||||
let programmeCounts = {}
|
||||
let programmeNames = {}
|
||||
|
||||
for (const { code, name } of facultyProgrammes) {
|
||||
const provider = mapToProviders([code])[0]
|
||||
const students = await getCorrectStudentnumbers({
|
||||
codes: [code],
|
||||
startDate: alltimeStartDate,
|
||||
endDate: alltimeEndDate,
|
||||
includeAllSpecials,
|
||||
})
|
||||
const thesisCourseCodes = await thesisWriters(provider, since, thesisTypes, students)
|
||||
thesisCourseCodes?.forEach(({ attainment_date, courseUnitType }) => {
|
||||
const thesisYear = defineYear(attainment_date, isAcademicYear)
|
||||
|
||||
if (!(code in programmeCounts)) {
|
||||
programmeCounts[code] = {}
|
||||
|
||||
Object.keys(bachelors.tableStats).forEach(year => (programmeCounts[code][year] = [0, 0, 0, 0, 0]))
|
||||
programmeNames[code] = name
|
||||
}
|
||||
programmeCounts[code][thesisYear][0] += 1
|
||||
|
||||
if (courseUnitType === thesisTypes[0]) {
|
||||
bachelors.graphStats[indexOf(years, thesisYear)] += 1
|
||||
bachelors.tableStats[thesisYear] += 1
|
||||
programmeCounts[code][thesisYear][1] += 1
|
||||
} else if (courseUnitType === thesisTypes[1]) {
|
||||
masters.graphStats[indexOf(years, thesisYear)] += 1
|
||||
masters.tableStats[thesisYear] += 1
|
||||
programmeCounts[code][thesisYear][2] += 1
|
||||
} else if (courseUnitType === thesisTypes[2]) {
|
||||
doctors.graphStats[indexOf(years, thesisYear)] += 1
|
||||
doctors.tableStats[thesisYear] += 1
|
||||
programmeCounts[code][thesisYear][3] += 1
|
||||
} else if (courseUnitType === thesisTypes[3]) {
|
||||
licentiates.graphStats[indexOf(years, thesisYear)] += 1
|
||||
licentiates.tableStats[thesisYear] += 1
|
||||
programmeCounts[code][thesisYear][4] += 1
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return { bachelors, masters, doctors, licentiates, programmeCounts, programmeNames }
|
||||
}
|
||||
|
||||
const getFacultyThesisWritersForStudyTrack = async (
|
||||
allThesisWriters,
|
||||
facultyProgrammes,
|
||||
isAcademicYear,
|
||||
includeAllSpecials
|
||||
) => {
|
||||
const since = isAcademicYear ? new Date('2017-08-01') : new Date('2017-01-01')
|
||||
const years = getYearsArray(since.getFullYear(), isAcademicYear)
|
||||
const queryParameters = { since, years, isAcademicYear, facultyProgrammes, includeAllSpecials }
|
||||
const { bachelors, masters, doctors, licentiates, programmeCounts, programmeNames } = await getFacultyThesisWriters(
|
||||
queryParameters
|
||||
)
|
||||
|
||||
allThesisWriters.years = years.map(year => year.toString())
|
||||
const reversedYears = years.reverse()
|
||||
allThesisWriters.tableStats = reversedYears.map(year => [
|
||||
year,
|
||||
bachelors.tableStats[year] + masters.tableStats[year] + doctors.tableStats[year] + licentiates.tableStats[year],
|
||||
bachelors.tableStats[year],
|
||||
masters.tableStats[year],
|
||||
doctors.tableStats[year],
|
||||
licentiates.tableStats[year],
|
||||
])
|
||||
allThesisWriters.graphStats.push({ name: 'Bachelors', data: bachelors.graphStats })
|
||||
allThesisWriters.graphStats.push({ name: 'Masters', data: masters.graphStats })
|
||||
allThesisWriters.graphStats.push({ name: 'Doctors', data: doctors.graphStats })
|
||||
allThesisWriters.graphStats.push({ name: 'Licentiates', data: licentiates.graphStats })
|
||||
|
||||
const programmes = programmeNames ? Object.keys(programmeNames) : facultyProgrammes.map(programme => programme.code)
|
||||
programmes.forEach(programmeCode => {
|
||||
reversedYears.forEach(year => {
|
||||
if (programmeCounts && programmeCode in programmeCounts) {
|
||||
if (!(programmeCode in allThesisWriters.programmeTableStats)) {
|
||||
allThesisWriters.programmeTableStats[programmeCode] = []
|
||||
}
|
||||
allThesisWriters.programmeTableStats[programmeCode].push([year, ...programmeCounts[programmeCode][year]])
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
allThesisWriters.programmeNames = programmeNames
|
||||
}
|
||||
|
||||
const combineFacultyThesisWriters = async (faculty, facultyProgrammes, yearType, specialGroups) => {
|
||||
let allThesisWriters = {
|
||||
id: faculty,
|
||||
years: [],
|
||||
tableStats: [],
|
||||
graphStats: [],
|
||||
programmeTableStats: {},
|
||||
titles: ['', 'All', 'Bachelors', 'Masters', 'Doctors', 'Licentiates'],
|
||||
programmeNames: {},
|
||||
status: 'Done',
|
||||
lastUpdated: '',
|
||||
}
|
||||
const isAcademicYear = yearType === 'ACADEMIC_YEAR'
|
||||
const includeAllSpecials = specialGroups === 'SPECIAL_INCLUDED'
|
||||
|
||||
await getFacultyThesisWritersForStudyTrack(allThesisWriters, facultyProgrammes, isAcademicYear, includeAllSpecials)
|
||||
|
||||
return allThesisWriters
|
||||
}
|
||||
|
||||
module.exports = { combineFacultyThesisWriters }
|
||||
@@ -0,0 +1,117 @@
|
||||
const logger = require('../../util/logger')
|
||||
const { findFacultyProgrammeCodes } = require('./faculty')
|
||||
const { combineFacultyBasics } = require('./facultyBasics')
|
||||
const { combineFacultyCredits } = require('./facultyCredits')
|
||||
const { combineFacultyThesisWriters } = require('./facultyThesisWriters')
|
||||
const { setFacultyProgrammes, setBasicStats, setCreditStats, setThesisWritersStats } = require('./facultyService')
|
||||
|
||||
const updateFacultyOverview = async (faculty, statsType) => {
|
||||
const calendarNewSpecial = {
|
||||
yearType: 'CALENDAR_YEAR',
|
||||
programmeFilter: 'NEW_STUDY_PROGRAMMES',
|
||||
specialGroups: 'SPECIAL_INCLUDED',
|
||||
}
|
||||
const calendarNewSpecialOut = {
|
||||
yearType: 'CALENDAR_YEAR',
|
||||
programmeFilter: 'NEW_STUDY_PROGRAMMES',
|
||||
specialGroups: 'SPECIAL_EXCLUDED',
|
||||
}
|
||||
|
||||
const calendarAllSpecial = {
|
||||
yearType: 'CALENDAR_YEAR',
|
||||
programmeFilter: 'ALL_PROGRAMMES',
|
||||
specialGroups: 'SPECIAL_INCLUDED',
|
||||
}
|
||||
const calendarAllSpecialOut = {
|
||||
yearType: 'CALENDAR_YEAR',
|
||||
programmeFilter: 'ALL_PROGRAMMES',
|
||||
specialGroups: 'SPECIAL_EXCLUDED',
|
||||
}
|
||||
const academicNewSpecial = {
|
||||
yearType: 'ACADEMIC_YEAR',
|
||||
programmeFilter: 'NEW_STUDY_PROGRAMMES',
|
||||
specialGroups: 'SPECIAL_INCLUDED',
|
||||
}
|
||||
|
||||
const academicAllSpecial = {
|
||||
yearType: 'ACADEMIC_YEAR',
|
||||
programmeFilter: 'ALL_PROGRAMMES',
|
||||
specialGroups: 'SPECIAL_INCLUDED',
|
||||
}
|
||||
|
||||
const academicNewSpecialOut = {
|
||||
yearType: 'ACADEMIC_YEAR',
|
||||
programmeFilter: 'NEW_STUDY_PROGRAMMES',
|
||||
specialGroups: 'SPECIAL_EXCLUDED',
|
||||
}
|
||||
|
||||
const academicAllSpecialOut = {
|
||||
yearType: 'ACADEMIC_YEAR',
|
||||
programmeFilter: 'ALL_PROGRAMMES',
|
||||
specialGroups: 'SPECIAL_EXCLUDED',
|
||||
}
|
||||
|
||||
const options = [
|
||||
calendarNewSpecial,
|
||||
calendarAllSpecial,
|
||||
academicNewSpecial,
|
||||
academicAllSpecial,
|
||||
calendarNewSpecialOut,
|
||||
calendarAllSpecialOut,
|
||||
academicNewSpecialOut,
|
||||
academicAllSpecialOut,
|
||||
]
|
||||
let allProgrammes = []
|
||||
let newProgrammes = []
|
||||
let allProgrammeCodes = []
|
||||
|
||||
try {
|
||||
const all = await findFacultyProgrammeCodes(faculty, 'ALL_PROGRAMMES')
|
||||
const onlyNew = await findFacultyProgrammeCodes(faculty, 'NEW_STUDY_PROGRAMMES')
|
||||
allProgrammes = await setFacultyProgrammes(faculty, all, 'ALL_PROGRAMMES')
|
||||
newProgrammes = await setFacultyProgrammes(faculty, onlyNew, 'NEW_STUDY_PROGRAMMES')
|
||||
allProgrammes?.data.forEach(prog => allProgrammeCodes.push(prog.code))
|
||||
} catch (e) {
|
||||
logger.error(e)
|
||||
}
|
||||
|
||||
for (const option of options) {
|
||||
const { yearType, programmeFilter, specialGroups } = option
|
||||
try {
|
||||
if (statsType === 'ALL' || statsType === 'STUDENT') {
|
||||
const updatedStudentInfo = await combineFacultyBasics(
|
||||
faculty,
|
||||
programmeFilter === 'ALL_PROGRAMMES' ? allProgrammes.data : newProgrammes.data,
|
||||
yearType,
|
||||
allProgrammeCodes,
|
||||
programmeFilter,
|
||||
specialGroups
|
||||
)
|
||||
await setBasicStats(updatedStudentInfo, option.yearType, option.programmeFilter, option.specialGroups)
|
||||
}
|
||||
if (statsType === 'ALL' || statsType === 'CREDITS') {
|
||||
const updatedCredits = await combineFacultyCredits(
|
||||
faculty,
|
||||
programmeFilter === 'ALL_PROGRAMMES' ? allProgrammes.data : newProgrammes.data,
|
||||
yearType,
|
||||
specialGroups
|
||||
)
|
||||
await setCreditStats(updatedCredits, yearType, programmeFilter, specialGroups)
|
||||
}
|
||||
if (statsType === 'ALL' || statsType === 'THESIS') {
|
||||
const updateThesisWriters = await combineFacultyThesisWriters(
|
||||
faculty,
|
||||
programmeFilter === 'ALL_PROGRAMMES' ? allProgrammes.data : newProgrammes.data,
|
||||
yearType,
|
||||
specialGroups
|
||||
)
|
||||
await setThesisWritersStats(updateThesisWriters, yearType, programmeFilter, specialGroups)
|
||||
}
|
||||
} catch (e) {
|
||||
logger.error(e)
|
||||
}
|
||||
}
|
||||
return 'OK'
|
||||
}
|
||||
|
||||
module.exports = { updateFacultyOverview }
|
||||
@@ -1,175 +0,0 @@
|
||||
const { indexOf } = require('lodash')
|
||||
const {
|
||||
startedStudyrights,
|
||||
graduatedStudyrights,
|
||||
transferredInsideFaculty,
|
||||
transferredAway,
|
||||
transferredTo,
|
||||
} = require('./faculty')
|
||||
const { getStatsBasis, getYearsArray, defineYear } = require('./studyprogrammeHelpers')
|
||||
|
||||
const filterDuplicateStudyrights = studyrights => {
|
||||
// bachelor+master students have two studyrights (separated by two last digits in studyrightid)
|
||||
// choose only the earlier started one, so we don't count start of masters as starting in faculty
|
||||
let rightsToCount = {}
|
||||
|
||||
studyrights.forEach(right => {
|
||||
const id = right.studyrightid.slice(0, -2)
|
||||
const start = new Date(right.studystartdate)
|
||||
if (id in rightsToCount) {
|
||||
if (new Date(rightsToCount[id].studystartdate) > start) {
|
||||
rightsToCount[id] = right
|
||||
}
|
||||
} else {
|
||||
rightsToCount[id] = right
|
||||
}
|
||||
})
|
||||
return Object.values(rightsToCount)
|
||||
}
|
||||
|
||||
const getFacultyStarters = async (
|
||||
faculty,
|
||||
since,
|
||||
isAcademicYear,
|
||||
yearsArray,
|
||||
graphStats,
|
||||
tableStats,
|
||||
counts,
|
||||
allBasics
|
||||
) => {
|
||||
const startedGraphStats = [...graphStats]
|
||||
const startedTableStats = { ...tableStats }
|
||||
const studyrights = await startedStudyrights(faculty, since)
|
||||
const filteredStudyrights = filterDuplicateStudyrights(studyrights)
|
||||
|
||||
filteredStudyrights.forEach(({ studystartdate }) => {
|
||||
const startYear = defineYear(studystartdate, isAcademicYear)
|
||||
startedGraphStats[indexOf(yearsArray, startYear)] += 1
|
||||
startedTableStats[startYear] += 1
|
||||
})
|
||||
allBasics.graphStats.push({ name: 'Started studying', data: startedGraphStats })
|
||||
|
||||
Object.keys(startedTableStats).forEach(year => {
|
||||
counts[year] = [startedTableStats[year]]
|
||||
})
|
||||
}
|
||||
|
||||
const getFacultyGraduates = async (
|
||||
faculty,
|
||||
since,
|
||||
isAcademicYear,
|
||||
yearsArray,
|
||||
graphStats,
|
||||
tableStats,
|
||||
allBasics,
|
||||
counts
|
||||
) => {
|
||||
const graduatedGraphStats = [[...graphStats], [...graphStats], [...graphStats], [...graphStats], [...graphStats]]
|
||||
const graduatedTableStats = {}
|
||||
Object.keys(tableStats).forEach(year => (graduatedTableStats[year] = [0, 0, 0, 0, 0]))
|
||||
|
||||
const graduatedRights = await graduatedStudyrights(faculty, since)
|
||||
|
||||
graduatedRights.forEach(({ enddate, extentcode }) => {
|
||||
const endYear = defineYear(enddate, isAcademicYear)
|
||||
graduatedGraphStats[0][indexOf(yearsArray, endYear)] += 1
|
||||
graduatedTableStats[endYear][0] += 1
|
||||
|
||||
if (extentcode === 1) {
|
||||
graduatedGraphStats[1][indexOf(yearsArray, endYear)] += 1
|
||||
graduatedTableStats[endYear][1] += 1
|
||||
} else if (extentcode === 2) {
|
||||
graduatedGraphStats[2][indexOf(yearsArray, endYear)] += 1
|
||||
graduatedTableStats[endYear][2] += 1
|
||||
} else if (extentcode === 4) {
|
||||
graduatedGraphStats[3][indexOf(yearsArray, endYear)] += 1
|
||||
graduatedTableStats[endYear][3] += 1
|
||||
} else {
|
||||
graduatedGraphStats[4][indexOf(yearsArray, endYear)] += 1
|
||||
graduatedTableStats[endYear][4] += 1
|
||||
}
|
||||
})
|
||||
|
||||
allBasics.graphStats.push({ name: 'All graduated', data: graduatedGraphStats[0] })
|
||||
allBasics.graphStats.push({ name: 'Graduated bachelors', data: graduatedGraphStats[1] })
|
||||
allBasics.graphStats.push({ name: 'Graduated masters', data: graduatedGraphStats[2] })
|
||||
allBasics.graphStats.push({ name: 'Graduated doctors', data: graduatedGraphStats[3] })
|
||||
allBasics.graphStats.push({ name: 'Other graduations', data: graduatedGraphStats[4] })
|
||||
|
||||
Object.keys(graduatedTableStats).forEach(year => {
|
||||
counts[year] = counts[year].concat(graduatedTableStats[year])
|
||||
})
|
||||
}
|
||||
|
||||
const getFacultyTransfers = async (
|
||||
programmes,
|
||||
since,
|
||||
isAcademicYear,
|
||||
yearsArray,
|
||||
graphStats,
|
||||
tableStats,
|
||||
allBasics,
|
||||
counts
|
||||
) => {
|
||||
const insideTransfers = await transferredInsideFaculty(programmes, since)
|
||||
const awayTransfers = await transferredAway(programmes, since)
|
||||
const toTransfers = await transferredTo(programmes, since)
|
||||
|
||||
const transferGraphStats = [[...graphStats], [...graphStats], [...graphStats]]
|
||||
const transferTableStats = {}
|
||||
Object.keys(tableStats).forEach(year => (transferTableStats[year] = [0, 0, 0]))
|
||||
|
||||
insideTransfers.forEach(({ transferdate }) => {
|
||||
const transferYear = defineYear(transferdate, isAcademicYear)
|
||||
transferGraphStats[0][indexOf(yearsArray, transferYear)] += 1
|
||||
transferTableStats[transferYear][0] += 1
|
||||
})
|
||||
|
||||
awayTransfers.forEach(({ transferdate }) => {
|
||||
const transferYear = defineYear(transferdate, isAcademicYear)
|
||||
transferGraphStats[1][indexOf(yearsArray, transferYear)] += 1
|
||||
transferTableStats[transferYear][1] += 1
|
||||
})
|
||||
|
||||
toTransfers.forEach(({ transferdate }) => {
|
||||
const transferYear = defineYear(transferdate, isAcademicYear)
|
||||
transferGraphStats[2][indexOf(yearsArray, transferYear)] += 1
|
||||
transferTableStats[transferYear][2] += 1
|
||||
})
|
||||
|
||||
allBasics.graphStats.push({ name: 'Transferred inside', data: transferGraphStats[0] })
|
||||
allBasics.graphStats.push({ name: 'Transferred away', data: transferGraphStats[1] })
|
||||
allBasics.graphStats.push({ name: 'Transferred to', data: transferGraphStats[2] })
|
||||
|
||||
Object.keys(transferTableStats).forEach(year => {
|
||||
counts[year] = counts[year].concat(transferTableStats[year])
|
||||
})
|
||||
}
|
||||
|
||||
const combineFacultyBasics = async (allBasics, faculty, programmes, yearType) => {
|
||||
let counts = {}
|
||||
let years = []
|
||||
const isAcademicYear = yearType === 'ACADEMIC_YEAR'
|
||||
const since = isAcademicYear ? new Date('2017-08-01') : new Date('2017-01-01')
|
||||
const yearsArray = getYearsArray(since.getFullYear(), isAcademicYear)
|
||||
const { graphStats, tableStats } = getStatsBasis(yearsArray)
|
||||
Object.keys(tableStats).forEach(year => years.push(year))
|
||||
|
||||
// Started studying in faculty
|
||||
await getFacultyStarters(faculty, since, isAcademicYear, yearsArray, graphStats, tableStats, counts, allBasics)
|
||||
|
||||
// Graduated in faculty
|
||||
await getFacultyGraduates(faculty, since, isAcademicYear, yearsArray, graphStats, tableStats, allBasics, counts)
|
||||
|
||||
// Transfers
|
||||
await getFacultyTransfers(programmes, since, isAcademicYear, yearsArray, graphStats, tableStats, allBasics, counts)
|
||||
|
||||
// combine tableStats from all categories
|
||||
allBasics.years = years
|
||||
years.forEach(year => {
|
||||
allBasics.tableStats.push([year, ...counts[year]])
|
||||
})
|
||||
allBasics.tableStats.reverse()
|
||||
}
|
||||
|
||||
module.exports = { combineFacultyBasics }
|
||||
@@ -24,6 +24,7 @@ const { Tag, TagStudent } = require('../models/models_kone')
|
||||
const { CourseStatsCounter } = require('./course_stats_counter')
|
||||
const { getPassingSemester, semesterEnd, semesterStart } = require('../util/semester')
|
||||
const { getAllProgrammes } = require('./studyrights')
|
||||
const { encrypt } = require('../services/encrypt')
|
||||
|
||||
const enrolmentDates = () => {
|
||||
const query = 'SELECT DISTINCT s.dateOfUniversityEnrollment as date FROM Student s'
|
||||
@@ -209,7 +210,7 @@ const getStudentsIncludeCoursesBetween = async (studentnumbers, startDate, endDa
|
||||
raw: true,
|
||||
}),
|
||||
Enrollment.findAll({
|
||||
attributes: ['course_code', 'state', 'enrollment_date_time', 'studentnumber'],
|
||||
attributes: ['course_code', 'state', 'enrollment_date_time', 'studentnumber', 'semestercode'],
|
||||
where: {
|
||||
enrollment_date_time: {
|
||||
[Op.between]: [attainmentDateFrom, endDate],
|
||||
@@ -413,7 +414,7 @@ const studentnumbersWithAllStudyrightElements = async (
|
||||
// db startdate is formatted to utc so need to change it when querying
|
||||
const formattedStartDate = new Date(moment.tz(startDate, 'Europe/Helsinki').format()).toUTCString()
|
||||
|
||||
const filteredExtents = [10] // always filter out secondary subject students
|
||||
const filteredExtents = [16] // always filter out secondary subject students
|
||||
let studyrightWhere = {
|
||||
extentcode: {
|
||||
[Op.notIn]: filteredExtents,
|
||||
@@ -423,7 +424,7 @@ const studentnumbersWithAllStudyrightElements = async (
|
||||
filteredExtents.push(7, 34)
|
||||
}
|
||||
if (!nondegreeStudents) {
|
||||
filteredExtents.push(33, 99, 14, 13)
|
||||
filteredExtents.push(22, 99, 14, 13)
|
||||
}
|
||||
|
||||
let studentWhere = {}
|
||||
@@ -981,7 +982,7 @@ const parseCreditInfo = credit => ({
|
||||
date: credit.attainment_date,
|
||||
})
|
||||
|
||||
const bottlenecksOf = async (query, studentnumberlist) => {
|
||||
const bottlenecksOf = async (query, studentnumberlist, encryptdata = false) => {
|
||||
const isValidRequest = async (query, params) => {
|
||||
const { studyRights, startDate, endDate, exchangeStudents, nondegreeStudents, transferredStudents } = params
|
||||
|
||||
@@ -1055,6 +1056,22 @@ const bottlenecksOf = async (query, studentnumberlist) => {
|
||||
return [allstudents, courses]
|
||||
}
|
||||
}
|
||||
|
||||
const encryptStudentnumbers = bottlenecks => {
|
||||
for (const course in bottlenecks.coursestatistics) {
|
||||
const encryptedStudentStats = {}
|
||||
for (const data in bottlenecks.coursestatistics[course].students) {
|
||||
encryptedStudentStats[data] = {}
|
||||
const studentnumbers = Object.keys(bottlenecks.coursestatistics[course].students[data])
|
||||
studentnumbers.forEach(studentnumber => {
|
||||
encryptedStudentStats[data][encrypt(studentnumber).encryptedData] =
|
||||
bottlenecks.coursestatistics[course].students[data][studentnumber]
|
||||
})
|
||||
}
|
||||
bottlenecks.coursestatistics[course].students = encryptedStudentStats
|
||||
}
|
||||
}
|
||||
|
||||
const params = parseQueryParams(query)
|
||||
const [[allstudents, courses], error] = await Promise.all([
|
||||
getStudentsAndCourses(query.selectedStudents, studentnumberlist, query.courses),
|
||||
@@ -1114,6 +1131,8 @@ const bottlenecksOf = async (query, studentnumberlist) => {
|
||||
bottlenecks.coursestatistics = Object.values(stats).map(coursestatistics => coursestatistics.getFinalStats())
|
||||
bottlenecks.allStudents = allstudentslength
|
||||
|
||||
if (encryptdata) encryptStudentnumbers(bottlenecks)
|
||||
|
||||
return bottlenecks
|
||||
}
|
||||
|
||||
|
||||
@@ -121,7 +121,7 @@ const getUnifyStatus = unifyCourses => {
|
||||
}
|
||||
|
||||
const findByCourseAndSemesters = async (coursecodes, from, to, separate, unifyCourses = 'unifyStats') => {
|
||||
const { startdate } = await Semester.findOne({
|
||||
const { startdate, semestercode: fromSemester } = await Semester.findOne({
|
||||
where: {
|
||||
[separate ? 'semestercode' : 'yearcode']: from,
|
||||
},
|
||||
@@ -129,7 +129,7 @@ const findByCourseAndSemesters = async (coursecodes, from, to, separate, unifyCo
|
||||
limit: 1,
|
||||
raw: true,
|
||||
})
|
||||
const { enddate } = await Semester.findOne({
|
||||
const { enddate, semestercode: toSemester } = await Semester.findOne({
|
||||
where: {
|
||||
[separate ? 'semestercode' : 'yearcode']: to,
|
||||
},
|
||||
@@ -164,7 +164,7 @@ const findByCourseAndSemesters = async (coursecodes, from, to, separate, unifyCo
|
||||
WHERE
|
||||
e.studentnumber = s.studentnumber
|
||||
AND e.course_code IN (:coursecodes)
|
||||
AND e.enrollment_date_time BETWEEN '${startdate.toISOString()}' AND '${enddate.toISOString()}'
|
||||
AND e.semestercode BETWEEN ${fromSemester} AND ${toSemester}
|
||||
AND e.enrollment_date_time >= '2021-05-31'
|
||||
AND e.state IN ('ENROLLED', 'CONFIRMED')
|
||||
);
|
||||
|
||||
@@ -31,6 +31,19 @@ const sinceDate = since => {
|
||||
const studytrackStudents = async studentnumbers =>
|
||||
(
|
||||
await Student.findAll({
|
||||
include: {
|
||||
model: Credit,
|
||||
separate: true,
|
||||
attributes: ['credits', 'attainment_date'],
|
||||
where: {
|
||||
isStudyModule: {
|
||||
[Op.eq]: null,
|
||||
},
|
||||
credittypecode: {
|
||||
[Op.in]: [4, 9],
|
||||
},
|
||||
},
|
||||
},
|
||||
where: {
|
||||
studentnumber: {
|
||||
[Op.in]: studentnumbers,
|
||||
@@ -577,6 +590,36 @@ const getOtherStudentsForProgrammeCourses = async (from, to, programmeCourses, s
|
||||
}))
|
||||
}
|
||||
|
||||
const getTransferStudentsForProgrammeCourses = async (from, to, programmeCourses) => {
|
||||
const res = await sequelize.query(
|
||||
`
|
||||
WITH Dist AS (
|
||||
SELECT DISTINCT cr.student_studentnumber AS student, cr.credits AS credits,
|
||||
co.code AS code, co.name AS course_name FROM credit cr
|
||||
INNER JOIN course co ON cr.course_code = co.code
|
||||
WHERE cr.attainment_date BETWEEN :from AND :to
|
||||
AND (cr."isStudyModule" = false OR cr."isStudyModule" IS NULL)
|
||||
AND cr.course_code IN (:programmeCourses)
|
||||
AND cr.credittypecode = 9
|
||||
)
|
||||
SELECT COUNT(student) AS total_students, SUM(credits) AS total_credits, code, course_name
|
||||
FROM Dist
|
||||
GROUP BY dist.code, course_name;
|
||||
`,
|
||||
{
|
||||
type: sequelize.QueryTypes.SELECT,
|
||||
replacements: { from, to, programmeCourses },
|
||||
}
|
||||
)
|
||||
return res.map(course => ({
|
||||
code: course.code,
|
||||
name: course.course_name,
|
||||
totalTransferStudents: parseInt(course.total_students),
|
||||
totalTransferCredits: parseInt(course.total_credits),
|
||||
type: 'transfer',
|
||||
}))
|
||||
}
|
||||
|
||||
const getStudentsWithoutStudyrightForProgrammeCourses = async (from, to, programmeCourses) => {
|
||||
const res = await sequelize.query(
|
||||
`
|
||||
@@ -716,4 +759,5 @@ module.exports = {
|
||||
getAllProgrammeCourses,
|
||||
getStudyRights,
|
||||
getCourseCodesForStudyProgramme,
|
||||
getTransferStudentsForProgrammeCourses,
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ const {
|
||||
getCurrentStudyYearStartDate,
|
||||
getOwnStudentsForProgrammeCourses,
|
||||
getStudentsWithoutStudyrightForProgrammeCourses,
|
||||
getTransferStudentsForProgrammeCourses,
|
||||
getOtherStudentsForProgrammeCourses,
|
||||
getAllProgrammeCourses,
|
||||
} = require('./studyprogramme')
|
||||
@@ -50,6 +51,9 @@ const makeYearlyPromises = (years, academicYear, type, programmeCourses, studypr
|
||||
case 'otherStudents':
|
||||
result = await getOtherStudentsForProgrammeCourses(from, to, programmeCourses, studyprogramme)
|
||||
break
|
||||
case 'transfer':
|
||||
result = await getTransferStudentsForProgrammeCourses(from, to, programmeCourses)
|
||||
break
|
||||
default:
|
||||
result = await getStudentsForProgrammeCourses(from, to, programmeCourses)
|
||||
}
|
||||
@@ -92,16 +96,26 @@ const getStudyprogrammeCoursesForStudytrack = async (unixMillis, studyprogramme,
|
||||
studyprogramme
|
||||
)
|
||||
|
||||
const yearlyTransferStudentsPromises = makeYearlyPromises(
|
||||
yearRange,
|
||||
academicYear,
|
||||
'transfer',
|
||||
programmeCourses,
|
||||
studyprogramme
|
||||
)
|
||||
|
||||
const [
|
||||
yearlyStudentByCourse,
|
||||
yearlyProgrammeStudents,
|
||||
yearlyStudentsWithoutStudyright,
|
||||
yearlyOtherProgrammeStudents,
|
||||
yearlyTransferStudents,
|
||||
] = await Promise.all([
|
||||
Promise.all(yearlyStudentByCoursePromises),
|
||||
Promise.all(yearlyProgrammeStudentsPromises),
|
||||
Promise.all(yearlyStudentsWithoutStudyrightPromises),
|
||||
Promise.all(yearlyOtherProgrammeStudentsPromises),
|
||||
Promise.all(yearlyTransferStudentsPromises),
|
||||
])
|
||||
|
||||
let maxYear = 0
|
||||
@@ -110,6 +124,7 @@ const getStudyprogrammeCoursesForStudytrack = async (unixMillis, studyprogramme,
|
||||
...yearlyProgrammeStudents.flat(),
|
||||
...yearlyStudentsWithoutStudyright.flat(),
|
||||
...yearlyOtherProgrammeStudents.flat(),
|
||||
...yearlyTransferStudents.flat(),
|
||||
].reduce((acc, curr) => {
|
||||
if (curr.year > maxYear) maxYear = curr.year
|
||||
if (!acc[curr.code]) {
|
||||
@@ -129,6 +144,8 @@ const getStudyprogrammeCoursesForStudytrack = async (unixMillis, studyprogramme,
|
||||
totalOtherProgrammeCredits: 0,
|
||||
totalWithoutStudyrightStudents: 0,
|
||||
totalWithoutStudyrightCredits: 0,
|
||||
totalTransferStudents: 0,
|
||||
totalTransferCredits: 0,
|
||||
}
|
||||
}
|
||||
switch (curr.type) {
|
||||
@@ -140,16 +157,17 @@ const getStudyprogrammeCoursesForStudytrack = async (unixMillis, studyprogramme,
|
||||
acc[curr.code]['years'][curr.year]['totalProgrammeStudents'] += curr.totalProgrammeStudents
|
||||
acc[curr.code]['years'][curr.year]['totalProgrammeCredits'] += curr.totalProgrammeCredits
|
||||
break
|
||||
|
||||
case 'otherProgramme':
|
||||
acc[curr.code]['years'][curr.year]['totalOtherProgrammeStudents'] += curr.totalOtherProgrammeStudents
|
||||
acc[curr.code]['years'][curr.year]['totalOtherProgrammeCredits'] += curr.totalOtherProgrammeCredits
|
||||
break
|
||||
|
||||
case 'noStudyright':
|
||||
acc[curr.code]['years'][curr.year]['totalWithoutStudyrightStudents'] += curr.totalWithoutStudyrightStudents
|
||||
acc[curr.code]['years'][curr.year]['totalWithoutStudyrightCredits'] += curr.totalWithoutStudyrightCredits
|
||||
break
|
||||
case 'transfer':
|
||||
acc[curr.code]['years'][curr.year]['totalTransferStudents'] += curr.totalTransferStudents
|
||||
acc[curr.code]['years'][curr.year]['totalTransferCredits'] += curr.totalTransferCredits
|
||||
}
|
||||
|
||||
return acc
|
||||
@@ -164,6 +182,8 @@ const getStudyprogrammeCoursesForStudytrack = async (unixMillis, studyprogramme,
|
||||
'totalOtherProgrammeCredits',
|
||||
'totalWithoutStudyrightStudents',
|
||||
'totalWithoutStudyrightCredits',
|
||||
'totalTransferCredits',
|
||||
'totalTransferStudents',
|
||||
]
|
||||
ayCourses.forEach(ayCourse => {
|
||||
const normCode = isOpenUniCourseCode(ayCourse)[1]
|
||||
@@ -187,6 +207,8 @@ const getStudyprogrammeCoursesForStudytrack = async (unixMillis, studyprogramme,
|
||||
totalOtherProgrammeCredits: 0,
|
||||
totalWithoutStudyrightStudents: 0,
|
||||
totalWithoutStudyrightCredits: 0,
|
||||
totalTransferCredits: 0,
|
||||
totalTransferStudents: 0,
|
||||
}
|
||||
} else {
|
||||
mergedCourse['years'][year] = { ...allCourses[normCode]['years'][year] }
|
||||
|
||||
@@ -76,12 +76,13 @@ const formatStudyright = studyright => {
|
||||
}
|
||||
|
||||
const formatStudent = student => {
|
||||
const { studentnumber, gender_code, home_country_en, creditcount } = student
|
||||
const { studentnumber, gender_code, home_country_en, creditcount, credits } = student
|
||||
return {
|
||||
studentnumber,
|
||||
gender_code,
|
||||
home_country_en,
|
||||
creditcount,
|
||||
credits,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -150,15 +151,16 @@ const isNonMajorCredit = (studyrights, attainment_date) =>
|
||||
)
|
||||
})
|
||||
|
||||
const isSpecialGroupCredit = (studyrights, attainment_date, transfers) =>
|
||||
const isSpecialGroupCredit = (studyrights, attainment_date, transfers) => {
|
||||
studyrights.some(studyright => {
|
||||
if (!studyright) return true // If there is no studyright matching the credit, is not a major student credit
|
||||
if (studyright.studystartdate > attainment_date) return true // Credits before the studyright started are not major student credits
|
||||
if (studyright.enddate && attainment_date > studyright.enddate) return true // Credits after studyright are not major student credits
|
||||
if ([7, 9, 16, 34, 33, 99, 14, 13].includes(studyright.extentcode)) return true // Excludes non-degree studyrights and exchange students
|
||||
if ([7, 9, 16, 34, 22, 99, 14, 13].includes(studyright.extentcode)) return true // Excludes non-degree studyrights and exchange students
|
||||
if (transfers.includes(studyright.studyrightid)) return true // Excludes both transfers in and out of the programme
|
||||
return false
|
||||
})
|
||||
}
|
||||
|
||||
const getMedian = values => {
|
||||
if (values.length === 0) return 0
|
||||
@@ -223,8 +225,12 @@ const getPercentage = (value, total) => {
|
||||
const getEmptyArray = length => new Array(length).fill(0)
|
||||
|
||||
const getBachelorCreditGraphStats = years => ({
|
||||
lte15: {
|
||||
name: 'Less than 15 credits',
|
||||
data: getEmptyArray(years.length),
|
||||
},
|
||||
lte30: {
|
||||
name: 'Less than 30 credits',
|
||||
name: '15-29 credits',
|
||||
data: getEmptyArray(years.length),
|
||||
},
|
||||
lte60: {
|
||||
@@ -284,6 +290,33 @@ const getMasterCreditGraphStats = years => ({
|
||||
},
|
||||
})
|
||||
|
||||
const getOnlyMasterCreditGraphStats = years => ({
|
||||
lte15: {
|
||||
name: 'Less than 15 credits',
|
||||
data: getEmptyArray(years.length),
|
||||
},
|
||||
lte30: {
|
||||
name: '15-29 credits',
|
||||
data: getEmptyArray(years.length),
|
||||
},
|
||||
lte60: {
|
||||
name: '30-59 credits',
|
||||
data: getEmptyArray(years.length),
|
||||
},
|
||||
lte90: {
|
||||
name: '60-89 credits',
|
||||
data: getEmptyArray(years.length),
|
||||
},
|
||||
lte120: {
|
||||
name: '90-119 credits',
|
||||
data: getEmptyArray(years.length),
|
||||
},
|
||||
mte120: {
|
||||
name: 'More than 120 credits',
|
||||
data: getEmptyArray(years.length),
|
||||
},
|
||||
})
|
||||
|
||||
const getDoctoralCreditGraphStats = years => ({
|
||||
lte50: {
|
||||
name: 'Less than 50 credits',
|
||||
@@ -321,11 +354,13 @@ const getCreditGraphStats = (studyprogramme, years) => {
|
||||
return getDoctoralCreditGraphStats(years)
|
||||
}
|
||||
|
||||
const bachelorCreditThresholds = ['lte30', 'lte60', 'lte90', 'lte120', 'lte150', 'lte180', 'mte180']
|
||||
const bachelorCreditThresholds = ['lte15', 'lte30', 'lte60', 'lte90', 'lte120', 'lte150', 'lte180', 'mte180']
|
||||
const masterCreditThresholds = ['lte200', 'lte220', 'lte240', 'lte260', 'lte280', 'lte300', 'mte300']
|
||||
// const onlyMasterCredtThresholds = ['lte15', 'lte30', 'lte60', 'lte90', 'mte120']
|
||||
const doctoralCreditThresholds = ['lte50', 'lte100', 'lte150', 'lte200', 'lte250', 'lte300', 'mte300']
|
||||
const bachelorCreditAmounts = [30, 60, 90, 120, 150, 180, 180]
|
||||
const bachelorCreditAmounts = [15, 30, 60, 90, 120, 150, 180, 180]
|
||||
const masterCreditAmounts = [200, 220, 240, 260, 280, 300, 300]
|
||||
// const onlyMasterCreditAmounts = [15, 30, 60, 90, 120, 120]
|
||||
const doctoralCreditAmounts = [50, 100, 150, 200, 250, 300, 300]
|
||||
|
||||
const getCreditThresholds = studyprogramme => {
|
||||
@@ -349,8 +384,8 @@ const tableTitles = {
|
||||
'',
|
||||
'Total',
|
||||
'Major students credits',
|
||||
'Non major students credits',
|
||||
'Non degree students credits',
|
||||
'Non-major students credits',
|
||||
'Non-degree students credits',
|
||||
'Transferred credits',
|
||||
],
|
||||
},
|
||||
@@ -358,7 +393,8 @@ const tableTitles = {
|
||||
bachelor: [
|
||||
'',
|
||||
'All',
|
||||
'< 30 credits',
|
||||
'< 15 credits',
|
||||
'15-29 credits',
|
||||
'30-59 credits',
|
||||
'60-89 credits',
|
||||
'90-119 credits',
|
||||
@@ -377,6 +413,16 @@ const tableTitles = {
|
||||
'280-299 credits',
|
||||
'> 300 credits',
|
||||
],
|
||||
masterOnly: [
|
||||
'',
|
||||
'All',
|
||||
'< 15 credits',
|
||||
'15-29 credits',
|
||||
'30-59 credits',
|
||||
'60-89 credits',
|
||||
'90-119 credits',
|
||||
'> 120 credits',
|
||||
],
|
||||
doctoral: [
|
||||
'',
|
||||
'All',
|
||||
@@ -431,6 +477,7 @@ module.exports = {
|
||||
getBachelorCreditGraphStats,
|
||||
getMasterCreditGraphStats,
|
||||
getDoctoralCreditGraphStats,
|
||||
getOnlyMasterCreditGraphStats,
|
||||
getCreditGraphStats,
|
||||
getCreditThresholds,
|
||||
tableTitles,
|
||||
|
||||
@@ -30,7 +30,6 @@ const updateBasicView = async code => {
|
||||
}
|
||||
|
||||
const options = [specialCalendar, specialAcademic, specialExcludedCalendar, specialExcludedAcademic]
|
||||
|
||||
for (const option of options) {
|
||||
try {
|
||||
const basicStats = await getBasicStatsForStudytrack({
|
||||
@@ -62,8 +61,8 @@ const updateBasicView = async code => {
|
||||
} catch (e) {
|
||||
logger.error(e)
|
||||
}
|
||||
return 'OK'
|
||||
}
|
||||
return 'OK'
|
||||
}
|
||||
|
||||
const updateStudytrackView = async code => {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
const {
|
||||
dbConnections: { sequelize },
|
||||
} = require('../database/connection')
|
||||
const { Studyright, StudyrightElement, ElementDetail } = require('../models')
|
||||
const { Studyright, StudyrightElement, ElementDetail, Transfer } = require('../models')
|
||||
const moment = require('moment')
|
||||
const { redisClient } = require('../services/redis')
|
||||
const _ = require('lodash')
|
||||
@@ -187,6 +187,10 @@ const StudyRightType = {
|
||||
|
||||
const calculateAssociationsFromDb = async (chunksize = 100000) => {
|
||||
// bottlenecked by Studyright.findAll in associatedStudyrightElements()
|
||||
const transfers = await Transfer.findAll({
|
||||
attributes: ['studyrightid'],
|
||||
})
|
||||
const transferedStudyrightIds = transfers.map(transfer => transfer.studyrightid)
|
||||
const getSemester = momentstartdate => {
|
||||
if (momentstartdate < moment(`${momentstartdate.utc().year()}-07-31 21:00:00+00`)) return 'SPRING'
|
||||
return 'FALL'
|
||||
@@ -247,8 +251,12 @@ const calculateAssociationsFromDb = async (chunksize = 100000) => {
|
||||
name: e.name,
|
||||
code: e.code,
|
||||
}
|
||||
if (!associations.programmes[code].studytracks.includes(e.code))
|
||||
if (
|
||||
!associations.programmes[code].studytracks.includes(e.code) &&
|
||||
!transferedStudyrightIds.includes(e.studyrightid)
|
||||
) {
|
||||
associations.programmes[code].studytracks.push(e.code)
|
||||
}
|
||||
}
|
||||
associations.studyTracks[e.code] = associations.studyTracks[e.code] || {
|
||||
type: e.type,
|
||||
|
||||
@@ -26,11 +26,15 @@ const {
|
||||
} = require('./studyprogramme')
|
||||
const { getAcademicYearDates } = require('../util/semester')
|
||||
|
||||
const getStudentData = (students, thresholdKeys, thresholdAmounts) => {
|
||||
const getStudentData = (startDate, students, thresholdKeys, thresholdAmounts) => {
|
||||
let data = { female: 0, male: 0, finnish: 0 }
|
||||
thresholdKeys.forEach(t => (data[t] = 0))
|
||||
|
||||
students.forEach(({ gender_code, home_country_en, creditcount }) => {
|
||||
students.forEach(({ gender_code, home_country_en, credits }) => {
|
||||
const creditcount = credits
|
||||
.filter(credit => moment(credit.attainment_date).isAfter(startDate))
|
||||
.reduce((prev, curr) => (prev += curr.credits), 0)
|
||||
|
||||
data.male += gender_code === '1' ? 1 : 0
|
||||
data.female += gender_code === '2' ? 1 : 0
|
||||
data.finnish += home_country_en === 'Finland' ? 1 : 0
|
||||
@@ -40,7 +44,13 @@ const getStudentData = (students, thresholdKeys, thresholdAmounts) => {
|
||||
data[thresholdKeys[3]] += creditcount >= thresholdAmounts[2] && creditcount < thresholdAmounts[3] ? 1 : 0
|
||||
data[thresholdKeys[4]] += creditcount >= thresholdAmounts[3] && creditcount < thresholdAmounts[4] ? 1 : 0
|
||||
data[thresholdKeys[5]] += creditcount >= thresholdAmounts[4] && creditcount < thresholdAmounts[5] ? 1 : 0
|
||||
data[thresholdKeys[6]] += creditcount >= thresholdAmounts[6] ? 1 : 0
|
||||
// bachelor programme has more columns than master and doctoral programmes
|
||||
if (thresholdKeys.length > 7) {
|
||||
data[thresholdKeys[6]] += creditcount >= thresholdAmounts[5] && creditcount < thresholdAmounts[6] ? 1 : 0
|
||||
data[thresholdKeys[7]] += creditcount >= thresholdAmounts[6] ? 1 : 0
|
||||
} else {
|
||||
data[thresholdKeys[6]] += creditcount >= thresholdAmounts[5] ? 1 : 0
|
||||
}
|
||||
})
|
||||
return data
|
||||
}
|
||||
@@ -121,7 +131,7 @@ const getStudytrackDataForTheYear = async ({
|
||||
// Get all the studyrights and students for the calculations
|
||||
const all = await allStudyrights(track, studentnumbers)
|
||||
const students = await studytrackStudents(studentnumbers)
|
||||
const studentData = getStudentData(students, creditThresholdKeys, creditThresholdAmounts)
|
||||
const studentData = getStudentData(startDate, students, creditThresholdKeys, creditThresholdAmounts)
|
||||
const started = await startedStudyrights(track, startDate, studentnumbers)
|
||||
const enrolled = await enrolledStudents(track, startDate, studentnumbers)
|
||||
const absent = await absentStudents(track, studentnumbers)
|
||||
|
||||
@@ -43,7 +43,7 @@ const getCategoriesAndYears = async () => {
|
||||
|
||||
const creditsWithTeachersForYear = yearcode =>
|
||||
Credit.findAll({
|
||||
attributes: ['id', 'credits', 'credittypecode', 'isStudyModule'],
|
||||
attributes: ['id', 'credits', 'credittypecode', 'isStudyModule', 'is_open'],
|
||||
include: [
|
||||
{
|
||||
model: Semester,
|
||||
@@ -106,21 +106,20 @@ const findTopTeachers = async yearcode => {
|
||||
credits
|
||||
.filter(isRegularCourse)
|
||||
.map(credit => {
|
||||
const { credits, course, credittypecode } = credit
|
||||
const { credits, credittypecode, is_open } = credit
|
||||
const teachers = credit.teachers
|
||||
.filter(({ id }) => !id.includes('hy-hlo-org')) // Remove faculties from the leaderboards
|
||||
.map(({ id, name }) => ({ id, name }))
|
||||
const passed = Credit.passed(credit) || Credit.improved(credit)
|
||||
const failed = Credit.failed(credit)
|
||||
const transferred = credittypecode === 9
|
||||
const isOpenUni = course.code[0] === 'A'
|
||||
return { passed, failed, credits, teachers, isOpenUni, transferred }
|
||||
return { passed, failed, credits, teachers, is_open, transferred }
|
||||
})
|
||||
.forEach(credit => {
|
||||
const { passed, failed, credits, teachers, isOpenUni, transferred } = credit
|
||||
const { passed, failed, credits, teachers, is_open, transferred } = credit
|
||||
teachers.forEach(teacher => {
|
||||
all[teacher.id] = updatedStats(all, teacher, passed, failed, credits, transferred)
|
||||
if (isOpenUni) {
|
||||
if (is_open) {
|
||||
openuni[teacher.id] = updatedStats(openuni, teacher, passed, failed, credits, transferred)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -10,6 +10,7 @@ const { User, UserElementDetails, AccessGroup, UserFaculties, sequelizeUser } =
|
||||
const { getOrganizationAccess } = require('../util/organizationAccess')
|
||||
|
||||
const courseStatisticsGroup = 'grp-oodikone-basic-users'
|
||||
const facultyStatisticsGroup = 'grp-oodikone-users'
|
||||
const hyOneGroup = 'hy-one'
|
||||
const toskaGroup = 'grp-toska'
|
||||
|
||||
@@ -191,7 +192,9 @@ const formatUser = async userFromDb => {
|
||||
|
||||
const rights = _.uniqBy([...programmes, ...enrichProgrammesFromFaculties(faculties)])
|
||||
|
||||
const iamRights = Object.keys(await getOrganizationAccess(userFromDb))
|
||||
const iamRights = Object.keys(await getOrganizationAccess(userFromDb)).filter(
|
||||
programmeCode => !rights.includes(programmeCode)
|
||||
)
|
||||
|
||||
const {
|
||||
dataValues: {
|
||||
@@ -265,6 +268,8 @@ const getUser = async ({ username, name, email, iamGroups, sisId }) => {
|
||||
let newAccessGroups = []
|
||||
if (await checkStudyGuidanceGroupsAccess(sisId)) newAccessGroups.push('studyGuidanceGroups')
|
||||
if (iamGroups.includes(courseStatisticsGroup)) newAccessGroups.push('courseStatistics')
|
||||
if (iamGroups.includes(facultyStatisticsGroup) || iamGroups.some(element => /-jory$/.test(element)))
|
||||
newAccessGroups.push('facultyStatistics')
|
||||
if (iamGroups.includes(hyOneGroup) || currentAccessGroups.includes('teachers')) newAccessGroups.push('teachers')
|
||||
if (iamGroups.includes(toskaGroup) || currentAccessGroups.includes('admin')) newAccessGroups.push('admin')
|
||||
if (_.difference(newAccessGroups, currentAccessGroups).length > 0) {
|
||||
|
||||
@@ -23,7 +23,14 @@
|
||||
"no-shadow": "off",
|
||||
"react/jsx-props-no-spreading": "off",
|
||||
"react/jsx-filename-extension": "off",
|
||||
"no-async-promise-executor": "off",
|
||||
"react/function-component-definition": [
|
||||
2,
|
||||
{
|
||||
"namedComponents": "arrow-function",
|
||||
"unnamedComponents": "arrow-function"
|
||||
}
|
||||
],
|
||||
"default-param-last": "off",
|
||||
"no-implied-eval": "off",
|
||||
"react/state-in-constructor": "off",
|
||||
"react/static-property-placement": "off",
|
||||
@@ -31,6 +38,9 @@
|
||||
"react/no-unescaped-entities": "off",
|
||||
"react/prop-types": "off",
|
||||
"react/prefer-stateless-function": "error",
|
||||
"react/no-unstable-nested-components": "off",
|
||||
"react/jsx-no-constructed-context-values": "off",
|
||||
"react/jsx-no-useless-fragment": "off",
|
||||
"import/no-unresolved": "off",
|
||||
"react/jsx-no-bind": "error",
|
||||
"prefer-object-spread/prefer-object-spread": "error",
|
||||
|
||||
Generated
+25235
-3179
File diff suppressed because it is too large
Load Diff
@@ -6,7 +6,7 @@
|
||||
"build": "DISABLE_ESLINT_PLUGIN=true react-scripts build"
|
||||
},
|
||||
"dependencies": {
|
||||
"@reduxjs/toolkit": "^1.8.0",
|
||||
"@reduxjs/toolkit": "^1.8.5",
|
||||
"@sentry/browser": "^6.18.2",
|
||||
"@sentry/tracing": "^6.18.2",
|
||||
"axios": "^0.21.4",
|
||||
@@ -18,7 +18,7 @@
|
||||
"history": "^4.10.1",
|
||||
"immer": "^9.0.12",
|
||||
"lodash": "^4.17.21",
|
||||
"moment": "^2.29.1",
|
||||
"moment": "^2.29.4",
|
||||
"path-to-regexp": "^0.1.7",
|
||||
"piwik-react-router": "^0.12.1",
|
||||
"prop-types": "^15.8.1",
|
||||
@@ -35,6 +35,7 @@
|
||||
"react-router-dom": "^5.3.0",
|
||||
"react-scripts": "4.0.3",
|
||||
"react-scroll-to-component": "^1.0.2",
|
||||
"scheduler": "^0.23.0",
|
||||
"semantic-ui-css": "^2.4.1",
|
||||
"semantic-ui-react": "^2.1.2",
|
||||
"unfuck-spa-shibboleth-session": "^3.1.4",
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
export default {
|
||||
ProgrammeToggle: `
|
||||
New study programmes: Näyttää kaikissa luvuissa ja kuvaajissa pelkästään uuden opetussuunnitelman ohjelmat.
|
||||
All programmes: Näyttää kaikissa luvuissa ja kuvaajissa kuvaajissa myös vanhan opetussuunnitelman ohjelmat.`,
|
||||
YearToggle: `
|
||||
Academic year: Näyttää tilastot akateemisille vuosille jaoteltuna, esimerkiksi 1.8.2019-31.7.2020
|
||||
Calendar year: Näyttää tilastot kalenterivuosille jaoteltuna, esimerkiksi 1.1.2019-31.12.2019
|
||||
`,
|
||||
StudentsOfTheFaculty: `
|
||||
**Started studying**: Sisältää kyseisenä vuonna tiedekunnassa aloittaneet opiskelijat (aloitetut opinto-oikeudet) riippumatta siitä, minä vuonna kyseinen opiskeluoikeus on myönnetty. Opiskelija on myös voinut myöhemmin keskeyttää opintonsa tai siirtyä pois ohjelmasta.
|
||||
Kandi-maisteri-opinto-oikeudellisista opiskelijoista lasketaan vain kandissa ts. tiedekunnassa aloitus, ei toistamiseen enää maisteriopintojen aloitusta.\n
|
||||
**Graduated**: Sisältää kyseisenä vuonna valmistuneet opiskelijat riippumatta siitä, minä vuonna opiskeluoikeus ohjelmaan on myönnetty. Jaottelu on tehty opinto-oikeuden päättymispäivän (=valmistumispäivän) perusteella.
|
||||
|
||||
**Transferred inside**: Sisältää kyseisenä vuonna tiedekunnan sisällä siirtyneet opiskelijat (opinto-oikeudet). Ohjelmakohtaisissa tilastoissa siirto on laskettu kohdeohjelman tilastoihin.
|
||||
|
||||
**Transferred away**: Sisältää kyseisenä vuonna pois tiedekunnasta siirtyneet opiskelijat (opinto-oikeudet). Sama opiskelija voi esiintyä tilastoissa useana vuonna, mikäli hän on siirtynyt tiedekuntaan ja siitä pois useasti.
|
||||
|
||||
**Transferred to**: Sisältää kyseisenä vuonna tiedekuntaan siirtyneet opiskelijat (opinto-oikeudet).
|
||||
|
||||
HUOM! Siirtyneiden määrä on erityisen suuri vuonna 2020, jolloin vanhoista koulutusohjelmista luovuttiin ja opiskelijat siirtyivät uusiin ohjelmiin.
|
||||
`,
|
||||
CreditsProducedByTheFaculty: `
|
||||
Sisältää opintopisteet suoritusvuosittain. Suoritukset on jaoteltu Sisussa näkyvän kurssin suorituspäivän mukaan.
|
||||
|
||||
**Major students credits**: Sisältää ne opintopisteet, joiden suorittajalla on suoritushetkellä ollut ensisijainen opinto-oikeus johonkin tiedekunnan koulutusohjelmaan. Sisältää myös tiedekunnan ohjelmaan siirtyneet ensisijaiset opiskelijat.
|
||||
|
||||
**Non-major faculty students credits**: Sisältää tiedekunnan koulutusohjelmien tuottamat opintopisteet, joiden suorittajalla EI ole ollut suoritushetkellä ensisijaista opinto-oikeutta kyseiseen koulutusohjelmaan,
|
||||
MUTTA hänellä on sellainen johonkin toiseen tiedekunnan ohjelmaan. Sisältää esimerkiksi vaihto-opiskelijat ja erillis-opinto-oikeudella opiskelevat.
|
||||
Sisältää myös opintopisteet, jotka opiskelija on suorittanut koulutusohjelmaan ennen opintojen aloittamista koulutusohjelmassa tai opintojen päätyttyä.
|
||||
|
||||
**Non-major other faculty students credits**: Sisältää tiedekunnan koulutusohjelmien tuottamat opintopisteet, joiden suorittajalla EI ole ollut suoritushetkellä ensisijaista opinto-oikeutta kyseiseen koulutusohjelmaan tai mihinkään muuhun kyseisen tiedekunnan koulutusohjelmaan.
|
||||
Sisältää esimerkiksi vaihto-opiskelijat ja erillis-opinto-oikeudella opiskelevat. Sisältää myös opintopisteet, jotka opiskelija on suorittanut koulutusohjelmaan ennen opintojen aloittamista koulutusohjelmassa tai opintojen päätyttyä.
|
||||
|
||||
**Non-degree student credits**: Sisältää tiedekuntaan suoritetut opintopisteet, joiden suorittajilla ei ole tutkinto-oikeutta Helsingin yliopistossa.
|
||||
`,
|
||||
GraduatedOfTheFaculty: `Sisältää kyseisenä vuonna tiedekunnasta valmistuneet opiskelijat riippumatta siitä, minä vuonna opiskeluoikeus ohjelmaan on myönnetty. Jaottelu on tehty opinto-oikeuden päättymispäivän (=valmistumispäivän) perusteella.`,
|
||||
ThesisWritersOfTheFaculty: `Sisältää kyseisenä vuonna kandidaatintutkielman, pro gradu -tutkielman, väitöskirjan tai lisensiaatintyön hyväksytysti suorittaneet opiskelijat. Mukana ei ole mahdollisia arvosanan korotuksia.`,
|
||||
StudentToggle: `
|
||||
All studyrights: Valinnassa ovat mukana vaihto-opiskelijat, erillisoikeudella opiskelevat ja ohjelmaan siirtyneet opiskelijat sekä ohjelmasta pois siirtyneet opiskelijat.\n
|
||||
Special studyrights excluded: Valinnasta ja kaikista luvuista on poissuljettu kaikki edellä mainitut erikoisryhmät.
|
||||
`,
|
||||
|
||||
// Graduation times
|
||||
GroupByToggle: `Group by: Starting year: Valmistuneet opiskelijat on jaoteltu opinto-oikeuden alkuvuoden mukaan.\n
|
||||
Graduation year: Valmistuneet opiskelijat on ryhmitelty opintojen valmistumisvuoden perusteella.`,
|
||||
AverageGraduationTimes: `
|
||||
Opiskelijoiden keskimääräiset valmistumisajat tutkintotasoittain.
|
||||
|
||||
- Bachelor: Kandidaatin tutkinnon suoritusajat
|
||||
- Bachelor + Master: Kandi+maisteriopinto-oikeudella opiskelleiden ja siitä maisteriksi valmistuneiden suoritusajat
|
||||
- Master: Pelkällä maisterintutkinnon opinto-oikeudella opiskelleiden suoritusajat
|
||||
- Doctor: Tohtorin tutkinnon suoritusajat
|
||||
- Licentiate: Lisensiaatin tutkinnon suoritusajat
|
||||
|
||||
Valmistumisajoista on **vähennetty lakisääteiset poissaolot**. Vihreä katkoviiva kuvastaa tavoiteaikaa.
|
||||
Vihreä palkki merkitsee, että keskimääräinen valmistumisaika on alle tai yhtä kuin tavoiteaika, keltainen palkki merkitsee korkeintaan vuoden ylitystä
|
||||
ja punainen yli vuoden tavoiteajan ylitystä.
|
||||
|
||||
Säädöt:
|
||||
|
||||
**Starting year**. Ryhmittelee tulokset opiskelijoiden opinto-oikeuden alkamisvuosittain. Näyttää siis vuonna xxxx aloittaneiden opiskelijoiden keskimääräiset valmistumisajat. Luvut voivat elää, koska osa vuonna xxxx
|
||||
aloittaneiden opiskelijoiden opinnoista voi olla edelleen kesken.
|
||||
|
||||
**Graduation year**: Ryhmittelee tulokset valmistumisvuosittain. Näyttää siis vuonna xxxx valmistuneiden opiskelijoiden keskimääräiset valmistumisajat.
|
||||
Luvut ovat lopullisia (kuluva vuosi poislukien), sillä edeltäville vuosille ei tule uusia valmistumisia.
|
||||
|
||||
**Median time**: Näyttää kyseisenä vuonna valmistuneiden opiskelijoiden opintojen keston **mediaanin** kuukausina.
|
||||
|
||||
**Mean time**: Näyttää kyseisenä vuonna valmistuneiden opiskelijoiden opintojen keston **keskiarvon** kuukausina.`,
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import CoolDataScience from './coolDataScience'
|
||||
import CourseStatistics from './courseStatistics'
|
||||
import CoursePopulation from './coursePopulation'
|
||||
import Studyprogramme from './studyprogramme'
|
||||
import Faculty from './faculty'
|
||||
|
||||
export default {
|
||||
PopulationStatistics,
|
||||
@@ -10,4 +11,5 @@ export default {
|
||||
CourseStatistics,
|
||||
CoursePopulation,
|
||||
Studyprogramme,
|
||||
Faculty,
|
||||
}
|
||||
|
||||
@@ -80,4 +80,10 @@ export default {
|
||||
NonDegree: `
|
||||
Opiskelijat, joilla ei ole tutkinto-oikeutta Helsingin yliopistossa.
|
||||
`,
|
||||
TransferredCourses: `
|
||||
Sisältää opiskelijat, jotka ovat suorittaneet kyseisenä vuonna hyväksiluettuja opintopisteitä tähän koulutusohjelmaan. Mukana ovat kaikenlaisilla opinto-oikeuksilla hyväksiluetut opintopisteet
|
||||
`,
|
||||
TransferredCredits: `
|
||||
Sisältää opintopisteet, jotka ovat suoritettu kyseisenä vuonna, ja hyväksiluettu tähän koulutusohjelmaan. Mukana ovat kaikenlaisilla opinto-oikeuksilla hyväksiluetut opintopisteet
|
||||
`,
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@ export const byName = (a, b) => a.name.localeCompare(b.name)
|
||||
|
||||
export const byCodeDesc = (a, b) => b.code.localeCompare(a.code)
|
||||
|
||||
export const studyRightRegex = new RegExp(/.*master|bachelor|doctor|licentiate|specialist.*/)
|
||||
export const studyRightRegex = /.*master|bachelor|doctor|licentiate|specialist.*/
|
||||
|
||||
export const studyrightElementTypes = { programme: 20, speciality: 30 }
|
||||
|
||||
@@ -150,8 +150,8 @@ export const getUnifyTextIn = unifyCourses => {
|
||||
export const cancelablePromise = promise => {
|
||||
let hasCanceled = false
|
||||
|
||||
// eslint-disable-next-line no-async-promise-executor
|
||||
const wrappedPromise = new Promise(async (res, rej) => {
|
||||
// eslint-disable-line no-async-promise-executor
|
||||
try {
|
||||
await promise
|
||||
if (hasCanceled) res(false)
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
}
|
||||
|
||||
.cool-data-science-tile.cool-data-science-tile--active {
|
||||
box-shadow: 0 3px 6px rgba(0, 0, 0, 0.16), 0 3px 6px rgba(0, 0, 0, 0.23);
|
||||
box-shadow: 0 3px 6px rgb(0 0 0 / 16%), 0 3px 6px rgb(0 0 0 / 23%);
|
||||
}
|
||||
|
||||
.cool-data-science-tile__title {
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
}
|
||||
|
||||
.status-card:hover {
|
||||
box-shadow: 0 3px 6px rgba(0, 0, 0, 0.16), 0 3px 6px rgba(0, 0, 0, 0.23);
|
||||
box-shadow: 0 3px 6px rgb(0 0 0 / 16%), 0 3px 6px rgb(0 0 0 / 23%);
|
||||
}
|
||||
|
||||
.years {
|
||||
|
||||
+1
-1
@@ -65,7 +65,7 @@ const countTotalStats = (formattedStats, userHasAccessToAllStats) => {
|
||||
categories: { passed, failed },
|
||||
grades: cgrades,
|
||||
enrollmentsByState: { ...acc.attempts.enrollmentsByState },
|
||||
totalEnrollments: acc.students.totalEnrollments + (curr.students.totalEnrollments || 0),
|
||||
totalEnrollments: acc.attempts.totalEnrollments + (curr.attempts.totalEnrollments || 0),
|
||||
},
|
||||
students: {
|
||||
totalEnrollments: acc.students.totalEnrollments + (curr.students.totalEnrollments || 0),
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable camelcase */
|
||||
import React, { useState, useRef, useMemo } from 'react'
|
||||
import { renderToString } from 'react-dom/server'
|
||||
import moment from 'moment'
|
||||
@@ -261,9 +262,7 @@ const createGoalSeries = (starting, ending, absences) => {
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line camelcase
|
||||
const resolveStudyRightElement = ({ studyright_elements }) => {
|
||||
// eslint-disable-next-line camelcase
|
||||
if (!studyright_elements || !studyright_elements.length) return {}
|
||||
return (
|
||||
studyright_elements
|
||||
|
||||
@@ -1,40 +1,167 @@
|
||||
import React from 'react'
|
||||
import { Divider, Loader } from 'semantic-ui-react'
|
||||
|
||||
import { useGetFacultyCreditStatsQuery, useGetFacultyBasicStatsQuery } from 'redux/facultyStats'
|
||||
import { Divider, Loader, Popup, Button, Message } from 'semantic-ui-react'
|
||||
import xlsx from 'xlsx'
|
||||
import {
|
||||
useGetFacultyCreditStatsQuery,
|
||||
useGetFacultyBasicStatsQuery,
|
||||
useGetFacultyThesisStatsQuery,
|
||||
} from 'redux/facultyStats'
|
||||
import LineGraph from 'components/StudyProgramme/BasicOverview/LineGraph'
|
||||
import StackedBarChart from 'components/StudyProgramme/BasicOverview/StackedBarChart'
|
||||
import DataTable from 'components/StudyProgramme/BasicOverview/DataTable'
|
||||
import InteractiveDataTable from '../InteractiveDataView'
|
||||
import Toggle from '../../StudyProgramme/Toggle'
|
||||
import InfoBox from '../../Info/InfoBox'
|
||||
import InfotoolTips from '../../../common/InfoToolTips'
|
||||
import '../faculty.css'
|
||||
|
||||
const Overview = ({ faculty, academicYear, setAcademicYear }) => {
|
||||
const toolTips = InfotoolTips.Studyprogramme
|
||||
const Overview = ({
|
||||
faculty,
|
||||
academicYear,
|
||||
setAcademicYear,
|
||||
studyProgrammes,
|
||||
setStudyProgrammes,
|
||||
specialGroups,
|
||||
setSpecialGroups,
|
||||
language,
|
||||
}) => {
|
||||
const toolTips = InfotoolTips.Faculty
|
||||
const yearType = academicYear ? 'ACADEMIC_YEAR' : 'CALENDAR_YEAR'
|
||||
const special = 'SPECIAL_INCLUDED' // specialGroups ? 'SPECIAL_EXCLUDED' : 'SPECIAL_INCLUDED'
|
||||
const credits = useGetFacultyCreditStatsQuery({ id: faculty?.code, yearType, specialGroups: special })
|
||||
const basics = useGetFacultyBasicStatsQuery({ id: faculty?.code, yearType, specialGroups: special })
|
||||
const studyProgrammeFilter = studyProgrammes ? 'ALL_PROGRAMMES' : 'NEW_STUDY_PROGRAMMES'
|
||||
const special = specialGroups ? 'SPECIAL_EXCLUDED' : 'SPECIAL_INCLUDED'
|
||||
const credits = useGetFacultyCreditStatsQuery({
|
||||
id: faculty?.code,
|
||||
yearType,
|
||||
studyProgrammeFilter,
|
||||
specialGroups: special,
|
||||
})
|
||||
const basics = useGetFacultyBasicStatsQuery({
|
||||
id: faculty?.code,
|
||||
yearType,
|
||||
studyProgrammeFilter,
|
||||
specialGroups: special,
|
||||
})
|
||||
const thesisWriters = useGetFacultyThesisStatsQuery({
|
||||
id: faculty?.code,
|
||||
yearType,
|
||||
studyProgrammeFilter,
|
||||
specialGroups: special,
|
||||
})
|
||||
|
||||
const getDivider = (title, toolTipText) => (
|
||||
/*
|
||||
Order of the programme keys: KH -> MH -> T -> FI -> K- -> Numbers containing letters at end -> Y- -> Numbers
|
||||
*/
|
||||
const regexValuesAll = [
|
||||
/^KH/,
|
||||
/^MH/,
|
||||
/^T/,
|
||||
/^LI/,
|
||||
/^K-/,
|
||||
/^FI/,
|
||||
/^00901$/,
|
||||
/^00910$/,
|
||||
/^\d.*a$/,
|
||||
/^Y/,
|
||||
/\d$/,
|
||||
/^\d.*e$/,
|
||||
]
|
||||
|
||||
const testKey = value => {
|
||||
for (let i = 0; i < regexValuesAll.length; i++) {
|
||||
if (regexValuesAll[i].test(value)) {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return 6
|
||||
}
|
||||
|
||||
const sortProgrammeKeys = programmeKeys => {
|
||||
return programmeKeys.sort((a, b) => {
|
||||
if (testKey(a) - testKey(b) === 0) {
|
||||
return a.localeCompare(b)
|
||||
}
|
||||
return testKey(a) - testKey(b)
|
||||
})
|
||||
}
|
||||
const downloadCsv = (titles, tableStats, programmeStats, programmeNames, toolTipText) => {
|
||||
const headers = titles.map(title => ({ label: title === '' ? 'Year' : title, key: title === '' ? 'Year' : title }))
|
||||
const csvData = sortProgrammeKeys(Object.keys(programmeStats)).reduce(
|
||||
(results, programme) => [
|
||||
...results,
|
||||
...programmeStats[programme].map(yearRow => {
|
||||
return {
|
||||
Programme: programme,
|
||||
Name: programmeNames[programme][language]
|
||||
? programmeNames[programme][language]
|
||||
: programmeNames[programme].fi,
|
||||
...yearRow.reduce((result, value, valueIndex) => ({ ...result, [headers[valueIndex].key]: value }), {}),
|
||||
}
|
||||
}),
|
||||
],
|
||||
[]
|
||||
)
|
||||
|
||||
const tableStatsAsCsv = tableStats.map(yearArray =>
|
||||
yearArray.reduce((result, value, yearIndex) => ({ ...result, [headers[yearIndex].key]: value }), {})
|
||||
)
|
||||
|
||||
const book = xlsx.utils.book_new()
|
||||
const tableSheet = xlsx.utils.json_to_sheet(tableStatsAsCsv)
|
||||
xlsx.utils.book_append_sheet(book, tableSheet, 'TableStats')
|
||||
const sheet = xlsx.utils.json_to_sheet(csvData)
|
||||
xlsx.utils.book_append_sheet(book, sheet, 'ProgrammeStats')
|
||||
xlsx.writeFile(book, `${faculty.code}-${toolTipText}.xlsx`)
|
||||
}
|
||||
|
||||
const getDivider = (title, toolTipText, titles, tableStats, programmeStats, programmeNames) => (
|
||||
<>
|
||||
<div className="divider">
|
||||
<Divider data-cy={`Section-${toolTipText}`} horizontal>
|
||||
{title}
|
||||
</Divider>
|
||||
</div>
|
||||
<InfoBox content="Sisältää opintopisteet suoritusvuosittain. Suoritukset on jaoteltu Sisussa näkyvän kurssin suorituspäivän mukaan. Kaikki valmistuneet -lukumäärä sisältää kaikki mahdolliset suoritetuksi merkityt opiskeluoikeudet." />
|
||||
{/* <InfoBox content={toolTips[toolTipText]} /> */}
|
||||
<InfoBox content={toolTips[toolTipText]} cypress={toolTipText} />
|
||||
<Popup
|
||||
content="Download statistics as csv"
|
||||
trigger={
|
||||
<Button
|
||||
icon="download"
|
||||
floated="right"
|
||||
onClick={() => downloadCsv(titles, tableStats, programmeStats, programmeNames, toolTipText)}
|
||||
style={{ backgroundColor: 'white', borderRadius: 0 }}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
const isFetchingOrLoading = credits.isLoading || credits.isFetching || basics.isLoading || basics.isFetching
|
||||
const isFetchingOrLoading =
|
||||
credits.isLoading ||
|
||||
credits.isFetching ||
|
||||
basics.isLoading ||
|
||||
basics.isFetching ||
|
||||
thesisWriters.isLoading ||
|
||||
thesisWriters.isFetching
|
||||
|
||||
const isError =
|
||||
(basics.isError && credits.isError) || (basics.isSuccess && !basics.data && credits.isSuccess && !credits.data)
|
||||
(basics.isError && credits.isError && thesisWriters.isError) ||
|
||||
(basics.isSuccess &&
|
||||
!basics.data &&
|
||||
credits.isSuccess &&
|
||||
!credits.data &&
|
||||
thesisWriters.isSuccess &&
|
||||
!thesisWriters.data)
|
||||
|
||||
if (isError) return <h3>Something went wrong, please try refreshing the page.</h3>
|
||||
|
||||
let creditShortTitles = []
|
||||
let transferShortTitles = []
|
||||
if (special === 'SPECIAL_INCLUDED') {
|
||||
creditShortTitles = ['Code', 'Total', 'Major', 'Non-major', 'Non-major other', 'Non-degree']
|
||||
transferShortTitles = ['Code', 'Started', 'Graduated', 'Transferred in', 'Transferred away', 'Transferred to']
|
||||
} else {
|
||||
creditShortTitles = ['Code', 'Major']
|
||||
transferShortTitles = ['Code', 'Started', 'Graduated']
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="faculty-overview">
|
||||
<div className="toggle-container">
|
||||
@@ -46,39 +173,174 @@ const Overview = ({ faculty, academicYear, setAcademicYear }) => {
|
||||
value={academicYear}
|
||||
setValue={setAcademicYear}
|
||||
/>
|
||||
<Toggle
|
||||
cypress="ProgrammeToggle"
|
||||
toolTips={toolTips.ProgrammeToggle}
|
||||
firstLabel="New study programmes"
|
||||
secondLabel="All study programmes"
|
||||
value={studyProgrammes}
|
||||
setValue={setStudyProgrammes}
|
||||
/>
|
||||
<Toggle
|
||||
cypress="StudentToggle"
|
||||
toolTips={toolTips.StudentToggle}
|
||||
firstLabel="All studyrights"
|
||||
secondLabel="Special studyrights excluded"
|
||||
value={specialGroups}
|
||||
setValue={setSpecialGroups}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{isFetchingOrLoading ? (
|
||||
<Loader active style={{ marginTop: '10em' }} />
|
||||
<Loader active style={{ marginTop: '15em' }} />
|
||||
) : (
|
||||
<>
|
||||
{studyProgrammeFilter === 'ALL_PROGRAMMES' && (
|
||||
<Message data-cy="FacultyProgrammesShownInfo">
|
||||
Please note that the data is complete only for current Bachelor, Masters and Doctoral programmes.
|
||||
Especially, credits and thesis writers contain only data for current programmes.
|
||||
</Message>
|
||||
)}
|
||||
{special === 'SPECIAL_EXCLUDED' && (
|
||||
<Message data-cy="FacultyExcludeSpecialsInfo">
|
||||
Please note that the data might be inaccurate in some fields. Checking the values is in progress.
|
||||
</Message>
|
||||
)}
|
||||
{basics.isSuccess && basics.data && (
|
||||
<>
|
||||
{getDivider('Students of the faculty', 'StudentsOfTheFaculty')}
|
||||
{getDivider(
|
||||
'Students of the faculty',
|
||||
'StudentsOfTheFaculty',
|
||||
basics?.data?.studentInfo.titles,
|
||||
basics?.data?.studentInfo.tableStats,
|
||||
basics?.data?.studentInfo.programmeTableStats,
|
||||
basics?.data?.programmeNames
|
||||
)}
|
||||
<div className="section-container">
|
||||
<LineGraph cypress="StudentsOfTheFaculty" data={basics?.data} />
|
||||
<DataTable
|
||||
cypress="StudentsOfTheFaculty"
|
||||
data={basics?.data?.tableStats}
|
||||
titles={basics?.data?.titles}
|
||||
/>
|
||||
<div className="graph-container-narrow">
|
||||
<LineGraph
|
||||
cypress="StudentsOfTheFaculty"
|
||||
data={{ ...basics?.data.studentInfo, years: basics.data.years }}
|
||||
/>
|
||||
</div>
|
||||
<div className="table-container-wide">
|
||||
<InteractiveDataTable
|
||||
cypress="StudentsOfTheFaculty"
|
||||
dataStats={basics?.data?.studentInfo.tableStats}
|
||||
dataProgrammeStats={basics?.data?.studentInfo.programmeTableStats}
|
||||
programmeNames={basics?.data?.programmeNames}
|
||||
sortedKeys={sortProgrammeKeys(Object.keys(basics?.data?.studentInfo.programmeTableStats))}
|
||||
titles={basics?.data?.studentInfo.titles}
|
||||
sliceStart={1}
|
||||
language={language}
|
||||
yearsVisible={Array(basics?.data?.studentInfo.tableStats.length).fill(false)}
|
||||
shortNames={transferShortTitles}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{credits.isSuccess && credits.data && (
|
||||
<>
|
||||
{getDivider('Credits produced by the faculty', 'CreditsProducedByTheFaculty')}
|
||||
{getDivider(
|
||||
'Credits produced by the faculty',
|
||||
'CreditsProducedByTheFaculty',
|
||||
credits?.data?.titles,
|
||||
credits?.data?.tableStats,
|
||||
credits?.data?.programmeTableStats,
|
||||
credits?.data?.programmeNames
|
||||
)}
|
||||
<div className="section-container">
|
||||
<StackedBarChart
|
||||
cypress="CreditsProducedByTheSFaculty"
|
||||
data={credits?.data?.graphStats}
|
||||
labels={credits?.data?.years}
|
||||
/>
|
||||
<DataTable
|
||||
cypress="CreditsProducedByTheFaculty"
|
||||
data={credits?.data?.tableStats}
|
||||
titles={credits?.data?.titles}
|
||||
/>
|
||||
<div className="graph-container-narrow">
|
||||
<StackedBarChart
|
||||
cypress="CreditsProducedByTheFaculty"
|
||||
data={credits?.data?.graphStats}
|
||||
labels={credits?.data?.years}
|
||||
wideTable="narrow"
|
||||
/>
|
||||
</div>
|
||||
<div className="table-container-wide">
|
||||
<InteractiveDataTable
|
||||
cypress="CreditsProducedByTheFaculty"
|
||||
dataStats={credits?.data?.tableStats}
|
||||
dataProgrammeStats={credits?.data?.programmeTableStats}
|
||||
programmeNames={credits?.data?.programmeNames}
|
||||
sortedKeys={sortProgrammeKeys(Object.keys(credits?.data?.programmeTableStats))}
|
||||
titles={credits?.data?.titles}
|
||||
extraHeight="EXTRA HEIGHT"
|
||||
sliceStart={2}
|
||||
language={language}
|
||||
yearsVisible={Array(credits?.data?.tableStats.length).fill(false)}
|
||||
shortNames={creditShortTitles}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{basics.isSuccess && basics.data && (
|
||||
<>
|
||||
{getDivider(
|
||||
'Graduated of the faculty',
|
||||
'GraduatedOfTheFaculty',
|
||||
basics?.data?.graduationInfo.titles,
|
||||
basics?.data?.graduationInfo.tableStats,
|
||||
basics?.data?.graduationInfo.programmeTableStats,
|
||||
basics?.data?.programmeNames
|
||||
)}
|
||||
<div className="section-container">
|
||||
<div className="graph-container-narrow">
|
||||
<LineGraph
|
||||
cypress="GraduatedOfTheFaculty"
|
||||
data={{ ...basics?.data.graduationInfo, years: basics.data.years }}
|
||||
/>
|
||||
</div>
|
||||
<div className="table-container-wide">
|
||||
<InteractiveDataTable
|
||||
cypress="GraduatedOfTheFaculty"
|
||||
dataStats={basics?.data?.graduationInfo.tableStats}
|
||||
dataProgrammeStats={basics?.data?.graduationInfo.programmeTableStats}
|
||||
programmeNames={basics?.data?.programmeNames}
|
||||
sortedKeys={sortProgrammeKeys(Object.keys(basics?.data?.graduationInfo.programmeTableStats))}
|
||||
titles={basics?.data?.graduationInfo.titles}
|
||||
sliceStart={2}
|
||||
language={language}
|
||||
yearsVisible={Array(basics?.data?.graduationInfo.tableStats.length).fill(false)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{thesisWriters.isSuccess && thesisWriters.data && (
|
||||
<>
|
||||
{getDivider(
|
||||
'Thesis writers of the faculty',
|
||||
'ThesisWritersOfTheFaculty',
|
||||
thesisWriters?.data?.titles,
|
||||
thesisWriters?.data.tableStats,
|
||||
thesisWriters?.data.programmeTableStats,
|
||||
thesisWriters?.data?.programmeNames
|
||||
)}
|
||||
<div className="section-container">
|
||||
<div className="graph-container-narrow">
|
||||
<LineGraph
|
||||
cypress="ThesisWritersOfTheFaculty"
|
||||
data={{ ...thesisWriters?.data, years: thesisWriters?.data.years }}
|
||||
/>
|
||||
</div>
|
||||
<div className="table-container-wide">
|
||||
<InteractiveDataTable
|
||||
cypress="ThesisWritersOfTheFaculty"
|
||||
dataStats={thesisWriters?.data.tableStats}
|
||||
dataProgrammeStats={thesisWriters?.data.programmeTableStats}
|
||||
programmeNames={thesisWriters?.data.programmeNames}
|
||||
sortedKeys={sortProgrammeKeys(Object.keys(thesisWriters?.data.programmeTableStats))}
|
||||
titles={thesisWriters?.data?.titles}
|
||||
sliceStart={2}
|
||||
extraHeight="EXTRA HEIGHT"
|
||||
language={language}
|
||||
yearsVisible={Array(thesisWriters?.data.tableStats.length).fill(false)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
import React from 'react'
|
||||
|
||||
const ReactHighcharts = require('react-highcharts')
|
||||
|
||||
const getColors = len => {
|
||||
if (len < 8) return ['#f57368', '#fb8c6e', '#fba678', '#dbda7d', '#9ec27c', '#60a866', '#008c59']
|
||||
return ['#e66067', '#f57368', '#fb8c6e', '#fba678', '#dbda7d', '#9ec27c', '#60a866', '#008c59']
|
||||
}
|
||||
|
||||
const FacultyBarChart = ({ cypress, data }) => {
|
||||
if (!data) return null
|
||||
|
||||
const colors = getColors(Object.keys(data.stats).length)
|
||||
const dataWithColors = Object.values(data?.stats).map((series, index) => ({ ...series, color: colors[index] }))
|
||||
const getFileName = () => {
|
||||
return `oodikone_progress_of_students_in_${data?.id}_by_study_start_year`
|
||||
}
|
||||
|
||||
const defaultConfig = {
|
||||
title: {
|
||||
text: '',
|
||||
},
|
||||
series: dataWithColors,
|
||||
credits: {
|
||||
text: 'oodikone | TOSKA',
|
||||
},
|
||||
xAxis: {
|
||||
categories: data?.years,
|
||||
},
|
||||
chart: {
|
||||
type: 'column',
|
||||
height: '450px',
|
||||
},
|
||||
plotOptions: {
|
||||
column: {
|
||||
stacking: 'percent',
|
||||
dataLabels: {
|
||||
enabled: true,
|
||||
format: '{point.percentage:.1f}%',
|
||||
},
|
||||
},
|
||||
},
|
||||
exporting: {
|
||||
filename: getFileName(),
|
||||
width: 2200,
|
||||
height: 1400,
|
||||
sourceWidth: 1200,
|
||||
sourceHeight: 600,
|
||||
buttons: {
|
||||
contextButton: {
|
||||
menuItems: ['viewFullscreen', 'downloadPNG', 'downloadSVG', 'downloadPDF'],
|
||||
},
|
||||
},
|
||||
},
|
||||
yAxis: {
|
||||
allowDecimals: false,
|
||||
min: 0,
|
||||
reversed: false,
|
||||
title: '',
|
||||
},
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="graph-container" data-cy={`Graph-${cypress}`}>
|
||||
<ReactHighcharts config={defaultConfig} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default FacultyBarChart
|
||||
+81
@@ -0,0 +1,81 @@
|
||||
import React, { useState } from 'react'
|
||||
import { Table } from 'semantic-ui-react'
|
||||
import ProgrammeProgressChart from './ProgrammeProgressChart'
|
||||
import BasicRow from '../InteractiveDataView/BasicRow'
|
||||
|
||||
const FacultyProgressTable = ({
|
||||
data,
|
||||
programmeStats,
|
||||
titles,
|
||||
sortedKeys,
|
||||
progressYearsVisible,
|
||||
programmeNames,
|
||||
language,
|
||||
cypress,
|
||||
}) => {
|
||||
const [visible, setVisible] = useState(progressYearsVisible)
|
||||
|
||||
if (!data) return null
|
||||
|
||||
const toggleVisibility = yearIndex => {
|
||||
const arrayToModify = [...visible]
|
||||
arrayToModify[yearIndex] = !visible[yearIndex]
|
||||
setVisible(arrayToModify)
|
||||
}
|
||||
const lenOfYearArray = data.length - 1
|
||||
return (
|
||||
<div>
|
||||
<Table data-cy={cypress} compact celled>
|
||||
<Table.Header>
|
||||
<Table.Row key={`${cypress}-${Math.random()}`} textAlign="center">
|
||||
{titles?.map(title => (
|
||||
<Table.HeaderCell key={title}>{title}</Table.HeaderCell>
|
||||
))}
|
||||
</Table.Row>
|
||||
</Table.Header>
|
||||
|
||||
<Table.Body>
|
||||
{data.map((yearArray, yearIndex) => (
|
||||
<React.Fragment key={`random-fragment-key-${Math.random()}`}>
|
||||
{yearIndex < lenOfYearArray ? (
|
||||
<BasicRow
|
||||
icon={visible[yearIndex] ? 'chevron down' : 'chevron right'}
|
||||
yearArray={yearArray}
|
||||
cypress={visible[yearIndex] ? `Hide-${cypress}` : `Show-${cypress}`}
|
||||
yearIndex={yearIndex}
|
||||
toggleVisibility={() => toggleVisibility(yearIndex)}
|
||||
/>
|
||||
) : (
|
||||
<Table.Row>
|
||||
{yearArray.map(value => (
|
||||
<Table.Cell key={`last-row-${Math.random()}`}>{value}</Table.Cell>
|
||||
))}
|
||||
</Table.Row>
|
||||
)}
|
||||
{yearIndex < lenOfYearArray ? (
|
||||
<Table.Row
|
||||
key={`${cypress}-row-${Math.random()}`}
|
||||
style={{ display: visible[yearIndex] ? '' : 'none' }}
|
||||
>
|
||||
<Table.Cell data-cy={`Cell-${cypress}-${yearIndex}`} colSpan={100}>
|
||||
<ProgrammeProgressChart
|
||||
data={sortedKeys?.map(programme => programmeStats[programme][yearIndex])}
|
||||
longLabels={programmeNames}
|
||||
labels={sortedKeys}
|
||||
language={language}
|
||||
names={titles?.slice(2)}
|
||||
/>
|
||||
</Table.Cell>
|
||||
</Table.Row>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</Table.Body>
|
||||
</Table>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default FacultyProgressTable
|
||||
+102
@@ -0,0 +1,102 @@
|
||||
/* eslint-disable react/no-this-in-sfc */
|
||||
import React from 'react'
|
||||
import Highcharts from 'highcharts'
|
||||
import ReactHighcharts from 'react-highcharts'
|
||||
|
||||
const getColors = len => {
|
||||
if (len < 8) return ['#f57368', '#fb8c6e', '#fba678', '#dbda7d', '#9ec27c', '#60a866', '#008c59']
|
||||
return ['#e66067', '#f57368', '#fb8c6e', '#fba678', '#dbda7d', '#9ec27c', '#60a866', '#008c59']
|
||||
}
|
||||
|
||||
const ProgrammeProgressChart = ({ data, labels, longLabels, names, language }) => {
|
||||
if (!data || data.length === 0)
|
||||
return (
|
||||
<>
|
||||
<b>No data available</b>
|
||||
</>
|
||||
)
|
||||
const transpose = matrix => {
|
||||
return matrix.reduce((prev, next) => next.map((_item, i) => (prev[i] || []).concat(next[i])), [])
|
||||
}
|
||||
|
||||
const colors = getColors(data[0]?.length || 6)
|
||||
const dataTranspose = transpose(data)
|
||||
.map((obj, idx) => ({ name: names[idx], data: obj, color: colors[idx] }))
|
||||
.reverse()
|
||||
|
||||
// Tooltip formatter function: this.<some value> needs to be used in order to obtain access to the values.
|
||||
const defaultConfig = {
|
||||
chart: {
|
||||
type: 'bar',
|
||||
marginTop: 60,
|
||||
},
|
||||
credits: {
|
||||
text: 'oodikone | TOSKA',
|
||||
},
|
||||
title: {
|
||||
text: '',
|
||||
style: {
|
||||
display: 'none',
|
||||
},
|
||||
},
|
||||
series: dataTranspose,
|
||||
xAxis: {
|
||||
categories: labels,
|
||||
fontSize: '24px',
|
||||
},
|
||||
yAxis: {
|
||||
allowDecimals: false,
|
||||
min: 0,
|
||||
reversed: true,
|
||||
title: '',
|
||||
},
|
||||
legend: {
|
||||
layout: 'horizontal',
|
||||
align: 'left',
|
||||
fontSize: '24px',
|
||||
x: 10,
|
||||
verticalAlign: 'top',
|
||||
y: -10,
|
||||
floating: true,
|
||||
backgroundColor: Highcharts.defaultOptions.legend.backgroundColor || 'white',
|
||||
borderColor: '#CCC',
|
||||
borderWidth: 1,
|
||||
shadow: false,
|
||||
},
|
||||
tooltip: {
|
||||
shared: true,
|
||||
backgroundColor: 'white',
|
||||
fontSize: '25px',
|
||||
formatter() {
|
||||
let tooltipString = `<b>${
|
||||
longLabels[this.x] && longLabels[this.x][language] ? longLabels[this.x][language] : longLabels[this.x].fi
|
||||
}</b><br /><p>${this.x}</p><br />`
|
||||
this.points.forEach(point => {
|
||||
tooltipString += `<span style="color:${point.color}">●</span> <b>${point.series.name}: ${point.y}</b>
|
||||
</span><br />`
|
||||
})
|
||||
return tooltipString
|
||||
},
|
||||
},
|
||||
plotOptions: {
|
||||
series: {
|
||||
stacking: 'percent',
|
||||
pointWidth: 24,
|
||||
dataLabels: {
|
||||
enabled: true,
|
||||
format: '{point.percentage:.1f}%',
|
||||
fontSize: '24px',
|
||||
filter: {
|
||||
property: 'y',
|
||||
operator: '>',
|
||||
value: 2,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
return <ReactHighcharts config={defaultConfig} />
|
||||
}
|
||||
|
||||
export default ProgrammeProgressChart
|
||||
+170
-2
@@ -1,7 +1,175 @@
|
||||
import React from 'react'
|
||||
import { Divider, Loader } from 'semantic-ui-react'
|
||||
import { useGetFacultyProgressStatsQuery } from 'redux/facultyStats'
|
||||
import FacultyProgressTable from './FacultyProgressTable'
|
||||
import FacultyBarChart from './FacultyBarChart'
|
||||
// import Toggle from '../../StudyProgramme/Toggle'
|
||||
import InfoBox from '../../Info/InfoBox'
|
||||
// import InfotoolTips from '../../../common/InfoToolTips'
|
||||
import '../faculty.css'
|
||||
|
||||
const FacultyProgrammeOverview = () => {
|
||||
return <div className="programmes-overview">Faculty programmes</div>
|
||||
// const toolTips = InfotoolTips.Faculty
|
||||
|
||||
const getDivider = (title, toolTipText) => (
|
||||
<>
|
||||
<div className="divider">
|
||||
<Divider data-cy={`Section-${toolTipText}`} horizontal>
|
||||
{title}
|
||||
</Divider>
|
||||
</div>
|
||||
<InfoBox content="To be done" />
|
||||
</>
|
||||
)
|
||||
|
||||
const FacultyProgrammeOverview = ({ faculty, language }) => {
|
||||
// const toolTipsProgramme = InfotoolTips.Faculty
|
||||
// const [programme, setProgramme] = useState(faculty)
|
||||
const studyProgrammeFilter = 'NEW_STUDY_PROGRAMMES'
|
||||
const progressStats = useGetFacultyProgressStatsQuery({ id: faculty?.code, studyProgrammeFilter })
|
||||
|
||||
const isFetchingOrLoading = progressStats.isLoading || progressStats.isFetching
|
||||
|
||||
const isError = progressStats.isError || (progressStats.isSuccess && !progressStats.data)
|
||||
|
||||
if (isError) return <h3>Something went wrong, please try refreshing the page.</h3>
|
||||
/*
|
||||
Order of the programme keys: KH -> MH -> T -> FI -> K- -> Numbers containing letters at end -> Y- -> Numbers
|
||||
*/
|
||||
const regexValuesAll = [
|
||||
/^KH/,
|
||||
/^MH/,
|
||||
/^T/,
|
||||
/^LI/,
|
||||
/^K-/,
|
||||
/^FI/,
|
||||
/^00901$/,
|
||||
/^00910$/,
|
||||
/^\d.*a$/,
|
||||
/^Y/,
|
||||
/\d$/,
|
||||
/^\d.*e$/,
|
||||
]
|
||||
|
||||
const testKey = value => {
|
||||
for (let i = 0; i < regexValuesAll.length; i++) {
|
||||
if (regexValuesAll[i].test(value)) {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return 6
|
||||
}
|
||||
|
||||
const sortProgrammeKeys = programmeKeys => {
|
||||
return programmeKeys.sort((a, b) => {
|
||||
if (testKey(a) - testKey(b) === 0) {
|
||||
return a.localeCompare(b)
|
||||
}
|
||||
return testKey(a) - testKey(b)
|
||||
})
|
||||
}
|
||||
// Toggles in studytrans ans student populations
|
||||
|
||||
return (
|
||||
<div className="programmes-overview">
|
||||
{isFetchingOrLoading ? (
|
||||
<Loader active style={{ marginTop: '15em' }} />
|
||||
) : (
|
||||
<div className="programmes-overview">
|
||||
{/* {placeholderdata && (
|
||||
<>{getDivider('Students of the faculty By Starting year', 'StudentsOfTheFacultyByStartingYear')}</>
|
||||
)} */}
|
||||
{progressStats.isSuccess && progressStats.data && (
|
||||
<>
|
||||
{getDivider(
|
||||
'Progress of bachelor students of the faculty by starting year',
|
||||
'ProgressOfBachelorStudentsOfTheFacultyByStartingYear'
|
||||
)}
|
||||
<div className="section-container">
|
||||
<div className="graph-container">
|
||||
<FacultyBarChart
|
||||
cypress="FacultyBachelorsProgress"
|
||||
data={{
|
||||
id: faculty.code,
|
||||
stats: progressStats?.data.bachelorsGraphStats,
|
||||
years: progressStats?.data.years,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="table-container">
|
||||
<FacultyProgressTable
|
||||
data={progressStats?.data.bachelorsTableStats}
|
||||
programmeStats={progressStats?.data.bachelorsProgrammeStats}
|
||||
titles={progressStats?.data.bachelorTitles}
|
||||
sortedKeys={sortProgrammeKeys(Object.keys(progressStats?.data.bachelorsProgrammeStats))}
|
||||
progressYearsVisible={Array(progressStats?.data.years.slice(1).length).fill(false)}
|
||||
programmeNames={progressStats?.data.programmeNames}
|
||||
language={language}
|
||||
cypress="FacultyBachelorsProgressTable"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{getDivider(
|
||||
'Progress of master students of the faculty by starting year',
|
||||
'ProgressOfMasterStudentsOfTheFacultyByStartingYear'
|
||||
)}
|
||||
<div className="section-container">
|
||||
<div className="graph-container">
|
||||
<FacultyBarChart
|
||||
cypress="FacultyMastersProgress"
|
||||
data={{
|
||||
id: faculty.code,
|
||||
stats: progressStats?.data.mastersGraphStats,
|
||||
years: progressStats?.data.years,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="table-container">
|
||||
<FacultyProgressTable
|
||||
data={progressStats?.data.mastersTableStats}
|
||||
programmeStats={progressStats?.data.mastersProgrammeStats}
|
||||
titles={progressStats?.data.mastersTitles}
|
||||
sortedKeys={sortProgrammeKeys(Object.keys(progressStats?.data.mastersProgrammeStats))}
|
||||
progressYearsVisible={Array(progressStats?.data.years.slice(1).length).fill(false)}
|
||||
programmeNames={progressStats?.data.programmeNames}
|
||||
language={language}
|
||||
cypress="FacultyBachelorsProgressTable"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{getDivider(
|
||||
'Progress of doctoral students of the faculty by starting year',
|
||||
'ProgressOfDoctoralStudentsOfTheFacultyByStartingYear'
|
||||
)}
|
||||
<div className="section-container">
|
||||
<div className="graph-container">
|
||||
<FacultyBarChart
|
||||
cypress="FacultyDoctoralProgress"
|
||||
data={{
|
||||
id: faculty.code,
|
||||
stats: progressStats?.data.doctoralGraphStats,
|
||||
years: progressStats?.data.years,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="table-container">
|
||||
<FacultyProgressTable
|
||||
data={progressStats?.data.doctoralTableStats}
|
||||
programmeStats={progressStats?.data.doctoralProgrammeStats}
|
||||
titles={progressStats?.data.doctoralTitles}
|
||||
sortedKeys={sortProgrammeKeys(Object.keys(progressStats?.data.doctoralProgrammeStats))}
|
||||
progressYearsVisible={Array(progressStats?.data.years.slice(1).length).fill(false)}
|
||||
programmeNames={progressStats?.data.programmeNames}
|
||||
language={language}
|
||||
cypress="FacultyBachelorsProgressTable"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default FacultyProgrammeOverview
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import React from 'react'
|
||||
import { Button, Icon, Table, Label } from 'semantic-ui-react'
|
||||
|
||||
const roundValue = value => {
|
||||
return Number.isInteger(value) ? value : value.toFixed(1)
|
||||
}
|
||||
|
||||
const BasicRow = ({ icon, yearArray, cypress, yearIndex, toggleVisibility, styles }) => {
|
||||
return (
|
||||
<Table.Row style={styles} key={`Basic-row-${cypress}-${yearIndex}`}>
|
||||
{yearArray?.map((value, idx) => (
|
||||
<Table.Cell key={`random-button-cell-key-${Math.random()}`}>
|
||||
{idx === 0 ? (
|
||||
<Button
|
||||
as="div"
|
||||
onClick={toggleVisibility}
|
||||
labelPosition="right"
|
||||
style={{ backgroundColor: 'white', borderRadius: 0 }}
|
||||
data-cy={`Button-${cypress}-${yearIndex}`}
|
||||
>
|
||||
<Button icon style={{ backgroundColor: 'white', borderRadius: 0 }}>
|
||||
<Icon name={icon} />
|
||||
</Button>
|
||||
<Label as="a" style={{ backgroundColor: 'white', borderRadius: 0 }}>
|
||||
{value}
|
||||
</Label>
|
||||
</Button>
|
||||
) : (
|
||||
roundValue(value)
|
||||
)}
|
||||
</Table.Cell>
|
||||
))}
|
||||
</Table.Row>
|
||||
)
|
||||
}
|
||||
|
||||
export default BasicRow
|
||||
+153
@@ -0,0 +1,153 @@
|
||||
/* eslint-disable react/no-this-in-sfc */
|
||||
import React from 'react'
|
||||
import Highcharts from 'highcharts'
|
||||
import ReactHighcharts from 'react-highcharts'
|
||||
|
||||
const colors = ['#7cb5ec', '#90ed7d', '#434348', '#f7a35c', '#FFF000', '#2b908f', '#f45b5b', '#91e8e1']
|
||||
|
||||
const CollapsedStackedBar = ({ data, labels, longLabels, names, language, differenceData, extraHeight }) => {
|
||||
const transpose = matrix => {
|
||||
return matrix.reduce((prev, next) => next.map((_item, i) => (prev[i] || []).concat(next[i])), [])
|
||||
}
|
||||
|
||||
const needsExtra = extraHeight === 'EXTRA HEIGHT'
|
||||
const manyProgrammes =
|
||||
labels.includes('KH57_001') ||
|
||||
labels.includes('KH50_001') ||
|
||||
labels.includes('KH70_001') ||
|
||||
labels.includes('KH40_001') ||
|
||||
labels.includes('KH30_001') ||
|
||||
labels.includes('KH80_001')
|
||||
const dataTranspose = transpose(data)
|
||||
.map((obj, idx) => ({ name: names[idx], data: obj, color: colors[idx] }))
|
||||
.reverse()
|
||||
|
||||
const differenceArray = Object.keys(differenceData).reduce(
|
||||
(programmes, programme) => ({
|
||||
...programmes,
|
||||
[programme]: differenceData[programme].reduce(
|
||||
(results, val, currentIdx) => ({
|
||||
...results,
|
||||
[names[currentIdx]]: val,
|
||||
}),
|
||||
{}
|
||||
),
|
||||
}),
|
||||
{}
|
||||
)
|
||||
const getCorrectSign = change => {
|
||||
if (change > 0) return `+${change.toString()}`
|
||||
return change
|
||||
}
|
||||
|
||||
// Point width is 24 px different multipliers adjusts the height.
|
||||
const getFlexHeight = (len, needsExtra, manyProgrammes, labels) => {
|
||||
if (labels.includes('KH74_001')) return `${len * 24 * 5}px`
|
||||
if (needsExtra && manyProgrammes) return `${len * 24 * 1.5}px`
|
||||
if (needsExtra) return `${len * 24 * 2.5}px`
|
||||
return `${len * 24 * 1.5}px`
|
||||
}
|
||||
|
||||
const getColor = change => {
|
||||
if (change > 0) return '#6ab04c'
|
||||
if (change < 0) return '#ff7979'
|
||||
return '#7B9FCF'
|
||||
}
|
||||
|
||||
// Tooltip formatter function: this.<some value> needs to be used in order to obtain access to the values.
|
||||
const defaultConfig = {
|
||||
chart: {
|
||||
type: 'bar',
|
||||
marginTop: 60,
|
||||
height: getFlexHeight(labels.length, needsExtra, manyProgrammes, labels),
|
||||
},
|
||||
credits: {
|
||||
text: 'oodikone | TOSKA',
|
||||
},
|
||||
title: {
|
||||
text: '',
|
||||
style: {
|
||||
display: 'none',
|
||||
},
|
||||
},
|
||||
series: dataTranspose,
|
||||
xAxis: {
|
||||
categories: labels,
|
||||
title: {
|
||||
text: '',
|
||||
},
|
||||
fontSize: '24px',
|
||||
},
|
||||
yAxis: {
|
||||
min: 0,
|
||||
title: {
|
||||
text: '',
|
||||
},
|
||||
stackLabels: {
|
||||
enabled: true,
|
||||
style: {
|
||||
color: (Highcharts.defaultOptions.title.style && Highcharts.defaultOptions.title.style.color) || 'gray',
|
||||
},
|
||||
fontSize: '24px',
|
||||
},
|
||||
},
|
||||
legend: {
|
||||
layout: 'horizontal',
|
||||
align: 'left',
|
||||
fontSize: '24px',
|
||||
x: 20,
|
||||
verticalAlign: 'top',
|
||||
y: -10,
|
||||
floating: true,
|
||||
backgroundColor: Highcharts.defaultOptions.legend.backgroundColor || 'white',
|
||||
borderColor: '#CCC',
|
||||
borderWidth: 1,
|
||||
shadow: false,
|
||||
},
|
||||
tooltip: {
|
||||
shared: true,
|
||||
backgroundColor: 'white',
|
||||
fontSize: '25px',
|
||||
formatter() {
|
||||
let tooltipString = `<b>${
|
||||
longLabels[this.x] && longLabels[this.x][language] ? longLabels[this.x][language] : longLabels[this.x].fi
|
||||
}</b><br /><p>${this.x}</p><br />`
|
||||
const diffArray = differenceArray[this.x]
|
||||
this.points.forEach(point => {
|
||||
tooltipString += `<span style="color:${point.color}">●</span> <b>${point.series.name}: ${point.y}</b>
|
||||
(<span style="color:${getColor(diffArray[point.series.name])};font-weight:bold">${getCorrectSign(
|
||||
diffArray[point.series.name]
|
||||
)}</span>)<br />`
|
||||
})
|
||||
tooltipString += `<b>Total: ${this.points.reduce((prev, current) => prev + current.y, 0)}</b>`
|
||||
return tooltipString
|
||||
},
|
||||
},
|
||||
plotOptions: {
|
||||
series: {
|
||||
stacking: 'normal',
|
||||
pointWidth: 24,
|
||||
dataLabels: {
|
||||
enabled: true,
|
||||
formatter() {
|
||||
if (Number.isInteger(this.y)) return `${this.y}`
|
||||
return `${this.y.toFixed(1)}`
|
||||
},
|
||||
fontSize: '24px',
|
||||
filter: {
|
||||
property: 'y',
|
||||
operator: '>',
|
||||
value: 2,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
if (!dataTranspose) {
|
||||
return <>No data provided</>
|
||||
}
|
||||
return <ReactHighcharts config={defaultConfig} />
|
||||
}
|
||||
|
||||
export default CollapsedStackedBar
|
||||
@@ -0,0 +1,158 @@
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { Table, Menu } from 'semantic-ui-react'
|
||||
import CollapsedStackedBar from './CollapsedStackedBar'
|
||||
import BasicRow from './BasicRow'
|
||||
|
||||
const InteractiveDataTable = ({
|
||||
cypress,
|
||||
dataStats,
|
||||
dataProgrammeStats,
|
||||
programmeNames,
|
||||
sortedKeys,
|
||||
titles,
|
||||
language,
|
||||
sliceStart,
|
||||
extraHeight,
|
||||
yearsVisible,
|
||||
shortNames,
|
||||
studentsTable,
|
||||
}) => {
|
||||
const [keyOrder, setkeyOrder] = useState({})
|
||||
const [sorter, setSorter] = useState('Code')
|
||||
const [sortDir, setSortDir] = useState(1)
|
||||
const [sortbyColumn, setSortByColumn] = useState(0)
|
||||
const [columnIndex, setSelectedIndex] = useState(0)
|
||||
const [visible, setVisible] = useState(yearsVisible)
|
||||
|
||||
const sortBySelectedColumn = () => {
|
||||
const keys = {}
|
||||
if (!(columnIndex === 0)) {
|
||||
const numbersOfYears = dataStats.length
|
||||
const data = Object.entries(dataProgrammeStats)
|
||||
for (let yearIdx = 0; yearIdx < numbersOfYears; yearIdx++) {
|
||||
const yearlySortedKeys = data
|
||||
.sort((a, b) => {
|
||||
if (sortDir === -1) return a[1][yearIdx][columnIndex] - b[1][yearIdx][columnIndex]
|
||||
return b[1][yearIdx][columnIndex] - a[1][yearIdx][columnIndex]
|
||||
})
|
||||
.map(yearlyProgrammes => yearlyProgrammes[0])
|
||||
keys[yearIdx] = yearlySortedKeys
|
||||
}
|
||||
}
|
||||
setkeyOrder(keys)
|
||||
setSortByColumn(columnIndex)
|
||||
}
|
||||
useEffect(sortBySelectedColumn, [columnIndex, dataStats, sortDir])
|
||||
|
||||
if (!dataStats || !titles || !dataProgrammeStats) return null
|
||||
/* Calculation works as follows
|
||||
1. Calculate difference between current and previous year: current year - previous year
|
||||
2. Calculations are done for the values starting from the index 2 (index 0: year, index 1: total)
|
||||
3. Last year, currently 2017 is initialized to zeros
|
||||
4. A new object with programme names as keys and the difference array as value is created
|
||||
*/
|
||||
const calculatDiffToPrevYear = programmeData => {
|
||||
const differenceMatrix = Object.keys(programmeData).reduce(
|
||||
(differenceMatrix, programme) => ({
|
||||
...differenceMatrix,
|
||||
[programme]: programmeData[programme].map((_yearlyValues, yearIndex) =>
|
||||
yearIndex < programmeData[programme].length - 1
|
||||
? programmeData[programme][yearIndex]
|
||||
.slice(sliceStart)
|
||||
.map((val, idx) => val - programmeData[programme][yearIndex + 1][idx + sliceStart])
|
||||
: new Array(programmeData[programme][yearIndex].length - sliceStart).fill(0)
|
||||
),
|
||||
}),
|
||||
{}
|
||||
)
|
||||
return differenceMatrix
|
||||
}
|
||||
|
||||
const handleClick = (sorterName, nameIndex) => {
|
||||
if (sorterName === sorter) setSortDir(-1 * sortDir)
|
||||
setSorter(sorterName)
|
||||
setSelectedIndex(nameIndex)
|
||||
}
|
||||
|
||||
const toggleVisibility = yearIndex => {
|
||||
const arrayToModify = [...visible]
|
||||
arrayToModify[yearIndex] = !visible[yearIndex]
|
||||
setVisible(arrayToModify)
|
||||
}
|
||||
|
||||
const sorterNames = shortNames || titles.map(title => (title === '' ? 'Code' : title))
|
||||
|
||||
const differenceToPrevYears = calculatDiffToPrevYear(dataProgrammeStats)
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Menu compact attached="top">
|
||||
<Menu.Item style={{ cursor: 'default', borderRadius: '1px', padding: '5px' }} active color="black">
|
||||
Sort by:
|
||||
</Menu.Item>
|
||||
{sorterNames.map((sorterName, nameIndex) => (
|
||||
<Menu.Item
|
||||
data-cy={`Menu-${cypress}-${sorterName}`}
|
||||
color={sorter === sorterName ? 'blue' : 'black'}
|
||||
key={sorterName}
|
||||
active={sorter === sorterName}
|
||||
onClick={() => handleClick(sorterName, nameIndex)}
|
||||
style={{ borderRadius: '1px', fontSize: '14px', padding: '5px' }}
|
||||
icon={sortDir === 1 ? 'triangle down' : 'triangle up'}
|
||||
content={sorterName}
|
||||
/>
|
||||
))}
|
||||
</Menu>
|
||||
<Table data-cy={`Table-${cypress}`} celled>
|
||||
<Table.Header>
|
||||
<Table.Row key={`randow-header-row-${Math.random()}`} textAlign="center">
|
||||
{titles?.map(title => (
|
||||
<Table.HeaderCell key={title}>{title}</Table.HeaderCell>
|
||||
))}
|
||||
</Table.Row>
|
||||
</Table.Header>
|
||||
<Table.Body>
|
||||
{dataStats?.map((yearArray, yearIndex) => (
|
||||
<React.Fragment key={`random-fragment-key-${Math.random()}`}>
|
||||
<BasicRow
|
||||
icon={visible[yearIndex] ? 'chevron down' : 'chevron right'}
|
||||
yearArray={yearArray}
|
||||
cypress={visible[yearIndex] ? `Hide-${cypress}` : `Show-${cypress}`}
|
||||
yearIndex={yearIndex}
|
||||
toggleVisibility={() => toggleVisibility(yearIndex)}
|
||||
/>
|
||||
<Table.Row key={`stack-row-key-${Math.random()}`} style={{ display: visible[yearIndex] ? '' : 'none' }}>
|
||||
<Table.Cell data-cy={`Cell-${cypress}-${yearIndex}`} key={`stack-cell${Math.random()}`} colSpan={100}>
|
||||
<CollapsedStackedBar
|
||||
data={
|
||||
sortbyColumn === 0
|
||||
? sortedKeys?.map(programme => dataProgrammeStats[programme][yearIndex].slice(sliceStart))
|
||||
: keyOrder[yearIndex]?.map(programme =>
|
||||
dataProgrammeStats[programme][yearIndex].slice(sliceStart)
|
||||
)
|
||||
}
|
||||
labels={sortbyColumn === 0 ? sortedKeys : keyOrder[yearIndex]}
|
||||
differenceData={Object.keys(differenceToPrevYears)?.reduce(
|
||||
(yearlyObject, programme) => ({
|
||||
...yearlyObject,
|
||||
[programme]: differenceToPrevYears[programme][yearIndex],
|
||||
}),
|
||||
{}
|
||||
)}
|
||||
longLabels={programmeNames}
|
||||
language={language}
|
||||
names={titles?.slice(sliceStart)}
|
||||
extraHeight={extraHeight}
|
||||
studentsTable={studentsTable}
|
||||
/>
|
||||
</Table.Cell>
|
||||
</Table.Row>
|
||||
</React.Fragment>
|
||||
))}
|
||||
</Table.Body>
|
||||
</Table>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default InteractiveDataTable
|
||||
@@ -0,0 +1,147 @@
|
||||
/* eslint-disable react/no-this-in-sfc */
|
||||
import React from 'react'
|
||||
import ReactHighcharts from 'react-highcharts'
|
||||
|
||||
const BarChart = ({
|
||||
data,
|
||||
categories,
|
||||
goal,
|
||||
handleClick,
|
||||
facultyGraph = true,
|
||||
year,
|
||||
label,
|
||||
programmeNames,
|
||||
language = null,
|
||||
showMeanTime,
|
||||
}) => {
|
||||
const maxValue = data.reduce((max, { y }) => {
|
||||
return y > max ? y : max
|
||||
}, goal * 2)
|
||||
|
||||
const config = {
|
||||
chart: {
|
||||
type: 'bar',
|
||||
width: 700,
|
||||
margin: [70, 0],
|
||||
},
|
||||
title: { text: ' ' },
|
||||
tooltip: {
|
||||
backgroundColor: 'white',
|
||||
fontSize: '25px',
|
||||
// eslint-disable-next-line
|
||||
formatter: function() {
|
||||
const sortingText =
|
||||
label === 'Start year'
|
||||
? `<b>From class of ${facultyGraph ? this.x : year}, ${this.point.amount} students have graduated</b>`
|
||||
: `<b>${this.point.amount} students graduated in year ${facultyGraph ? this.x : year}</b>`
|
||||
// eslint-disable-next-line prettier/prettier
|
||||
const timeText = `<br /><p>${sortingText}, <br /><b>${showMeanTime ? 'mean' : 'median'} study time: ${this.y} months</p></b>`
|
||||
const statistics = `<br /><p>${this.point.statistics.onTime} graduated on time</p><br />
|
||||
<p>${this.point.statistics.yearOver} graduated max year overtime</p>
|
||||
<br /><p>${this.point.statistics.wayOver} graduated over year late</p>`
|
||||
|
||||
if (!facultyGraph)
|
||||
return `<b>${
|
||||
programmeNames[this.x]?.[language] ? programmeNames[this.x]?.[language] : programmeNames[this.x]?.fi
|
||||
}</b><br />${this.x}${timeText}${statistics}`
|
||||
|
||||
// eslint-disable-next-line
|
||||
return `${timeText}${statistics}` ;
|
||||
},
|
||||
},
|
||||
plotOptions: {
|
||||
series: {
|
||||
dataLabels: {
|
||||
enabled: true,
|
||||
inside: true,
|
||||
},
|
||||
pointPadding: 0.0,
|
||||
|
||||
// groupPadding: 0.1,
|
||||
},
|
||||
},
|
||||
series: [
|
||||
{
|
||||
data,
|
||||
dataLabels: [
|
||||
{
|
||||
align: 'left',
|
||||
format: '{point.amount} students',
|
||||
color: '#EBECF0',
|
||||
},
|
||||
{
|
||||
align: 'right',
|
||||
format: '{y}',
|
||||
color: '#EBECF0',
|
||||
},
|
||||
],
|
||||
showInLegend: false,
|
||||
zones: [
|
||||
{
|
||||
value: goal + 0.1,
|
||||
color: '#90A959',
|
||||
},
|
||||
{
|
||||
value: goal + 12.1,
|
||||
color: '#FEE191',
|
||||
},
|
||||
{
|
||||
color: '#FB6962',
|
||||
},
|
||||
],
|
||||
point: {
|
||||
events: {
|
||||
click(e) {
|
||||
handleClick(e, facultyGraph)
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
xAxis: {
|
||||
categories,
|
||||
title: {
|
||||
text: facultyGraph ? label : 'Programme',
|
||||
align: 'high',
|
||||
offset: 0,
|
||||
rotation: 0,
|
||||
y: -10,
|
||||
},
|
||||
},
|
||||
yAxis: {
|
||||
min: 0,
|
||||
max: maxValue,
|
||||
title: { text: 'Graduation time (months)' },
|
||||
labels: {
|
||||
overflow: 'justify',
|
||||
},
|
||||
allowDecimals: false,
|
||||
showFirstLabel: false,
|
||||
plotLines: [
|
||||
{
|
||||
color: '#90A959',
|
||||
width: 2,
|
||||
value: goal,
|
||||
dashStyle: 'shortDash',
|
||||
// label: {
|
||||
// text: `${goal}`, // 'optimal',
|
||||
// verticalAlign: 'bottom',
|
||||
// },
|
||||
},
|
||||
],
|
||||
},
|
||||
credits: {
|
||||
text: 'oodikone | TOSKA',
|
||||
},
|
||||
}
|
||||
|
||||
if (!facultyGraph) config.title.text = `Year ${year} by ${label.toLowerCase()}`
|
||||
|
||||
return (
|
||||
<div>
|
||||
<ReactHighcharts config={config} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default BarChart
|
||||
@@ -0,0 +1,72 @@
|
||||
import React, { useState } from 'react'
|
||||
import { Divider, Message } from 'semantic-ui-react'
|
||||
import BarChart from './BarChart'
|
||||
import useLanguage from '../../LanguagePicker/useLanguage'
|
||||
|
||||
const GraduationTimes = ({
|
||||
title,
|
||||
years,
|
||||
data,
|
||||
level,
|
||||
goal,
|
||||
label,
|
||||
levelProgrammeData,
|
||||
programmeNames,
|
||||
showMeanTime,
|
||||
}) => {
|
||||
const [programmeData, setProgrammeData] = useState(false)
|
||||
const [year, setYear] = useState(null)
|
||||
const { language } = useLanguage()
|
||||
if (!data.some(a => a.amount > 0)) return null
|
||||
|
||||
const handleClick = (e, isFacultyGraph) => {
|
||||
if (isFacultyGraph) {
|
||||
setYear(e.point.category)
|
||||
setProgrammeData(true)
|
||||
} else {
|
||||
setProgrammeData(false)
|
||||
setYear(null)
|
||||
}
|
||||
}
|
||||
// TODO clean up inline styling
|
||||
return (
|
||||
<>
|
||||
<Divider data-cy={`Section-${level}`} horizontal>
|
||||
{title}
|
||||
</Divider>
|
||||
<div>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', paddingBottom: '100px' }}>
|
||||
<BarChart
|
||||
categories={years}
|
||||
data={data}
|
||||
goal={goal}
|
||||
handleClick={handleClick}
|
||||
label={label}
|
||||
programmeNames={programmeNames}
|
||||
showMeanTime={showMeanTime}
|
||||
/>
|
||||
{!programmeData ? (
|
||||
<div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center' }}>
|
||||
<Message compact>Click a bar to view that year's programme level breakdown</Message>
|
||||
</div>
|
||||
) : (
|
||||
<BarChart
|
||||
categories={levelProgrammeData[year]?.programmes}
|
||||
data={levelProgrammeData[year]?.data}
|
||||
goal={goal}
|
||||
facultyGraph={false}
|
||||
handleClick={handleClick}
|
||||
year={year} // programmeData?.year}
|
||||
label={label}
|
||||
programmeNames={programmeNames}
|
||||
language={language}
|
||||
showMeanTime={showMeanTime}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default GraduationTimes
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
import React from 'react'
|
||||
import { Dropdown } from 'semantic-ui-react'
|
||||
|
||||
import '../../StudyProgramme/studyprogramme.css'
|
||||
|
||||
const ProgrammeSelector = ({ programme, setProgramme, programmes }) => {
|
||||
if (!programmes) return null
|
||||
|
||||
const handleStudytrackChange = (event, { value }) => {
|
||||
event.preventDefault()
|
||||
setProgramme(value)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="programme-selector">
|
||||
<h4>Choose programme</h4>
|
||||
<Dropdown
|
||||
fluid
|
||||
selection
|
||||
name="programme"
|
||||
placeholder="All programmes of the faculty"
|
||||
value={programme}
|
||||
onChange={handleStudytrackChange}
|
||||
options={programmes.map(({ code, name }) => ({
|
||||
key: code,
|
||||
value: code,
|
||||
text: code === 'faculty' ? name?.fi : `${name?.fi}, ${code}`,
|
||||
}))}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ProgrammeSelector
|
||||
@@ -0,0 +1,132 @@
|
||||
import React, { useState } from 'react'
|
||||
import { Divider, Loader } from 'semantic-ui-react'
|
||||
|
||||
import { useGetFacultyGraduationTimesQuery } from 'redux/facultyStats'
|
||||
import Toggle from '../../StudyProgramme/Toggle'
|
||||
import InfoBox from '../../Info/InfoBox'
|
||||
import GraduationTimes from './GraduationTimes'
|
||||
import InfotoolTips from '../../../common/InfoToolTips'
|
||||
import '../faculty.css'
|
||||
|
||||
const TimesAndPathsView = ({ faculty, studyProgrammes, setStudyProgrammes }) => {
|
||||
const toolTips = InfotoolTips.Faculty
|
||||
const [showMeanTime, setShowMeanTime] = useState(false)
|
||||
const [groupByGradYear, setGroupByGradYear] = useState(false)
|
||||
const studyProgrammeFilter = studyProgrammes ? 'ALL_PROGRAMMES' : 'NEW_STUDY_PROGRAMMES'
|
||||
const graduationStats = useGetFacultyGraduationTimesQuery({ id: faculty?.code, studyProgrammeFilter })
|
||||
|
||||
const groupBy = groupByGradYear ? 'byGradYear' : 'byStartYear'
|
||||
const label = groupByGradYear ? 'Graduation year' : 'Start year'
|
||||
const data = showMeanTime ? graduationStats?.data?.[groupBy].means : graduationStats?.data?.[groupBy].medians
|
||||
const years = graduationStats?.data?.years
|
||||
const goals = graduationStats?.data?.goals
|
||||
const programmeData = showMeanTime
|
||||
? graduationStats?.data?.[groupBy].programmes.means
|
||||
: graduationStats?.data?.[groupBy].programmes.medians
|
||||
const programmeNames = graduationStats?.data?.programmeNames
|
||||
const commonProps = { years, label, programmeNames, showMeanTime }
|
||||
|
||||
const isFetchingOrLoading = graduationStats.isLoading || graduationStats.isFetching
|
||||
|
||||
const isError = graduationStats.isError || (graduationStats.isSuccess && !graduationStats.data)
|
||||
|
||||
if (isError) return <h3>Something went wrong, please try refreshing the page.</h3>
|
||||
|
||||
const getDivider = (title, toolTipText) => (
|
||||
<>
|
||||
<div className="divider">
|
||||
<Divider data-cy={`Section-${toolTipText}`} horizontal>
|
||||
{title}
|
||||
</Divider>
|
||||
</div>
|
||||
<InfoBox content={toolTips[toolTipText]} />
|
||||
</>
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="programmes-overview">
|
||||
<div className="toggle-container">
|
||||
<Toggle
|
||||
cypress="ProgrammeToggle"
|
||||
toolTips={toolTips.ProgrammeToggle}
|
||||
firstLabel="New study programmes"
|
||||
secondLabel="All study programmes"
|
||||
value={studyProgrammes}
|
||||
setValue={setStudyProgrammes}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{isFetchingOrLoading ? (
|
||||
<Loader active style={{ marginTop: '15em' }} />
|
||||
) : (
|
||||
<>
|
||||
{graduationStats.isSuccess && graduationStats.data && (
|
||||
<>
|
||||
{getDivider('Average graduation times', 'AverageGraduationTimes')}
|
||||
<div className="toggle-container">
|
||||
<Toggle
|
||||
cypress="GroupByToggle"
|
||||
firstLabel="Group by: Starting year"
|
||||
secondLabel="Graduation year"
|
||||
value={groupByGradYear}
|
||||
setValue={setGroupByGradYear}
|
||||
/>
|
||||
<Toggle
|
||||
cypress="GraduationTimeToggle"
|
||||
firstLabel="Median time"
|
||||
secondLabel="Mean time"
|
||||
value={showMeanTime}
|
||||
setValue={setShowMeanTime}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<GraduationTimes
|
||||
level="bachelor"
|
||||
title="Bachelor"
|
||||
data={data?.bachelor}
|
||||
goal={goals?.bachelor}
|
||||
levelProgrammeData={programmeData?.bachelor}
|
||||
{...commonProps}
|
||||
/>
|
||||
<GraduationTimes
|
||||
level="bcMsCombo"
|
||||
title="Bachelor + Master"
|
||||
data={data?.bcMsCombo}
|
||||
goal={goals?.bcMsCombo}
|
||||
levelProgrammeData={programmeData?.bcMsCombo}
|
||||
{...commonProps}
|
||||
/>
|
||||
<GraduationTimes
|
||||
level="master"
|
||||
title="Master"
|
||||
data={data?.master}
|
||||
goal={goals?.master}
|
||||
levelProgrammeData={programmeData?.master}
|
||||
{...commonProps}
|
||||
/>
|
||||
<GraduationTimes
|
||||
level="doctor"
|
||||
title="Doctor"
|
||||
data={data?.doctor}
|
||||
goal={goals?.doctor}
|
||||
levelProgrammeData={programmeData?.doctor}
|
||||
{...commonProps}
|
||||
/>
|
||||
<GraduationTimes
|
||||
level="licentiate"
|
||||
title="Licentiate"
|
||||
data={data?.licentiate}
|
||||
goal={goals?.licentiate}
|
||||
levelProgrammeData={programmeData?.licentiate}
|
||||
{...commonProps}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default TimesAndPathsView
|
||||
@@ -0,0 +1,51 @@
|
||||
import React, { useState } from 'react'
|
||||
import { Button, Icon, Loader } from 'semantic-ui-react'
|
||||
import { useUpdateFacultyBasicViewQuery } from '../../../redux/facultyStats'
|
||||
|
||||
const getStatusIcon = stats => {
|
||||
if (stats.isLoading) return <Loader active />
|
||||
if (stats.isSuccess) return <Icon name="check" color="green" />
|
||||
if (stats.isError) return <Icon name="close" color="red" />
|
||||
return ''
|
||||
}
|
||||
|
||||
const UpdateView = ({ faculty }) => {
|
||||
const [skipBasic, setSkipBasic] = useState(true)
|
||||
const [skipCredits, setSkipCredits] = useState(true)
|
||||
const [skipThesis, setSkipThesis] = useState(true)
|
||||
const basicBasicTabStats = useUpdateFacultyBasicViewQuery({ id: faculty, statsType: 'STUDENT' }, { skip: skipBasic })
|
||||
|
||||
const creditsBasicTabStats = useUpdateFacultyBasicViewQuery(
|
||||
{ id: faculty, statsType: 'CREDITS' },
|
||||
{ skip: skipCredits }
|
||||
)
|
||||
const thesisBasicTabStats = useUpdateFacultyBasicViewQuery({ id: faculty, statsType: 'THESIS' }, { skip: skipThesis })
|
||||
|
||||
return (
|
||||
<div className="update-view">
|
||||
<div className="button-container">
|
||||
<h3>Update data</h3>
|
||||
<p>
|
||||
<i>(For large faculties updates can take a couple of minutes)</i>
|
||||
</p>
|
||||
<h4>Update students data on Basic information -view</h4>
|
||||
<Button disabled={basicBasicTabStats.isLoading} color="blue" onClick={() => setSkipBasic(false)}>
|
||||
Update
|
||||
</Button>
|
||||
{getStatusIcon(basicBasicTabStats)}
|
||||
<h4>Update credits data on Basic information -view</h4>
|
||||
<Button disabled={creditsBasicTabStats.isLoading} color="blue" onClick={() => setSkipCredits(false)}>
|
||||
Update
|
||||
</Button>
|
||||
{getStatusIcon(creditsBasicTabStats)}
|
||||
<h4>Update thesiswriters data on Basic information -view</h4>
|
||||
<Button disabled={thesisBasicTabStats.isLoading} color="blue" onClick={() => setSkipThesis(false)}>
|
||||
Update
|
||||
</Button>
|
||||
{getStatusIcon(thesisBasicTabStats)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default UpdateView
|
||||
@@ -27,13 +27,13 @@
|
||||
.faculty-courses .radio-toggle {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin: 20px 35px 20px 35px;
|
||||
margin: 20px 35px;
|
||||
}
|
||||
|
||||
.programmes-overview .radio-toggle {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin: 0 35px 70px 35px;
|
||||
margin: 0 35px 70px;
|
||||
}
|
||||
|
||||
.programmes-overview .toggle-label,
|
||||
@@ -61,7 +61,7 @@
|
||||
.programmes-overview .ui.toggle.checkbox input:focus:checked ~ label::before,
|
||||
.faculty-overview .ui.toggle.checkbox input:focus:checked ~ label::before,
|
||||
.faculty-courses .ui.toggle.checkbox input:focus:checked ~ label::before {
|
||||
background-color: rgba(0, 0, 0, 0.05) !important;
|
||||
background-color: rgb(0 0 0 / 5%) !important;
|
||||
}
|
||||
|
||||
.faculty-overview .divider {
|
||||
@@ -110,12 +110,12 @@
|
||||
|
||||
.programmes-overview .datatable .total-row-cell,
|
||||
.programmes-overview .table-container .total-row-cell {
|
||||
border-top: 3px solid rgb(189, 180, 180);
|
||||
border-top: 3px solid rgb(189 180 180);
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.programmes-overview .programmes-selector {
|
||||
margin: 60px 60px 30px 60px;
|
||||
margin: 60px 60px 30px;
|
||||
}
|
||||
|
||||
.programmes-overview .header-row {
|
||||
@@ -123,7 +123,7 @@
|
||||
}
|
||||
|
||||
.programmes-overview .datatable {
|
||||
margin: 2em 0 5em 0;
|
||||
margin: 2em 0 5em;
|
||||
}
|
||||
|
||||
.programmes-overview .no-data-message {
|
||||
|
||||
@@ -2,30 +2,34 @@ import React, { useCallback, useEffect, useState } from 'react'
|
||||
import { withRouter, useHistory } from 'react-router-dom'
|
||||
import { Segment, Header, Tab, Loader } from 'semantic-ui-react'
|
||||
import { useGetFacultiesQuery } from 'redux/facultyStats'
|
||||
import { useGetAuthorizedUserQuery } from 'redux/auth'
|
||||
import { getTextIn } from 'common'
|
||||
import { useTabs, useTitle } from '../../common/hooks'
|
||||
import FacultySelector from './facultySelector'
|
||||
import BasicOverview from './BasicOverview'
|
||||
import ProgrammeOverview from './FacultyProgrammeOverview'
|
||||
import TimesAndPathsView from './TimesAndPaths'
|
||||
import UpdateView from './UpdateView'
|
||||
import useLanguage from '../LanguagePicker/useLanguage'
|
||||
import TSA from '../../common/tsa'
|
||||
|
||||
const ignore = ['Y', 'H99', 'Y01', 'H92', 'H930']
|
||||
|
||||
const FacultyStatistics = props => {
|
||||
useTitle('Faculties')
|
||||
const history = useHistory()
|
||||
const { language } = useLanguage()
|
||||
const allFaculties = useGetFacultiesQuery()
|
||||
const faculties = allFaculties?.data && allFaculties.data.filter(f => !ignore.includes(f.code))
|
||||
const faculties = allFaculties?.data
|
||||
|
||||
const { match } = props
|
||||
const { facultyCode } = match.params
|
||||
const faculty = faculties && facultyCode && faculties.find(f => f.code === facultyCode)
|
||||
const facultyName = faculty && getTextIn(faculty.name, language)
|
||||
|
||||
const { isAdmin } = useGetAuthorizedUserQuery()
|
||||
const [tab, setTab] = useTabs('p_tab', 0, history)
|
||||
const [academicYear, setAcademicYear] = useState(false)
|
||||
const [studyProgrammes, setStudyProgrammes] = useState(false)
|
||||
const [specialGroups, setSpecialGroups] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (!facultyName) {
|
||||
@@ -56,13 +60,49 @@ const FacultyStatistics = props => {
|
||||
const panes = [
|
||||
{
|
||||
menuItem: 'Basic information',
|
||||
render: () => <BasicOverview faculty={faculty} academicYear={academicYear} setAcademicYear={setAcademicYear} />,
|
||||
},
|
||||
{
|
||||
menuItem: 'Programmes and student populations',
|
||||
render: () => <ProgrammeOverview />,
|
||||
render: () => (
|
||||
<BasicOverview
|
||||
faculty={faculty}
|
||||
academicYear={academicYear}
|
||||
setAcademicYear={setAcademicYear}
|
||||
studyProgrammes={studyProgrammes}
|
||||
setStudyProgrammes={setStudyProgrammes}
|
||||
specialGroups={specialGroups}
|
||||
setSpecialGroups={setSpecialGroups}
|
||||
language={language}
|
||||
/>
|
||||
),
|
||||
},
|
||||
]
|
||||
// REMEMBER to change cypress test 'Correct tabs are shown', when visibility of
|
||||
// Graduation times and Programmes and student Populations -tabs is changed for facultyStatistic users
|
||||
if (isAdmin) {
|
||||
panes.push({
|
||||
menuItem: 'Programmes and student populations',
|
||||
render: () => <ProgrammeOverview faculty={faculty} language={language} />,
|
||||
})
|
||||
}
|
||||
|
||||
if (isAdmin) {
|
||||
panes.push({
|
||||
menuItem: 'Graduation times',
|
||||
render: () => (
|
||||
<TimesAndPathsView
|
||||
faculty={faculty}
|
||||
studyProgrammes={studyProgrammes}
|
||||
setStudyProgrammes={setStudyProgrammes}
|
||||
/>
|
||||
),
|
||||
})
|
||||
}
|
||||
|
||||
if (isAdmin) {
|
||||
panes.push({
|
||||
menuItem: 'Update statistics',
|
||||
render: () => <UpdateView faculty={faculty?.code} />,
|
||||
})
|
||||
}
|
||||
|
||||
return panes
|
||||
}
|
||||
|
||||
@@ -81,7 +121,7 @@ const FacultyStatistics = props => {
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="segmentContainer">
|
||||
<div className="segmentContainer" data-cy="FacultySegmentContainer">
|
||||
<Segment className="contentSegment">
|
||||
<div align="center" style={{ padding: '30px' }}>
|
||||
<Header textAlign="center">{facultyName}</Header>
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
}
|
||||
|
||||
.filter-tray-toggle .button .button-label > .label {
|
||||
padding: 0.5em 0.7em 0.3em 0.7em;
|
||||
padding: 0.5em 0.7em 0.3em;
|
||||
margin-top: -2px;
|
||||
margin-bottom: -2px;
|
||||
}
|
||||
@@ -68,7 +68,7 @@
|
||||
margin: 0 0 0 3rem;
|
||||
overflow-y: scroll;
|
||||
overflow-x: hidden;
|
||||
padding: 1.5rem 0 10rem 0;
|
||||
padding: 1.5rem 0 10rem;
|
||||
}
|
||||
|
||||
#filter-tray-container > .header {
|
||||
@@ -112,7 +112,7 @@
|
||||
}
|
||||
|
||||
#filter-tray .card {
|
||||
background-color: rgba(255, 255, 255, 0.08);
|
||||
background-color: rgb(255 255 255 / 8%);
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
margin-top: 0;
|
||||
@@ -140,7 +140,7 @@
|
||||
|
||||
#filter-tray.cards .description,
|
||||
#filter-tray.cards .extra {
|
||||
padding: 0 0.8rem 0.8rem 0.8rem;
|
||||
padding: 0 0.8rem 0.8rem;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
@@ -171,7 +171,7 @@
|
||||
#filter-tray .description-text {
|
||||
color: #9e9e9e;
|
||||
font-style: italic;
|
||||
margin: 1rem 0.2rem 0 0.2rem;
|
||||
margin: 1rem 0.2rem 0;
|
||||
}
|
||||
|
||||
#filter-tray .card-content {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
.filter-card-header .filter-clear-icon {
|
||||
margin-top: -0.2em;
|
||||
color: rgb(136, 136, 136);
|
||||
color: rgb(136 136 136);
|
||||
}
|
||||
|
||||
.filter-card-header .filter-clear-icon:hover {
|
||||
|
||||
@@ -10,9 +10,19 @@ const filterTexts = {
|
||||
[FilterType.PASSED_AFTER_FAILURE]: { label: 'Passed After Failure' },
|
||||
[FilterType.FAILED]: { label: 'Failed' },
|
||||
[FilterType.FAILED_MANY_TIMES]: { label: 'Failed Multiple Times' },
|
||||
[FilterType.NOT_PARTICIPATED]: { label: 'Not Participated' },
|
||||
// [FilterType.NOT_PARTICIPATED]: { label: 'Not Participated' },
|
||||
[FilterType.DID_NOT_PASS]: { label: "Didn't Pass" },
|
||||
}
|
||||
// a bandaid solution to prevent oodikone crashing
|
||||
const translate = {
|
||||
ALL: 'all',
|
||||
PASSED: 'passed',
|
||||
PASSED_AFTER_FAILURE: 'retryPassed',
|
||||
FAILED: 'failed',
|
||||
FAILED_MANY_TIMES: 'failedMany',
|
||||
NOT_PARTICIPATED: 'notParticipated',
|
||||
DID_NOT_PASS: 'didNotPass',
|
||||
}
|
||||
|
||||
const CourseCard = ({ course, filterType, onChange }) => {
|
||||
const { language } = useLanguage()
|
||||
@@ -27,7 +37,7 @@ const CourseCard = ({ course, filterType, onChange }) => {
|
||||
return (
|
||||
<>
|
||||
<Label style={{ marginTop: '0.5rem' }}>
|
||||
{getTextIn(course.name, language)}
|
||||
{getTextIn(course.course.name, language)}
|
||||
|
||||
<Dropdown
|
||||
text={filterTexts[filterType].label}
|
||||
@@ -35,7 +45,7 @@ const CourseCard = ({ course, filterType, onChange }) => {
|
||||
fluid
|
||||
className="mini"
|
||||
button
|
||||
data-cy={`${name}-${course.code}-dropdown`}
|
||||
data-cy={`${name}-${course.course.code}-dropdown`}
|
||||
style={{ marginTop: '0.5rem' }}
|
||||
>
|
||||
<Dropdown.Menu>
|
||||
@@ -50,6 +60,8 @@ const CourseCard = ({ course, filterType, onChange }) => {
|
||||
/>
|
||||
)
|
||||
}
|
||||
if (Object.keys(course?.students[translate[type]]).length === 0)
|
||||
return <Dropdown.Item key={label} text={label} value={type} onClick={onClick} disabled />
|
||||
return <Dropdown.Item key={label} text={label} value={type} onClick={onClick} />
|
||||
})}
|
||||
</Dropdown.Menu>
|
||||
@@ -60,7 +72,7 @@ const CourseCard = ({ course, filterType, onChange }) => {
|
||||
size="tiny"
|
||||
onClick={clear}
|
||||
icon
|
||||
data-cy={`${name}-${course.code}-clear`}
|
||||
data-cy={`${name}-${course.course.code}-clear`}
|
||||
style={{ marginTop: '0.5rem' }}
|
||||
>
|
||||
<Icon name="close" />
|
||||
|
||||
@@ -62,7 +62,7 @@ const CourseFilterCard = ({ courseStats, options, onOptionsChange }) => {
|
||||
/>
|
||||
{Object.entries(courseFilters).map(([code]) => (
|
||||
<CourseCard
|
||||
course={courseStats[code].course}
|
||||
course={courseStats[code]}
|
||||
filterType={courseFilters[code] ?? FilterType.ALL}
|
||||
onChange={type => setCourseFilter(code, type)}
|
||||
key={`course-filter-selected-course-${code}`}
|
||||
@@ -83,9 +83,10 @@ const filterFunctions = {
|
||||
[FilterType.PASSED_AFTER_FAILURE]: createFilterFunc('retryPassed'),
|
||||
[FilterType.FAILED]: createFilterFunc('failed'),
|
||||
[FilterType.FAILED_MANY_TIMES]: createFilterFunc('failedMany'),
|
||||
[FilterType.NOT_PARTICIPATED]: createFilterFunc('all', true),
|
||||
[FilterType.DID_NOT_PASS]: ({ studentNumber }, { students }) =>
|
||||
!Object.keys(students.all).includes(studentNumber) || Object.keys(students.failed).includes(studentNumber),
|
||||
[FilterType.DID_NOT_PASS]: createFilterFunc('didNotPass'),
|
||||
// [FilterType.NOT_PARTICIPATED]: createFilterFunc('all', true),
|
||||
// [FilterType.DID_NOT_PASS]: ({ studentNumber }, { students }) =>
|
||||
// !Object.keys(students.all).includes(studentNumber) || Object.keys(students.failed).includes(studentNumber),
|
||||
}
|
||||
|
||||
const filter = createFilter({
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable camelcase */
|
||||
import React from 'react'
|
||||
import { Form, Dropdown } from 'semantic-ui-react'
|
||||
import { getTextIn } from 'common'
|
||||
@@ -11,6 +12,7 @@ const StudyTrackFilterCard = ({ options, onOptionsChange, withoutSelf, activeAt,
|
||||
const activeAtMoment = activeAt && moment(activeAt)
|
||||
|
||||
const dropdownOptions = _.chain(withoutSelf())
|
||||
.filter(student => !student.transferredStudyright)
|
||||
.flatMap(student => student.studyrights)
|
||||
.map(sr => sr.studyright_elements)
|
||||
.filter(sre => sre.some(e => e.code === code))
|
||||
@@ -76,27 +78,23 @@ export default createFilter({
|
||||
filter: (student, { selected, args }) => {
|
||||
const activeAt = _.get(args, 'activeAt', moment())
|
||||
|
||||
return (
|
||||
student.studyrights
|
||||
// eslint-disable-next-line camelcase
|
||||
.filter(({ studyright_elements, graduated }) =>
|
||||
studyright_elements.some(
|
||||
sre =>
|
||||
sre.element_detail.type === 30 &&
|
||||
(!activeAt ||
|
||||
moment(activeAt).isBetween(
|
||||
moment(sre.startdate),
|
||||
graduated ? moment() : moment(sre.enddate),
|
||||
'day',
|
||||
'[]'
|
||||
))
|
||||
)
|
||||
return student.studyrights
|
||||
.filter(({ studyright_elements, graduated }) =>
|
||||
studyright_elements.some(
|
||||
sre =>
|
||||
sre.element_detail.type === 30 &&
|
||||
(!activeAt ||
|
||||
moment(activeAt).isBetween(
|
||||
moment(sre.startdate),
|
||||
graduated ? moment() : moment(sre.enddate),
|
||||
'day',
|
||||
'[]'
|
||||
))
|
||||
)
|
||||
// eslint-disable-next-line camelcase
|
||||
.flatMap(({ studyright_elements }) => studyright_elements)
|
||||
.map(sre => sre.element_detail.code)
|
||||
.some(code => selected.includes(code))
|
||||
)
|
||||
)
|
||||
.flatMap(({ studyright_elements }) => studyright_elements)
|
||||
.map(sre => sre.element_detail.code)
|
||||
.some(code => selected.includes(code))
|
||||
},
|
||||
|
||||
render: (props, { args }) => (
|
||||
|
||||
@@ -6,6 +6,7 @@ blockquote {
|
||||
position: relative;
|
||||
padding: 40px 20px;
|
||||
}
|
||||
|
||||
blockquote::before {
|
||||
content: open-quote;
|
||||
top: 0;
|
||||
|
||||
@@ -4,24 +4,32 @@ import { Message, Icon, Button } from 'semantic-ui-react'
|
||||
import ReactMarkdown from 'react-markdown'
|
||||
import './infoBox.css'
|
||||
|
||||
const InfoBox = ({ content }) => {
|
||||
const InfoBox = ({ content, cypress }) => {
|
||||
const [open, setOpen] = useState(false)
|
||||
const toggleOpen = () => setOpen(prev => !prev)
|
||||
|
||||
return open ? (
|
||||
<Message icon className="ok-infobox" color="green">
|
||||
<div className="content-container">
|
||||
<div className="content-container" data-cy={`${cypress}-info-content`}>
|
||||
<Icon name="info circle" size="huge" />
|
||||
<Message.Content className="ok-infobox content">
|
||||
<ReactMarkdown escapeHtml={false}>{content}</ReactMarkdown>
|
||||
</Message.Content>
|
||||
</div>
|
||||
<Button onClick={toggleOpen} className="ok-infobox-close">
|
||||
<Button onClick={toggleOpen} className="ok-infobox-close" data-cy={`${cypress}-close-info`}>
|
||||
Close Info
|
||||
</Button>
|
||||
</Message>
|
||||
) : (
|
||||
<Button icon labelPosition="left" className="ok-infobox-collapsed" onClick={toggleOpen} basic color="green">
|
||||
<Button
|
||||
icon
|
||||
labelPosition="left"
|
||||
className="ok-infobox-collapsed"
|
||||
onClick={toggleOpen}
|
||||
basic
|
||||
color="green"
|
||||
data-cy={`${cypress}-open-info`}
|
||||
>
|
||||
<Icon name="info circle" />
|
||||
Show Info
|
||||
</Button>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 0;
|
||||
background-color: rgba(0, 0, 0, 0.05);
|
||||
background-color: rgb(0 0 0 / 5%);
|
||||
}
|
||||
|
||||
.ok-infobox.ok-infobox.ok-infobox .content-container {
|
||||
|
||||
@@ -9,7 +9,8 @@ import LanguagePicker from '../LanguagePicker'
|
||||
import { isDev, adminerUrls } from '../../conf'
|
||||
|
||||
const allNavigationItems = {
|
||||
faculty: { path: '/faculties', key: 'faculties', label: 'Faculties', reqRights: ['admin'] },
|
||||
trends: { path: '/trends', key: 'trends', label: 'Trends' },
|
||||
faculty: { path: '/faculties', key: 'faculties', label: 'Faculties' },
|
||||
populations: {
|
||||
key: 'studyProgramme',
|
||||
label: 'Study programme',
|
||||
@@ -18,11 +19,10 @@ const allNavigationItems = {
|
||||
{ path: '/study-programme', key: 'overview', label: 'Overview' },
|
||||
],
|
||||
},
|
||||
students: { path: '/students', key: 'students', label: 'Student statistics' },
|
||||
courseStatistics: { path: '/coursestatistics', key: 'courseStatistics', label: 'Course statistics' },
|
||||
students: { path: '/students', key: 'students', label: 'Student statistics' },
|
||||
teachers: { path: '/teachers', key: 'teachers', label: 'Teachers', reqRights: ['teachers'] },
|
||||
users: { path: '/users', key: 'users', label: 'Users', reqRights: ['admin'] },
|
||||
trends: { path: '/trends', key: 'trends', label: 'Trends' },
|
||||
studyGuidanceGroups: {
|
||||
path: '/studyguidancegroups',
|
||||
key: 'studyGuidanceGroups',
|
||||
@@ -43,11 +43,6 @@ const NavigationBar = () => {
|
||||
const showAsUser = useShowAsUser()
|
||||
const [logout] = useLogoutMutation()
|
||||
|
||||
// Remove population tab for user with only IAM rights, delete when limited access ready
|
||||
if (!isLoading && !isAdmin && rights.length === 0 && iamRights.length !== 0) {
|
||||
delete allNavigationItems.populations.items[0]
|
||||
}
|
||||
|
||||
const refreshNavigationRoutes = () => {
|
||||
const visibleNavigationItems = {}
|
||||
if (isLoading) return visibleNavigationItems
|
||||
@@ -59,6 +54,8 @@ const NavigationBar = () => {
|
||||
if (!checkUserAccess(['admin', 'studyGuidanceGroups'], roles) && rights.length === 0) return
|
||||
} else if (key === 'courseStatistics') {
|
||||
if (!checkUserAccess(['courseStatistics', 'admin'], roles) && rights.length === 0) return
|
||||
} else if (key === 'faculty') {
|
||||
if (!checkUserAccess(['facultyStatistics', 'admin'], roles)) return
|
||||
}
|
||||
const { reqRights } = allNavigationItems[key]
|
||||
if (!reqRights || reqRights.every(r => roles.includes(r))) {
|
||||
|
||||
@@ -29,11 +29,11 @@ const mapCourseData = course => ({
|
||||
},
|
||||
})
|
||||
|
||||
const GradeDistribution = ({ flat }) => {
|
||||
const GradeDistribution = ({ flat, onlyIamRights }) => {
|
||||
const { modules, courseStatistics, onGoToCourseStatisticsClick } = UsePopulationCourseContext()
|
||||
|
||||
const columns = useMemo(
|
||||
() => [
|
||||
const columns = useMemo(() => {
|
||||
const columns = [
|
||||
{
|
||||
key: 'name-parent',
|
||||
mergeHeader: true,
|
||||
@@ -110,9 +110,12 @@ const GradeDistribution = ({ flat }) => {
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
[onGoToCourseStatisticsClick]
|
||||
)
|
||||
]
|
||||
|
||||
if (onlyIamRights) columns[0].children.pop()
|
||||
|
||||
return columns
|
||||
}, [onGoToCourseStatisticsClick])
|
||||
|
||||
const data = useMemo(() => {
|
||||
if (flat) {
|
||||
|
||||
@@ -23,11 +23,11 @@ const createModuleAggregateRow = ({ definition, children }) => ({
|
||||
},
|
||||
})
|
||||
|
||||
const PassFail = ({ flat }) => {
|
||||
const PassFail = ({ flat, onlyIamRights }) => {
|
||||
const { modules, courseStatistics, onGoToCourseStatisticsClick } = UsePopulationCourseContext()
|
||||
|
||||
const columns = useMemo(
|
||||
() => [
|
||||
const columns = useMemo(() => {
|
||||
const columns = [
|
||||
{
|
||||
key: 'course',
|
||||
title: 'Course',
|
||||
@@ -197,9 +197,11 @@ const PassFail = ({ flat }) => {
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
[onGoToCourseStatisticsClick]
|
||||
)
|
||||
]
|
||||
if (onlyIamRights) columns[0].children[0].children.pop()
|
||||
|
||||
return columns
|
||||
}, [onGoToCourseStatisticsClick])
|
||||
|
||||
const data = useMemo(() => {
|
||||
if (flat) {
|
||||
|
||||
+8
-6
@@ -26,11 +26,11 @@ const createModuleAggregateRow = ({ definition, children }) => ({
|
||||
},
|
||||
})
|
||||
|
||||
const PassFailEnrollments = ({ flat }) => {
|
||||
const PassFailEnrollments = ({ flat, onlyIamRights }) => {
|
||||
const { modules, courseStatistics, onGoToCourseStatisticsClick } = UsePopulationCourseContext()
|
||||
|
||||
const columns = useMemo(
|
||||
() => [
|
||||
const columns = useMemo(() => {
|
||||
const columns = [
|
||||
{
|
||||
key: 'course',
|
||||
title: 'Course',
|
||||
@@ -204,9 +204,11 @@ const PassFailEnrollments = ({ flat }) => {
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
[onGoToCourseStatisticsClick]
|
||||
)
|
||||
]
|
||||
if (onlyIamRights) columns[0].children[0].children.pop()
|
||||
|
||||
return columns
|
||||
}, [onGoToCourseStatisticsClick])
|
||||
|
||||
const data = useMemo(() => {
|
||||
if (flat) {
|
||||
|
||||
@@ -34,12 +34,12 @@ const yearColumn = (year, cumulative) => ({
|
||||
children: [semesterColumn(year, 'Fall', cumulative), semesterColumn(year, 'Spring', cumulative)],
|
||||
})
|
||||
|
||||
const PassingSemesters = () => {
|
||||
const PassingSemesters = ({ onlyIamRights }) => {
|
||||
const { modules, onGoToCourseStatisticsClick } = UsePopulationCourseContext()
|
||||
const [cumulativeStats, setCumulativeStats] = useState(false)
|
||||
|
||||
const columns = useMemo(
|
||||
() => [
|
||||
const columns = useMemo(() => {
|
||||
const columns = [
|
||||
{
|
||||
key: 'course-name',
|
||||
mergeHeader: true,
|
||||
@@ -118,9 +118,12 @@ const PassingSemesters = () => {
|
||||
..._.range(0, 6).map(i => yearColumn(i, cumulativeStats)),
|
||||
],
|
||||
},
|
||||
],
|
||||
[cumulativeStats, onGoToCourseStatisticsClick]
|
||||
)
|
||||
]
|
||||
|
||||
if (onlyIamRights) columns[0].children.pop()
|
||||
|
||||
return columns
|
||||
}, [cumulativeStats, onGoToCourseStatisticsClick])
|
||||
|
||||
const data = useMemo(
|
||||
() =>
|
||||
|
||||
@@ -294,7 +294,11 @@ const PopulationCourseStats = props => {
|
||||
menuItem: 'pass/fail',
|
||||
render: () => (
|
||||
<Tab.Pane className="menuTab">
|
||||
<PassFail expandedGroups={expandedGroups} toggleGroupExpansion={toggleGroupExpansion} />
|
||||
<PassFail
|
||||
expandedGroups={expandedGroups}
|
||||
toggleGroupExpansion={toggleGroupExpansion}
|
||||
onlyIamRights={props.onlyIamRights}
|
||||
/>
|
||||
</Tab.Pane>
|
||||
),
|
||||
},
|
||||
@@ -302,7 +306,11 @@ const PopulationCourseStats = props => {
|
||||
menuItem: 'pass/fail w/ enrollments ',
|
||||
render: () => (
|
||||
<Tab.Pane className="menuTab">
|
||||
<PassFailEnrollments expandedGroups={expandedGroups} toggleGroupExpansion={toggleGroupExpansion} />
|
||||
<PassFailEnrollments
|
||||
expandedGroups={expandedGroups}
|
||||
toggleGroupExpansion={toggleGroupExpansion}
|
||||
onlyIamRights={props.onlyIamRights}
|
||||
/>
|
||||
</Tab.Pane>
|
||||
),
|
||||
},
|
||||
@@ -310,7 +318,11 @@ const PopulationCourseStats = props => {
|
||||
menuItem: 'grades',
|
||||
render: () => (
|
||||
<Tab.Pane className="menuTab">
|
||||
<GradeDistribution expandedGroups={expandedGroups} toggleGroupExpansion={toggleGroupExpansion} />
|
||||
<GradeDistribution
|
||||
expandedGroups={expandedGroups}
|
||||
toggleGroupExpansion={toggleGroupExpansion}
|
||||
onlyIamRights={props.onlyIamRights}
|
||||
/>
|
||||
</Tab.Pane>
|
||||
),
|
||||
},
|
||||
@@ -323,6 +335,7 @@ const PopulationCourseStats = props => {
|
||||
courseStatistics={courseStatistics}
|
||||
expandedGroups={expandedGroups}
|
||||
toggleGroupExpansion={toggleGroupExpansion}
|
||||
onlyIamRights={props.onlyIamRights}
|
||||
/>
|
||||
</Tab.Pane>
|
||||
),
|
||||
@@ -352,6 +365,10 @@ const PopulationCourseStats = props => {
|
||||
if (pending) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (props.onlyIamRights) {
|
||||
panes.pop()
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
<PopulationCourseContext.Provider value={contextValue}>
|
||||
|
||||
@@ -9,7 +9,7 @@ import FilterDegreeCoursesModal from './FilterDegreeCoursesModal'
|
||||
import { getPopulationSelectedStudentCourses } from '../../redux/populationSelectedStudentCourses'
|
||||
import infotooltips from '../../common/InfoToolTips'
|
||||
|
||||
const PopulationCourses = ({ query = {}, filteredStudents, selectedStudentsByYear }) => {
|
||||
const PopulationCourses = ({ query = {}, filteredStudents, selectedStudentsByYear, onlyIamRights }) => {
|
||||
const [showByStudytrack, setShowByStudytrack] = useState(true)
|
||||
const populationCourses = useSelector(({ populationCourses }) => populationCourses)
|
||||
const mandatoryCourses = useSelector(({ populationMandatoryCourses }) => populationMandatoryCourses)
|
||||
@@ -23,12 +23,17 @@ const PopulationCourses = ({ query = {}, filteredStudents, selectedStudentsByYea
|
||||
return populationSelectedStudentCourses.query.selectedStudents?.length === filteredStudents.length
|
||||
}
|
||||
|
||||
const getSelectedStudents = students =>
|
||||
onlyIamRights
|
||||
? students.map(({ studentNumber, iv }) => ({ encryptedData: studentNumber, iv }))
|
||||
: students.map(student => student.studentNumber)
|
||||
|
||||
const fetch = courses => {
|
||||
dispatch(
|
||||
getPopulationSelectedStudentCourses({
|
||||
...query,
|
||||
studyRights: [query.studyRights.programme],
|
||||
selectedStudents: filteredStudents.map(s => s.studentNumber),
|
||||
selectedStudents: getSelectedStudents(filteredStudents),
|
||||
selectedStudentsByYear,
|
||||
courses,
|
||||
})
|
||||
@@ -79,7 +84,9 @@ const PopulationCourses = ({ query = {}, filteredStudents, selectedStudentsByYea
|
||||
<Segment basic>
|
||||
<InfoBox content={infotooltips.PopulationStatistics.CoursesOfPopulation} />
|
||||
{renderToggleStructureButton()}
|
||||
{query.studyRights.programme && <FilterDegreeCoursesModal studyProgramme={query.studyRights.programme} />}
|
||||
{query.studyRights.programme && !onlyIamRights && (
|
||||
<FilterDegreeCoursesModal studyProgramme={query.studyRights.programme} />
|
||||
)}
|
||||
|
||||
<SegmentDimmer isLoading={pending} />
|
||||
{showByStudytrack ? (
|
||||
@@ -88,6 +95,7 @@ const PopulationCourses = ({ query = {}, filteredStudents, selectedStudentsByYea
|
||||
courses={selectedPopulationCourses.data}
|
||||
pending={pending}
|
||||
filteredStudents={filteredStudents}
|
||||
onlyIamRights={onlyIamRights}
|
||||
/>
|
||||
) : (
|
||||
<CustomPopulationCourses
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user