mirror of
https://github.com/UniversityOfHelsinkiCS/oodikone.git
synced 2026-10-13 03:13:40 +03:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8dbbf00b51 | ||
|
|
584a1b474a | ||
|
|
8151c4f463 | ||
|
|
bb818ee9ac | ||
|
|
cd3b9bb843 | ||
|
|
aaa012894c | ||
|
|
4d06eb7558 | ||
|
|
b563b29a36 | ||
|
|
617e234798 | ||
|
|
754d525f87 | ||
|
|
a0e6d342b9 | ||
|
|
60f500c9e7 | ||
|
|
dc9ce89aa7 | ||
|
|
8a1fb2725b | ||
|
|
0ca6bca243 | ||
|
|
7457bc412b | ||
|
|
3d2e8fb1ca | ||
|
|
1d980795c6 | ||
|
|
6bc6a28127 | ||
|
|
a2f84931cd | ||
|
|
1f1bae8557 | ||
|
|
c714c61269 | ||
|
|
e85deff947 | ||
|
|
8c87135f58 | ||
|
|
7eac6c686e | ||
|
|
11172f7d77 | ||
|
|
be29486cdd | ||
|
|
04cbec19a5 | ||
|
|
7c3cfb56b5 | ||
|
|
090305be3b | ||
|
|
c1e0d34670 | ||
|
|
9cfc51382d | ||
|
|
3f179d3460 | ||
|
|
0ad0d73d18 | ||
|
|
7952b3703f | ||
|
|
e9902a0bbe | ||
|
|
b225e03bd5 | ||
|
|
374cef4a1a | ||
|
|
6dff16f683 | ||
|
|
73a1d50ec1 | ||
|
|
b473b393f0 | ||
|
|
218f09ea65 | ||
|
|
5948a34c6e | ||
|
|
6db0e982d9 | ||
|
|
205637c4db | ||
|
|
7ac397969a | ||
|
|
9fc927daff | ||
|
|
72635b53ac | ||
|
|
980a7a6703 | ||
|
|
80709a129c | ||
|
|
26c7f3e00b | ||
|
|
cc60c9f415 | ||
|
|
7f8f40a193 | ||
|
|
1749503aa4 | ||
|
|
da55bfefac | ||
|
|
0c2f197460 | ||
|
|
6503971915 | ||
|
|
c30624d319 | ||
|
|
a4c798ea25 | ||
|
|
bf175e9140 | ||
|
|
70f4440974 | ||
|
|
cde5dd5ced | ||
|
|
35777287b2 | ||
|
|
3ec010f347 | ||
|
|
36792ac667 | ||
|
|
d4f187ce92 | ||
|
|
0af1e0d82e | ||
|
|
74803f7af4 | ||
|
|
4817393665 | ||
|
|
80de8768e3 | ||
|
|
3f696acf99 | ||
|
|
831cab9e2b | ||
|
|
94b0c0f241 | ||
|
|
5d51096557 | ||
|
|
be90065bd4 | ||
|
|
9501055406 | ||
|
|
66685ee0ff | ||
|
|
84cb0ca7a1 | ||
|
|
c16e85d688 | ||
|
|
0781f2a9b0 | ||
|
|
0689e83cbd | ||
|
|
6e407fc196 | ||
|
|
4d6abef0b9 | ||
|
|
1f1e2d45fa | ||
|
|
3028cdb6ec | ||
|
|
ed71a0ce3b | ||
|
|
f3c290a5ee | ||
|
|
7ab8236e14 | ||
|
|
a884069e77 | ||
|
|
1d8a06c477 | ||
|
|
88ba4af98c | ||
|
|
4ed75331ff | ||
|
|
63c2d9756f | ||
|
|
2592adfd18 | ||
|
|
d8976adb46 | ||
|
|
f47f4bca29 | ||
|
|
a5d824b927 | ||
|
|
e20e1137ae | ||
|
|
11792b0397 | ||
|
|
3cb2e370aa | ||
|
|
db5b0a29cd | ||
|
|
79f9a59116 | ||
|
|
fdba5dc6a6 | ||
|
|
d0e8a9632d | ||
|
|
16b7debbb5 | ||
|
|
6081bb3e78 | ||
|
|
de2823f3b5 | ||
|
|
07e2dabe3f | ||
|
|
1cccecc7f5 | ||
|
|
37a50731aa | ||
|
|
eba296f5fd | ||
|
|
fbe0b0af3a | ||
|
|
ea4d299f16 | ||
|
|
f2f9bdd9e8 | ||
|
|
4b86ed252f | ||
|
|
f535a5dbdb | ||
|
|
ffe6425999 | ||
|
|
256997633a | ||
|
|
1d3fef6c8d | ||
|
|
428e46084c | ||
|
|
09f4814f80 | ||
|
|
a220050a53 | ||
|
|
115674636f | ||
|
|
f901fec6a3 | ||
|
|
a77ba0e1e1 | ||
|
|
434183bac0 | ||
|
|
7993c2b45a | ||
|
|
8551a76fc9 | ||
|
|
b25f9ba55f | ||
|
|
f28276e086 | ||
|
|
6327ca7e2c | ||
|
|
625f2962eb | ||
|
|
f85e9f0a5d | ||
|
|
d4d67b239e | ||
|
|
83d17a3026 | ||
|
|
e3f8fdb4ae | ||
|
|
b7051ccd41 | ||
|
|
b0a0be7969 | ||
|
|
896cb46ad1 | ||
|
|
17f7289b1c | ||
|
|
a019dd9672 | ||
|
|
881aaf93b4 | ||
|
|
5c973d4f8c | ||
|
|
f9e8bf6d31 | ||
|
|
a2726d4894 | ||
|
|
11377da7bd | ||
|
|
8c4515b158 | ||
|
|
db3cd52d69 | ||
|
|
b4c8bf0f4a | ||
|
|
384afb1e5f | ||
|
|
090d327405 | ||
|
|
30117e60b3 | ||
|
|
9a4fec0974 | ||
|
|
37114c5570 | ||
|
|
63b7c5876d | ||
|
|
9790c829c6 | ||
|
|
082aaa4022 | ||
|
|
3906efbcca | ||
|
|
9d575d6c88 | ||
|
|
4d4a288719 | ||
|
|
a39f522501 | ||
|
|
32293eb050 | ||
|
|
ed56d32cce | ||
|
|
f4b04d3283 | ||
|
|
3a6e5132f3 | ||
|
|
694e537baa | ||
|
|
372ac53a46 | ||
|
|
5d9bbe368c | ||
|
|
3d552a5811 | ||
|
|
624a103f08 | ||
|
|
9f56554ed9 | ||
|
|
0d1bc3c519 | ||
|
|
aa1d21bc09 | ||
|
|
eaf5d7574e | ||
|
|
9db14a839f | ||
|
|
b55d649622 | ||
|
|
47c792e1f1 | ||
|
|
fffa3d3ee1 | ||
|
|
c9407ff4b0 | ||
|
|
1b43d4fc39 | ||
|
|
c0ce396d4d | ||
|
|
f15ffa95f5 | ||
|
|
765d2f771a | ||
|
|
e1271d7d1f | ||
|
|
7f632bde34 | ||
|
|
9dd47d002d | ||
|
|
16a07ace39 | ||
|
|
ac756cb35b | ||
|
|
b9059eb058 | ||
|
|
1f967a1f2c | ||
|
|
e8ef944a9d | ||
|
|
0e9ea20235 | ||
|
|
8d4f1c7794 | ||
|
|
6f3e4b8f64 | ||
|
|
316ed2af2f | ||
|
|
2f753d976b | ||
|
|
9b6aa96531 | ||
|
|
229a64aa45 | ||
|
|
6456b84777 | ||
|
|
9d34c34de7 | ||
|
|
2573976ea4 | ||
|
|
389b71cfbf | ||
|
|
c0ee74b7f8 | ||
|
|
e3fbf91b99 | ||
|
|
3d1f7cc655 | ||
|
|
069bd97729 | ||
|
|
96fded285b | ||
|
|
9c172c2f97 | ||
|
|
d37b037cc6 | ||
|
|
a55eef0f34 | ||
|
|
01ec83d909 | ||
|
|
81e8a0c77e | ||
|
|
7782f4f863 | ||
|
|
80f52355bb | ||
|
|
28cf6c855d | ||
|
|
6d446a1c05 | ||
|
|
d8214e7432 | ||
|
|
a8b04d56d6 | ||
|
|
0fc7bfda51 | ||
|
|
5614a6eb3f | ||
|
|
dfde4adf87 | ||
|
|
32a8c7c261 | ||
|
|
b7390912f4 | ||
|
|
6af7a1a5d0 | ||
|
|
e24b63da74 | ||
|
|
bb5bb8d04e | ||
|
|
a91fb5d7b7 | ||
|
|
5369e75b61 | ||
|
|
7a43433947 | ||
|
|
656870028e | ||
|
|
1315d7ca81 | ||
|
|
3b1f002ac8 | ||
|
|
00116544bf | ||
|
|
7bbccc9a81 | ||
|
|
c18187d094 | ||
|
|
7688a22c53 | ||
|
|
49df0a0d21 | ||
|
|
449e8976d9 | ||
|
|
6871502cb9 | ||
|
|
b72bedc49f | ||
|
|
9f300ccab0 | ||
|
|
cb8aa00f6f | ||
|
|
4a05f136cc | ||
|
|
50840a3fe2 | ||
|
|
ac440ff843 | ||
|
|
b6d28c6cff | ||
|
|
f79539c2a6 | ||
|
|
c46173cc98 | ||
|
|
a13c0e4628 | ||
|
|
a4887a30bc | ||
|
|
bf3adf446f | ||
|
|
a03d1c9707 | ||
|
|
2d9fe72481 | ||
|
|
15ef2f156e | ||
|
|
5470d53a19 | ||
|
|
65eedf1de8 | ||
|
|
1a9e51534b | ||
|
|
0d7d059541 | ||
|
|
2c76703085 | ||
|
|
81c45c383f | ||
|
|
9bb456e258 | ||
|
|
c9641bb118 | ||
|
|
06d129b685 | ||
|
|
b78446a11a | ||
|
|
3511b947c4 | ||
|
|
a338b2f4d5 | ||
|
|
3de3bbab43 | ||
|
|
b24b30b179 | ||
|
|
8af06138a9 | ||
|
|
8b30e2b416 | ||
|
|
562ee801d7 | ||
|
|
eb9b3ccaae | ||
|
|
3bf1c0e47d | ||
|
|
2f028ff9e0 | ||
|
|
b529c219ea | ||
|
|
6369bdb62c | ||
|
|
69803e528f | ||
|
|
e71f75bd72 | ||
|
|
964500fb52 | ||
|
|
9241efb16a | ||
|
|
e585e59907 | ||
|
|
5a4e724021 | ||
|
|
7adc101024 | ||
|
|
57bfa879ae | ||
|
|
38174d36d9 | ||
|
|
062bdfbe0b | ||
|
|
d0540610c1 | ||
|
|
db43907ce3 | ||
|
|
be906d262e |
@@ -0,0 +1,88 @@
|
||||
name: 'Test oodikone and release to staging'
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
paths:
|
||||
- '.github/workflows/test-oodikone.yaml'
|
||||
- '.github/**'
|
||||
- 'cypress/**'
|
||||
- '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:
|
||||
test:
|
||||
uses: ./.github/workflows/test.yaml
|
||||
secrets:
|
||||
username: ${{ secrets.TOSKAREGISTRY_USERNAME }}
|
||||
password: ${{ secrets.TOSKAREGISTRY_PASSWORD }}
|
||||
|
||||
release_frontend:
|
||||
name: 'Build and release frontend'
|
||||
runs-on: ubuntu-latest
|
||||
needs: test
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Build frontend image
|
||||
id: build-image
|
||||
uses: redhat-actions/buildah-build@v2
|
||||
with:
|
||||
image: oodikone-frontend
|
||||
tags: staging ${{ github.sha }}
|
||||
context: services/frontend
|
||||
containerfiles: |
|
||||
services/frontend/Dockerfile
|
||||
build-args: |
|
||||
BUILT_AT=${{ steps.get_current_time.outputs.NOW }}
|
||||
BASE_PATH=/oodikone
|
||||
GIT_SHA=${{ github.sha }}
|
||||
STAGING=true
|
||||
|
||||
- name: Push to quay.io
|
||||
uses: redhat-actions/push-to-registry@v2
|
||||
with:
|
||||
image: ${{ steps.build-image.outputs.image }}
|
||||
tags: ${{ steps.build-image.outputs.tags }}
|
||||
registry: quay.io/toska
|
||||
username: toska+github
|
||||
password: ${{ secrets.QUAY_IO_TOKEN }}
|
||||
|
||||
release_backend:
|
||||
name: 'Build and release backend'
|
||||
runs-on: ubuntu-latest
|
||||
needs: test
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Build backend image
|
||||
id: build-image
|
||||
uses: redhat-actions/buildah-build@v2
|
||||
with:
|
||||
image: oodikone-backend
|
||||
tags: staging ${{ github.sha }}
|
||||
context: services/backend
|
||||
containerfiles: |
|
||||
services/backend/Dockerfile
|
||||
build-args: |
|
||||
BASE_PATH=/oodikone
|
||||
GIT_SHA=${{ github.sha }}
|
||||
STAGING=true
|
||||
|
||||
- name: Push to quay.io
|
||||
uses: redhat-actions/push-to-registry@v2
|
||||
with:
|
||||
image: ${{ steps.build-image.outputs.image }}
|
||||
tags: ${{ steps.build-image.outputs.tags }}
|
||||
registry: quay.io/toska
|
||||
username: toska+github
|
||||
password: ${{ secrets.QUAY_IO_TOKEN }}
|
||||
@@ -1,25 +1,17 @@
|
||||
name: 'Test oodikone and release to staging'
|
||||
name: Lint and test
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
paths:
|
||||
- '.github/workflows/test-oodikone.yaml'
|
||||
- '.github/actions/**'
|
||||
- 'cypress/**'
|
||||
- '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/**'
|
||||
workflow_call:
|
||||
secrets:
|
||||
username:
|
||||
required: true
|
||||
description: 'Toska registry username'
|
||||
password:
|
||||
required: true
|
||||
description: 'Toska registry password'
|
||||
|
||||
jobs:
|
||||
# === Static tests / linting ===
|
||||
|
||||
lint_backend:
|
||||
name: 'Lint backend'
|
||||
runs-on: ubuntu-latest
|
||||
@@ -62,13 +54,11 @@ jobs:
|
||||
SENTRY_RELEASE=${{ github.sha }}
|
||||
SENTRY_ENVIRONMENT=staging
|
||||
cache-from: |
|
||||
type=registry,ref=toska/oodikone2-backend:staging
|
||||
type=registry,ref=toska/oodikone2-backend:latest
|
||||
type=registry,ref=quay.io/toska/oodikone-backend:staging
|
||||
cache-to: type=inline
|
||||
context: services/backend
|
||||
push: false
|
||||
tags: toska/oodikone2-backend:staging
|
||||
target: production
|
||||
tags: oodikone-backend:staging
|
||||
outputs: type=docker,dest=/tmp/backend.tar
|
||||
- name: 'Upload image for other jobs'
|
||||
uses: actions/upload-artifact@v3
|
||||
@@ -92,13 +82,11 @@ jobs:
|
||||
SENTRY_ENVIRONMENT=staging
|
||||
BUILT_AT=${{ steps.get_current_time.outputs.NOW }}
|
||||
cache-from: |
|
||||
type=registry,ref=toska/oodikone2-frontend:staging
|
||||
type=registry,ref=toska/oodikone2-frontend:latest
|
||||
type=registry,ref=toska/oodikone-frontend:staging
|
||||
cache-to: type=inline
|
||||
context: services/frontend
|
||||
push: false
|
||||
tags: toska/oodikone2-frontend:staging
|
||||
target: production
|
||||
tags: oodikone-frontend:staging
|
||||
outputs: type=docker,dest=/tmp/frontend.tar
|
||||
- name: 'Upload image for other jobs'
|
||||
uses: actions/upload-artifact@v3
|
||||
@@ -121,8 +109,8 @@ jobs:
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
registry: registry.toska.cs.helsinki.fi
|
||||
username: ${{ secrets.TOSKAREGISTRY_USERNAME }}
|
||||
password: ${{ secrets.TOSKAREGISTRY_PASSWORD }}
|
||||
username: ${{ secrets.username }}
|
||||
password: ${{ secrets.password }}
|
||||
- name: 'Let sis-updater-worker run migrations, then push new version of image to toska hub'
|
||||
if: steps.check_migrations.outputs.migrations == 'true'
|
||||
run: |
|
||||
@@ -154,8 +142,8 @@ jobs:
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
registry: registry.toska.cs.helsinki.fi
|
||||
username: ${{ secrets.TOSKAREGISTRY_USERNAME }}
|
||||
password: ${{ secrets.TOSKAREGISTRY_PASSWORD }}
|
||||
username: ${{ secrets.username }}
|
||||
password: ${{ secrets.password }}
|
||||
- name: 'Let backend run migrations, then push new version of image to toska hub'
|
||||
if: steps.check_migrations.outputs.migrations == 'true'
|
||||
run: |
|
||||
@@ -187,8 +175,8 @@ jobs:
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
registry: registry.toska.cs.helsinki.fi
|
||||
username: ${{ secrets.TOSKAREGISTRY_USERNAME }}
|
||||
password: ${{ secrets.TOSKAREGISTRY_PASSWORD }}
|
||||
username: ${{ secrets.username }}
|
||||
password: ${{ secrets.password }}
|
||||
- name: 'Let backend run migrations, then push new version of image to toska hub'
|
||||
if: steps.check_migrations.outputs.migrations == 'true'
|
||||
run: |
|
||||
@@ -211,8 +199,8 @@ jobs:
|
||||
- name: 'Run tests'
|
||||
uses: ./.github/actions/run-tests
|
||||
with:
|
||||
username: ${{ secrets.TOSKAREGISTRY_USERNAME }}
|
||||
password: ${{ secrets.TOSKAREGISTRY_PASSWORD }}
|
||||
username: ${{ secrets.username }}
|
||||
password: ${{ secrets.password }}
|
||||
spec: cypress/e2e/Course_statistics.js
|
||||
|
||||
test_custom_population:
|
||||
@@ -229,8 +217,8 @@ jobs:
|
||||
- name: 'Run tests'
|
||||
uses: ./.github/actions/run-tests
|
||||
with:
|
||||
username: ${{ secrets.TOSKAREGISTRY_USERNAME }}
|
||||
password: ${{ secrets.TOSKAREGISTRY_PASSWORD }}
|
||||
username: ${{ secrets.username }}
|
||||
password: ${{ secrets.password }}
|
||||
spec: cypress/e2e/Custom_population.js
|
||||
|
||||
test_population_statistics:
|
||||
@@ -247,8 +235,8 @@ jobs:
|
||||
- name: 'Run tests'
|
||||
uses: ./.github/actions/run-tests
|
||||
with:
|
||||
username: ${{ secrets.TOSKAREGISTRY_USERNAME }}
|
||||
password: ${{ secrets.TOSKAREGISTRY_PASSWORD }}
|
||||
username: ${{ secrets.username }}
|
||||
password: ${{ secrets.password }}
|
||||
spec: cypress/e2e/Population_statistics.js
|
||||
|
||||
test_filters:
|
||||
@@ -265,8 +253,8 @@ jobs:
|
||||
- name: 'Run tests'
|
||||
uses: ./.github/actions/run-tests
|
||||
with:
|
||||
username: ${{ secrets.TOSKAREGISTRY_USERNAME }}
|
||||
password: ${{ secrets.TOSKAREGISTRY_PASSWORD }}
|
||||
username: ${{ secrets.username }}
|
||||
password: ${{ secrets.password }}
|
||||
spec: cypress/e2e/Filters.js
|
||||
|
||||
test_student_statistics:
|
||||
@@ -283,10 +271,28 @@ jobs:
|
||||
- name: 'Run tests'
|
||||
uses: ./.github/actions/run-tests
|
||||
with:
|
||||
username: ${{ secrets.TOSKAREGISTRY_USERNAME }}
|
||||
password: ${{ secrets.TOSKAREGISTRY_PASSWORD }}
|
||||
username: ${{ secrets.username }}
|
||||
password: ${{ secrets.password }}
|
||||
spec: cypress/e2e/Student_statistics.js
|
||||
|
||||
test_completed_courses:
|
||||
name: 'Test completed courses'
|
||||
needs:
|
||||
- build_backend
|
||||
- build_frontend
|
||||
- build_and_push_anon_sis_db
|
||||
- build_and_push_anon_kone_db
|
||||
- build_and_push_anon_user_db
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: 'Run tests'
|
||||
uses: ./.github/actions/run-tests
|
||||
with:
|
||||
username: ${{ secrets.username }}
|
||||
password: ${{ secrets.password }}
|
||||
spec: cypress/e2e/Completed_courses.js
|
||||
|
||||
test_studyprogramme_overview:
|
||||
name: 'Test studyprogramme overview'
|
||||
needs:
|
||||
@@ -301,8 +307,8 @@ jobs:
|
||||
- name: 'Run tests'
|
||||
uses: ./.github/actions/run-tests
|
||||
with:
|
||||
username: ${{ secrets.TOSKAREGISTRY_USERNAME }}
|
||||
password: ${{ secrets.TOSKAREGISTRY_PASSWORD }}
|
||||
username: ${{ secrets.username }}
|
||||
password: ${{ secrets.password }}
|
||||
spec: cypress/e2e/Studyprogramme_overview.js
|
||||
|
||||
test_teachers_and_users:
|
||||
@@ -319,8 +325,8 @@ jobs:
|
||||
- name: 'Run tests'
|
||||
uses: ./.github/actions/run-tests
|
||||
with:
|
||||
username: ${{ secrets.TOSKAREGISTRY_USERNAME }}
|
||||
password: ${{ secrets.TOSKAREGISTRY_PASSWORD }}
|
||||
username: ${{ secrets.username }}
|
||||
password: ${{ secrets.password }}
|
||||
spec: |
|
||||
cypress/e2e/Teachers.js
|
||||
cypress/e2e/Users.js
|
||||
@@ -339,8 +345,8 @@ jobs:
|
||||
- name: 'Run tests'
|
||||
uses: ./.github/actions/run-tests
|
||||
with:
|
||||
username: ${{ secrets.TOSKAREGISTRY_USERNAME }}
|
||||
password: ${{ secrets.TOSKAREGISTRY_PASSWORD }}
|
||||
username: ${{ secrets.username }}
|
||||
password: ${{ secrets.password }}
|
||||
spec: cypress/e2e/Faculty_statistics.js
|
||||
|
||||
test_open_uni_search:
|
||||
@@ -357,12 +363,11 @@ jobs:
|
||||
- name: 'Run tests'
|
||||
uses: ./.github/actions/run-tests
|
||||
with:
|
||||
username: ${{ secrets.TOSKAREGISTRY_USERNAME }}
|
||||
password: ${{ secrets.TOSKAREGISTRY_PASSWORD }}
|
||||
username: ${{ secrets.username }}
|
||||
password: ${{ secrets.password }}
|
||||
spec: cypress/e2e/Openuni.js
|
||||
|
||||
# === Other tests ===
|
||||
|
||||
test_service_exit_statuses:
|
||||
name: 'Check services run correctly'
|
||||
needs:
|
||||
@@ -386,8 +391,8 @@ jobs:
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
registry: registry.toska.cs.helsinki.fi
|
||||
username: ${{ secrets.TOSKAREGISTRY_USERNAME }}
|
||||
password: ${{ secrets.TOSKAREGISTRY_PASSWORD }}
|
||||
username: ${{ secrets.username }}
|
||||
password: ${{ secrets.password }}
|
||||
- name: 'Run services, pull rest of the needed images'
|
||||
run: docker-compose --file docker-compose.ci.yml up --detach --no-recreate
|
||||
- name: 'Check that exit statuses are not errors'
|
||||
@@ -400,98 +405,3 @@ jobs:
|
||||
- name: 'Print logs on failure'
|
||||
if: failure()
|
||||
run: docker-compose --file docker-compose.ci.yml logs
|
||||
|
||||
# === Release ===
|
||||
|
||||
release_images:
|
||||
name: 'Release built images'
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- lint_backend
|
||||
- lint_frontend
|
||||
- test_course_statistics
|
||||
- test_custom_population
|
||||
- test_filters
|
||||
- test_population_statistics
|
||||
- test_student_statistics
|
||||
- test_studyprogramme_overview
|
||||
- test_teachers_and_users
|
||||
- test_faculty_statistics
|
||||
- test_open_uni_search
|
||||
- test_service_exit_statuses
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: 'Download images'
|
||||
uses: actions/download-artifact@v3
|
||||
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 DockerHub'
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
- name: 'Create a container from frontend'
|
||||
run: docker create --name frontend toska/oodikone2-frontend:staging
|
||||
- name: 'Copy assets from the container'
|
||||
run: docker cp frontend:/usr/src/app/build ./build
|
||||
- name: 'Create Sentry release'
|
||||
uses: getsentry/action-release@v1
|
||||
env:
|
||||
SENTRY_URL: ${{ secrets.SENTRY_URL }}
|
||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||
SENTRY_ORG: toska
|
||||
SENTRY_PROJECT: oodikone
|
||||
with:
|
||||
environment: staging
|
||||
set_commits: 'skip'
|
||||
sourcemaps: './build/static/js'
|
||||
url_prefix: '~/static/js'
|
||||
version: ${{ github.sha }}
|
||||
- name: 'Push to docker hub'
|
||||
run: |
|
||||
docker push toska/oodikone2-backend:staging
|
||||
docker push toska/oodikone2-frontend:staging
|
||||
- name: 'Send deployment success notification to slack'
|
||||
if: success()
|
||||
uses: UniversityOfHelsinkiCS/slackbot-action@v1.4.2
|
||||
with:
|
||||
webhook-url: ${{ secrets.WEBHOOK_URL }}
|
||||
message-type: deployment
|
||||
softa-url: https://oodikone-staging.cs.helsinki.fi/
|
||||
deployment-target: staging
|
||||
- name: 'Send deployment failure notification to slack'
|
||||
if: failure()
|
||||
uses: UniversityOfHelsinkiCS/slackbot-action@v1.4.2
|
||||
with:
|
||||
webhook-url: ${{ secrets.WEBHOOK_URL }}
|
||||
message-type: deployment-failure
|
||||
deployment-target: staging
|
||||
|
||||
report_test_failure:
|
||||
runs-on: ubuntu-latest
|
||||
name: 'Report test failure to oodikone notifications'
|
||||
needs:
|
||||
- lint_backend
|
||||
- lint_frontend
|
||||
- test_course_statistics
|
||||
- test_custom_population
|
||||
- test_filters
|
||||
- test_population_statistics
|
||||
- test_student_statistics
|
||||
- test_studyprogramme_overview
|
||||
- test_teachers_and_users
|
||||
- test_faculty_statistics
|
||||
- test_open_uni_search
|
||||
- test_service_exit_statuses
|
||||
if: failure()
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: 'Send notification to slack'
|
||||
uses: UniversityOfHelsinkiCS/slackbot-action@v1.4.2
|
||||
with:
|
||||
webhook-url: ${{ secrets.WEBHOOK_URL }}
|
||||
message-type: test-failure
|
||||
+1
-1
@@ -7,7 +7,7 @@ module.exports = {
|
||||
'{services,updater}/**/*.{js,jsx}': files => `eslint --fix ${files.join(' ')}`,
|
||||
'*.{js,json,md,yml,yaml,html}': files => `prettier --write ${files.join(' ')}`,
|
||||
'*.css': files => `stylelint --fix ${files.join(' ')}`,
|
||||
Dockerfile: files => `${dockerCmdBase} hadolint/hadolint:v2.7.0 hadolint ${relativeFilePaths(files)}`,
|
||||
Dockerfile: files => `${dockerCmdBase} hadolint/hadolint:v2.12.0 hadolint ${relativeFilePaths(files)}`,
|
||||
'*.sh': files => `${dockerCmdBase} koalaman/shellcheck:v0.7.2 ${relativeFilePaths(files)} -x`,
|
||||
'.github/{workflows,actions}/*': files => `npm run actionlint ${relativeFilePaths(files)}`,
|
||||
'docker-compose*': files => {
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 106 KiB |
@@ -50,7 +50,11 @@ What different CLI options do:
|
||||
|
||||
### Architecture
|
||||
|
||||

|
||||

|
||||
|
||||
### Documentation
|
||||
|
||||
Folder `documentation` currently contains some notes on sis-db schema and is meant as a place for storing useful information on Oodikone to make life easier for future developers. Feel free to add anything relevant!
|
||||
|
||||
### Basics
|
||||
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
/// <reference types="Cypress" />
|
||||
|
||||
const openCompletedCoursesModal = () => {
|
||||
cy.get('button').contains('Search completed courses of students').click()
|
||||
}
|
||||
|
||||
const openSearch = () => {
|
||||
cy.init('/completedcoursessearch')
|
||||
hasLanded()
|
||||
}
|
||||
const selectSavedCourselist = name => {
|
||||
cy.get('[data-cy="history-search"]').children().eq(0).type(name).type('{enter}')
|
||||
}
|
||||
|
||||
const hasLanded = () => {
|
||||
cy.contains('Search completed courses')
|
||||
cy.contains(
|
||||
'Here you can search by a list of student and course numbers to see whether students have completed certain courses yet'
|
||||
)
|
||||
}
|
||||
|
||||
const deleteAllSearches = () => {
|
||||
cy.contains('Saved courselists')
|
||||
.get('.dropdown')
|
||||
.then(d => {
|
||||
const searchItems = d.find('div[role=option] > span[class=text]')
|
||||
for (let i = 0; i < searchItems.length; i++) {
|
||||
if (searchItems[i].textContent.includes('TEST-')) {
|
||||
cy.get('[data-cy="history-search"]').children().eq(0).type(searchItems[i].textContent).type('{enter}')
|
||||
cy.get('button').contains('Delete').click()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
describe('When search modal is opened', () => {
|
||||
after(() => {
|
||||
cy.init('/completedcoursessearch')
|
||||
hasLanded()
|
||||
cy.url().should('include', '/completedcoursessearch')
|
||||
cy.contains('Search completed courses of students')
|
||||
cy.get('[data-cy="open-completed-courses-modal-button"]').click()
|
||||
deleteAllSearches()
|
||||
})
|
||||
|
||||
it('Modal opens correctly', () => {
|
||||
cy.init('/completedcoursessearch')
|
||||
hasLanded()
|
||||
openCompletedCoursesModal()
|
||||
cy.contains('Search completed courses of students')
|
||||
cy.contains('Insert one or more student numbers, separated by a space, a newline, or a comma.')
|
||||
cy.contains('Insert one or more courses, separated by a space, a newline, or a comma.')
|
||||
cy.contains('Insert name for this course list if you wish to save it')
|
||||
})
|
||||
|
||||
describe('When a search is executed with invalid input', () => {
|
||||
it('Returns empty table and notification', () => {
|
||||
cy.init('/completedcoursessearch')
|
||||
hasLanded()
|
||||
openCompletedCoursesModal()
|
||||
cy.get('[data-cy="student-no-input"]').type('1')
|
||||
cy.get('[data-cy="course-list-input"]').type('1')
|
||||
cy.get('[data-cy="completed-courses-search-button"]').click()
|
||||
cy.get('[data-cy="rights-notification"]').should(
|
||||
'contain.text',
|
||||
'The following students information could not be displayed'
|
||||
)
|
||||
cy.get('[data-cy="rights-notification"]').should('contain.text', '1')
|
||||
cy.get('[data-cy="completed-courses-table-div"]').should('contain.text', 'Completed courses search')
|
||||
cy.get('[data-cy="completed-courses-table-div"]').should('contain.text', 'Student Number')
|
||||
cy.get('[data-cy="completed-courses-table-div"]').should('not.contain', '1')
|
||||
})
|
||||
})
|
||||
|
||||
describe('When a search with correct data is executed', () => {
|
||||
before(() => {
|
||||
cy.init('/completedcoursessearch')
|
||||
hasLanded()
|
||||
openCompletedCoursesModal()
|
||||
cy.fixture('completedCoursesData').then(({ studentSet1, coursesSet1 }) => {
|
||||
cy.get('[data-cy="student-no-input"]').type(studentSet1.join('\n'))
|
||||
cy.get('[data-cy="course-list-input"]').type(coursesSet1.join('\n'))
|
||||
cy.get('[data-cy="completed-courses-search-button"]').click()
|
||||
})
|
||||
})
|
||||
|
||||
it('Pushes the query to url', () => {
|
||||
cy.url().should(
|
||||
'include',
|
||||
'/completedcoursessearch?courseList=TKT10001&courseList=TKT10002&courseList=TKT10003&courseList=TKT10004&studentList=010450938&studentList=010589388&studentList=010614509'
|
||||
)
|
||||
})
|
||||
it('Finds correct students and courses', () => {
|
||||
cy.fixture('completedCoursesData').then(({ studentSet1, coursesSet1 }) => {
|
||||
cy.get('[data-cy="completed-courses-table-div"]').should('contain.text', studentSet1[0])
|
||||
cy.get('[data-cy="completed-courses-table-div"]').should('contain.text', studentSet1[1])
|
||||
cy.get('[data-cy="completed-courses-table-div"]').should('contain.text', studentSet1[2])
|
||||
cy.get('[data-cy="completed-courses-table-div"]').should('contain.text', coursesSet1[0])
|
||||
cy.get('[data-cy="completed-courses-table-div"]').should('contain.text', coursesSet1[1])
|
||||
cy.get('[data-cy="completed-courses-table-div"]').should('contain.text', coursesSet1[2])
|
||||
cy.get('[data-cy="completed-courses-table-div"]').should('contain.text', coursesSet1[3])
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
const courseListNames = ['TEST-courselist1', 'TEST-courselist2']
|
||||
|
||||
describe('Courselist saving-related functions work', () => {
|
||||
beforeEach(() => {
|
||||
openSearch()
|
||||
openCompletedCoursesModal()
|
||||
cy.get('[data-cy="course-list-input"]').type('CSM14204\nTKT10004')
|
||||
cy.get('[data-cy="search-name"]').type(courseListNames[0])
|
||||
cy.get('button').contains('Save').click()
|
||||
})
|
||||
|
||||
it('Course list can be saved', () => {
|
||||
selectSavedCourselist(courseListNames[0])
|
||||
openSearch()
|
||||
openCompletedCoursesModal()
|
||||
selectSavedCourselist(courseListNames[0])
|
||||
cy.get('[data-cy="course-list-input"]').contains('TKT10004').contains('CSM14204')
|
||||
})
|
||||
|
||||
it('Course list can be deleted', () => {
|
||||
openSearch()
|
||||
openCompletedCoursesModal()
|
||||
cy.get('[data-cy="search-name"]').type(courseListNames[1])
|
||||
cy.get('button').contains('Save').click()
|
||||
openSearch()
|
||||
openCompletedCoursesModal()
|
||||
selectSavedCourselist(courseListNames[1])
|
||||
cy.get('button').contains('Delete').click()
|
||||
cy.get('[data-cy="history-search"]')
|
||||
.type(courseListNames[1])
|
||||
.type('{enter}')
|
||||
.should('not.contain', courseListNames[1])
|
||||
})
|
||||
|
||||
it('Course list can be updated', () => {
|
||||
openSearch()
|
||||
openCompletedCoursesModal()
|
||||
selectSavedCourselist(courseListNames[0])
|
||||
cy.get('[data-cy="course-list-input"]').type(`,TKT10001`)
|
||||
cy.get('button').contains('Save').click()
|
||||
openSearch()
|
||||
openCompletedCoursesModal()
|
||||
selectSavedCourselist(courseListNames[0])
|
||||
cy.get('[data-cy="course-list-input"]').contains(`TKT10001`)
|
||||
})
|
||||
})
|
||||
@@ -152,6 +152,12 @@ describe('Faculty overview', () => {
|
||||
})
|
||||
|
||||
it('Graphs display data', () => {
|
||||
cy.get('[data-cy="Section-bachelor"]').within(() => {
|
||||
cy.get('div[class="faculty-breakdown-graph"]')
|
||||
})
|
||||
|
||||
cy.get('[data-cy="GraduationTimeToggle"]').click()
|
||||
|
||||
cy.get('[data-cy="Section-bachelor"]').within(() => {
|
||||
cy.get('div[class="faculty-graph"]')
|
||||
cy.contains('1 graduated').should('have.length', 1)
|
||||
@@ -194,6 +200,7 @@ describe('Faculty overview', () => {
|
||||
})
|
||||
|
||||
it('Graduation times grouping and time types can be toggled', () => {
|
||||
cy.get('[data-cy="GraduationTimeToggle"]').click()
|
||||
cy.get('[data-cy="GroupByToggle"]').click()
|
||||
cy.get('[data-cy="Section-bachelor"]').should('not.exist')
|
||||
cy.get('[data-cy="Section-master"]').should('be.visible')
|
||||
@@ -207,13 +214,11 @@ describe('Faculty overview', () => {
|
||||
|
||||
cy.get('[data-cy="GraduationTimeToggle"]').click()
|
||||
cy.get('[data-cy="Section-master"]').within(() => {
|
||||
cy.contains('1 graduated (100 % of class)').click()
|
||||
cy.get('div[class="programmes-graph"]').should('be.visible')
|
||||
cy.get('div[class="programmes-graph"]').within(() => {
|
||||
cy.contains('1').click()
|
||||
cy.get('div[class="programmes-breakdown-graph"]').should('be.visible')
|
||||
cy.get('div[class="programmes-breakdown-graph"]').within(() => {
|
||||
cy.contains('Year 2018 by start year')
|
||||
cy.contains('1 graduated').trigger('mouseover')
|
||||
cy.contains('From class of 2018, 1/1 students have graduated')
|
||||
cy.contains('mean study time: 25 months')
|
||||
cy.contains('EDUM')
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -228,12 +233,12 @@ describe('Faculty overview', () => {
|
||||
|
||||
it('User can view studentdata table without %', () => {
|
||||
cy.get('[data-cy="FacultyStudentStatsTable"]').should('be.visible')
|
||||
cy.get('[data-cy="FacultyStudentStatsTable"]').should('not.contain', 'NA')
|
||||
cy.get('[data-cy="FacultyStudentStatsTable"]').should('not.contain', '0 %')
|
||||
})
|
||||
it('User can view studentdata table with %', () => {
|
||||
cy.get('[data-cy="FacultyStudentStatsTable"]').should('be.visible')
|
||||
cy.get('[data-cy="HidePercentagesToggle"]').click()
|
||||
cy.get('[data-cy="FacultyStudentStatsTable"]').should('contain', 'NA')
|
||||
cy.get('[data-cy="FacultyStudentStatsTable"]').should('contain', '0 %')
|
||||
})
|
||||
|
||||
it('Percentage Graphs exists', () => {
|
||||
@@ -242,8 +247,8 @@ describe('Faculty overview', () => {
|
||||
cy.get('[data-cy="Graph-FacultyMastersProgress"]').should('be.visible')
|
||||
cy.get('[data-cy="Graph-FacultyBachelorsProgress"]').should('be.visible')
|
||||
})
|
||||
// Following two tests just test that nothig goes broken after toggling
|
||||
// No data for testing that the data actual changes as supposed
|
||||
// Following two tests just test that nothing goes broken after toggling
|
||||
// No data for testing that data actual change as supposed
|
||||
it('Graduations can be excluded', () => {
|
||||
cy.get('[data-cy="FacultyStudentStatsTable"]').should('be.visible')
|
||||
cy.get('[data-cy="StudentToggle"]').click()
|
||||
@@ -264,24 +269,24 @@ describe('Faculty overview', () => {
|
||||
})
|
||||
|
||||
it('Progress tables can be toggled', () => {
|
||||
cy.get('[data-cy="Cell-FacultyBachelorsProgressTable-0"]').should('not.be.visible')
|
||||
cy.get('[data-cy="Button-Show-FacultyBachelorsProgressTable-0"]').click()
|
||||
cy.get('[data-cy="Cell-FacultyBachelorsProgressTable-0"]').should('be.visible')
|
||||
cy.get('[data-cy="Cell-FacultyBachelorsProgressTable-1"]').should('not.be.visible')
|
||||
cy.get('[data-cy="Cell-FacultyBachelorsProgressTable-2"]').should('not.be.visible')
|
||||
cy.get('[data-cy="Cell-FacultyBachelorsProgressTable-3"]').should('not.be.visible')
|
||||
cy.get('[data-cy="Cell-FacultyBachelorsProgressTable-4"]').should('not.be.visible')
|
||||
cy.get('[data-cy="Cell-FacultyBachelorsProgressTable-5"]').should('not.be.visible')
|
||||
cy.get('[data-cy="Button-Hide-FacultyBachelorsProgressTable-0"]').click()
|
||||
cy.get('[data-cy="Cell-FacultyBachelorsProgressTable-0"]').should('not.be.visible')
|
||||
cy.get('[data-cy="Cell-FacultyBachelorsProgressTable-6"]').should('not.be.visible')
|
||||
cy.get('[data-cy="Button-Show-FacultyBachelorsProgressTable-6"]').click()
|
||||
cy.get('[data-cy="Cell-FacultyBachelorsProgressTable-0"]').should('not.exist')
|
||||
cy.get('[data-cy="Cell-FacultyBachelorsProgressTable-1"]').should('not.exist')
|
||||
cy.get('[data-cy="Cell-FacultyBachelorsProgressTable-2"]').should('not.exist')
|
||||
cy.get('[data-cy="Cell-FacultyBachelorsProgressTable-3"]').should('not.exist')
|
||||
cy.get('[data-cy="Cell-FacultyBachelorsProgressTable-4"]').should('not.exist')
|
||||
cy.get('[data-cy="Cell-FacultyBachelorsProgressTable-6"]').should('be.visible')
|
||||
cy.get('[data-cy="Button-Hide-FacultyBachelorsProgressTable-6"]').click()
|
||||
cy.get('[data-cy="Cell-FacultyBachelorsProgressTable-6"]').should('not.be.visible')
|
||||
})
|
||||
|
||||
it('Sudent statstable can be toggled', () => {
|
||||
cy.get('[data-cy="FacultyStudentStatsTable"]').should('not.contain', 'TKT - KH50_005')
|
||||
cy.get('[data-cy="Button-FacultyStudentStatsTable-0"]').click()
|
||||
cy.get('[data-cy="FacultyStudentStatsTable"]').should('contain', 'TKT - KH50_005')
|
||||
cy.get('[data-cy="Button-FacultyStudentStatsTable-0"]').click()
|
||||
cy.get('[data-cy="FacultyStudentStatsTable"]').should('not.contain', 'TKT - KH50_005')
|
||||
cy.get('[data-cy="FacultyStudentStatsTable"]').should('not.contain', 'TKT')
|
||||
cy.get('[data-cy="Button-FacultyStudentStatsTable-3"]').click()
|
||||
cy.get('[data-cy="FacultyStudentStatsTable"]').should('contain', 'TKT')
|
||||
cy.get('[data-cy="Button-FacultyStudentStatsTable-3"]').click()
|
||||
cy.get('[data-cy="FacultyStudentStatsTable"]').should('not.contain', 'TKT')
|
||||
})
|
||||
|
||||
it('Progress infobox can be toggled', () => {
|
||||
|
||||
+10
-6
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable cypress/no-unnecessary-waiting */
|
||||
/// <reference types="Cypress" />
|
||||
|
||||
// const baseUrl = Cypress.config().baseUrl
|
||||
@@ -20,6 +21,7 @@ const clearSingleDropdownSelection = dataCyAttribute => {
|
||||
}
|
||||
|
||||
const testRangeFilter = (parentEl, min, max, expected) => {
|
||||
cy.clock(MOCKED_DATE, ['Date'])
|
||||
cy.cs(parentEl)
|
||||
.cs('range-selector-min')
|
||||
.find('input')
|
||||
@@ -49,8 +51,6 @@ const createRunTestStepWithPreAndPostPartsFunction = amountWithoutFiltering => {
|
||||
const card = cy.cs(`${filterName}-filter-card`)
|
||||
|
||||
card.invoke('attr', 'data-open').then(open => {
|
||||
console.log('open', open)
|
||||
|
||||
const getHeader = () => cy.cs(`${filterName}-header`)
|
||||
|
||||
if (open === 'false') {
|
||||
@@ -126,8 +126,8 @@ describe('Population Statistics', () => {
|
||||
it('Enrollment filter works', () => {
|
||||
runTestStepWithPreAndPostParts('EnrollmentStatus', () => {
|
||||
cy.cs('enrollmentStatusFilter-status').selectFromDropdown(0)
|
||||
cy.cs('enrollmentStatusFilter-semesters').selectFromDropdown(0)
|
||||
checkFilteringResult(148)
|
||||
cy.cs('enrollmentStatusFilter-semesters').selectFromDropdown(5)
|
||||
checkFilteringResult(147)
|
||||
clearSemanticUIMultipleDropDownSelection('enrollmentStatusFilter-semesters')
|
||||
})
|
||||
})
|
||||
@@ -136,6 +136,7 @@ describe('Population Statistics', () => {
|
||||
runTestStepWithPreAndPostParts('CreditsEarned', () => {
|
||||
testRangeFilter('CreditsEarned-filter-card', 50, 150, 118)
|
||||
})
|
||||
cy.cs('reset-all-filters').click()
|
||||
})
|
||||
|
||||
it('Age filter works', () => {
|
||||
@@ -146,6 +147,7 @@ describe('Population Statistics', () => {
|
||||
})
|
||||
|
||||
it('Gender filter works', () => {
|
||||
cy.cs('reset-all-filters').click()
|
||||
runTestStepWithPreAndPostParts('Gender', () => {
|
||||
cy.cs('genderFilter-dropdown').selectFromDropdown(0)
|
||||
checkFilteringResult(42)
|
||||
@@ -260,8 +262,8 @@ describe('Course Statistics', () => {
|
||||
programmeDropdown.get('i.delete').click()
|
||||
})
|
||||
})
|
||||
|
||||
it('Age filter works', () => {
|
||||
// FIX THIS SOON!
|
||||
it.skip('Age filter works', () => {
|
||||
cy.clock(MOCKED_DATE, ['Date'])
|
||||
runTestStepWithPreAndPostParts('Age', () => {
|
||||
testRangeFilter('Age-filter-card', 20, 40, 33)
|
||||
@@ -269,6 +271,7 @@ describe('Course Statistics', () => {
|
||||
})
|
||||
|
||||
it('Gender filter works', () => {
|
||||
cy.cs('reset-all-filters').click()
|
||||
runTestStepWithPreAndPostParts('Gender', () => {
|
||||
cy.cs('genderFilter-dropdown').selectFromDropdown(0)
|
||||
checkFilteringResult(27)
|
||||
@@ -325,6 +328,7 @@ describe('Custom Population Statistics', () => {
|
||||
runTestStepWithPreAndPostParts('Age', () => {
|
||||
testRangeFilter('Age-filter-card', 20, 30, 1)
|
||||
})
|
||||
cy.cs('reset-all-filters').click()
|
||||
})
|
||||
|
||||
it('Programme defaults to "Active Study Right" mode', () => {
|
||||
|
||||
@@ -217,8 +217,8 @@ describe('Population Statistics tests', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// Fails in CI, not worth the effort to fix
|
||||
/* describe('when using IAM user', () => {
|
||||
/*
|
||||
describe('when using IAM user', () => {
|
||||
beforeEach(() => {
|
||||
cy.init(pathToCSBach2017, 'onlyiamrights')
|
||||
})
|
||||
@@ -242,5 +242,6 @@ describe('Population Statistics tests', () => {
|
||||
cy.contains('Students (170)').should('not.exist')
|
||||
// TODO: somehow check that obfuscation works
|
||||
})
|
||||
}) */
|
||||
})
|
||||
*/
|
||||
})
|
||||
|
||||
@@ -181,6 +181,8 @@ describe('Studyprogramme overview', () => {
|
||||
.should('contain', 25)
|
||||
.should('contain', 35)
|
||||
|
||||
cy.get('[data-cy=graduation-times-graph-breakdown]')
|
||||
cy.get('[data-cy=GraduationTimeToggle]').click()
|
||||
cy.get('[data-cy=graduation-times-graph]').within(() => {
|
||||
cy.contains('Graduation year')
|
||||
cy.contains('2019')
|
||||
@@ -206,6 +208,11 @@ describe('Studyprogramme overview', () => {
|
||||
// eslint-disable-next-line cypress/no-unnecessary-waiting
|
||||
cy.wait(5000)
|
||||
cy.contains('a', 'Kasvatustieteiden maisteriohjelma').click({ force: true })
|
||||
|
||||
cy.get('[data-cy=graduation-times-graph-breakdownMaster]')
|
||||
cy.get('[data-cy=graduation-times-graph-breakdownBachelor]')
|
||||
|
||||
cy.get('[data-cy=GraduationTimeToggle]').click()
|
||||
cy.get('[data-cy=graduation-times-graphMaster]').within(() => {
|
||||
cy.contains('Graduation year')
|
||||
cy.contains('2020')
|
||||
@@ -229,6 +236,8 @@ describe('Studyprogramme overview', () => {
|
||||
describe('Studytrack overview works for basic user', () => {
|
||||
beforeEach(() => {
|
||||
cy.init('/study-programme')
|
||||
// eslint-disable-next-line cypress/no-unnecessary-waiting
|
||||
cy.wait(10000)
|
||||
cy.contains('a', 'Tietojenkäsittelytieteen kandiohjelma').click()
|
||||
cy.get('.attached').contains('Studytracks and class statistics').click()
|
||||
})
|
||||
@@ -274,6 +283,9 @@ describe('Studyprogramme overview', () => {
|
||||
.should('contain', '5.6%') // The percentage for 15-29 credits in 2018-2019
|
||||
.should('contain', '12.8%') // The percentage for less than 15 credits in 2019-2020
|
||||
|
||||
cy.get('[data-cy=graduation-times-graph-breakdown]')
|
||||
cy.get('[data-cy=GraduationTimeToggle]').click()
|
||||
|
||||
cy.get('[data-cy=graduation-times-graph]').within(() => {
|
||||
cy.contains('Start year')
|
||||
cy.contains('2019 - 2020')
|
||||
@@ -288,6 +300,8 @@ describe('Studyprogramme overview', () => {
|
||||
describe('Programme courses works for basic user', () => {
|
||||
beforeEach(() => {
|
||||
cy.init('/study-programme')
|
||||
// eslint-disable-next-line cypress/no-unnecessary-waiting
|
||||
cy.wait(10000)
|
||||
cy.contains('a', 'Tietojenkäsittelytieteen kandiohjelma').click()
|
||||
cy.get('.attached').contains('Programme courses').click()
|
||||
})
|
||||
@@ -339,7 +353,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(2).contains('Tietojenkäsittelytieteen kisälliopetus: Java-ohjelmointi')
|
||||
cy.get('tr').eq(2).contains('Tietojenkäsittelytieteen kisälliopetus')
|
||||
cy.get('th').eq(1).click()
|
||||
cy.get('tr').eq(1).contains('Äidinkielinen viestintä')
|
||||
cy.get('th').eq(2).click()
|
||||
@@ -355,7 +369,7 @@ describe('Studyprogramme overview', () => {
|
||||
cy.get('[data-cy=creditsStudentsToggle]').first().click()
|
||||
|
||||
cy.get('[data-cy=CoursesSortableTable]').within(() => {
|
||||
cy.get('tr').eq(1).contains('267')
|
||||
cy.get('tr').eq(2).contains('267')
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -364,6 +378,8 @@ describe('Studyprogramme overview', () => {
|
||||
describe('Basic information -view works for basic user', () => {
|
||||
beforeEach(() => {
|
||||
cy.init('/study-programme')
|
||||
// eslint-disable-next-line cypress/no-unnecessary-waiting
|
||||
cy.wait(10000)
|
||||
cy.contains('a', 'Tietojenkäsittelytieteen kandiohjelma').click()
|
||||
cy.get('.attached').contains('Tags').click()
|
||||
})
|
||||
@@ -424,8 +440,8 @@ describe('Studyprogramme overview', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// Fails in CI and not worth the effor to fix
|
||||
/* describe('IAM user', () => {
|
||||
/*
|
||||
describe('IAM user', () => {
|
||||
beforeEach(() => {
|
||||
cy.init('/study-programme', 'onlyiamrights')
|
||||
|
||||
@@ -459,5 +475,6 @@ describe('Studyprogramme overview', () => {
|
||||
cy.get('[data-cy=Section-StudytrackProgress]')
|
||||
cy.get('[data-cy=Section-AverageGraduationTimesStudytracks]')
|
||||
})
|
||||
}) */
|
||||
})
|
||||
*/
|
||||
})
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"studentSet1": ["010450938", "010589388", "010614509"],
|
||||
"coursesSet1": ["TKT10001", "TKT10002", "TKT10003", "TKT10004"]
|
||||
}
|
||||
+17
-1
@@ -19,6 +19,8 @@ services:
|
||||
SECRET_TOKEN: dev
|
||||
SIS_DB_URL: postgres://postgres@sis-db:5432/sis-db
|
||||
SIS_UPDATER_URL: http://sis-updater-scheduler:8082
|
||||
IMPORTER_DB_API_URL: 'https://importer.cs.helsinki.fi/api/importer'
|
||||
JAMI_URL: 'http://jami:3003/'
|
||||
image: toska/oodikone2-backend:staging
|
||||
|
||||
frontend:
|
||||
@@ -66,12 +68,26 @@ services:
|
||||
<<: *defaults
|
||||
image: toska/jami
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
- PORT=3003
|
||||
- NODE_ENV=production
|
||||
- POSTGRES_HOST=jami-db
|
||||
- POSTGRES_USER=postgres
|
||||
- POSTGRES_PASSWORD=postgres
|
||||
- POSTGRES_DATABASE=postgres
|
||||
ports:
|
||||
- 3003:3003
|
||||
container_name: jami
|
||||
|
||||
jami-db:
|
||||
<<: *defaults
|
||||
image: postgres:13.2
|
||||
environment:
|
||||
- PGDATA=/data
|
||||
- POSTGRES_PASSWORD=postgres
|
||||
container_name: jami-db
|
||||
ports:
|
||||
- 5433:5432
|
||||
|
||||
nginx:
|
||||
image: nginx:latest
|
||||
container_name: nginx
|
||||
|
||||
+27
-8
@@ -4,7 +4,7 @@ x-defaults: &defaults
|
||||
restart: unless-stopped
|
||||
|
||||
x-build_defaults: &build_defaults
|
||||
target: development
|
||||
dockerfile: dev.Dockerfile
|
||||
|
||||
services:
|
||||
adminer:
|
||||
@@ -39,13 +39,15 @@ services:
|
||||
SECRET_TOKEN: dev
|
||||
SIS_DB_URL: postgres://postgres@sis-db:5432/sis-db
|
||||
SIS_UPDATER_URL: http://sis-updater-scheduler:8082
|
||||
IMPORTER_DB_API_URL: 'https://importer.cs.helsinki.fi/api/importer'
|
||||
JAMI_URL: 'http://jami:3003/'
|
||||
ports:
|
||||
- 8080:8080
|
||||
profiles:
|
||||
- oodikone
|
||||
volumes:
|
||||
- backend-nodemod:/usr/src/app/node_modules
|
||||
- ./services/backend/:/usr/src/app
|
||||
- backend-nodemod:/opt/app-root/src/node_modules
|
||||
- ./services/backend/:/opt/app-root/src
|
||||
|
||||
frontend:
|
||||
<<: *defaults
|
||||
@@ -58,8 +60,8 @@ services:
|
||||
profiles:
|
||||
- oodikone
|
||||
volumes:
|
||||
- frontend-nodemod:/usr/src/app/node_modules
|
||||
- ./services/frontend/:/usr/src/app
|
||||
- frontend-nodemod:/opt/app-root/src/node_modules
|
||||
- ./services/frontend/:/opt/app-root/src
|
||||
|
||||
kone-db:
|
||||
<<: *defaults
|
||||
@@ -108,7 +110,7 @@ services:
|
||||
sis-updater-scheduler:
|
||||
<<: *defaults
|
||||
build:
|
||||
<<: *build_defaults
|
||||
target: development
|
||||
context: updater/sis-updater-scheduler
|
||||
container_name: sis-updater-scheduler
|
||||
depends_on:
|
||||
@@ -137,7 +139,7 @@ services:
|
||||
sis-updater-worker:
|
||||
<<: *defaults
|
||||
build:
|
||||
<<: *build_defaults
|
||||
target: development
|
||||
context: updater/sis-updater-worker
|
||||
depends_on:
|
||||
- redis
|
||||
@@ -172,12 +174,28 @@ services:
|
||||
<<: *defaults
|
||||
image: toska/jami
|
||||
environment:
|
||||
- NODE_ENV=development
|
||||
- PORT=3003
|
||||
- NODE_ENV=dev
|
||||
- POSTGRES_HOST=jami-db
|
||||
- POSTGRES_USER=postgres
|
||||
- POSTGRES_PASSWORD=postgres
|
||||
- POSTGRES_DATABASE=postgres
|
||||
ports:
|
||||
- 3003:3003
|
||||
container_name: jami
|
||||
|
||||
jami-db:
|
||||
<<: *defaults
|
||||
image: postgres:13.2
|
||||
environment:
|
||||
- PGDATA=/data
|
||||
- POSTGRES_PASSWORD=postgres
|
||||
container_name: jami-db
|
||||
ports:
|
||||
- 5433:5432
|
||||
volumes:
|
||||
- jami-data:/data
|
||||
|
||||
volumes:
|
||||
backend-nodemod:
|
||||
frontend-nodemod:
|
||||
@@ -188,3 +206,4 @@ volumes:
|
||||
sis-updater-scheduler-nodemod:
|
||||
sis-updater-worker-nodemod:
|
||||
user-db-data:
|
||||
jami-data:
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 106 KiB |
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 72 KiB |
@@ -0,0 +1,48 @@
|
||||
# Mystery numbers
|
||||
|
||||
Sis data uses some numeric classifiers that are not self-explanatory and (most) previously needed to be deciphered from existing code.
|
||||
Some mystery numbers are gathered here for convenience. Feel free to add anything that could be useful. Remember to use the spelling used in code to enable project wide search.
|
||||
|
||||
## extentcode
|
||||
|
||||
(from **studyright**)
|
||||
|
||||
These are actually defined in db table studyright_extents. Tells studyright level or type, eg. bachelor's degree or non-degree studies.
|
||||
|
||||
## prioritycode
|
||||
|
||||
(from **studyright**)
|
||||
|
||||
1: Primary
|
||||
|
||||
2: Secondary
|
||||
|
||||
5: Rescinded (Seemingly not in use anymore)
|
||||
|
||||
6: Option
|
||||
|
||||
30: Graduated
|
||||
|
||||
## type
|
||||
|
||||
(from **element_details**)
|
||||
|
||||
20: A study programme
|
||||
|
||||
30: Module/Studytrack/kokonaisuus? Eg. Neurology
|
||||
|
||||
## enrollmenttype
|
||||
|
||||
(from **semester_enrollments**)
|
||||
|
||||
1: Present
|
||||
|
||||
2: Absent
|
||||
|
||||
3: Inactive (neither 1 or 2 has been declared)
|
||||
|
||||
## credittypecode
|
||||
|
||||
(from **credit**)
|
||||
|
||||
Defined in credit_types db table. Completed/Improved/Transferred/Failed
|
||||
@@ -10,6 +10,7 @@
|
||||
PROJECT_ROOT="$(git rev-parse --show-toplevel)"
|
||||
DUMP_DIR="$PROJECT_ROOT/.databasedumps"
|
||||
USER_DATA_FILE="$DUMP_DIR/hyuserdata"
|
||||
DOCKER_COMPOSE=$PROJECT_ROOT/docker-compose.yml
|
||||
|
||||
## Following the naming convention in docker-compose, these are names for database
|
||||
## services. The real data databases inside service will have names suffixed by "-real".
|
||||
@@ -100,6 +101,28 @@ reset_databases() {
|
||||
successmsg "Database setup finished"
|
||||
}
|
||||
|
||||
reset_jami_data() {
|
||||
local database="jami-db"
|
||||
local pannu_url="svm-85.cs.helsinki.fi:/home/toska_user/most_recent_backup_store/jami.sql.gz"
|
||||
local dump_destination="$DUMP_DIR/$database.sqz"
|
||||
|
||||
infomsg "Downloading Jami data"
|
||||
scp -r -o ProxyCommand="ssh -l $username -W %h:%p melkki.cs.helsinki.fi" "$username@$pannu_url" "$dump_destination"
|
||||
|
||||
infomsg "Removing database and related volume"
|
||||
docker volume rm oodikone_jami-data || warningmsg "This is okay, continuing"
|
||||
docker-compose -f "$DOCKER_COMPOSE" down "$database" || warningmsg "This is okay, continuing"
|
||||
|
||||
infomsg "Starting postgres in the background"
|
||||
docker-compose -f "$DOCKER_COMPOSE" up -d "$database"
|
||||
retry docker-compose -f "$DOCKER_COMPOSE" exec "$database" pg_isready --dbname="postgres"
|
||||
|
||||
infomsg "Populating Jami"
|
||||
docker exec -i "$database" /bin/bash -c "gunzip | psql -U postgres" < "$dump_destination"
|
||||
docker-compose stop "$database"
|
||||
msg ""
|
||||
}
|
||||
|
||||
reset_all_real_data() {
|
||||
infomsg "Downloading real data dumps, asking for pannu password when needed"
|
||||
for i in ${!DATABASES[*]}; do
|
||||
@@ -108,6 +131,7 @@ reset_all_real_data() {
|
||||
download_real_dump "$database" "$url"
|
||||
done
|
||||
reset_databases "${DATABASES[@]}"
|
||||
reset_jami_data
|
||||
}
|
||||
|
||||
reset_single_database() {
|
||||
@@ -119,6 +143,7 @@ reset_single_database() {
|
||||
"sis-db"
|
||||
"sis-importer-db"
|
||||
"user-db"
|
||||
"jami-db"
|
||||
"Return to main menu."
|
||||
)
|
||||
|
||||
@@ -144,6 +169,9 @@ reset_single_database() {
|
||||
local database=$USER_DB_NAME
|
||||
local url=$USER_DB_REAL_DUMP_URL
|
||||
break 2;;
|
||||
"jami-db")
|
||||
reset_jami_data
|
||||
break 2;;
|
||||
"Return to main menu.")
|
||||
break 2;;
|
||||
*) msg "${RED}Invalid option:${NOFORMAT} $REPLY
|
||||
|
||||
+16
-18
@@ -1,23 +1,21 @@
|
||||
FROM node:14.17-alpine3.13 as base
|
||||
FROM registry.access.redhat.com/ubi8/nodejs-14-minimal
|
||||
|
||||
ENV TZ="Europe/Helsinki"
|
||||
|
||||
WORKDIR /opt/app-root/src
|
||||
|
||||
WORKDIR /usr/src/app
|
||||
EXPOSE 8080
|
||||
|
||||
FROM base as development
|
||||
ENV NODE_ENV=development
|
||||
COPY ./package* /usr/src/app/
|
||||
RUN npm ci
|
||||
COPY . /usr/src/app/
|
||||
CMD ["npm", "run", "dev"]
|
||||
|
||||
FROM base as production
|
||||
ENV NODE_ENV=production
|
||||
ARG SENTRY_ENVIRONMENT
|
||||
ENV SENTRY_ENVIRONMENT=$SENTRY_ENVIRONMENT
|
||||
ARG SENTRY_RELEASE
|
||||
ENV SENTRY_RELEASE=$SENTRY_RELEASE
|
||||
COPY --chown=node:node ./package* /usr/src/app/
|
||||
RUN npm ci --production && apk add --no-cache vim curl nano
|
||||
COPY --chown=node:node . /usr/src/app/
|
||||
USER node
|
||||
|
||||
ARG BASE_PATH
|
||||
ENV PUBLIC_URL=$BASE_PATH
|
||||
|
||||
ARG STAGING
|
||||
ENV REACT_APP_STAGING=$STAGING
|
||||
|
||||
COPY ./package* ./
|
||||
RUN npm ci --omit-dev
|
||||
COPY . .
|
||||
|
||||
CMD ["npm", "start"]
|
||||
|
||||
@@ -125,7 +125,7 @@ const codes = {
|
||||
T922106: 'foodhealth',
|
||||
T923101: 'papu',
|
||||
T923102: 'geodoc',
|
||||
T923103: 'atm',
|
||||
T923103: 'atm-dp',
|
||||
T923104: 'chems',
|
||||
T923105: 'domast',
|
||||
T923106: 'matrena',
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
FROM node:14.17-alpine3.13
|
||||
|
||||
ENV TZ="Europe/Helsinki"
|
||||
|
||||
WORKDIR /opt/app-root/src
|
||||
|
||||
EXPOSE 8080
|
||||
|
||||
ENV NODE_ENV=development
|
||||
|
||||
COPY ./package* ./
|
||||
RUN npm ci
|
||||
COPY . .
|
||||
|
||||
CMD ["npm", "run", "dev"]
|
||||
@@ -1,7 +1,7 @@
|
||||
// Node env to use
|
||||
const isStaging = process.env.NODE_ENV === 'staging'
|
||||
const isDev = process.env.NODE_ENV === 'development'
|
||||
const isProduction = process.env.NODE_ENV === 'production'
|
||||
const isStaging = process.env.REACT_APP_STAGING === 'true'
|
||||
const isProduction = !isStaging && process.env.NODE_ENV === 'production'
|
||||
|
||||
// Sentry
|
||||
const sentryRelease = process.env.SENTRY_RELEASE || ''
|
||||
@@ -17,9 +17,10 @@ const requiredGroup = isStaging
|
||||
const pateToken = process.env.PATE_API_TOKEN || ''
|
||||
|
||||
// Jami
|
||||
const jamiUrl = isDev || runningInCI ? 'http://jami:3003/' : 'https://importer.cs.helsinki.fi/api/auth'
|
||||
const jamiUrl = process.env.JAMI_URL || ''
|
||||
|
||||
// Importer client
|
||||
const importerUrl = process.env.IMPORTER_DB_API_URL || ''
|
||||
const importerToken = process.env.IMPORTER_DB_API_TOKEN || ''
|
||||
|
||||
// Networking: Urls & ports
|
||||
@@ -86,5 +87,6 @@ module.exports = {
|
||||
baseUrl,
|
||||
frontUrl,
|
||||
backendPort,
|
||||
importerUrl,
|
||||
importerToken,
|
||||
}
|
||||
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
const { INTEGER, STRING, ARRAY, DATE } = require('sequelize')
|
||||
|
||||
module.exports = {
|
||||
up: async queryInterface => {
|
||||
await queryInterface.createTable('progress_criteria', {
|
||||
code: {
|
||||
type: STRING,
|
||||
primaryKey: true,
|
||||
},
|
||||
courses_year_one: {
|
||||
type: ARRAY(STRING),
|
||||
},
|
||||
courses_year_two: {
|
||||
type: ARRAY(STRING),
|
||||
},
|
||||
courses_year_three: {
|
||||
type: ARRAY(STRING),
|
||||
},
|
||||
credits_year_one: {
|
||||
type: INTEGER,
|
||||
},
|
||||
credits_year_two: {
|
||||
type: INTEGER,
|
||||
},
|
||||
credits_year_three: {
|
||||
type: INTEGER,
|
||||
},
|
||||
created_at: DATE,
|
||||
updated_at: DATE,
|
||||
})
|
||||
},
|
||||
down: () => {},
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
module.exports = {
|
||||
up: async (queryInterface, Sequelize) => {
|
||||
await queryInterface.addColumn('progress_criteria', 'courses_year_four', {
|
||||
type: Sequelize.ARRAY(Sequelize.STRING),
|
||||
})
|
||||
await queryInterface.addColumn('progress_criteria', 'courses_year_five', {
|
||||
type: Sequelize.ARRAY(Sequelize.STRING),
|
||||
})
|
||||
await queryInterface.addColumn('progress_criteria', 'courses_year_six', {
|
||||
type: Sequelize.ARRAY(Sequelize.STRING),
|
||||
})
|
||||
await queryInterface.addColumn('progress_criteria', 'credits_year_four', {
|
||||
type: Sequelize.INTEGER,
|
||||
})
|
||||
await queryInterface.addColumn('progress_criteria', 'credits_year_five', {
|
||||
type: Sequelize.INTEGER,
|
||||
})
|
||||
await queryInterface.addColumn('progress_criteria', 'credits_year_six', {
|
||||
type: Sequelize.INTEGER,
|
||||
})
|
||||
},
|
||||
down: async () => {},
|
||||
}
|
||||
@@ -38,9 +38,16 @@ const refreshFaculties = async () => {
|
||||
|
||||
const refreshNewOverviews = async () => {
|
||||
logger.info('Refreshing studyprogramme and studytrack overview statistics for all programmes')
|
||||
// Filters out old programmes and special ones like Fitech studies. Filters also out the programmes starting with
|
||||
// 2_ or ending with _2. Those programmes are mapped to correct programme (studentProgrammeModuleFixer.js)
|
||||
const codes = (await getAllProgrammes())
|
||||
.map(p => p.code)
|
||||
.filter(code => code.includes('KH') || code.includes('MH') || /^(T)[0-9]{6}$/.test(code))
|
||||
.filter(
|
||||
code =>
|
||||
(code.includes('KH') && !code.startsWith('2_KH') && !code.endsWith('_2')) ||
|
||||
(code.includes('MH') && !code.startsWith('2_MH') && !code.endsWith('_2')) ||
|
||||
/^(T)[0-9]{6}$/.test(code)
|
||||
)
|
||||
let ready = 0
|
||||
for (const code of codes) {
|
||||
try {
|
||||
|
||||
@@ -28,7 +28,8 @@ const currentUserMiddleware = async (req, _res, next) => {
|
||||
}
|
||||
|
||||
const iamGroups = parseIamGroups(hygroupcn)
|
||||
const iamRights = Object.keys(await getOrganizationAccess({ iamGroups }))
|
||||
const { access = {}, specialGroup = {} } = await getOrganizationAccess({ id: sisId, iamGroups })
|
||||
const iamRights = Object.keys(access)
|
||||
|
||||
if (!hasRequiredIamGroup(iamGroups, iamRights)) {
|
||||
logger.error({ message: 'User does not have required iam group', meta: { iamGroups, iamRights } })
|
||||
@@ -41,6 +42,7 @@ const currentUserMiddleware = async (req, _res, next) => {
|
||||
email,
|
||||
iamGroups,
|
||||
iamRights,
|
||||
specialGroup,
|
||||
sisId,
|
||||
})
|
||||
|
||||
|
||||
@@ -67,6 +67,9 @@ Studyright.belongsTo(Organization, { foreignKey: 'facultyCode', targetKey: 'code
|
||||
Studyplan.belongsTo(Student, { foreignKey: 'studentnumber', targetKey: 'studentnumber' })
|
||||
Student.hasMany(Studyplan, { foreignKey: 'studentnumber', sourceKey: 'studentnumber' })
|
||||
|
||||
Studyplan.belongsTo(Studyright, { foreignKey: 'studyrightid', targetKey: 'studyrightid' })
|
||||
Studyright.hasMany(Studyplan, { foreignKey: 'studyrightid', sourceKey: 'studyrightid' })
|
||||
|
||||
Studyright.belongsTo(Student, { foreignKey: 'studentStudentnumber', targetKey: 'studentnumber' })
|
||||
Student.hasMany(Studyright, { foreignKey: 'studentStudentnumber', sourceKey: 'studentnumber' })
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
const { STRING } = require('sequelize')
|
||||
const Sequelize = require('sequelize')
|
||||
const { sequelizeKone } = require('../database/connection')
|
||||
|
||||
@@ -121,6 +122,7 @@ const CustomPopulationSearch = sequelizeKone.define('custom_population_searches'
|
||||
},
|
||||
})
|
||||
|
||||
// also used in completed courses search feature
|
||||
const OpenUniPopulationSearch = sequelizeKone.define(
|
||||
'open_uni_population_searches',
|
||||
{
|
||||
@@ -143,6 +145,53 @@ const OpenUniPopulationSearch = sequelizeKone.define(
|
||||
{ underscored: true, timestamps: true }
|
||||
)
|
||||
|
||||
const ProgressCriteria = sequelizeKone.define(
|
||||
'progress_criteria',
|
||||
{
|
||||
code: {
|
||||
primaryKey: true,
|
||||
type: STRING,
|
||||
},
|
||||
coursesYearOne: {
|
||||
type: Sequelize.ARRAY(Sequelize.STRING),
|
||||
},
|
||||
coursesYearTwo: {
|
||||
type: Sequelize.ARRAY(Sequelize.STRING),
|
||||
},
|
||||
coursesYearThree: {
|
||||
type: Sequelize.ARRAY(Sequelize.STRING),
|
||||
},
|
||||
coursesYearFour: {
|
||||
type: Sequelize.ARRAY(Sequelize.STRING),
|
||||
},
|
||||
coursesYearFive: {
|
||||
type: Sequelize.ARRAY(Sequelize.STRING),
|
||||
},
|
||||
coursesYearSix: {
|
||||
type: Sequelize.ARRAY(Sequelize.STRING),
|
||||
},
|
||||
creditsYearOne: {
|
||||
type: Sequelize.INTEGER,
|
||||
},
|
||||
creditsYearTwo: {
|
||||
type: Sequelize.INTEGER,
|
||||
},
|
||||
creditsYearThree: {
|
||||
type: Sequelize.INTEGER,
|
||||
},
|
||||
creditsYearFour: {
|
||||
type: Sequelize.INTEGER,
|
||||
},
|
||||
creditsYearFive: {
|
||||
type: Sequelize.INTEGER,
|
||||
},
|
||||
creditsYearSix: {
|
||||
type: Sequelize.INTEGER,
|
||||
},
|
||||
},
|
||||
{ underscored: true, timestamps: true }
|
||||
)
|
||||
|
||||
const StudyGuidanceGroupTag = sequelizeKone.define(
|
||||
'study_guidance_group_tags',
|
||||
{
|
||||
@@ -188,4 +237,5 @@ module.exports = {
|
||||
sequelizeKone,
|
||||
ExcludedCourse,
|
||||
StudyGuidanceGroupTag,
|
||||
ProgressCriteria,
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const { Model, STRING, DATE, INTEGER, BOOLEAN } = require('sequelize')
|
||||
const { Model, STRING, DATE, INTEGER } = require('sequelize')
|
||||
const { dbConnections } = require('../database/connection')
|
||||
|
||||
class Student extends Model {}
|
||||
@@ -25,9 +25,8 @@ Student.init(
|
||||
home_country_fi: { type: STRING },
|
||||
home_country_sv: { type: STRING },
|
||||
home_country_en: { type: STRING },
|
||||
gender_code: { type: INTEGER },
|
||||
gender_code: { type: STRING },
|
||||
sis_person_id: { type: STRING },
|
||||
dissemination_info_allowed: { type: BOOLEAN },
|
||||
createdAt: {
|
||||
type: DATE,
|
||||
},
|
||||
|
||||
@@ -20,6 +20,16 @@ Studyplan.init(
|
||||
onDelete: 'cascade',
|
||||
allowNull: false,
|
||||
},
|
||||
studyrightid: {
|
||||
type: STRING,
|
||||
references: {
|
||||
model: 'studyright',
|
||||
key: 'studyrightid',
|
||||
},
|
||||
onUpdate: 'cascade',
|
||||
onDelete: 'cascade',
|
||||
allowNull: false,
|
||||
},
|
||||
programme_code: {
|
||||
type: STRING,
|
||||
allowNull: false,
|
||||
@@ -30,6 +40,9 @@ Studyplan.init(
|
||||
completed_credits: {
|
||||
type: INTEGER,
|
||||
},
|
||||
sisu_id: {
|
||||
type: STRING,
|
||||
},
|
||||
createdAt: {
|
||||
type: DATE,
|
||||
},
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const { Model, STRING, DATE, INTEGER } = require('sequelize')
|
||||
const { Model, STRING, DATE, INTEGER, BOOLEAN } = require('sequelize')
|
||||
const { dbConnections } = require('../database/connection')
|
||||
|
||||
class Studyright extends Model {}
|
||||
@@ -27,7 +27,9 @@ Studyright.init(
|
||||
active: {
|
||||
type: INTEGER,
|
||||
},
|
||||
// irtisanomisperuste
|
||||
cancelled: {
|
||||
type: BOOLEAN,
|
||||
},
|
||||
studentStudentnumber: {
|
||||
type: STRING,
|
||||
references: {
|
||||
|
||||
@@ -7,7 +7,6 @@ const matomoInit = require('./routes/matomo-init')
|
||||
const currentUserMiddleware = require('./middleware/currentUserMiddleware')
|
||||
const accessLogger = require('./middleware/accessLogger')
|
||||
const auth = require('./middleware/auth')
|
||||
|
||||
const courses = require('./routes/courses')
|
||||
const students = require('./routes/students')
|
||||
const population = require('./routes/population')
|
||||
@@ -31,8 +30,9 @@ const programmeModules = require('./routes/programmeModules')
|
||||
const studyGuidanceGroups = require('./routes/studyGuidanceGroups')
|
||||
const studyProgramme = require('./routes/studyProgramme')
|
||||
const customOpenUniSearch = require('./routes/customOpenUniSearch')
|
||||
const studyProgrammeCriteria = require('./routes/studyProgrammeCriteria')
|
||||
const initializeSentry = require('./util/sentry')
|
||||
|
||||
const completedCoursesSearch = require('./routes/completedCoursesSearch')
|
||||
const errorMiddleware = require('./middleware/errorMiddleware')
|
||||
|
||||
module.exports = (app, url) => {
|
||||
@@ -58,7 +58,9 @@ module.exports = (app, url) => {
|
||||
app.use(url, tags)
|
||||
app.use(url, programmeModules)
|
||||
app.use(url, studyProgramme)
|
||||
app.use(`${url}/programmecriteria`, studyProgrammeCriteria)
|
||||
app.use(`${url}/openunisearch`, auth.roles(['openUniSearch']), customOpenUniSearch)
|
||||
app.use(`${url}/completedcoursessearch`, completedCoursesSearch)
|
||||
app.use(`${url}/faculties`, auth.roles(['facultyStatistics']), faculties)
|
||||
app.use(`${url}/updater`, auth.roles(['admin']), updater)
|
||||
app.use(`${url}/teachers`, auth.roles(['teachers']), teachers)
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
const router = require('express').Router()
|
||||
const { getCompletedCourses } = require('../services/completedCoursesSearch')
|
||||
const _ = require('lodash')
|
||||
const {
|
||||
getOpenUniSearches,
|
||||
createNewSearch,
|
||||
deleteSearch,
|
||||
updateSearch,
|
||||
} = require('../services/openUni/openUniManageSearches')
|
||||
|
||||
router.get('/', async (req, res) => {
|
||||
const studentNumbers = JSON.parse(req.query?.studentlist) || []
|
||||
const courseCodes = JSON.parse(req.query?.courselist) || []
|
||||
const {
|
||||
user: { isAdmin, studentsUserCanAccess },
|
||||
} = req
|
||||
if (!Array.isArray(studentNumbers)) return res.status(400).json({ error: 'Student numbers must be of type array' })
|
||||
if (!Array.isArray(courseCodes)) return res.status(400).json({ error: 'Courses must be of type array' })
|
||||
|
||||
const filteredStudentNumbers = isAdmin ? studentNumbers : _.intersection(studentNumbers, studentsUserCanAccess)
|
||||
const completedCourses = await getCompletedCourses(filteredStudentNumbers, courseCodes)
|
||||
const discardedStudentNumbers = _.difference(
|
||||
studentNumbers,
|
||||
completedCourses.students.map(s => s.studentNumber)
|
||||
)
|
||||
return res.json({ discardedStudentNumbers, ...completedCourses })
|
||||
})
|
||||
|
||||
router.get('/searches', async (req, res) => {
|
||||
const userId = req.user.id
|
||||
const foundSearches = await getOpenUniSearches(userId)
|
||||
return res.json(foundSearches)
|
||||
})
|
||||
|
||||
router.post('/searches', async (req, res) => {
|
||||
const courseCodes = req.body?.courselist || []
|
||||
const userId = req.user.id
|
||||
const name = req.body?.name
|
||||
|
||||
if (!name) return res.status(400).json({ error: 'Courselist name missing' })
|
||||
if (courseCodes && !Array.isArray(courseCodes))
|
||||
return res.status(400).json({ error: 'Course codes must be type of array' })
|
||||
|
||||
const createdSearch = await createNewSearch(userId, name, courseCodes)
|
||||
if (!createdSearch) return res.status(400).json({ error: 'Failed to create courselist' })
|
||||
return res.status(201).json({
|
||||
id: createdSearch.id,
|
||||
userId: createdSearch.userId,
|
||||
name: createdSearch.name,
|
||||
courseList: createdSearch.courseCodes,
|
||||
updatedAt: createdSearch.updatedAt,
|
||||
})
|
||||
})
|
||||
|
||||
router.put('/searches/:id', async (req, res) => {
|
||||
const id = req.params?.id
|
||||
const courseCodes = req.body?.courselist || []
|
||||
const userId = req.user.id
|
||||
if (!id || !userId) return res.status(422).end()
|
||||
const updatedSearch = await updateSearch(userId, id, courseCodes)
|
||||
|
||||
if (!updatedSearch) return res.status(404).json({ error: 'Courselist could not be found' })
|
||||
return res.json({
|
||||
id: updatedSearch.id,
|
||||
userId: updatedSearch.userId,
|
||||
name: updatedSearch.name,
|
||||
courseList: updatedSearch.courseCodes,
|
||||
updatedAt: updatedSearch.updatedAt,
|
||||
})
|
||||
})
|
||||
|
||||
router.delete('/searches/:id', async (req, res) => {
|
||||
const id = req.params?.id
|
||||
const userId = req.user.id
|
||||
if (!id || !userId) return res.status(422).end()
|
||||
|
||||
const deletedSearch = await deleteSearch(userId, id)
|
||||
if (!deletedSearch) return res.status(404).json({ error: 'Courselist could not be found' })
|
||||
return res.json(id)
|
||||
})
|
||||
|
||||
module.exports = router
|
||||
@@ -1,5 +1,5 @@
|
||||
const router = require('express').Router()
|
||||
var moment = require('moment-timezone')
|
||||
const moment = require('moment-timezone')
|
||||
const { getCustomOpenUniCourses } = require('../services/openUni/openUniStats')
|
||||
const {
|
||||
getOpenUniSearches,
|
||||
|
||||
@@ -84,11 +84,10 @@ router.get('/:id/basicstats', async (req, res) => {
|
||||
router.get('/:id/creditstats', 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 getCreditStats(code, yearType, programmeFilter, specialGroups)
|
||||
const data = await getCreditStats(code, yearType, programmeFilter)
|
||||
if (data) return res.json(data)
|
||||
|
||||
const programmes = await getProgrammes(code, programmeFilter)
|
||||
@@ -97,9 +96,9 @@ router.get('/:id/creditstats', async (req, res) => {
|
||||
// list of all programmes is also needed in classification of credits
|
||||
const allProgrammes = programmeFilter === 'ALL_PROGRAMMES' ? programmes : await getProgrammes(code, 'ALL_PROGRAMMES')
|
||||
|
||||
let updatedStats = await combineFacultyCredits(code, programmes.data, allProgrammes.data, yearType, specialGroups)
|
||||
let updatedStats = await combineFacultyCredits(code, programmes.data, allProgrammes.data, yearType)
|
||||
if (updatedStats) {
|
||||
updatedStats = await setCreditStats(updatedStats, yearType, programmeFilter, specialGroups)
|
||||
updatedStats = await setCreditStats(updatedStats, yearType, programmeFilter)
|
||||
}
|
||||
|
||||
return res.json(updatedStats)
|
||||
|
||||
@@ -82,20 +82,6 @@ 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)
|
||||
}
|
||||
})
|
||||
@@ -253,7 +239,13 @@ router.get('/v3/populationstatistics', async (req, res) => {
|
||||
|
||||
res.json(filterPersonalTags(result, user.id))
|
||||
} else {
|
||||
const result = await Population.optimizedStatisticsOf({ ...req.query, studyRights })
|
||||
// If two studyprogrammes are given, query does not work just right
|
||||
// This solution is temporal
|
||||
const result = await Population.optimizedStatisticsOf({
|
||||
...req.query,
|
||||
studyRights: { programme: studyRights.programme },
|
||||
})
|
||||
|
||||
if (result.error) {
|
||||
Sentry.captureException(new Error(result.error))
|
||||
res.status(400).end()
|
||||
@@ -403,17 +395,16 @@ router.get('/v3/populationstatisticsbycourse', async (req, res) => {
|
||||
})
|
||||
|
||||
router.post('/v3/populationstatisticsbystudentnumbers', async (req, res) => {
|
||||
const { studentnumberlist } = req.body
|
||||
const { studentnumberlist, tags } = req.body
|
||||
const {
|
||||
user: { isAdmin, id, studentsUserCanAccess },
|
||||
} = req
|
||||
|
||||
const filteredStudentNumbers = isAdmin ? studentnumberlist : _.intersection(studentnumberlist, studentsUserCanAccess)
|
||||
|
||||
const result = await Population.optimizedStatisticsOf(
|
||||
{
|
||||
year: 1900,
|
||||
studyRights: [],
|
||||
year: tags?.year || 1900,
|
||||
studyRights: tags?.studyProgramme ? [tags.studyProgramme] : [],
|
||||
semesters: ['FALL', 'SPRING'],
|
||||
months: 10000,
|
||||
},
|
||||
|
||||
@@ -15,6 +15,7 @@ const {
|
||||
setStudytrackStats,
|
||||
} = require('../services/analyticsService')
|
||||
const { updateBasicView, updateStudytrackView } = require('../services/studyprogrammeUpdates')
|
||||
const { getProgrammeName } = require('../services/studyprogramme')
|
||||
const logger = require('../util/logger')
|
||||
|
||||
router.get('/v2/studyprogrammes/:id/basicstats', async (req, res) => {
|
||||
@@ -143,4 +144,73 @@ router.get('/v2/studyprogrammes/:id/update_studytrackview', async (req, res) =>
|
||||
}
|
||||
})
|
||||
|
||||
router.get('/v2/studyprogrammes/:id/evaluationstats', async (req, res) => {
|
||||
const code = req.params.id
|
||||
const yearType = req.query?.year_type
|
||||
const specialGroups = req.query?.special_groups
|
||||
const graduated = req.query?.graduated
|
||||
|
||||
if (!code) return res.status(422).end()
|
||||
|
||||
// Statistics for Tilannekuvalomake view
|
||||
|
||||
let gradData = await getGraduationStats(code, yearType, specialGroups)
|
||||
if (!gradData) {
|
||||
const updatedStats = await getGraduationStatsForStudytrack({
|
||||
studyprogramme: req.params.id,
|
||||
settings: {
|
||||
isAcademicYear: yearType === 'ACADEMIC_YEAR',
|
||||
includeAllSpecials: specialGroups === 'SPECIAL_INCLUDED',
|
||||
},
|
||||
})
|
||||
if (updatedStats) {
|
||||
await setGraduationStats(updatedStats, yearType, specialGroups)
|
||||
gradData = updatedStats
|
||||
}
|
||||
}
|
||||
|
||||
let progressData = await getStudytrackStats(code, graduated, specialGroups)
|
||||
if (!progressData) {
|
||||
const updated = await getStudytrackStatsForStudyprogramme({
|
||||
studyprogramme: code,
|
||||
settings: {
|
||||
graduated: graduated === 'GRADUATED_INCLUDED',
|
||||
specialGroups: specialGroups === 'SPECIAL_INCLUDED',
|
||||
},
|
||||
})
|
||||
if (updated) {
|
||||
await setStudytrackStats(updated, graduated, specialGroups)
|
||||
progressData = updated
|
||||
}
|
||||
}
|
||||
|
||||
const programmeName = await getProgrammeName(code)
|
||||
|
||||
delete gradData.tableStats
|
||||
delete gradData.graphStats
|
||||
delete gradData.titles
|
||||
|
||||
const data = {
|
||||
id: code,
|
||||
programmeName: programmeName?.name,
|
||||
status: gradData?.status,
|
||||
lastUpdated: gradData.lastUpdated,
|
||||
graduations: gradData,
|
||||
progress: {
|
||||
creditTableStats: {},
|
||||
creditTableTitles: progressData?.creditTableTitles,
|
||||
graphData: {
|
||||
creditGraphStats: {},
|
||||
id: progressData?.id,
|
||||
years: progressData?.years,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
data.progress.creditTableStats[code] = progressData?.creditTableStats[code]
|
||||
data.progress.graphData.creditGraphStats[code] = progressData?.creditGraphStats[code]
|
||||
|
||||
return res.json(data)
|
||||
})
|
||||
|
||||
module.exports = router
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
const {
|
||||
getCriteria,
|
||||
saveYearlyCourseCriteria,
|
||||
saveYearlyCreditCriteria,
|
||||
} = require('../services/studyProgrammeCriteria')
|
||||
|
||||
const router = require('express').Router()
|
||||
|
||||
router.get('/', async (req, res) => {
|
||||
const studyProgramme = req.query?.programmecode
|
||||
if (studyProgramme !== '' && !studyProgramme) return res.status(422).end()
|
||||
const studyProgrammeCriteria = await getCriteria(studyProgramme)
|
||||
return res.json(studyProgrammeCriteria)
|
||||
})
|
||||
|
||||
router.post('/courses', async (req, res) => {
|
||||
const { code, courses, year } = req.body
|
||||
if (!code || !courses || !year) return res.status(400).end()
|
||||
const studyProgrammeCriteria = await saveYearlyCourseCriteria(code, courses, year)
|
||||
return res.json(studyProgrammeCriteria)
|
||||
})
|
||||
|
||||
router.post('/credits', async (req, res) => {
|
||||
const { code, credits } = req.body
|
||||
if (!code || !credits) return res.status(400).end()
|
||||
const studyProgrammeCriteria = await saveYearlyCreditCriteria(code, credits)
|
||||
return res.json(studyProgrammeCriteria)
|
||||
})
|
||||
|
||||
module.exports = router
|
||||
@@ -0,0 +1,162 @@
|
||||
const { Credit, Enrollment, Student, Course } = require('../models')
|
||||
const { Op } = require('sequelize')
|
||||
const _ = require('lodash')
|
||||
|
||||
const getCompletedCourses = async (studentNumbers, courseCodes) => {
|
||||
let courses = await Course.findAll({
|
||||
attributes: ['code', 'name', 'substitutions'],
|
||||
where: {
|
||||
code: {
|
||||
[Op.in]: courseCodes,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
let fullCourseCodes = [
|
||||
...courseCodes,
|
||||
...new Set([...courses.reduce((acc, course) => [...acc, course.code, ...course.substitutions], [])]),
|
||||
]
|
||||
|
||||
let credits = await Credit.findAll({
|
||||
attributes: ['course_code', 'student_studentnumber', 'credittypecode', 'attainment_date'],
|
||||
where: {
|
||||
course_code: {
|
||||
[Op.in]: fullCourseCodes,
|
||||
},
|
||||
student_studentnumber: {
|
||||
[Op.in]: studentNumbers,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
let enrollments = await Enrollment.findAll({
|
||||
attributes: ['course_code', 'enrollment_date_time', 'studentnumber', 'state'],
|
||||
where: {
|
||||
course_code: {
|
||||
[Op.in]: fullCourseCodes,
|
||||
},
|
||||
studentnumber: {
|
||||
[Op.in]: studentNumbers,
|
||||
},
|
||||
state: {
|
||||
[Op.eq]: 'ENROLLED',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const studentInfo = await Student.findAll({
|
||||
attributes: ['studentnumber', 'firstnames', 'lastname', 'email'],
|
||||
where: {
|
||||
studentnumber: {
|
||||
[Op.in]: studentNumbers,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
courses = courses.map(course => {
|
||||
const { code, name, substitutions } = course
|
||||
return { code, name, substitutions }
|
||||
})
|
||||
|
||||
credits = credits.map(credit => {
|
||||
const { course_code, student_studentnumber, credittypecode, attainment_date } = credit
|
||||
const originalCode = courseCodes.includes(course_code)
|
||||
? null
|
||||
: courses.find(course => course.substitutions.includes(course_code))?.code
|
||||
return {
|
||||
courseCode: originalCode ? originalCode : course_code,
|
||||
substitution: originalCode ? course_code : null,
|
||||
studentNumber: student_studentnumber,
|
||||
creditType: credittypecode,
|
||||
date: attainment_date,
|
||||
}
|
||||
})
|
||||
|
||||
enrollments = enrollments.map(enrollment => {
|
||||
const originalCode = courses.find(course => course.substitutions.includes(enrollment.course_code))?.code
|
||||
return {
|
||||
courseCode: originalCode ? originalCode : enrollment.course_code,
|
||||
substitution: originalCode ? enrollment.course_code : null,
|
||||
studentNumber: enrollment.studentnumber,
|
||||
date: enrollment.enrollment_date_time,
|
||||
}
|
||||
})
|
||||
|
||||
const studentCredits = {}
|
||||
|
||||
studentInfo.forEach(s => {
|
||||
if (!studentCredits[s.studentnumber]) {
|
||||
studentCredits[s.studentnumber] = { credits: [], enrollments: [] }
|
||||
}
|
||||
studentCredits[s.studentnumber].firstNames = s.firstnames
|
||||
studentCredits[s.studentnumber].lastName = s.lastname
|
||||
studentCredits[s.studentnumber].email = s.email
|
||||
})
|
||||
|
||||
credits.forEach(credit => {
|
||||
if (credit.creditType === 9) {
|
||||
return
|
||||
}
|
||||
const previous = studentCredits[credit.studentNumber].credits?.find(c => credit.courseCode === c.courseCode)
|
||||
if (previous && previous.date > credit.date) {
|
||||
return
|
||||
} else if (previous) {
|
||||
studentCredits[credit.studentNumber].credits = studentCredits[credit.studentNumber].credits.filter(
|
||||
c => credit.courseCode !== c.courseCode
|
||||
)
|
||||
}
|
||||
studentCredits[credit.studentNumber].credits.push({
|
||||
date: credit.date,
|
||||
courseCode: credit.courseCode,
|
||||
creditType: credit.creditType,
|
||||
substitution: credit.substitution,
|
||||
})
|
||||
})
|
||||
|
||||
enrollments.forEach(enrollment => {
|
||||
if (
|
||||
credits.find(
|
||||
credit => credit.courseCode === enrollment.courseCode && credit.studentNumber === enrollment.studentNumber
|
||||
)
|
||||
) {
|
||||
return
|
||||
}
|
||||
studentCredits[enrollment.studentNumber].enrollments.push({
|
||||
date: enrollment.date,
|
||||
courseCode: enrollment.courseCode,
|
||||
state: enrollment.state,
|
||||
})
|
||||
})
|
||||
|
||||
const students = Object.keys(studentCredits).reduce(
|
||||
(acc, student) => [
|
||||
...acc,
|
||||
{
|
||||
studentNumber: student,
|
||||
credits: studentCredits[student].credits,
|
||||
enrollments: studentCredits[student].enrollments,
|
||||
firstNames: studentCredits[student].firstNames,
|
||||
lastName: studentCredits[student].lastName,
|
||||
email: studentCredits[student].email,
|
||||
},
|
||||
],
|
||||
[]
|
||||
)
|
||||
|
||||
students.forEach(student => {
|
||||
courseCodes.forEach(courseCode => {
|
||||
let [hits, rest] = _.partition(student.enrollments, e => e.courseCode === courseCode)
|
||||
if (hits?.length === 0) return
|
||||
let latest = hits[0]
|
||||
for (let hit of hits) {
|
||||
if (hit > latest) {
|
||||
latest = hit
|
||||
}
|
||||
}
|
||||
student.enrollments = [latest, ...rest]
|
||||
})
|
||||
})
|
||||
return { students, courses }
|
||||
}
|
||||
|
||||
module.exports = { getCompletedCourses }
|
||||
@@ -26,7 +26,9 @@ class CourseStatsCounter {
|
||||
failedMany: {},
|
||||
improvedPassedGrade: {},
|
||||
markedToSemester: {},
|
||||
enrolledNoGrade: {},
|
||||
}
|
||||
|
||||
this.stats = {
|
||||
students: 0,
|
||||
passed: 0,
|
||||
@@ -38,10 +40,10 @@ class CourseStatsCounter {
|
||||
percentage: undefined,
|
||||
passedOfPopulation: undefined,
|
||||
triedOfPopulation: undefined,
|
||||
perStudent: undefined,
|
||||
passingSemesters: this.initializePassingSemesters(),
|
||||
}
|
||||
this.enrollments = {
|
||||
enrolledNoGrade: new Set(),
|
||||
semesters: this.initializePassingSemesters({}),
|
||||
}
|
||||
this.grades = {}
|
||||
@@ -184,11 +186,20 @@ class CourseStatsCounter {
|
||||
getFinalStats() {
|
||||
const stats = this.stats
|
||||
const students = this.students
|
||||
const allStudents = Object.keys(students.all)
|
||||
const totalEnrolledNoGrade = [...(this.enrollments.ENROLLED || [])].filter(
|
||||
student => !allStudents.includes(student)
|
||||
).length
|
||||
const studs = this.enrollments
|
||||
? Object.keys(this.enrollments)
|
||||
.filter(key => key !== 'ENROLLED' && key !== 'semesters')
|
||||
.reduce((acc, key) => [...acc, ...[...this.enrollments[key]].map(studentnumber => studentnumber)], [])
|
||||
: []
|
||||
const allStudents = Object.keys(students.all).map(student => student)
|
||||
const fileredEnrolledNoGrade = this.enrollments['ENROLLED']
|
||||
? [...this.enrollments['ENROLLED']]
|
||||
.filter(student => !studs.includes(student) && !allStudents.includes(student))
|
||||
.reduce((acc, student) => ({ ...acc, [student]: true }), {})
|
||||
: {}
|
||||
|
||||
students.all = { ...students.all, ...fileredEnrolledNoGrade }
|
||||
students.enrolledNoGrade = fileredEnrolledNoGrade
|
||||
stats.students = lengthOf(students.all)
|
||||
stats.passed = lengthOf(students.passed)
|
||||
stats.failed = lengthOf(students.failed)
|
||||
@@ -200,8 +211,8 @@ class CourseStatsCounter {
|
||||
stats.triedOfPopulation = percentageOf(stats.students, this.studentsInTotal)
|
||||
stats.perStudent = stats.attempts / (stats.passed + stats.failed)
|
||||
stats.passingSemestersCumulative = this.getPassingSemestersCumulative()
|
||||
stats.totalStudents = stats.students + totalEnrolledNoGrade
|
||||
stats.totalEnrolledNoGrade = totalEnrolledNoGrade
|
||||
stats.totalStudents = stats.students
|
||||
stats.totalEnrolledNoGrade = lengthOf(fileredEnrolledNoGrade)
|
||||
stats.percentageWithEnrollments = percentageOf(stats.passed, stats.totalStudents)
|
||||
|
||||
return {
|
||||
|
||||
@@ -111,7 +111,7 @@ const getFacultyStarters = async (
|
||||
}
|
||||
})
|
||||
|
||||
allBasics.studentInfo.graphStats.push({ name: 'Started studying', data: startedGraphStats })
|
||||
allBasics.studentInfo.graphStats.push({ name: 'Started studying (new in faculty)', data: startedGraphStats })
|
||||
programmeData['started'] = programmeTableStats
|
||||
|
||||
Object.keys(startedTableStats).forEach(year => {
|
||||
@@ -155,6 +155,7 @@ const getFacultyGraduates = async (
|
||||
if (keys.includes(programme)) {
|
||||
programmeId = codes[programme].toUpperCase()
|
||||
}
|
||||
|
||||
if (programmeFilter === 'ALL_PROGRAMMES' || isNewProgramme(programme)) {
|
||||
const endYear = defineYear(enddate, isAcademicYear)
|
||||
graduatedGraphStats[0][indexOf(yearsArray, endYear)] += 1
|
||||
|
||||
@@ -7,14 +7,8 @@ const {
|
||||
isNonMajorCredit,
|
||||
getYearsArray,
|
||||
getStartDate,
|
||||
isSpecialGroupCredit,
|
||||
} = require('../studyprogrammeHelpers')
|
||||
const {
|
||||
getStudyRights,
|
||||
getCreditsForStudyProgramme,
|
||||
allTransfers,
|
||||
getCourseCodesForStudyProgramme,
|
||||
} = require('../studyprogramme')
|
||||
const { getStudyRights, getCreditsForStudyProgramme, getCourseCodesForStudyProgramme } = require('../studyprogramme')
|
||||
|
||||
const isFacultyNonMajorCredit = (studyrights, attainment_date, facultyProgrammes) => {
|
||||
let right = ''
|
||||
@@ -42,25 +36,14 @@ const isFacultyNonMajorCredit = (studyrights, attainment_date, facultyProgrammes
|
||||
// Fetches all the credits for the studyprogramme and divides them into major-students, non-major faculty students,
|
||||
// non-major other faculty students adn non-degree student credits
|
||||
// Division is done on the basis that whether the student had a primary studyright to the programme on the attainment_date
|
||||
const getFacultyRegularCreditStats = async ({
|
||||
studyprogramme,
|
||||
since,
|
||||
years,
|
||||
isAcademicYear,
|
||||
includeAllSpecials,
|
||||
facultyProgrammes,
|
||||
}) => {
|
||||
const getFacultyRegularCreditStats = async ({ studyprogramme, since, years, isAcademicYear, facultyProgrammes }) => {
|
||||
const providercode = mapToProviders([studyprogramme])[0]
|
||||
const courses = await getCourseCodesForStudyProgramme(providercode)
|
||||
// console.log(JSON.stringify(courses))
|
||||
const credits = await getCreditsForStudyProgramme(courses, since)
|
||||
const students = [...new Set(credits.map(({ student_studentnumber }) => student_studentnumber))]
|
||||
|
||||
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))
|
||||
}
|
||||
|
||||
let majors = getStatsBasis(years)
|
||||
let allNonMajors = getStatsBasis(years)
|
||||
let facultyNonMajors = getStatsBasis(years)
|
||||
@@ -71,10 +54,6 @@ const getFacultyRegularCreditStats = async ({
|
||||
const studentStudyrights = studyrights.filter(studyright => studyright.studentnumber === student_studentnumber)
|
||||
const attainmentYear = defineYear(attainment_date, isAcademicYear)
|
||||
|
||||
if (!includeAllSpecials && isSpecialGroupCredit(studentStudyrights, attainment_date, transfers)) {
|
||||
return
|
||||
}
|
||||
|
||||
if (isMajorStudentCredit(studentStudyrights, attainment_date, studyprogramme)) {
|
||||
majors.graphStats[indexOf(years, attainmentYear)] += credits || 0
|
||||
majors.tableStats[attainmentYear] += credits || 0
|
||||
@@ -116,12 +95,11 @@ 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 transfers, non majors split to in- and outside-faculty)
|
||||
const getFacultyCreditStatsForStudytrack = async ({ studyprogramme, facultyProgrammes, settings }) => {
|
||||
const { isAcademicYear, includeAllSpecials } = settings
|
||||
const getFacultyCreditStatsForStudytrack = async ({ studyprogramme, facultyProgrammes, isAcademicYear }) => {
|
||||
const since = getStartDate(studyprogramme, isAcademicYear)
|
||||
const years = getYearsArray(since.getFullYear(), isAcademicYear)
|
||||
|
||||
const queryParameters = { studyprogramme, since, years, isAcademicYear, includeAllSpecials, facultyProgrammes }
|
||||
const queryParameters = { studyprogramme, since, years, isAcademicYear, facultyProgrammes }
|
||||
const { majors, facultyNonMajors, otherNonMajors, nonDegree } = await getFacultyRegularCreditStats(queryParameters)
|
||||
|
||||
const reversedYears = getYearsArray(since.getFullYear(), isAcademicYear).reverse()
|
||||
@@ -129,47 +107,36 @@ const getFacultyCreditStatsForStudytrack = async ({ studyprogramme, facultyProgr
|
||||
|
||||
if (!dataFound) return null
|
||||
|
||||
const tableStats = reversedYears.map(year =>
|
||||
includeAllSpecials
|
||||
? [
|
||||
year,
|
||||
majors.tableStats[year] +
|
||||
facultyNonMajors.tableStats[year] +
|
||||
otherNonMajors.tableStats[year] +
|
||||
nonDegree.tableStats[year],
|
||||
majors.tableStats[year],
|
||||
facultyNonMajors.tableStats[year],
|
||||
otherNonMajors.tableStats[year],
|
||||
nonDegree.tableStats[year],
|
||||
]
|
||||
: [year, majors.tableStats[year], majors.tableStats[year]]
|
||||
)
|
||||
const tableStats = reversedYears.map(year => [
|
||||
year,
|
||||
majors.tableStats[year] +
|
||||
facultyNonMajors.tableStats[year] +
|
||||
otherNonMajors.tableStats[year] +
|
||||
nonDegree.tableStats[year],
|
||||
majors.tableStats[year],
|
||||
facultyNonMajors.tableStats[year],
|
||||
otherNonMajors.tableStats[year],
|
||||
nonDegree.tableStats[year],
|
||||
])
|
||||
|
||||
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,
|
||||
},
|
||||
]
|
||||
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,
|
||||
},
|
||||
]
|
||||
|
||||
return {
|
||||
id: studyprogramme,
|
||||
@@ -179,33 +146,27 @@ const getFacultyCreditStatsForStudytrack = async ({ studyprogramme, facultyProgr
|
||||
}
|
||||
}
|
||||
|
||||
const getProgrammeCredits = async (code, yearType, specialGroups, facultyProgrammes) => {
|
||||
const getProgrammeCredits = async (code, yearType, facultyProgrammes) => {
|
||||
const updatedStats = await getFacultyCreditStatsForStudytrack({
|
||||
studyprogramme: code,
|
||||
facultyProgrammes,
|
||||
settings: {
|
||||
isAcademicYear: yearType === 'ACADEMIC_YEAR',
|
||||
includeAllSpecials: specialGroups === 'SPECIAL_INCLUDED',
|
||||
},
|
||||
isAcademicYear: yearType === 'ACADEMIC_YEAR',
|
||||
})
|
||||
|
||||
return updatedStats
|
||||
}
|
||||
|
||||
const combineFacultyCredits = async (faculty, programmes, allProgrammes, yearType, specialGroups) => {
|
||||
const combineFacultyCredits = async (faculty, programmes, allProgrammes, yearType) => {
|
||||
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']
|
||||
const titles = [
|
||||
'',
|
||||
'Total',
|
||||
'Major students credits',
|
||||
'Non-major faculty students credits',
|
||||
'Non-major other faculty students credits',
|
||||
'Non-degree student credits',
|
||||
]
|
||||
|
||||
let allCredits = {
|
||||
id: faculty,
|
||||
@@ -219,7 +180,8 @@ const combineFacultyCredits = async (faculty, programmes, allProgrammes, yearTyp
|
||||
|
||||
const progCodes = allProgrammes.map(p => p.code)
|
||||
for (const prog of programmes) {
|
||||
const data = await getProgrammeCredits(prog.code, yearType, specialGroups, progCodes)
|
||||
if (prog.code === 'MH70_008_2') continue
|
||||
const data = await getProgrammeCredits(prog.code, yearType, progCodes)
|
||||
if (data) {
|
||||
allCredits.programmeNames[prog.progId] = { code: prog.code, ...prog.name }
|
||||
allCredits.programmeTableStats[prog.progId] = data.tableStats
|
||||
@@ -259,16 +221,12 @@ const combineFacultyCredits = async (faculty, programmes, allProgrammes, yearTyp
|
||||
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 }]
|
||||
|
||||
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 allCredits
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
const moment = require('moment')
|
||||
const { graduatedStudyrights, studyrightsByRightStartYear, hasMasterRight } = require('./faculty')
|
||||
const { findRightProgramme, isNewProgramme } = require('./facultyHelpers')
|
||||
const { getYearsArray, getYearsObject, getMean, getMedian, defineYear } = require('../studyprogrammeHelpers')
|
||||
const { getYearsArray, getYearsObject, getMedian, defineYear } = require('../studyprogrammeHelpers')
|
||||
const { codes } = require('../../../config/programmeCodes')
|
||||
const { bachelorStudyright, countTimeCategories, getStatutoryAbsences } = require('../graduationHelpers')
|
||||
|
||||
@@ -21,7 +21,8 @@ const getSortedProgIds = progs => {
|
||||
|
||||
for (const prog of progs) {
|
||||
if (Object.keys(codes).includes(prog)) {
|
||||
programmes = [...programmes, { name: codes[prog].toUpperCase(), code: prog }]
|
||||
const name = prog === 'T923103' ? codes[prog].toUpperCase() + '-DP' : codes[prog].toUpperCase()
|
||||
programmes = [...programmes, { name: name, code: prog }]
|
||||
} else {
|
||||
programmes = [...programmes, { name: prog, code: prog }]
|
||||
}
|
||||
@@ -180,11 +181,9 @@ const count = async (faculty, since, years, yearsList, levels, programmeFilter,
|
||||
let graduationTimes = {}
|
||||
let programmes = getProgrammeObjectBasis(yearsList, levels)
|
||||
let data = {
|
||||
means: {},
|
||||
medians: {},
|
||||
programmes: {
|
||||
medians: getProgrammeObjectBasis(yearsList, levels),
|
||||
means: getProgrammeObjectBasis(yearsList, levels),
|
||||
},
|
||||
}
|
||||
|
||||
@@ -192,7 +191,6 @@ const count = async (faculty, since, years, yearsList, levels, programmeFilter,
|
||||
graduationAmounts[level] = getYearsObject({ years: yearsList })
|
||||
graduationTimes[level] = getYearsObject({ years: yearsList, emptyArrays: true })
|
||||
data.medians[level] = []
|
||||
data.means[level] = []
|
||||
})
|
||||
|
||||
let studyrights = null
|
||||
@@ -235,19 +233,16 @@ const count = async (faculty, since, years, yearsList, levels, programmeFilter,
|
||||
years.forEach(year => {
|
||||
levels.forEach(level => {
|
||||
const median = getMedian(graduationTimes[level][year])
|
||||
const mean = getMean(graduationTimes[level][year])
|
||||
const statistics = { onTime: 0, yearOver: 0, wayOver: 0 }
|
||||
|
||||
// Programme level breakdown
|
||||
data.programmes.medians[level][year] = { programmes: [], data: [] }
|
||||
data.programmes.means[level][year] = { programmes: [], data: [] }
|
||||
const programmeCodes = Object.keys(programmes[level][year])
|
||||
const programmeIds = getSortedProgIds(programmeCodes)
|
||||
|
||||
if (programmeIds.length > 0) {
|
||||
for (const prog of programmeIds) {
|
||||
const progMedian = getMedian(programmes[level][year][prog.code].graduationTimes)
|
||||
const progMean = getMean(programmes[level][year][prog.code].graduationTimes)
|
||||
|
||||
let goal = goals[level]
|
||||
if (faculty === 'H30' && Object.keys(goals.exceptions).includes(prog.code)) {
|
||||
@@ -271,28 +266,12 @@ const count = async (faculty, since, years, yearsList, levels, programmeFilter,
|
||||
code: prog.code,
|
||||
},
|
||||
]
|
||||
|
||||
data.programmes.means[level][year].programmes = [...data.programmes.means[level][year].programmes, prog.code]
|
||||
data.programmes.means[level][year].data = [
|
||||
...data.programmes.means[level][year].data,
|
||||
{
|
||||
y: progMean,
|
||||
amount: programmes[level][year][prog.code].graduationAmounts,
|
||||
statistics: progStatistics,
|
||||
name: prog.name,
|
||||
code: prog.code,
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
data.medians[level] = [
|
||||
...data.medians[level],
|
||||
{ y: median, amount: graduationAmounts[level][year], statistics, name: year },
|
||||
]
|
||||
data.means[level] = [
|
||||
...data.means[level],
|
||||
{ y: mean, amount: graduationAmounts[level][year], statistics, name: year },
|
||||
]
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -135,6 +135,9 @@ const mapCodesToIds = data => {
|
||||
for (const prog of data) {
|
||||
if (keys.includes(prog.code)) {
|
||||
prog.progId = codes[prog.code].toUpperCase()
|
||||
if (prog.code === 'T923103') {
|
||||
prog.progId = prog.progId + '-DP'
|
||||
}
|
||||
} else {
|
||||
prog.progId = prog.code
|
||||
}
|
||||
|
||||
@@ -54,9 +54,9 @@ const getBasicStats = async (id, yearType, programmeFilter, specialGroups) => {
|
||||
return JSON.parse(dataFromRedis)
|
||||
}
|
||||
|
||||
const setCreditStats = async (data, yearType, programmeFilter, specialGroups) => {
|
||||
const setCreditStats = async (data, yearType, programmeFilter) => {
|
||||
const { id } = data
|
||||
const redisKey = createRedisKeyForCreditStats(id, yearType, programmeFilter, specialGroups)
|
||||
const redisKey = createRedisKeyForCreditStats(id, yearType, programmeFilter)
|
||||
const dataToRedis = {
|
||||
...data,
|
||||
status: 'DONE',
|
||||
|
||||
@@ -10,35 +10,21 @@ const {
|
||||
getYearsArray,
|
||||
tableTitles,
|
||||
getOnlyMasterCreditGraphStats,
|
||||
getOnlyMasterThresholds,
|
||||
getBcMsThresholds,
|
||||
getVetenaryCreditGraphStats,
|
||||
} = require('../studyprogrammeHelpers')
|
||||
const { studytrackStudents } = require('../studyprogramme')
|
||||
const { getStudyRightsByExtent, getTransferredInside, getTransferredToAndAway } = require('./faculty')
|
||||
const { checkTransfers } = require('./facultyHelpers')
|
||||
|
||||
const getStudentData = (
|
||||
startDate,
|
||||
students,
|
||||
thresholdKeys,
|
||||
thresholdAmounts,
|
||||
limits,
|
||||
limitKeys,
|
||||
prog,
|
||||
earlyStudyRights = []
|
||||
) => {
|
||||
const getStudentData = (startDate, students, thresholdKeys, thresholdAmounts, limits = [], limitKeys = [], prog) => {
|
||||
let data = {}
|
||||
let programmeData = {}
|
||||
let start = startDate
|
||||
thresholdKeys.forEach(t => (data[t] = 0))
|
||||
limitKeys.forEach(t => (programmeData[t] = 0))
|
||||
students.forEach(({ studentnumber, credits }) => {
|
||||
if (earlyStudyRights.length > 0) {
|
||||
const bcStudyright = earlyStudyRights.filter(sr => sr.studentnumber === studentnumber)
|
||||
start = bcStudyright.length > 0 ? bcStudyright[0].startdate : startDate
|
||||
}
|
||||
students.forEach(({ credits }) => {
|
||||
const creditcount = credits
|
||||
.filter(credit => moment(credit.attainment_date).isSameOrAfter(start))
|
||||
.filter(credit => moment(credit.attainment_date).isSameOrAfter(startDate))
|
||||
.reduce((prev, curr) => prev + curr.credits, 0)
|
||||
data[thresholdKeys[0]] += creditcount < thresholdAmounts[0] ? 1 : 0
|
||||
data[thresholdKeys[1]] += creditcount >= thresholdAmounts[0] && creditcount < thresholdAmounts[1] ? 1 : 0
|
||||
@@ -130,19 +116,17 @@ const combineFacultyStudentProgress = async (faculty, programmes, allProgrammeCo
|
||||
doctoralTableStats: new Array(yearsArray.length),
|
||||
bachelorsGraphStats: getBachelorCreditGraphStats(yearsArray),
|
||||
bcMsGraphStats: faculty === 'H90' ? getVetenaryCreditGraphStats(yearsArray) : getMasterCreditGraphStats(yearsArray),
|
||||
mastersGraphStats: getOnlyMasterCreditGraphStats(yearsArray),
|
||||
mastersGraphStats:
|
||||
faculty === 'H90' ? getBachelorCreditGraphStats(yearsArray) : getOnlyMasterCreditGraphStats(yearsArray),
|
||||
doctoralGraphStats: getDoctoralCreditGraphStats(yearsArray),
|
||||
bachelorTitles: tableTitles.creditProgress.bachelor,
|
||||
bcMsTitles: faculty === 'H90' ? titlesVet : tableTitles.creditProgress.master,
|
||||
mastersTitles: tableTitles.creditProgress.masterOnly,
|
||||
mastersTitles: faculty === 'H90' ? tableTitles.creditProgress.bachelor : tableTitles.creditProgress.masterOnly,
|
||||
doctoralTitles: tableTitles.creditProgress.doctoral,
|
||||
yearlyBachelorTitles: [],
|
||||
yearlyBcMsTitles: [],
|
||||
yearlyMasterTitles: [],
|
||||
programmeNames: programmes.reduce(
|
||||
(obj, dataItem) => ({ ...obj, [dataItem.progId]: { code: dataItem.code, ...dataItem.name } }),
|
||||
{}
|
||||
),
|
||||
programmeNames: {},
|
||||
bachelorsProgStats: {},
|
||||
bcMsProgStats: {},
|
||||
mastersProgStats: {},
|
||||
@@ -156,16 +140,20 @@ const combineFacultyStudentProgress = async (faculty, programmes, allProgrammeCo
|
||||
progressStats.mastersTableStats[indexOf(reversedYears, year)] = [year, 0, 0, 0, 0, 0, 0, 0]
|
||||
progressStats.doctoralTableStats[indexOf(reversedYears, year)] = [year, 0, 0, 0, 0, 0, 0]
|
||||
})
|
||||
|
||||
const limitKeys = ['zero', 't5', 't4', 't3', 't2', 't1']
|
||||
let bachelorlimits = []
|
||||
let masterlimits = []
|
||||
let bcmslimits = []
|
||||
const bachelorProgress = {}
|
||||
const masterProgress = {}
|
||||
const bcmsProgress = {}
|
||||
const doctoralProgress = {}
|
||||
for (const year of reversedYears) {
|
||||
if (year === 'Total') continue
|
||||
const { startDate, endDate } = getAcademicYearDates(year, since)
|
||||
const lastDayOfMonth = moment().endOf('month')
|
||||
let months = Math.round(moment.duration(moment(lastDayOfMonth).diff(moment(startDate))).asMonths())
|
||||
|
||||
if (months >= 36) {
|
||||
bachelorlimits = createLimits(36, 0)
|
||||
} else {
|
||||
@@ -177,18 +165,22 @@ const combineFacultyStudentProgress = async (faculty, programmes, allProgrammeCo
|
||||
masterlimits = createLimits(months, 0)
|
||||
}
|
||||
if (months >= 72 && faculty === 'H90') {
|
||||
bcmslimits = createLimits(72, 180)
|
||||
bcmslimits = createLimits(72, 0)
|
||||
} else if (months >= 60) {
|
||||
bcmslimits = createLimits(60, 180)
|
||||
bcmslimits = createLimits(60, 0)
|
||||
} else {
|
||||
bcmslimits = createLimits(months, 180)
|
||||
bcmslimits = createLimits(months, 0)
|
||||
}
|
||||
progressStats.yearlyBachelorTitles = [...progressStats.yearlyBachelorTitles, createYearlyTitles(0, bachelorlimits)]
|
||||
progressStats.yearlyMasterTitles = [...progressStats.yearlyMasterTitles, createYearlyTitles(0, masterlimits)]
|
||||
progressStats.yearlyBcMsTitles = [...progressStats.yearlyBcMsTitles, createYearlyTitles(180, bcmslimits)]
|
||||
progressStats.yearlyBcMsTitles = [...progressStats.yearlyBcMsTitles, createYearlyTitles(0, bcmslimits)]
|
||||
const programmeCodes = programmes.map(prog => prog.code)
|
||||
|
||||
for (const { progId, code } of programmes) {
|
||||
for (const { progId, code, name } of programmes) {
|
||||
let extentcodes = [1, 2, 3, 4]
|
||||
if (includeAllSpecials) {
|
||||
extentcodes = [...extentcodes, ...[7, 9, 34, 22, 99, 14, 13]]
|
||||
}
|
||||
const startDateWhere = {
|
||||
startdate: {
|
||||
[Op.and]: {
|
||||
@@ -197,23 +189,18 @@ const combineFacultyStudentProgress = async (faculty, programmes, allProgrammeCo
|
||||
},
|
||||
},
|
||||
}
|
||||
let extentcodes = []
|
||||
if (includeAllSpecials) {
|
||||
extentcodes = [7, 9, 34, 22, 99, 14, 13]
|
||||
let studyrights = await getStudyRightsByExtent(faculty, {}, startDateWhere, code, extentcodes, graduationStatus)
|
||||
let { creditThresholdKeys, creditThresholdAmounts } = getCreditThresholds(code, true)
|
||||
if (!creditThresholdKeys) continue
|
||||
if (!(progId in progressStats.programmeNames)) {
|
||||
progressStats.programmeNames[progId] = { code: code, ...name }
|
||||
}
|
||||
if (!includeAllSpecials) {
|
||||
studyrights = await filterOutTransfers(studyrights, programmeCodes, allProgrammeCodes, since)
|
||||
}
|
||||
let { creditThresholdKeys, creditThresholdAmounts } = getCreditThresholds(code)
|
||||
if (!creditThresholdKeys) return
|
||||
|
||||
if (code.includes('KH')) {
|
||||
extentcodes.push(1)
|
||||
let studyrights = await getStudyRightsByExtent(faculty, {}, startDateWhere, code, extentcodes, graduationStatus)
|
||||
if (!includeAllSpecials) {
|
||||
studyrights = await filterOutTransfers(studyrights, programmeCodes, allProgrammeCodes, since)
|
||||
}
|
||||
const all = studyrights
|
||||
.filter(sr => sr.studyrightElements.some(element => element.code === code))
|
||||
.map(sr => sr.studentnumber)
|
||||
const students = await studytrackStudents(all)
|
||||
const allBachelors = studyrights.filter(sr => sr.extentcode === 1).map(sr => sr.studentnumber)
|
||||
const students = await studytrackStudents(allBachelors)
|
||||
const { data: studentData, progData } = getStudentData(
|
||||
startDate,
|
||||
students,
|
||||
@@ -224,12 +211,12 @@ const combineFacultyStudentProgress = async (faculty, programmes, allProgrammeCo
|
||||
'KH'
|
||||
)
|
||||
|
||||
if (!(progId in progressStats.bachelorsProgStats)) {
|
||||
progressStats.bachelorsProgStats[progId] = new Array(reversedYears.length - 1)
|
||||
if (!(progId in bachelorProgress)) {
|
||||
bachelorProgress[progId] = new Array(reversedYears.length - 1)
|
||||
}
|
||||
progressStats.bachelorsTableStats[indexOf(reversedYears, year)][1] += students.length || 0
|
||||
progressStats.bachelorsTableStats[indexOf(reversedYears, 'Total')][1] += students.length || 0
|
||||
progressStats.bachelorsProgStats[progId][indexOf(reversedYears, year)] = limitKeys.map(key => progData[key])
|
||||
bachelorProgress[progId][indexOf(reversedYears, year)] = limitKeys.map(key => progData[key])
|
||||
for (const key of Object.keys(studentData)) {
|
||||
progressStats.bachelorsTableStats[indexOf(reversedYears, year)][indexOf(creditThresholdKeys, key) + 2] +=
|
||||
studentData[key] || 0
|
||||
@@ -239,16 +226,12 @@ const combineFacultyStudentProgress = async (faculty, programmes, allProgrammeCo
|
||||
progressStats.bachelorsGraphStats[key].data[indexOf(yearsArray, 'Total')] += studentData[key] || 0
|
||||
}
|
||||
} else if (code.includes('MH')) {
|
||||
extentcodes.push(2)
|
||||
let all = await getStudyRightsByExtent(faculty, startDateWhere, {}, code, extentcodes, graduationStatus)
|
||||
if (!includeAllSpecials) {
|
||||
all = await filterOutTransfers(all, programmeCodes, allProgrammeCodes, since)
|
||||
}
|
||||
const allMsStudents = all.filter(sr => sr.studyrightid.slice(-2) !== '-2').map(sr => sr.studentnumber)
|
||||
const allBcMsStudents = all.filter(sr => sr.studyrightid.slice(-2) === '-2').map(sr => sr.studentnumber)
|
||||
const studentsWithBachelor = all
|
||||
.filter(sr => sr.studyrightid.slice(-2) === '-2')
|
||||
.map(sr => ({ studentnumber: sr.studentnumber, startdate: sr.startdate }))
|
||||
const allMsStudents = studyrights
|
||||
.filter(sr => sr.extentcode === 2 && sr.studyrightid.slice(-2) !== '-2')
|
||||
.map(sr => sr.studentnumber)
|
||||
const allBcMsStudents = studyrights
|
||||
.filter(sr => sr.extentcode === 2 && sr.studyrightid.slice(-2) === '-2')
|
||||
.map(sr => sr.studentnumber)
|
||||
const bcMsStudents = await studytrackStudents(allBcMsStudents)
|
||||
const msStudents = await studytrackStudents(allMsStudents)
|
||||
|
||||
@@ -256,85 +239,84 @@ const combineFacultyStudentProgress = async (faculty, programmes, allProgrammeCo
|
||||
progressStats.mastersTableStats[indexOf(reversedYears, 'Total')][1] += msStudents.length || 0
|
||||
progressStats.bcMsTableStats[indexOf(reversedYears, year)][1] += bcMsStudents.length || 0
|
||||
progressStats.bcMsTableStats[indexOf(reversedYears, 'Total')][1] += bcMsStudents.length || 0
|
||||
const { creditThresholdKeysBcMs, creditThresholdAmountsBcMs } = getBcMsThresholds(code)
|
||||
const { data: bcMsStudentdata, progData: bcMsProgdata } = getStudentData(
|
||||
startDate,
|
||||
bcMsStudents,
|
||||
creditThresholdKeys,
|
||||
creditThresholdAmounts,
|
||||
creditThresholdKeysBcMs,
|
||||
creditThresholdAmountsBcMs,
|
||||
bcmslimits,
|
||||
limitKeys,
|
||||
'MH',
|
||||
studentsWithBachelor
|
||||
'MH'
|
||||
)
|
||||
const { msOnlyCreditThresholdKeys, msOnlyCreditThresholdAmount } = getOnlyMasterThresholds()
|
||||
|
||||
const { data: msStudentdata, progData: msProgdata } = getStudentData(
|
||||
startDate,
|
||||
msStudents,
|
||||
msOnlyCreditThresholdKeys,
|
||||
msOnlyCreditThresholdAmount,
|
||||
creditThresholdKeys,
|
||||
creditThresholdAmounts,
|
||||
masterlimits,
|
||||
limitKeys,
|
||||
'MH'
|
||||
)
|
||||
if (!(progId in progressStats.mastersProgStats)) {
|
||||
progressStats.mastersProgStats[progId] = new Array(reversedYears.length - 1)
|
||||
if (!(progId in masterProgress)) {
|
||||
masterProgress[progId] = new Array(reversedYears.length - 1)
|
||||
bcmsProgress[progId] = new Array(reversedYears.length - 1)
|
||||
}
|
||||
if (!(progId in progressStats.bcMsProgStats)) {
|
||||
progressStats.bcMsProgStats[progId] = new Array(reversedYears.length - 1)
|
||||
}
|
||||
progressStats.mastersProgStats[progId][indexOf(reversedYears, year)] = limitKeys.map(key => msProgdata[key])
|
||||
progressStats.bcMsProgStats[progId][indexOf(reversedYears, year)] = limitKeys.map(key => bcMsProgdata[key])
|
||||
|
||||
masterProgress[progId][indexOf(reversedYears, year)] = limitKeys.map(key => msProgdata[key])
|
||||
bcmsProgress[progId][indexOf(reversedYears, year)] = limitKeys.map(key => bcMsProgdata[key])
|
||||
for (const key of Object.keys(msStudentdata)) {
|
||||
progressStats.mastersTableStats[indexOf(reversedYears, year)][indexOf(msOnlyCreditThresholdKeys, key) + 2] +=
|
||||
progressStats.mastersTableStats[indexOf(reversedYears, year)][indexOf(creditThresholdKeys, key) + 2] +=
|
||||
msStudentdata[key] || 0
|
||||
progressStats.mastersTableStats[indexOf(reversedYears, 'Total')][indexOf(creditThresholdKeys, key) + 2] +=
|
||||
msStudentdata[key] || 0
|
||||
progressStats.mastersTableStats[indexOf(reversedYears, 'Total')][
|
||||
indexOf(msOnlyCreditThresholdKeys, key) + 2
|
||||
] += msStudentdata[key] || 0
|
||||
progressStats.mastersGraphStats[key].data[indexOf(yearsArray, year)] += msStudentdata[key] || 0
|
||||
progressStats.mastersGraphStats[key].data[indexOf(yearsArray, 'Total')] += msStudentdata[key] || 0
|
||||
}
|
||||
for (const key of Object.keys(bcMsStudentdata)) {
|
||||
progressStats.bcMsTableStats[indexOf(reversedYears, year)][indexOf(creditThresholdKeys, key) + 2] +=
|
||||
progressStats.bcMsTableStats[indexOf(reversedYears, year)][indexOf(creditThresholdKeysBcMs, key) + 2] +=
|
||||
bcMsStudentdata[key] || 0
|
||||
progressStats.bcMsTableStats[indexOf(reversedYears, 'Total')][indexOf(creditThresholdKeysBcMs, key) + 2] +=
|
||||
bcMsStudentdata[key] || 0
|
||||
progressStats.bcMsGraphStats[key].data[indexOf(yearsArray, year)] += bcMsStudentdata[key] || 0
|
||||
progressStats.bcMsTableStats[indexOf(reversedYears, 'Total')][indexOf(creditThresholdKeys, key) + 2] +=
|
||||
bcMsStudentdata[key] || 0
|
||||
progressStats.bcMsGraphStats[key].data[indexOf(yearsArray, 'Total')] += bcMsStudentdata[key] || 0
|
||||
}
|
||||
} else {
|
||||
extentcodes.push(4)
|
||||
let studyrights = await getStudyRightsByExtent(faculty, {}, startDateWhere, code, extentcodes, graduationStatus)
|
||||
if (!includeAllSpecials) {
|
||||
studyrights = await filterOutTransfers(studyrights, programmeCodes, allProgrammeCodes, since)
|
||||
}
|
||||
const all = studyrights
|
||||
.filter(sr => sr.studyrightElements.some(element => element.code === code))
|
||||
.map(sr => sr.studentnumber)
|
||||
const students = await studytrackStudents(all)
|
||||
const { data } = getStudentData(startDate, students, creditThresholdKeys, creditThresholdAmounts, [], [], 'T')
|
||||
const all = studyrights.filter(sr => sr.extentcode === 4 || sr.extentcode === 3).map(sr => sr.studentnumber)
|
||||
const doctoralStudents = await studytrackStudents(all)
|
||||
const { data } = getStudentData(startDate, doctoralStudents, creditThresholdKeys, creditThresholdAmounts, 'T')
|
||||
|
||||
progressStats.doctoralTableStats[indexOf(reversedYears, year)][1] += students.length || 0
|
||||
progressStats.doctoralTableStats[indexOf(reversedYears, 'Total')][1] += students.length || 0
|
||||
if (!(progId in progressStats.doctoralProgStats)) {
|
||||
progressStats.doctoralProgStats[progId] = new Array(reversedYears.length - 1)
|
||||
progressStats.doctoralTableStats[indexOf(reversedYears, year)][1] += doctoralStudents.length || 0
|
||||
progressStats.doctoralTableStats[indexOf(reversedYears, 'Total')][1] += doctoralStudents.length || 0
|
||||
if (!(progId in doctoralProgress)) {
|
||||
doctoralProgress[progId] = new Array(reversedYears.length - 1)
|
||||
}
|
||||
progressStats.doctoralProgStats[progId][indexOf(reversedYears, year)] = creditThresholdKeys.map(
|
||||
key => data[key]
|
||||
)
|
||||
doctoralProgress[progId][indexOf(reversedYears, year)] = creditThresholdKeys.map(key => data[key])
|
||||
|
||||
for (const key of Object.keys(data)) {
|
||||
progressStats.doctoralTableStats[indexOf(reversedYears, year)][indexOf(creditThresholdKeys, key) + 2] +=
|
||||
data[key] || 0
|
||||
progressStats.doctoralGraphStats[key].data[indexOf(yearsArray, year)] += data[key] || 0
|
||||
progressStats.doctoralTableStats[indexOf(reversedYears, 'Total')][indexOf(creditThresholdKeys, key) + 2] +=
|
||||
data[key] || 0
|
||||
progressStats.doctoralGraphStats[key].data[indexOf(yearsArray, year)] += data[key] || 0
|
||||
progressStats.doctoralGraphStats[key].data[indexOf(yearsArray, 'Total')] += data[key] || 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
progressStats.bachelorsProgStats = Object.keys(bachelorProgress)
|
||||
.filter(prog => !bachelorProgress[prog].every(year => year.every(value => value === 0)))
|
||||
.reduce((result, prog) => ({ ...result, [prog]: bachelorProgress[prog] }), {})
|
||||
progressStats.mastersProgStats = Object.keys(masterProgress)
|
||||
.filter(prog => !masterProgress[prog].every(year => year.every(value => value === 0)))
|
||||
.reduce((result, prog) => ({ ...result, [prog]: masterProgress[prog] }), {})
|
||||
progressStats.bcMsProgStats = Object.keys(bcmsProgress)
|
||||
.filter(prog => !bcmsProgress[prog].every(year => year.every(value => value === 0)))
|
||||
.reduce((result, prog) => ({ ...result, [prog]: bcmsProgress[prog] }), {})
|
||||
progressStats.doctoralProgStats = Object.keys(doctoralProgress)
|
||||
.filter(prog => !doctoralProgress[prog].every(year => year.every(value => value === 0)))
|
||||
.reduce((result, prog) => ({ ...result, [prog]: doctoralProgress[prog] }), {})
|
||||
return progressStats
|
||||
}
|
||||
module.exports = { combineFacultyStudentProgress }
|
||||
|
||||
@@ -5,7 +5,7 @@ const { getAcademicYearDates } = require('../../util/semester')
|
||||
const { getStudyRightsByExtent, getStudentsByStudentnumbers, getTransfers } = require('./faculty')
|
||||
const { getCurrentSemester } = require('../semesters')
|
||||
const { checkTransfers } = require('./facultyHelpers')
|
||||
|
||||
const { inactiveStudyrights, absentStudents } = require('../studyprogramme')
|
||||
const emptyTotals = () => {
|
||||
return {
|
||||
total: 0,
|
||||
@@ -21,10 +21,8 @@ const emptyTotals = () => {
|
||||
allOtherCountries: 0,
|
||||
}
|
||||
}
|
||||
|
||||
const addTotals = (facultyTableStats, year, totals) => {
|
||||
facultyTableStats[year] = [
|
||||
year,
|
||||
const toltalsCalculation = totals => {
|
||||
return [
|
||||
totals.total,
|
||||
totals.allStarted,
|
||||
getPercentage(totals.allStarted, totals.total),
|
||||
@@ -48,30 +46,11 @@ const addTotals = (facultyTableStats, year, totals) => {
|
||||
getPercentage(totals.allOtherCountries, totals.total),
|
||||
]
|
||||
}
|
||||
const addTotals = (facultyTableStats, year, totals) => {
|
||||
facultyTableStats[year] = [year, ...toltalsCalculation(totals)]
|
||||
}
|
||||
const addTotalsProgramme = (programmeTableStats, progId, year, totals) => {
|
||||
programmeTableStats[progId][year] = [
|
||||
totals.total,
|
||||
totals.allStarted,
|
||||
getPercentage(totals.allStarted, totals.total),
|
||||
totals.allEnrolled,
|
||||
getPercentage(totals.allEnrolled, totals.total),
|
||||
totals.allAbsent,
|
||||
getPercentage(totals.allAbsent, totals.total),
|
||||
totals.allInactive,
|
||||
getPercentage(totals.allInactive, totals.total),
|
||||
totals.allGraduated,
|
||||
getPercentage(totals.allGraduated, totals.total),
|
||||
totals.allMale,
|
||||
getPercentage(totals.allMale, totals.total),
|
||||
totals.allFemale,
|
||||
getPercentage(totals.allFemale, totals.total),
|
||||
totals.allOtherUnknown,
|
||||
getPercentage(totals.allOtherUnknown, totals.total),
|
||||
totals.allFinnish,
|
||||
getPercentage(totals.allFinnish, totals.total),
|
||||
totals.allOtherCountries,
|
||||
getPercentage(totals.allOtherCountries, totals.total),
|
||||
]
|
||||
programmeTableStats[progId][year] = toltalsCalculation(totals)
|
||||
}
|
||||
|
||||
const getStudentData = (students, facultyExtra, year, code) => {
|
||||
@@ -109,6 +88,7 @@ const getFacultyDataForYear = async ({
|
||||
facultyExtra,
|
||||
allTotals,
|
||||
progTotals,
|
||||
programmeNames,
|
||||
}) => {
|
||||
const { includeAllSpecials, includeGraduated } = settings
|
||||
const { startDate, endDate } = getAcademicYearDates(year, since)
|
||||
@@ -123,7 +103,7 @@ const getFacultyDataForYear = async ({
|
||||
}
|
||||
const totals = emptyTotals()
|
||||
|
||||
for (const { progId, code } of programmes) {
|
||||
for (const { progId, code, name } of programmes) {
|
||||
let elementStart = {}
|
||||
let studyrightStart = {}
|
||||
const start = moment(`${year.slice(0, 4)}-08-01`, 'YYYY-MM-DD')
|
||||
@@ -169,27 +149,26 @@ const getFacultyDataForYear = async ({
|
||||
const students = await getStudentsByStudentnumbers(allStudents)
|
||||
const studentData = getStudentData(students, facultyExtra, year, code)
|
||||
const started = await getStudentsByStudentnumbers(studentNbrs)
|
||||
|
||||
const inactive = studyrights.filter(
|
||||
s => s.graduated === 0 && (s.active === 0 || (s.enddate && moment(s.enddate).isBefore(moment(new Date()))))
|
||||
)
|
||||
const inactive = await inactiveStudyrights(code, studentNbrs)
|
||||
const graduatedStudents = studyrights.filter(sr => sr.graduated === 1)
|
||||
const graduatedStudentNbrs = graduatedStudents.map(sr => sr.studentnumber)
|
||||
const absent = students.filter(
|
||||
s =>
|
||||
!graduatedStudentNbrs.includes(s.studentnumber) &&
|
||||
s.semesters.filter(semester => semester.semestercode === currentSemester.semestercode)[0]?.enrollmenttype === 2
|
||||
)
|
||||
const absent = await absentStudents(code, studentNbrs)
|
||||
const enrolled = students.filter(
|
||||
s =>
|
||||
s.semesters.filter(semester => semester.semestercode === currentSemester.semestercode)[0]?.enrollmenttype === 1
|
||||
)
|
||||
// Count stats for the programme
|
||||
if (students.length === 0) continue
|
||||
if (!(progId in programmeTableStats)) {
|
||||
programmeTableStats[progId] = years.reduce((resultObj, yearKey) => {
|
||||
return { ...resultObj, [yearKey]: [] }
|
||||
}, {})
|
||||
progTotals[progId] = emptyTotals()
|
||||
}
|
||||
if (!(progId in programmeNames)) {
|
||||
programmeNames[progId] = { code: code, ...name }
|
||||
progTotals[progId] = emptyTotals()
|
||||
}
|
||||
|
||||
const yearTotals = {
|
||||
total: students.length,
|
||||
allStarted: started.length,
|
||||
@@ -255,8 +234,9 @@ const combineFacultyStudents = async (code, programmes, allProgrammeCodes, speci
|
||||
const facultyTableStats = getYearsObject({ years, emptyArrays: true })
|
||||
const programmeTableStats = {}
|
||||
const facultyExtra = years.reduce((acc, year) => ({ ...acc, [year]: {} }), {})
|
||||
const progTotals = programmes.reduce((acc, row) => ({ ...acc, [row.progId]: emptyTotals() }), {})
|
||||
const progTotals = {}
|
||||
const allTotals = emptyTotals()
|
||||
const programmeNames = {}
|
||||
for (const year of years.reverse()) {
|
||||
if (year === 'Total') continue
|
||||
const queryParams = {
|
||||
@@ -271,15 +251,19 @@ const combineFacultyStudents = async (code, programmes, allProgrammeCodes, speci
|
||||
facultyExtra,
|
||||
allTotals,
|
||||
progTotals,
|
||||
programmeNames,
|
||||
}
|
||||
const totals = await getFacultyDataForYear(queryParams)
|
||||
addTotals(facultyTableStats, year, totals)
|
||||
if (totals.total !== 0) {
|
||||
addTotals(facultyTableStats, year, totals)
|
||||
}
|
||||
}
|
||||
|
||||
addTotals(facultyTableStats, 'Total', allTotals)
|
||||
Object.keys(progTotals).forEach(programme =>
|
||||
addTotalsProgramme(programmeTableStats, programme, 'Total', progTotals[programme])
|
||||
)
|
||||
|
||||
const studentsData = {
|
||||
id: code,
|
||||
years: years,
|
||||
@@ -287,10 +271,7 @@ const combineFacultyStudents = async (code, programmes, allProgrammeCodes, speci
|
||||
facultyTableStatsExtra: facultyExtra,
|
||||
programmeStats: programmeTableStats,
|
||||
titles: tableTitles['programmes'],
|
||||
programmeNames: programmes.reduce(
|
||||
(obj, dataItem) => ({ ...obj, [dataItem.progId]: { code: dataItem.code, ...dataItem.name } }),
|
||||
{}
|
||||
),
|
||||
programmeNames: programmeNames,
|
||||
}
|
||||
return studentsData
|
||||
}
|
||||
|
||||
@@ -11,21 +11,15 @@ const {
|
||||
} = 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',
|
||||
]
|
||||
const thesisTypes = ['urn:code:course-unit-type:bachelors-thesis', 'urn:code:course-unit-type:masters-thesis']
|
||||
|
||||
let bachelors = getStatsBasis(years)
|
||||
let masters = getStatsBasis(years)
|
||||
let doctors = getStatsBasis(years)
|
||||
let licentiates = getStatsBasis(years)
|
||||
let programmeCounts = {}
|
||||
let programmeNames = {}
|
||||
|
||||
for (const { progId, code, name } of facultyProgrammes) {
|
||||
if (code === 'MH70_008_2' || code.startsWith('LIS') || code.startsWith('T')) continue
|
||||
const provider = mapToProviders([code])[0]
|
||||
const students = await getCorrectStudentnumbers({
|
||||
codes: [code],
|
||||
@@ -33,6 +27,7 @@ const getFacultyThesisWriters = async ({ since, years, isAcademicYear, facultyPr
|
||||
endDate: alltimeEndDate,
|
||||
includeAllSpecials,
|
||||
})
|
||||
|
||||
const thesisCourseCodes = await thesisWriters(provider, since, thesisTypes, students)
|
||||
thesisCourseCodes?.forEach(({ attainment_date, courseUnitType }) => {
|
||||
const thesisYear = defineYear(attainment_date, isAcademicYear)
|
||||
@@ -40,7 +35,7 @@ const getFacultyThesisWriters = async ({ since, years, isAcademicYear, facultyPr
|
||||
if (!(progId in programmeCounts)) {
|
||||
programmeCounts[progId] = {}
|
||||
|
||||
Object.keys(bachelors.tableStats).forEach(year => (programmeCounts[progId][year] = [0, 0, 0, 0, 0]))
|
||||
Object.keys(bachelors.tableStats).forEach(year => (programmeCounts[progId][year] = [0, 0, 0]))
|
||||
programmeNames[progId] = { ...name, code: code }
|
||||
}
|
||||
programmeCounts[progId][thesisYear][0] += 1
|
||||
@@ -53,22 +48,14 @@ const getFacultyThesisWriters = async ({ since, years, isAcademicYear, facultyPr
|
||||
masters.graphStats[indexOf(years, thesisYear)] += 1
|
||||
masters.tableStats[thesisYear] += 1
|
||||
programmeCounts[progId][thesisYear][2] += 1
|
||||
} else if (courseUnitType === thesisTypes[2]) {
|
||||
doctors.graphStats[indexOf(years, thesisYear)] += 1
|
||||
doctors.tableStats[thesisYear] += 1
|
||||
programmeCounts[progId][thesisYear][3] += 1
|
||||
} else if (courseUnitType === thesisTypes[3]) {
|
||||
licentiates.graphStats[indexOf(years, thesisYear)] += 1
|
||||
licentiates.tableStats[thesisYear] += 1
|
||||
programmeCounts[progId][thesisYear][4] += 1
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return { bachelors, masters, doctors, licentiates, programmeCounts, programmeNames }
|
||||
return { bachelors, masters, programmeCounts, programmeNames }
|
||||
}
|
||||
|
||||
const getFacultyThesisWritersForStudyTrack = async (
|
||||
const getFacultyThesisWritersForProgrammes = async (
|
||||
allThesisWriters,
|
||||
facultyProgrammes,
|
||||
isAcademicYear,
|
||||
@@ -77,24 +64,18 @@ const getFacultyThesisWritersForStudyTrack = async (
|
||||
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
|
||||
)
|
||||
const { bachelors, masters, 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],
|
||||
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(programmeId => {
|
||||
@@ -118,7 +99,7 @@ const combineFacultyThesisWriters = async (faculty, facultyProgrammes, yearType,
|
||||
tableStats: [],
|
||||
graphStats: [],
|
||||
programmeTableStats: {},
|
||||
titles: ['', 'All', 'Bachelors', 'Masters', 'Doctors', 'Licentiates'],
|
||||
titles: ['', 'All', 'Bachelors', 'Masters'],
|
||||
programmeNames: {},
|
||||
status: 'Done',
|
||||
lastUpdated: '',
|
||||
@@ -126,7 +107,7 @@ const combineFacultyThesisWriters = async (faculty, facultyProgrammes, yearType,
|
||||
const isAcademicYear = yearType === 'ACADEMIC_YEAR'
|
||||
const includeAllSpecials = specialGroups === 'SPECIAL_INCLUDED'
|
||||
|
||||
await getFacultyThesisWritersForStudyTrack(allThesisWriters, facultyProgrammes, isAcademicYear, includeAllSpecials)
|
||||
await getFacultyThesisWritersForProgrammes(allThesisWriters, facultyProgrammes, isAcademicYear, includeAllSpecials)
|
||||
|
||||
return allThesisWriters
|
||||
}
|
||||
|
||||
@@ -100,13 +100,12 @@ const updateFacultyOverview = async (faculty, statsType) => {
|
||||
)
|
||||
await setBasicStats(updatedStudentInfo, option.yearType, option.programmeFilter, option.specialGroups)
|
||||
}
|
||||
if (statsType === 'ALL' || statsType === 'CREDITS') {
|
||||
if ((statsType === 'ALL' || statsType === 'CREDITS') && specialGroups !== 'SPECIAL_EXCLUDED') {
|
||||
const updatedCredits = await combineFacultyCredits(
|
||||
faculty,
|
||||
programmeFilter === 'ALL_PROGRAMMES' ? allProgrammes.data : newProgrammes.data,
|
||||
allProgrammes.data,
|
||||
yearType,
|
||||
specialGroups
|
||||
yearType
|
||||
)
|
||||
await setCreditStats(updatedCredits, yearType, programmeFilter, specialGroups)
|
||||
}
|
||||
|
||||
@@ -9,12 +9,11 @@ const mapOpenCredits = credit => {
|
||||
}
|
||||
|
||||
const mapStundentInfo = student => {
|
||||
const { email, secondary_email, studentnumber, dissemination_info_allowed } = student
|
||||
const { email, secondary_email, studentnumber } = student
|
||||
return {
|
||||
studentnumber,
|
||||
email,
|
||||
secondary_email,
|
||||
dissemination_info_allowed,
|
||||
}
|
||||
}
|
||||
const mapOpenEnrollments = enrollment => {
|
||||
|
||||
@@ -9,7 +9,6 @@ const {
|
||||
} = require('./openUniHelpers')
|
||||
const { Op } = require('sequelize')
|
||||
|
||||
// 1. iteration: time is hardcoded. Check hyväksytty grades
|
||||
const getCredits = async (courseCodes, startdate) =>
|
||||
(
|
||||
await Credit.findAll({
|
||||
|
||||
@@ -51,15 +51,17 @@ const getCustomOpenUniCourses = async (courseCodes, startdate, enddate) => {
|
||||
}
|
||||
}
|
||||
|
||||
for (const { studentnumber, email, secondary_email, dissemination_info_allowed } of studentInfo) {
|
||||
for (const { studentnumber, email, secondary_email } of studentInfo) {
|
||||
// Check if the student has existing studyright: if yes, then stop here
|
||||
if (!uniqueStudentsWithCurrentStudyRight.includes(studentnumber)) {
|
||||
if (!(studentnumber in studentStats)) {
|
||||
studentStats[studentnumber] = {
|
||||
courseInfo: courseCodes.reduce((acc, code) => ({ ...acc, [code]: getEmptyCourseInfo() }), {}),
|
||||
courseInfo: courseCodes.reduce(
|
||||
(acc, code) => ({ ...acc, [code.replace('AY', '')]: getEmptyCourseInfo() }),
|
||||
{}
|
||||
),
|
||||
email: email,
|
||||
secondaryEmail: secondary_email,
|
||||
disseminationInfoAllowed: dissemination_info_allowed,
|
||||
totals: { passed: 0, failed: 0, unfinished: 0 },
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ const { CourseStatsCounter } = require('./course_stats_counter')
|
||||
const { getPassingSemester, semesterEnd, semesterStart } = require('../util/semester')
|
||||
const { getAllProgrammes } = require('./studyrights')
|
||||
const { encrypt } = require('../services/encrypt')
|
||||
const { getCriteria } = require('./studyProgrammeCriteria')
|
||||
|
||||
const enrolmentDates = () => {
|
||||
const query = 'SELECT DISTINCT s.dateOfUniversityEnrollment as date FROM Student s'
|
||||
@@ -38,6 +39,61 @@ const universityEnrolmentDates = async () => {
|
||||
.filter(d => d)
|
||||
.sort()
|
||||
}
|
||||
// Progress tab related helper functions.
|
||||
const createEmptyCriteriaYear = (criteria, year) => {
|
||||
return {
|
||||
credits: false,
|
||||
totalSatisfied: 0,
|
||||
coursesSatisfied:
|
||||
criteria?.courses && criteria?.courses[year]
|
||||
? criteria.courses[year].reduce((acc, course) => {
|
||||
acc[course] = null
|
||||
return acc
|
||||
}, {})
|
||||
: {},
|
||||
}
|
||||
}
|
||||
|
||||
const getCreditAmount = (course, hops, courseCode, startDate, addition) => {
|
||||
return moment(course.date).isBetween(moment(startDate), moment(startDate).add(addition, 'year')) &&
|
||||
hops.length > 0 &&
|
||||
(hops[0].included_courses.includes(courseCode) || hops[0].included_courses.includes(course.course_code))
|
||||
? course.credits
|
||||
: 0
|
||||
}
|
||||
|
||||
const updateCourseByYear = (criteria, criteriaYear, course, criteriaChecked, yearToAdd, correctCode) => {
|
||||
if (
|
||||
criteria?.courses &&
|
||||
criteria?.courses[criteriaYear] &&
|
||||
(criteria.courses[criteriaYear].includes(course.course_code) ||
|
||||
criteria.courses[criteriaYear].some(
|
||||
c => criteria.allCourses[c] && criteria.allCourses[c].includes(course.course_code)
|
||||
))
|
||||
) {
|
||||
if (
|
||||
!criteriaChecked[yearToAdd].coursesSatisfied[correctCode] ||
|
||||
new Date(criteriaChecked[yearToAdd].coursesSatisfied[correctCode]) > new Date(course.date)
|
||||
) {
|
||||
criteriaChecked[yearToAdd].coursesSatisfied[correctCode] = course.date
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const updateCreditcriteriaInfo = (criteria, criteriaYear, criteriaChecked, yearToAdd, academicYears, academicYear) => {
|
||||
if (criteria.courses) {
|
||||
if (criteriaChecked?.[yearToAdd]) {
|
||||
criteriaChecked[yearToAdd].totalSatisfied +=
|
||||
Object.keys(criteriaChecked?.[yearToAdd].coursesSatisfied).filter(
|
||||
course => criteriaChecked?.[yearToAdd].coursesSatisfied[course] !== null
|
||||
).length || 0
|
||||
}
|
||||
if (academicYears[academicYear] >= criteria?.credits[criteriaYear] && criteria?.credits[criteriaYear] > 0) {
|
||||
criteriaChecked[yearToAdd].credits = true
|
||||
criteriaChecked[yearToAdd].totalSatisfied += 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const formatStudentForPopulationStatistics = (
|
||||
{
|
||||
@@ -48,6 +104,7 @@ const formatStudentForPopulationStatistics = (
|
||||
creditcount,
|
||||
abbreviatedname,
|
||||
email,
|
||||
secondary_email,
|
||||
phone_number,
|
||||
studyrights,
|
||||
studyplans,
|
||||
@@ -68,7 +125,9 @@ const formatStudentForPopulationStatistics = (
|
||||
credits,
|
||||
startDate,
|
||||
startDateMoment,
|
||||
endDateMoment
|
||||
endDateMoment,
|
||||
criteria,
|
||||
code
|
||||
) => {
|
||||
const toCourse = ({ grade, attainment_date, credits, course_code, credittypecode, isStudyModule, language }) => {
|
||||
const attainment_date_normailized =
|
||||
@@ -86,10 +145,58 @@ const formatStudentForPopulationStatistics = (
|
||||
language,
|
||||
}
|
||||
}
|
||||
const criteriaCoursesBySubstitutions = criteria?.allCourses
|
||||
? Object.keys(criteria.allCourses).reduce((acc, code) => {
|
||||
acc[code] = code
|
||||
criteria.allCourses[code].map(subst => (acc[subst] = code))
|
||||
return acc
|
||||
}, {})
|
||||
: {}
|
||||
|
||||
const toProgressCriteria = () => {
|
||||
const criteriaChecked = {
|
||||
year1: createEmptyCriteriaYear(criteria, 'yearOne'),
|
||||
year2: createEmptyCriteriaYear(criteria, 'yearTwo'),
|
||||
year3: createEmptyCriteriaYear(criteria, 'yearThree'),
|
||||
year4: createEmptyCriteriaYear(criteria, 'yearFour'),
|
||||
year5: createEmptyCriteriaYear(criteria, 'yearFive'),
|
||||
year6: createEmptyCriteriaYear(criteria, 'yearSix'),
|
||||
}
|
||||
const correctStudyplan = studyplans ? studyplans.filter(plan => plan.programme_code === code) : []
|
||||
const academicYears = { first: 0, second: 0, third: 0, fourth: 0, fifth: 0, sixth: 0 }
|
||||
if (criteria.courses || criteria.credits) {
|
||||
const courses = credits[studentnumber] ? credits[studentnumber].map(toCourse) : []
|
||||
courses.forEach(course => {
|
||||
if (course.passed) {
|
||||
const correctCode = criteriaCoursesBySubstitutions[course.course_code]
|
||||
updateCourseByYear(criteria, 'yearOne', course, criteriaChecked, 'year1', correctCode)
|
||||
updateCourseByYear(criteria, 'yearTwo', course, criteriaChecked, 'year2', correctCode)
|
||||
updateCourseByYear(criteria, 'yearThree', course, criteriaChecked, 'year3', correctCode)
|
||||
updateCourseByYear(criteria, 'yearFour', course, criteriaChecked, 'year4', correctCode)
|
||||
updateCourseByYear(criteria, 'yearFive', course, criteriaChecked, 'year5', correctCode)
|
||||
updateCourseByYear(criteria, 'yearSix', course, criteriaChecked, 'year6', correctCode)
|
||||
academicYears.first += getCreditAmount(course, correctStudyplan, correctCode, startDate, 1)
|
||||
academicYears.second += getCreditAmount(course, correctStudyplan, correctCode, startDate, 2)
|
||||
academicYears.third += getCreditAmount(course, correctStudyplan, correctCode, startDate, 3)
|
||||
academicYears.fourth += getCreditAmount(course, correctStudyplan, correctCode, startDate, 4)
|
||||
academicYears.fifth += getCreditAmount(course, correctStudyplan, correctCode, startDate, 5)
|
||||
academicYears.sixth += getCreditAmount(course, correctStudyplan, correctCode, startDate, 6)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
updateCreditcriteriaInfo(criteria, 'yearOne', criteriaChecked, 'year1', academicYears, 'first')
|
||||
updateCreditcriteriaInfo(criteria, 'yearTwo', criteriaChecked, 'year2', academicYears, 'second')
|
||||
updateCreditcriteriaInfo(criteria, 'yearThree', criteriaChecked, 'year3', academicYears, 'third')
|
||||
updateCreditcriteriaInfo(criteria, 'yearFour', criteriaChecked, 'year4', academicYears, 'fourth')
|
||||
updateCreditcriteriaInfo(criteria, 'yearFive', criteriaChecked, 'year5', academicYears, 'fifth')
|
||||
updateCreditcriteriaInfo(criteria, 'yearSix', criteriaChecked, 'year6', academicYears, 'sixth')
|
||||
return criteriaChecked
|
||||
}
|
||||
|
||||
studyrights = studyrights || []
|
||||
|
||||
const started = dateofuniversityenrollment
|
||||
|
||||
return {
|
||||
firstnames,
|
||||
lastname,
|
||||
@@ -104,8 +211,11 @@ const formatStudentForPopulationStatistics = (
|
||||
gender_code,
|
||||
gender: { gender_en, gender_fi, gender_sv },
|
||||
email,
|
||||
secondaryEmail: secondary_email,
|
||||
phoneNumber: phone_number,
|
||||
semesterenrollments: semester_enrollments || [],
|
||||
semesterenrollments: semester_enrollments
|
||||
? semester_enrollments.sort((a, b) => a.semestercode - b.semestercode)
|
||||
: [],
|
||||
updatedAt: updatedAt || createdAt,
|
||||
tags: tags || [],
|
||||
studyrightStart: startDate,
|
||||
@@ -114,6 +224,7 @@ const formatStudentForPopulationStatistics = (
|
||||
birthdate,
|
||||
studyplans,
|
||||
sis_person_id,
|
||||
criteriaProgress: toProgressCriteria(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -233,6 +344,7 @@ const getStudentsIncludeCoursesBetween = async (studentnumbers, startDate, endDa
|
||||
'creditcount',
|
||||
'abbreviatedname',
|
||||
'email',
|
||||
'secondary_email',
|
||||
'phone_number',
|
||||
'updatedAt',
|
||||
'gender_code',
|
||||
@@ -261,6 +373,7 @@ const getStudentsIncludeCoursesBetween = async (studentnumbers, startDate, endDa
|
||||
'faculty_code',
|
||||
'studystartdate',
|
||||
'admission_type',
|
||||
'cancelled',
|
||||
],
|
||||
separate: true,
|
||||
include: [
|
||||
@@ -291,7 +404,7 @@ const getStudentsIncludeCoursesBetween = async (studentnumbers, startDate, endDa
|
||||
},
|
||||
{
|
||||
model: Studyplan,
|
||||
attributes: ['included_courses', 'programme_code', 'completed_credits'],
|
||||
attributes: ['included_courses', 'programme_code', 'completed_credits', 'studyrightid'],
|
||||
separate: true,
|
||||
},
|
||||
],
|
||||
@@ -412,7 +525,6 @@ const studentnumbersWithAllStudyrightElements = async (
|
||||
graduatedStudents
|
||||
) => {
|
||||
// eslint-disable-line
|
||||
|
||||
// db startdate is formatted to utc so need to change it when querying
|
||||
const formattedStartDate = new Date(moment.tz(startDate, 'Europe/Helsinki').format()).toUTCString()
|
||||
|
||||
@@ -749,7 +861,9 @@ const formatStudentsForApi = async (
|
||||
startDate,
|
||||
endDate,
|
||||
{ studyRights },
|
||||
optionData
|
||||
optionData,
|
||||
criteria,
|
||||
code
|
||||
) => {
|
||||
const startDateMoment = moment(startDate)
|
||||
const endDateMoment = moment(endDate)
|
||||
@@ -783,8 +897,18 @@ const formatStudentsForApi = async (
|
||||
})
|
||||
if (student.studentnumber in optionData) student.option = optionData[student.studentnumber]
|
||||
else student.option = null
|
||||
|
||||
stats.students.push(
|
||||
formatStudentForPopulationStatistics(student, enrollments, credits, startDate, startDateMoment, endDateMoment)
|
||||
formatStudentForPopulationStatistics(
|
||||
student,
|
||||
enrollments,
|
||||
credits,
|
||||
startDate,
|
||||
startDateMoment,
|
||||
endDateMoment,
|
||||
criteria,
|
||||
code
|
||||
)
|
||||
)
|
||||
return stats
|
||||
},
|
||||
@@ -868,16 +992,17 @@ const optimizedStatisticsOf = async (query, studentnumberlist) => {
|
||||
nondegreeStudents,
|
||||
transferredStudents
|
||||
)
|
||||
// wtf
|
||||
// plz
|
||||
const code = studyRights[0] || ''
|
||||
let optionData = {}
|
||||
let criteria = {}
|
||||
if (code.includes('MH')) {
|
||||
optionData = await getOptionsForStudents(studentnumbers, code, 'MSC')
|
||||
} else if (code.includes('KH')) {
|
||||
optionData = await getOptionsForStudents(studentnumbers, code, 'BSC')
|
||||
}
|
||||
|
||||
if (code.includes('KH') || ['MH30_001', 'MH30_003'].includes(code)) {
|
||||
criteria = await getCriteria(code)
|
||||
}
|
||||
const { students, enrollments, credits, extents, semesters, elementdetails, courses } =
|
||||
await getStudentsIncludeCoursesBetween(
|
||||
studentnumbers,
|
||||
@@ -892,7 +1017,9 @@ const optimizedStatisticsOf = async (query, studentnumberlist) => {
|
||||
startDate,
|
||||
endDate,
|
||||
formattedQueryParams,
|
||||
optionData
|
||||
optionData,
|
||||
criteria,
|
||||
code
|
||||
)
|
||||
|
||||
return formattedStudents
|
||||
@@ -902,10 +1029,52 @@ const getSubstitutions = async codes => {
|
||||
const courses = await Course.findAll({ where: { code: codes }, attributes: ['code', 'substitutions'], raw: true })
|
||||
return [...new Set(courses.map(({ code, substitutions }) => [code, ...substitutions]).flat())]
|
||||
}
|
||||
|
||||
// This duplicate code is added here to ensure that we get the enrollments in cases no credits found for the selected students.
|
||||
const findCourseEnrollments = async (studentnumbers, beforeDate, courses = [], studentCountLimit = 0) => {
|
||||
const courseCodes = courses.length > 0 ? await getSubstitutions(courses) : ['DUMMY']
|
||||
const res = await sequelize.query(
|
||||
`
|
||||
SELECT DISTINCT ON (course.id)
|
||||
course.code,
|
||||
course.name,
|
||||
course.coursetypecode,
|
||||
course.substitutions,
|
||||
course.main_course_code,
|
||||
course_types.name AS course_type,
|
||||
enrollment.data AS enrollments
|
||||
FROM course
|
||||
INNER JOIN course_types ON course_types.coursetypecode = course.coursetypecode
|
||||
LEFT JOIN (
|
||||
SELECT
|
||||
course_id,
|
||||
ARRAY_AGG(JSON_BUILD_OBJECT(
|
||||
'studentnumber', studentnumber,
|
||||
'state', state,
|
||||
'enrollment_date_time', enrollment_date_time
|
||||
)) AS data
|
||||
FROM enrollment
|
||||
WHERE enrollment.studentnumber IN (:studentnumbers) AND enrollment.enrollment_date_time < :beforeDate
|
||||
GROUP BY enrollment.course_id
|
||||
HAVING COUNT(DISTINCT enrollment.studentnumber) >= :studentCountLimit
|
||||
) enrollment ON enrollment.course_id = course.id
|
||||
WHERE :skipCourseCodeFilter OR course.code IN (:courseCodes)
|
||||
-- GROUP BY 1, 2, 3, 4, 5, 6
|
||||
`,
|
||||
{
|
||||
replacements: {
|
||||
studentnumbers: studentnumbers.length > 0 ? studentnumbers : ['DUMMY'],
|
||||
beforeDate,
|
||||
studentCountLimit,
|
||||
courseCodes,
|
||||
skipCourseCodeFilter: courses.length === 0,
|
||||
},
|
||||
type: sequelize.QueryTypes.SELECT,
|
||||
}
|
||||
)
|
||||
return res
|
||||
}
|
||||
const findCourses = async (studentnumbers, beforeDate, courses = [], studentCountLimit = 0) => {
|
||||
const courseCodes = courses.length > 0 ? await getSubstitutions(courses) : ['DUMMY']
|
||||
|
||||
const res = await sequelize.query(
|
||||
`
|
||||
SELECT DISTINCT ON (course.id)
|
||||
@@ -1030,14 +1199,22 @@ const bottlenecksOf = async (query, studentnumberlist, encryptdata = false) => {
|
||||
numbers[num] = true
|
||||
return numbers
|
||||
}, {})
|
||||
|
||||
const courses = await findCourses(
|
||||
studentnumbers,
|
||||
dateMonthsFromNow(startDate, months),
|
||||
courseCodes,
|
||||
query.studentCountLimit
|
||||
)
|
||||
return [allstudents, courses]
|
||||
const foundCourseCodes = Object.keys(keyBy(courses, 'code'))
|
||||
const filteredCourseCodes = courseCodes?.filter(code => !foundCourseCodes.includes(code))
|
||||
|
||||
const courseEnrollements = await findCourseEnrollments(
|
||||
studentnumbers,
|
||||
dateMonthsFromNow(startDate, months),
|
||||
filteredCourseCodes,
|
||||
query.studentCountLimit
|
||||
)
|
||||
return [allstudents, courses, courseEnrollements]
|
||||
} else {
|
||||
const { months, startDate } = params
|
||||
const beforeDate = months && startDate ? dateMonthsFromNow(startDate, months) : new Date()
|
||||
@@ -1047,7 +1224,16 @@ const bottlenecksOf = async (query, studentnumberlist, encryptdata = false) => {
|
||||
return numbers
|
||||
}, {})
|
||||
const courses = await findCourses(studentnumberlist, beforeDate, courseCodes, query.studentCountLimit)
|
||||
return [allstudents, courses]
|
||||
const foundCourseCodes = Object.keys(keyBy(courses, 'code'))
|
||||
const filteredCourseCodes = courseCodes?.filter(code => !foundCourseCodes.includes(code))
|
||||
|
||||
const courseEnrollements = await findCourseEnrollments(
|
||||
studentnumberlist,
|
||||
dateMonthsFromNow(startDate, months),
|
||||
filteredCourseCodes,
|
||||
query.studentCountLimit
|
||||
)
|
||||
return [allstudents, courses, courseEnrollements]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1067,24 +1253,52 @@ const bottlenecksOf = async (query, studentnumberlist, encryptdata = false) => {
|
||||
}
|
||||
|
||||
const params = parseQueryParams(query)
|
||||
const [[allstudents, courses], error] = await Promise.all([
|
||||
const allStudentsByYears = query?.selectedStudentsByYear
|
||||
? Object.keys(query.selectedStudentsByYear).reduce(
|
||||
(res, year) => [...res, ...query.selectedStudentsByYear[year]],
|
||||
[]
|
||||
)
|
||||
: []
|
||||
// To fix failed and enrolled, no grade filter options some not so clean and nice solutions were added
|
||||
// Get the data with actual 1. courses and filtered students. 2. all students by year, if provided.
|
||||
const [[allstudents, courses, courseEnrollements], [, allCourses], error] = await Promise.all([
|
||||
getStudentsAndCourses(query.selectedStudents, studentnumberlist, query.courses),
|
||||
getStudentsAndCourses(allStudentsByYears, null, query.courses),
|
||||
isValidRequest(query, params),
|
||||
])
|
||||
if (error) return error
|
||||
|
||||
if (error) return error
|
||||
// Get the substitution codes for the fetch data by selscted students
|
||||
const substitutionCodes = Object.entries(courses).reduce(
|
||||
(res, [, obj]) => [...res, ...(obj?.substitutions || [])],
|
||||
[]
|
||||
)
|
||||
const codes = Object.keys(keyBy(courses, 'code')).map(code => code)
|
||||
// Filter substitution courses for fetched courses -> this avoid the situation in which there is only
|
||||
// courses with old course codes. Frontend NEEDS in most cases the current course.
|
||||
const substitutionCourses = allCourses.filter(
|
||||
obj => substitutionCodes.includes(obj.code) && !codes.includes(obj.code)
|
||||
)
|
||||
const bottlenecks = {
|
||||
disciplines: {},
|
||||
coursetypes: {},
|
||||
}
|
||||
|
||||
const stats = {}
|
||||
|
||||
const startYear = parseInt(query.year, 10)
|
||||
const allstudentslength = Object.keys(allstudents).length
|
||||
const coursesByCode = keyBy(courses, 'code')
|
||||
let coursesToLoop = courses.concat(substitutionCourses)
|
||||
const codesList = coursesToLoop.map(course => course.code)
|
||||
// This fix problem when Enrolled no grade is chosen. The sql query for fetching
|
||||
// credits do not fetch enrollments if no credits found for selected students.
|
||||
// This and other sql query ensures that enrollments are added.
|
||||
const coursesOnlyWithEnrollments = courseEnrollements.filter(
|
||||
course => !codesList.includes(course.code) && course.enrollments
|
||||
)
|
||||
coursesToLoop = coursesToLoop.concat(coursesOnlyWithEnrollments)
|
||||
|
||||
for (const course of courses) {
|
||||
const coursesByCode = keyBy(coursesToLoop, 'code')
|
||||
for (const course of coursesToLoop) {
|
||||
let { course_type } = course
|
||||
let maincourse = course
|
||||
|
||||
@@ -1101,28 +1315,32 @@ const bottlenecksOf = async (query, studentnumberlist, encryptdata = false) => {
|
||||
}
|
||||
|
||||
const coursestats = stats[maincourse.code]
|
||||
|
||||
coursestats.addCourseType(course_type.coursetypecode, course_type.name)
|
||||
coursestats.addCourseSubstitutions(course.substitutions)
|
||||
bottlenecks.coursetypes[course_type.coursetypecode] = course_type.name
|
||||
|
||||
course.credits.forEach(credit => {
|
||||
const { studentnumber, passingGrade, improvedGrade, failingGrade, grade, date } = parseCreditInfo(credit)
|
||||
const semester = getPassingSemester(startYear, date)
|
||||
coursestats.markCredit(studentnumber, grade, passingGrade, failingGrade, improvedGrade, semester)
|
||||
})
|
||||
|
||||
if (course.enrollments) {
|
||||
course.enrollments.forEach(({ studentnumber, state, enrollment_date_time }) => {
|
||||
const semester = getPassingSemester(startYear, enrollment_date_time)
|
||||
coursestats.markEnrollment(studentnumber, state, semester)
|
||||
if ((query?.selectedStudents && query?.selectedStudents.includes(studentnumber)) || !query?.selectedStudents) {
|
||||
const semester = getPassingSemester(startYear, enrollment_date_time)
|
||||
coursestats.markEnrollment(studentnumber, state, semester, enrollment_date_time)
|
||||
}
|
||||
})
|
||||
}
|
||||
if (course.credits) {
|
||||
course.credits.forEach(credit => {
|
||||
const { studentnumber, passingGrade, improvedGrade, failingGrade, grade, date } = parseCreditInfo(credit)
|
||||
if ((query?.selectedStudents && query?.selectedStudents.includes(studentnumber)) || !query?.selectedStudents) {
|
||||
const semester = getPassingSemester(startYear, date)
|
||||
coursestats.markCredit(studentnumber, grade, passingGrade, failingGrade, improvedGrade, semester)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
stats[maincourse.code] = coursestats
|
||||
}
|
||||
|
||||
bottlenecks.coursestatistics = Object.values(stats).map(coursestatistics => coursestatistics.getFinalStats())
|
||||
const allStats = Object.values(stats).map(coursestatistics => coursestatistics.getFinalStats())
|
||||
bottlenecks.coursestatistics = allStats.filter(course => course.stats.students > 0)
|
||||
bottlenecks.allStudents = allstudentslength
|
||||
|
||||
if (encryptdata) encryptStudentnumbers(bottlenecks)
|
||||
|
||||
@@ -13,6 +13,7 @@ const {
|
||||
SemesterEnrollment,
|
||||
Semester,
|
||||
Studyplan,
|
||||
Transfer,
|
||||
} = require('../models')
|
||||
const { TagStudent, Tag } = require('../models/models_kone')
|
||||
const Op = Sequelize.Op
|
||||
@@ -60,7 +61,10 @@ const byId = async id => {
|
||||
},
|
||||
{
|
||||
model: Studyplan,
|
||||
attributes: ['included_courses', 'programme_code', 'completed_credits'],
|
||||
attributes: ['included_courses', 'programme_code', 'completed_credits', 'studyrightid'],
|
||||
},
|
||||
{
|
||||
model: Transfer,
|
||||
},
|
||||
],
|
||||
}),
|
||||
@@ -347,6 +351,20 @@ const studentnumberLike = terms => {
|
||||
const bySearchTerm = async searchterm => {
|
||||
const terms = splitByEmptySpace(searchterm)
|
||||
const matches = await Student.findAll({
|
||||
include: {
|
||||
model: Studyright,
|
||||
include: {
|
||||
model: StudyrightElement,
|
||||
include: {
|
||||
model: ElementDetail,
|
||||
where: {
|
||||
type: {
|
||||
[Op.in]: [10, 20, 30],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
where: {
|
||||
[Op.or]: [nameLike(terms), studentnumberLike(terms)],
|
||||
},
|
||||
@@ -357,6 +375,20 @@ const bySearchTerm = async searchterm => {
|
||||
const bySearchTermAndStudentNumbers = async (searchterm, studentnumbers) => {
|
||||
const terms = splitByEmptySpace(searchterm)
|
||||
const matches = await Student.findAll({
|
||||
include: {
|
||||
model: Studyright,
|
||||
include: {
|
||||
model: StudyrightElement,
|
||||
include: {
|
||||
model: ElementDetail,
|
||||
where: {
|
||||
type: {
|
||||
[Op.in]: [10, 20, 30],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
where: {
|
||||
[Op.and]: {
|
||||
[Op.or]: [nameLike(terms), studentnumberLike(terms)],
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
const { ProgressCriteria } = require('../models/models_kone')
|
||||
const { Course } = require('../models')
|
||||
const logger = require('../util/logger')
|
||||
|
||||
const getCriteriaByStudyProgramme = async code => {
|
||||
if (code === '') return null
|
||||
return await ProgressCriteria.findOne({
|
||||
where: {
|
||||
code: code,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const getSubstitutions = async codes => {
|
||||
if (codes === []) return []
|
||||
const courses = await Course.findAll({ where: { code: codes }, attributes: ['code', 'substitutions'], raw: true })
|
||||
return courses.reduce((acc, { code, substitutions }) => ({ ...acc, [code]: substitutions }), {})
|
||||
}
|
||||
|
||||
const formattedData = async data => {
|
||||
const yearOne = data.coursesYearOne ? data.coursesYearOne : []
|
||||
const yearTwo = data.coursesYearTwo ? data.coursesYearTwo : []
|
||||
const yearThree = data.coursesYearThree ? data.coursesYearThree : []
|
||||
const yearFour = data.coursesYearFour ? data.coursesYearFour : []
|
||||
const yearFive = data.coursesYearFive ? data.coursesYearFive : []
|
||||
const yearSix = data.coursesYearSix ? data.coursesYearSix : []
|
||||
const courseCodes = [...yearOne, ...yearTwo, ...yearThree, ...yearFour, ...yearFive, ...yearSix]
|
||||
const allCourses = await getSubstitutions(courseCodes)
|
||||
|
||||
const criteria = {
|
||||
courses: { yearOne, yearTwo, yearThree, yearFour, yearFive, yearSix },
|
||||
allCourses,
|
||||
credits: {
|
||||
yearOne: data.creditsYearOne ? data.creditsYearOne : 0,
|
||||
yearTwo: data.creditsYearTwo ? data.creditsYearTwo : 0,
|
||||
yearThree: data.creditsYearThree ? data.creditsYearThree : 0,
|
||||
yearFour: data.creditsYearFour ? data.creditsYearFour : 0,
|
||||
yearFive: data.creditsYearFive ? data.creditsYearFive : 0,
|
||||
yearSix: data.creditsYearSix ? data.creditsYearSix : 0,
|
||||
},
|
||||
}
|
||||
return criteria
|
||||
}
|
||||
|
||||
const createCriteria = async (studyProgramme, courseObj, creditsObj) => {
|
||||
const newProgrammeCriteria = {
|
||||
code: studyProgramme,
|
||||
coursesYearOne: courseObj.year1,
|
||||
coursesYearTwo: courseObj.year2,
|
||||
coursesYearThree: courseObj.year3,
|
||||
coursesYearFour: courseObj.year4,
|
||||
coursesYearFive: courseObj.year5,
|
||||
coursesYearSix: courseObj.year6,
|
||||
creditsYearOne: creditsObj.year1,
|
||||
creditsYearTwo: creditsObj.year2,
|
||||
creditsYearThree: creditsObj.year3,
|
||||
creditsYearFour: creditsObj.year4,
|
||||
creditsYearFive: creditsObj.year5,
|
||||
creditsYearSix: creditsObj.year6,
|
||||
}
|
||||
try {
|
||||
const createdData = await ProgressCriteria.create(newProgrammeCriteria)
|
||||
return await formattedData(createdData)
|
||||
} catch (error) {
|
||||
logger.error(`Create criteria failed: ${error}`)
|
||||
}
|
||||
}
|
||||
|
||||
const saveYearlyCreditCriteria = async (studyProgramme, credits) => {
|
||||
const studyProgrammeToUpdate = await getCriteriaByStudyProgramme(studyProgramme)
|
||||
if (!studyProgrammeToUpdate) {
|
||||
const courseObj = { year1: [], year2: [], year3: [], year4: [], year5: [], year6: [] }
|
||||
const creditsObj = {
|
||||
year1: parseInt(credits.year1),
|
||||
year2: parseInt(credits.year2),
|
||||
year3: parseInt(credits.year3),
|
||||
year4: parseInt(credits.year4),
|
||||
year5: parseInt(credits.year5),
|
||||
year6: parseInt(credits.year6),
|
||||
}
|
||||
return await createCriteria(studyProgramme, courseObj, creditsObj)
|
||||
}
|
||||
const yearlyCredits = {
|
||||
creditsYearOne: parseInt(credits.year1),
|
||||
creditsYearTwo: parseInt(credits.year2),
|
||||
creditsYearThree: parseInt(credits.year3),
|
||||
creditsYearFour: parseInt(credits.year4),
|
||||
creditsYearFive: parseInt(credits.year5),
|
||||
creditsYearSix: parseInt(credits.year6),
|
||||
}
|
||||
try {
|
||||
const updatedData = await studyProgrammeToUpdate.update({ ...yearlyCredits })
|
||||
return await formattedData(updatedData)
|
||||
} catch (error) {
|
||||
logger.error(`Update yearly credit criteria failed: ${error}`)
|
||||
}
|
||||
}
|
||||
|
||||
const saveYearlyCourseCriteria = async (studyProgramme, courses, year) => {
|
||||
const studyProgrammeToUpdate = await getCriteriaByStudyProgramme(studyProgramme)
|
||||
if (!studyProgrammeToUpdate) {
|
||||
const courseObj = { year1: [], year2: [], year3: [], year4: [], year5: [], year6: [] }
|
||||
const creditObj = { year1: 0, year2: 0, year3: 0, year4: 0, year5: 0, year6: 0 }
|
||||
if (year === 1) {
|
||||
courseObj.year1 = courses
|
||||
} else if (year === 2) {
|
||||
courseObj.year2 = courses
|
||||
} else if (year === 3) {
|
||||
courseObj.year3 = courses
|
||||
} else if (year === 4) {
|
||||
courseObj.year4 = courses
|
||||
} else if (year === 5) {
|
||||
courseObj.year5 = courses
|
||||
} else {
|
||||
courseObj.year6 = courses
|
||||
}
|
||||
return await createCriteria(studyProgramme, courseObj, creditObj)
|
||||
}
|
||||
|
||||
try {
|
||||
let updatedData = {}
|
||||
if (year === 1) updatedData = await studyProgrammeToUpdate.update({ coursesYearOne: courses })
|
||||
if (year === 2) updatedData = await studyProgrammeToUpdate.update({ coursesYearTwo: courses })
|
||||
if (year === 3) updatedData = await studyProgrammeToUpdate.update({ coursesYearThree: courses })
|
||||
if (year === 4) updatedData = await studyProgrammeToUpdate.update({ coursesYearFour: courses })
|
||||
if (year === 5) updatedData = await studyProgrammeToUpdate.update({ coursesYearFive: courses })
|
||||
if (year === 6) updatedData = await studyProgrammeToUpdate.update({ coursesYearSix: courses })
|
||||
return await formattedData(updatedData)
|
||||
} catch (error) {
|
||||
logger.error(`Update yearly credit criteria failed: ${error}`)
|
||||
}
|
||||
}
|
||||
|
||||
const getCriteria = async studyProgramme => {
|
||||
const studyProgrammeCriteria = await getCriteriaByStudyProgramme(studyProgramme)
|
||||
const criteria = {
|
||||
courses: { yearOne: [], yearTwo: [], yearThree: [], yearFour: [], yearFive: [], yearSix: [] },
|
||||
allCourses: [],
|
||||
credits: { yearOne: 0, yearTwo: 0, yearThree: 0, yearFour: 0, yearFive: 0, yearSix: 0 },
|
||||
}
|
||||
return studyProgrammeCriteria ? await formattedData(studyProgrammeCriteria) : criteria
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getCriteria,
|
||||
saveYearlyCourseCriteria,
|
||||
saveYearlyCreditCriteria,
|
||||
}
|
||||
@@ -12,13 +12,16 @@ const {
|
||||
Studyright,
|
||||
StudyrightElement,
|
||||
ElementDetail,
|
||||
ProgrammeModule,
|
||||
Transfer,
|
||||
Student,
|
||||
SemesterEnrollment,
|
||||
Semester,
|
||||
Enrollment,
|
||||
} = require('../models')
|
||||
const { formatStudyright, formatStudent, formatTransfer } = require('./studyprogrammeHelpers')
|
||||
const { getCurrentSemester } = require('./semesters')
|
||||
const logger = require('../util/logger')
|
||||
|
||||
const whereStudents = studentnumbers => {
|
||||
return studentnumbers ? studentnumbers : { [Op.not]: null }
|
||||
@@ -356,7 +359,7 @@ const allTransfers = async (studytrack, since) => {
|
||||
const getProgrammesStudyrights = async studyprogramme =>
|
||||
(
|
||||
await Studyright.findAll({
|
||||
attributes: ['studyrightid', 'studystartdate', 'enddate', 'graduated', 'prioritycode', 'extentcode'],
|
||||
attributes: ['studyrightid', 'studystartdate', 'enddate', 'graduated', 'prioritycode', 'extentcode', 'cancelled'],
|
||||
include: [
|
||||
{
|
||||
model: StudyrightElement,
|
||||
@@ -379,7 +382,16 @@ const getProgrammesStudyrights = async studyprogramme =>
|
||||
const getStudyRights = async students =>
|
||||
(
|
||||
await Studyright.findAll({
|
||||
attributes: ['studyrightid', 'studystartdate', 'enddate', 'graduated', 'prioritycode', 'extentcode'],
|
||||
attributes: [
|
||||
'studyrightid',
|
||||
'startdate',
|
||||
'studystartdate',
|
||||
'enddate',
|
||||
'graduated',
|
||||
'prioritycode',
|
||||
'extentcode',
|
||||
'cancelled',
|
||||
],
|
||||
where: {
|
||||
studentStudentnumber: students,
|
||||
},
|
||||
@@ -415,9 +427,7 @@ const getCreditsForStudyProgramme = async (codes, since) => {
|
||||
},
|
||||
},
|
||||
where: {
|
||||
credittypecode: {
|
||||
[Op.notIn]: [10, 9, 7],
|
||||
},
|
||||
credittypecode: 4,
|
||||
isStudyModule: {
|
||||
[Op.not]: true,
|
||||
},
|
||||
@@ -498,16 +508,110 @@ const getStudentsForProgrammeCourses = async (from, to, programmeCourses) => {
|
||||
code: course.code,
|
||||
name: course.course_name,
|
||||
year: course.year,
|
||||
totalAllStudents: parseInt(course.total_students),
|
||||
totalPassed: parseInt(course.total_students),
|
||||
totalAllcredits: parseInt(course.total_credits),
|
||||
type: 'total',
|
||||
type: 'passed',
|
||||
}))
|
||||
} catch (e) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(e)
|
||||
logger.log(e)
|
||||
}
|
||||
}
|
||||
const getNotCompletedForProgrammeCourses = async (from, to, programmeCourses) => {
|
||||
try {
|
||||
const enrollmentsCourses = await Enrollment.findAll({
|
||||
attributes: ['studentnumber', 'course_code'],
|
||||
where: {
|
||||
course_code: {
|
||||
[Op.in]: programmeCourses,
|
||||
},
|
||||
studentnumber: {
|
||||
[Op.ne]: null,
|
||||
},
|
||||
enrollment_date_time: {
|
||||
[Op.between]: [from, to],
|
||||
},
|
||||
state: 'ENROLLED',
|
||||
},
|
||||
})
|
||||
const allEnrollments = {}
|
||||
for (const { studentnumber, course_code } of enrollmentsCourses) {
|
||||
if (!(course_code in allEnrollments)) {
|
||||
allEnrollments[course_code] = []
|
||||
}
|
||||
allEnrollments[course_code].push(studentnumber)
|
||||
}
|
||||
|
||||
const credits = await Credit.findAll({
|
||||
attributes: ['id', 'course_code', 'student_studentnumber', 'credittypecode', 'attainment_date'],
|
||||
include: {
|
||||
model: Course,
|
||||
attributes: ['code', 'name'],
|
||||
required: true,
|
||||
where: {
|
||||
is_study_module: false,
|
||||
code: programmeCourses,
|
||||
},
|
||||
},
|
||||
where: {
|
||||
credittypecode: [4, 7, 10],
|
||||
isStudyModule: {
|
||||
[Op.not]: true,
|
||||
},
|
||||
attainment_date: {
|
||||
[Op.between]: [from, to],
|
||||
},
|
||||
},
|
||||
})
|
||||
const creditsObj = credits.map(credit => {
|
||||
return {
|
||||
code: credit.course_code,
|
||||
studentnumber: credit.student_studentnumber,
|
||||
credittype: credit.credittypecode,
|
||||
courseName: credit.course.name,
|
||||
year: moment(credit.attainment_date).year(),
|
||||
}
|
||||
})
|
||||
|
||||
const passedByCourseCodes = {}
|
||||
const notCompletedByCourseCodes = {}
|
||||
const courses = {}
|
||||
for (const course of creditsObj) {
|
||||
if (!(course.code in courses)) {
|
||||
courses[course.code] = {
|
||||
code: course.code,
|
||||
name: course.courseName,
|
||||
year: course.year,
|
||||
}
|
||||
|
||||
passedByCourseCodes[course.code] = []
|
||||
notCompletedByCourseCodes[course.code] = []
|
||||
}
|
||||
if ([4, 7].includes(course.credittype)) {
|
||||
passedByCourseCodes[course.code].push(course.studentnumber)
|
||||
}
|
||||
if (
|
||||
(allEnrollments[course.code]?.includes(course.studentnumber) || course.credittype === 10) &&
|
||||
!passedByCourseCodes[course.code].includes(course.studentnumber)
|
||||
) {
|
||||
notCompletedByCourseCodes[course.code].push(course.studentnumber)
|
||||
}
|
||||
}
|
||||
|
||||
return Object.keys(courses)
|
||||
.reduce((acc, val) => [...acc, { ...courses[val] }], [])
|
||||
.map(course => ({
|
||||
code: course.code,
|
||||
name: course.name,
|
||||
year: course.year,
|
||||
totalNotCompleted: [...new Set(notCompletedByCourseCodes[course.code])].length,
|
||||
type: 'notCompleted',
|
||||
}))
|
||||
} catch (e) {
|
||||
// eslint-disable-next-line no-console
|
||||
logger.log(e)
|
||||
}
|
||||
}
|
||||
const getOwnStudentsForProgrammeCourses = async (from, to, programmeCourses, studyprogramme) => {
|
||||
const res = await sequelize.query(
|
||||
`
|
||||
@@ -534,7 +638,6 @@ const getOwnStudentsForProgrammeCourses = async (from, to, programmeCourses, stu
|
||||
}
|
||||
)
|
||||
return res.map(course => {
|
||||
// console.log('course: ', course)
|
||||
const res = {
|
||||
code: course.code,
|
||||
name: course.course_name,
|
||||
@@ -732,6 +835,15 @@ const getCurrentStudyYearStartDate = _.memoize(
|
||||
)
|
||||
)
|
||||
|
||||
const getProgrammeName = async code => {
|
||||
return ProgrammeModule.findOne({
|
||||
attributes: ['name'],
|
||||
where: {
|
||||
code,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
studytrackStudents,
|
||||
enrolledStudents,
|
||||
@@ -758,4 +870,6 @@ module.exports = {
|
||||
getStudyRights,
|
||||
getCourseCodesForStudyProgramme,
|
||||
getTransferStudentsForProgrammeCourses,
|
||||
getProgrammeName,
|
||||
getNotCompletedForProgrammeCourses,
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ const {
|
||||
getStudentsForProgrammeCourses,
|
||||
getCurrentStudyYearStartDate,
|
||||
getOwnStudentsForProgrammeCourses,
|
||||
getNotCompletedForProgrammeCourses,
|
||||
getStudentsWithoutStudyrightForProgrammeCourses,
|
||||
getTransferStudentsForProgrammeCourses,
|
||||
getOtherStudentsForProgrammeCourses,
|
||||
@@ -39,9 +40,12 @@ const makeYearlyPromises = (years, academicYear, type, programmeCourses, studypr
|
||||
let result = null
|
||||
|
||||
switch (type) {
|
||||
case 'allStudents':
|
||||
case 'passed':
|
||||
result = await getStudentsForProgrammeCourses(from, to, programmeCourses)
|
||||
break
|
||||
case 'notCompleted':
|
||||
result = await getNotCompletedForProgrammeCourses(from, to, programmeCourses)
|
||||
break
|
||||
case 'ownStudents':
|
||||
result = await getOwnStudentsForProgrammeCourses(from, to, programmeCourses, studyprogramme)
|
||||
break
|
||||
@@ -74,7 +78,13 @@ const getStudyprogrammeCoursesForStudytrack = async (unixMillis, studyprogramme,
|
||||
const yearRange = _.range(2017, startYear + 1)
|
||||
const programmeCourses = await getAllStudyprogrammeCourses(studyprogramme)
|
||||
|
||||
const yearlyStudentByCoursePromises = makeYearlyPromises(yearRange, academicYear, 'allStudents', programmeCourses)
|
||||
const yearlyPassedStudentByCoursePromises = makeYearlyPromises(yearRange, academicYear, 'passed', programmeCourses)
|
||||
const yearlyNotCompletedStudentByCoursePromises = makeYearlyPromises(
|
||||
yearRange,
|
||||
academicYear,
|
||||
'notCompleted',
|
||||
programmeCourses
|
||||
)
|
||||
const yearlyProgrammeStudentsPromises = makeYearlyPromises(
|
||||
yearRange,
|
||||
academicYear,
|
||||
@@ -105,22 +115,24 @@ const getStudyprogrammeCoursesForStudytrack = async (unixMillis, studyprogramme,
|
||||
)
|
||||
|
||||
const [
|
||||
yearlyStudentByCourse,
|
||||
yearlyPassedStudentByCourse,
|
||||
yearlyNotCompletedStudentByCourse,
|
||||
yearlyProgrammeStudents,
|
||||
yearlyStudentsWithoutStudyright,
|
||||
yearlyOtherProgrammeStudents,
|
||||
yearlyTransferStudents,
|
||||
] = await Promise.all([
|
||||
Promise.all(yearlyStudentByCoursePromises),
|
||||
Promise.all(yearlyPassedStudentByCoursePromises),
|
||||
Promise.all(yearlyNotCompletedStudentByCoursePromises),
|
||||
Promise.all(yearlyProgrammeStudentsPromises),
|
||||
Promise.all(yearlyStudentsWithoutStudyrightPromises),
|
||||
Promise.all(yearlyOtherProgrammeStudentsPromises),
|
||||
Promise.all(yearlyTransferStudentsPromises),
|
||||
])
|
||||
|
||||
let maxYear = 0
|
||||
const allCourses = [
|
||||
...yearlyStudentByCourse.flat(),
|
||||
...yearlyPassedStudentByCourse.flat(),
|
||||
...yearlyNotCompletedStudentByCourse.flat(),
|
||||
...yearlyProgrammeStudents.flat(),
|
||||
...yearlyStudentsWithoutStudyright.flat(),
|
||||
...yearlyOtherProgrammeStudents.flat(),
|
||||
@@ -134,9 +146,12 @@ const getStudyprogrammeCoursesForStudytrack = async (unixMillis, studyprogramme,
|
||||
years: {},
|
||||
}
|
||||
}
|
||||
|
||||
if (!acc[curr.code]['years'][curr.year]) {
|
||||
acc[curr.code]['years'][curr.year] = {
|
||||
totalAllStudents: 0,
|
||||
totalPassed: 0,
|
||||
totalNotCompleted: 0,
|
||||
totalAllCredits: 0,
|
||||
totalProgrammeStudents: 0,
|
||||
totalProgrammeCredits: 0,
|
||||
@@ -149,10 +164,16 @@ const getStudyprogrammeCoursesForStudytrack = async (unixMillis, studyprogramme,
|
||||
}
|
||||
}
|
||||
switch (curr.type) {
|
||||
case 'total':
|
||||
acc[curr.code]['years'][curr.year]['totalAllStudents'] += curr.totalAllStudents
|
||||
case 'passed':
|
||||
acc[curr.code]['years'][curr.year]['totalPassed'] += curr.totalPassed
|
||||
acc[curr.code]['years'][curr.year]['totalAllStudents'] += acc[curr.code]['years'][curr.year]['totalPassed']
|
||||
acc[curr.code]['years'][curr.year]['totalAllCredits'] += curr.totalAllcredits
|
||||
break
|
||||
case 'notCompleted':
|
||||
acc[curr.code]['years'][curr.year]['totalNotCompleted'] += curr.totalNotCompleted
|
||||
acc[curr.code]['years'][curr.year]['totalAllStudents'] +=
|
||||
acc[curr.code]['years'][curr.year]['totalNotCompleted']
|
||||
break
|
||||
case 'ownProgramme':
|
||||
acc[curr.code]['years'][curr.year]['totalProgrammeStudents'] += curr.totalProgrammeStudents
|
||||
acc[curr.code]['years'][curr.year]['totalProgrammeCredits'] += curr.totalProgrammeCredits
|
||||
@@ -175,6 +196,8 @@ const getStudyprogrammeCoursesForStudytrack = async (unixMillis, studyprogramme,
|
||||
const ayCourses = Object.keys(allCourses).filter(c => c.startsWith('AY'))
|
||||
const properties = [
|
||||
'totalAllStudents',
|
||||
'totalPassed',
|
||||
'totalNotCompleted',
|
||||
'totalAllCredits',
|
||||
'totalProgrammeStudents',
|
||||
'totalProgrammeCredits',
|
||||
@@ -200,6 +223,8 @@ const getStudyprogrammeCoursesForStudytrack = async (unixMillis, studyprogramme,
|
||||
if (!allCourses[normCode]['years'][year]) {
|
||||
mergedCourse['years'][year] = {
|
||||
totalAllStudents: 0,
|
||||
totalPassed: 0,
|
||||
totalNotCompleted: 0,
|
||||
totalAllCredits: 0,
|
||||
totalProgrammeStudents: 0,
|
||||
totalProgrammeCredits: 0,
|
||||
|
||||
@@ -9,7 +9,6 @@ const {
|
||||
getYearsObject,
|
||||
getYearsArray,
|
||||
getMedian,
|
||||
getMean,
|
||||
getStartDate,
|
||||
getThesisType,
|
||||
alltimeStartDate,
|
||||
@@ -125,29 +124,22 @@ const getGraduationTimeStats = async ({ studyprogramme, since, years, isAcademic
|
||||
}
|
||||
|
||||
const goal = getGoal(studyprogramme)
|
||||
const times = { medians: [], means: [], goal }
|
||||
const comboTimes = { medians: [], means: [], goal: goal + 36 }
|
||||
const times = { medians: [], goal }
|
||||
const comboTimes = { medians: [], goal: goal + 36 }
|
||||
|
||||
const rev = [...years].reverse()
|
||||
for (const year of rev) {
|
||||
const median = getMedian(graduationTimes[year])
|
||||
const mean = getMean(graduationTimes[year])
|
||||
const statistics = countTimeCategories(graduationTimes[year], goal)
|
||||
times.medians = [...times.medians, { y: median, amount: graduationAmounts[year], name: year, statistics }]
|
||||
times.means = [...times.means, { y: mean, amount: graduationAmounts[year], name: year, statistics }]
|
||||
|
||||
if (doCombo) {
|
||||
const median = getMedian(graduationTimesCombo[year])
|
||||
const mean = getMean(graduationTimesCombo[year])
|
||||
const statistics = countTimeCategories(graduationTimesCombo[year], goal + 36)
|
||||
comboTimes.medians = [
|
||||
...comboTimes.medians,
|
||||
{ y: median, amount: graduationAmountsCombo[year], name: year, statistics },
|
||||
]
|
||||
comboTimes.means = [
|
||||
...comboTimes.means,
|
||||
{ y: mean, amount: graduationAmountsCombo[year], name: year, statistics },
|
||||
]
|
||||
}
|
||||
}
|
||||
return { times, doCombo, comboTimes }
|
||||
@@ -268,25 +260,39 @@ const getGraduationStatsForStudytrack = async ({ studyprogramme, settings }) =>
|
||||
|
||||
const reversedYears = getYearsArray(since.getFullYear(), isAcademicYear).reverse()
|
||||
|
||||
const titles = ['', 'Graduated', 'Wrote thesis']
|
||||
const titles =
|
||||
studyprogramme.includes('LIS') || studyprogramme.includes('T')
|
||||
? ['', 'Graduated']
|
||||
: ['', 'Graduated', 'Wrote thesis']
|
||||
const programmesBeforeOrAfterTitles = ['Code', 'Id', 'Programme', ...years]
|
||||
const tableStats = reversedYears.map(year => [year, graduated.tableStats[year], thesis.tableStats[year]])
|
||||
const tableStats =
|
||||
studyprogramme.includes('LIS') || studyprogramme.includes('T')
|
||||
? reversedYears.map(year => [year, graduated.tableStats[year]])
|
||||
: reversedYears.map(year => [year, graduated.tableStats[year], thesis.tableStats[year]])
|
||||
|
||||
return {
|
||||
id: studyprogramme,
|
||||
years,
|
||||
tableStats,
|
||||
titles,
|
||||
graphStats: [
|
||||
{
|
||||
name: 'Graduated students',
|
||||
data: graduated.graphStats,
|
||||
},
|
||||
{
|
||||
name: 'Wrote thesis',
|
||||
data: thesis.graphStats,
|
||||
},
|
||||
],
|
||||
graphStats:
|
||||
studyprogramme.includes('LIS') || studyprogramme.includes('T')
|
||||
? [
|
||||
{
|
||||
name: 'Graduated students',
|
||||
data: graduated.graphStats,
|
||||
},
|
||||
]
|
||||
: [
|
||||
{
|
||||
name: 'Graduated students',
|
||||
data: graduated.graphStats,
|
||||
},
|
||||
{
|
||||
name: 'Wrote thesis',
|
||||
data: thesis.graphStats,
|
||||
},
|
||||
],
|
||||
graduationTimes: graduationTimeStats.times,
|
||||
doCombo: graduationTimeStats.doCombo,
|
||||
comboTimes: graduationTimeStats.comboTimes,
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
const { mean } = require('lodash')
|
||||
const { studentnumbersWithAllStudyrightElements } = require('./populations')
|
||||
const { codes } = require('../../config/programmeCodes')
|
||||
|
||||
@@ -55,6 +54,7 @@ const formatStudyright = studyright => {
|
||||
extentcode,
|
||||
student,
|
||||
studyright_elements,
|
||||
cancelled,
|
||||
} = studyright
|
||||
|
||||
return {
|
||||
@@ -70,6 +70,7 @@ const formatStudyright = studyright => {
|
||||
studentnumber: student.studentnumber,
|
||||
code: resolveStudyRightCode(studyright_elements),
|
||||
studyrightElements: studyright_elements,
|
||||
cancelled,
|
||||
name:
|
||||
studyright_elements?.length && studyright_elements[0].element_detail && studyright_elements[0].element_detail.name
|
||||
? studyright_elements[0].element_detail.name
|
||||
@@ -153,16 +154,15 @@ 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 (studyright.studystartdate && new Date(studyright.studystartdate) > new Date(attainment_date)) return true // Credits before the studyright started are not major student credits
|
||||
if (studyright.enddate && new Date(attainment_date) > new Date(studyright.enddate)) return true // Credits after studyright are not major student credits
|
||||
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
|
||||
@@ -172,11 +172,6 @@ const getMedian = values => {
|
||||
return (values[half - 1] + values[half]) / 2.0
|
||||
}
|
||||
|
||||
const getMean = values => {
|
||||
if (values.length === 0) return 0
|
||||
return Math.round(mean(values))
|
||||
}
|
||||
|
||||
const defineYear = (date, isAcademicYear) => {
|
||||
if (!date) return ''
|
||||
const year = date.getFullYear()
|
||||
@@ -321,6 +316,7 @@ const getVetenaryCreditGraphStats = years => ({
|
||||
data: getEmptyArray(years.length),
|
||||
},
|
||||
})
|
||||
|
||||
const getOnlyMasterCreditGraphStats = years => ({
|
||||
lte15: {
|
||||
name: 'Less than 15 credits',
|
||||
@@ -371,10 +367,11 @@ const getDoctoralCreditGraphStats = years => ({
|
||||
},
|
||||
})
|
||||
|
||||
const getCreditGraphStats = (studyprogramme, years) => {
|
||||
const getCreditGraphStats = (studyprogramme, years, studyprog = false) => {
|
||||
if (studyprogramme.includes('KH')) return getBachelorCreditGraphStats(years)
|
||||
if (studyprogramme === 'MH90_001') return getVetenaryCreditGraphStats(years)
|
||||
if (studyprogramme.includes('MH')) return getMasterCreditGraphStats(years)
|
||||
if (studyprogramme === 'MH90_001')
|
||||
return studyprog ? getBachelorCreditGraphStats(years) : getVetenaryCreditGraphStats(years)
|
||||
if (studyprogramme.includes('MH')) return getOnlyMasterCreditGraphStats(years)
|
||||
return getDoctoralCreditGraphStats(years)
|
||||
}
|
||||
|
||||
@@ -388,18 +385,22 @@ const onlyMasterCreditAmounts = [15, 30, 60, 90, 120, 120]
|
||||
const doctoralCreditAmounts = [10, 20, 30, 40, 40]
|
||||
const vetenaryCreditThresholds = ['lte210', 'lte240', 'lte270', 'lte300', 'lte330', 'lte360', 'mte360']
|
||||
const vetenaryCreditAmounts = [210, 240, 270, 300, 330, 360, 360]
|
||||
const getOnlyMasterThresholds = () => {
|
||||
return { msOnlyCreditThresholdKeys: onlyMasterCreditThresholds, msOnlyCreditThresholdAmount: onlyMasterCreditAmounts }
|
||||
const getBcMsThresholds = programmeCode => {
|
||||
if (programmeCode === 'MH90_001')
|
||||
return { creditThresholdKeysBcMs: vetenaryCreditThresholds, creditThresholdAmountsBcMs: vetenaryCreditAmounts }
|
||||
return { creditThresholdKeysBcMs: masterCreditThresholds, creditThresholdAmountsBcMs: masterCreditAmounts }
|
||||
}
|
||||
const getCreditThresholds = studyprogramme => {
|
||||
const getCreditThresholds = (studyprogramme, studyProg = false) => {
|
||||
if (studyprogramme === 'MH90_001') {
|
||||
return { creditThresholdKeys: vetenaryCreditThresholds, creditThresholdAmounts: vetenaryCreditAmounts }
|
||||
return studyProg
|
||||
? { creditThresholdKeys: bachelorCreditThresholds, creditThresholdAmounts: bachelorCreditAmounts }
|
||||
: { creditThresholdKeys: vetenaryCreditThresholds, creditThresholdAmounts: vetenaryCreditAmounts }
|
||||
}
|
||||
if (studyprogramme.includes('KH')) {
|
||||
return { creditThresholdKeys: bachelorCreditThresholds, creditThresholdAmounts: bachelorCreditAmounts }
|
||||
}
|
||||
if (studyprogramme.includes('MH')) {
|
||||
return { creditThresholdKeys: masterCreditThresholds, creditThresholdAmounts: masterCreditAmounts }
|
||||
return { creditThresholdKeys: onlyMasterCreditThresholds, creditThresholdAmounts: onlyMasterCreditAmounts }
|
||||
}
|
||||
return { creditThresholdKeys: doctoralCreditThresholds, creditThresholdAmounts: doctoralCreditAmounts }
|
||||
}
|
||||
@@ -466,7 +467,7 @@ const tableTitles = {
|
||||
'Graduated',
|
||||
'Men',
|
||||
'Women',
|
||||
'Finnish',
|
||||
'Finland',
|
||||
],
|
||||
programmes: [
|
||||
'',
|
||||
@@ -479,14 +480,14 @@ const tableTitles = {
|
||||
'Men',
|
||||
'Women',
|
||||
'Other/Unknown',
|
||||
'Finnish',
|
||||
'Finland',
|
||||
'Other',
|
||||
],
|
||||
}
|
||||
|
||||
const getCreditProgressTableTitles = studyprogramme => {
|
||||
if (studyprogramme.includes('KH')) return tableTitles.creditProgress.bachelor
|
||||
if (studyprogramme.includes('MH')) return tableTitles.creditProgress.master
|
||||
if (studyprogramme.includes('MH')) return tableTitles.creditProgress.masterOnly
|
||||
return tableTitles.creditProgress.doctoral
|
||||
}
|
||||
|
||||
@@ -543,7 +544,6 @@ module.exports = {
|
||||
isMajorStudentCredit,
|
||||
isSpecialGroupCredit,
|
||||
getMedian,
|
||||
getMean,
|
||||
defineYear,
|
||||
getStartDate,
|
||||
alltimeStartDate,
|
||||
@@ -559,7 +559,7 @@ module.exports = {
|
||||
getCreditThresholds,
|
||||
tableTitles,
|
||||
getCreditProgressTableTitles,
|
||||
getOnlyMasterThresholds,
|
||||
getBcMsThresholds,
|
||||
isNonMajorCredit,
|
||||
mapCodesToIds,
|
||||
getId,
|
||||
|
||||
@@ -4,7 +4,6 @@ const moment = require('moment')
|
||||
const { getAssociations } = require('./studyrights')
|
||||
const {
|
||||
getMedian,
|
||||
getMean,
|
||||
getStartDate,
|
||||
getYearsArray,
|
||||
getPercentage,
|
||||
@@ -31,7 +30,6 @@ const { countTimeCategories } = require('./graduationHelpers')
|
||||
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, credits }) => {
|
||||
const creditcount = credits
|
||||
.filter(credit => moment(credit.attainment_date).isAfter(startDate))
|
||||
@@ -77,16 +75,34 @@ const getGraduationTimeStats = async ({ year, graduated, track, graduationAmount
|
||||
})
|
||||
|
||||
const median = getMedian(times)
|
||||
const mean = getMean(times)
|
||||
const statistics = countTimeCategories(times, graduationTimes.goal)
|
||||
|
||||
graduationTimes[track].medians = [
|
||||
...graduationTimes[track].medians,
|
||||
{ y: median, amount: graduationAmounts[track][year], name: year, statistics, classSize },
|
||||
]
|
||||
graduationTimes[track].means = [
|
||||
...graduationTimes[track].means,
|
||||
{ y: mean, amount: graduationAmounts[track][year], name: year, statistics, classSize },
|
||||
}
|
||||
|
||||
const getStats = (all, started, enrolled, absent, inactive, graduated, studentData) => {
|
||||
return [
|
||||
all.length,
|
||||
getPercentage(all.length, all.length),
|
||||
started.length,
|
||||
getPercentage(started.length, all.length),
|
||||
enrolled.length,
|
||||
getPercentage(enrolled.length, all.length),
|
||||
absent.length,
|
||||
getPercentage(absent.length, all.length),
|
||||
inactive.length,
|
||||
getPercentage(inactive.length, all.length),
|
||||
graduated.length,
|
||||
getPercentage(graduated.length, all.length),
|
||||
studentData.male,
|
||||
getPercentage(studentData.male, all.length),
|
||||
studentData.female,
|
||||
getPercentage(studentData.female, all.length),
|
||||
studentData.finnish,
|
||||
getPercentage(studentData.finnish, all.length),
|
||||
]
|
||||
}
|
||||
|
||||
@@ -180,7 +196,6 @@ const getStudytrackDataForTheYear = async ({
|
||||
threshold => (creditGraphStats[track][threshold].data[indexOf(years, 'Total')] += studentData[threshold])
|
||||
)
|
||||
// Count stats for the credit progress table for the year per track
|
||||
|
||||
creditTableStats[track] = [
|
||||
[year, all.length, ...creditThresholdKeys.map(threshold => studentData[threshold])],
|
||||
...creditTableStats[track],
|
||||
@@ -190,32 +205,12 @@ const getStudytrackDataForTheYear = async ({
|
||||
(a, b) => a.map((colVal, idx) => colVal + (b[idx] || 0)),
|
||||
Array(creditThresholdKeys.length + 2).fill(0)
|
||||
)
|
||||
creditTableStats[track] = [[year, all.length, ...totalCredits.slice(2)], ...creditTableStats[track]]
|
||||
creditTableStats[track] = [[year, ...totalCredits.slice(1)], ...creditTableStats[track]]
|
||||
}
|
||||
|
||||
// Count stats for the main studytrack table grouped by tracks
|
||||
mainStatsByTrack[track] = [
|
||||
[
|
||||
year,
|
||||
all.length,
|
||||
getPercentage(all.length, all.length),
|
||||
started.length,
|
||||
getPercentage(started.length, all.length),
|
||||
enrolled.length,
|
||||
getPercentage(enrolled.length, all.length),
|
||||
absent.length,
|
||||
getPercentage(absent.length, all.length),
|
||||
inactive.length,
|
||||
getPercentage(inactive.length, all.length),
|
||||
graduated.length,
|
||||
getPercentage(graduated.length, all.length),
|
||||
studentData.male,
|
||||
getPercentage(studentData.male, all.length),
|
||||
studentData.female,
|
||||
getPercentage(studentData.female, all.length),
|
||||
studentData.finnish,
|
||||
getPercentage(studentData.finnish, all.length),
|
||||
],
|
||||
[year, ...getStats(all, started, enrolled, absent, inactive, graduated, studentData)],
|
||||
...mainStatsByTrack[track],
|
||||
]
|
||||
|
||||
@@ -223,31 +218,14 @@ const getStudytrackDataForTheYear = async ({
|
||||
mainStatsByYear[year] = [
|
||||
[
|
||||
studytrackNames[track]?.name['fi'] ? `${studytrackNames[track]?.name['fi']}, ${track}` : year,
|
||||
all.length,
|
||||
getPercentage(all.length, all.length),
|
||||
started.length,
|
||||
getPercentage(started.length, all.length),
|
||||
enrolled.length,
|
||||
getPercentage(enrolled.length, all.length),
|
||||
absent.length,
|
||||
getPercentage(absent.length, all.length),
|
||||
inactive.length,
|
||||
getPercentage(inactive.length, all.length),
|
||||
graduated.length,
|
||||
getPercentage(graduated.length, all.length),
|
||||
studentData.male,
|
||||
getPercentage(studentData.male, all.length),
|
||||
studentData.female,
|
||||
getPercentage(studentData.female, all.length),
|
||||
studentData.finnish,
|
||||
getPercentage(studentData.finnish, all.length),
|
||||
...getStats(all, started, enrolled, absent, inactive, graduated, studentData),
|
||||
],
|
||||
...mainStatsByYear[year],
|
||||
]
|
||||
|
||||
// Count stats for the graduation time charts grouped by year
|
||||
if (!(track in graduationTimes)) {
|
||||
graduationTimes[track] = { medians: [], means: [] }
|
||||
graduationTimes[track] = { medians: [] }
|
||||
}
|
||||
totalAmounts[track][year] = all.length
|
||||
await getGraduationTimeStats({
|
||||
@@ -263,9 +241,7 @@ const getStudytrackDataForTheYear = async ({
|
||||
Object.keys(graduationTimes).forEach(track => {
|
||||
if (track !== 'goal') {
|
||||
const sortedMedians = graduationTimes[track].medians.sort((a, b) => b.name.localeCompare(a.name))
|
||||
const sortedMeans = graduationTimes[track].means.sort((a, b) => b.name.localeCompare(a.name))
|
||||
graduationTimes[track].medians = sortedMedians
|
||||
graduationTimes[track].means = sortedMeans
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -296,7 +272,7 @@ const getEmptyStatsObjects = (years, studytracks, studyprogramme) => {
|
||||
const totals = {}
|
||||
studytracks.forEach(async track => {
|
||||
mainStatsByTrack[track] = []
|
||||
creditGraphStats[track] = getCreditGraphStats(studyprogramme, years)
|
||||
creditGraphStats[track] = getCreditGraphStats(studyprogramme, years, true)
|
||||
creditTableStats[track] = []
|
||||
graduationAmounts[track] = getYearsObject({ years })
|
||||
totalAmounts[track] = getYearsObject({ years })
|
||||
@@ -339,7 +315,7 @@ const getStudytrackStatsForStudyprogramme = async ({ studyprogramme, settings })
|
||||
const studytrackNames = associations.studyTracks
|
||||
|
||||
const data = getEmptyStatsObjects(years, studytracks, studyprogramme)
|
||||
const { creditThresholdKeys, creditThresholdAmounts } = getCreditThresholds(studyprogramme)
|
||||
const { creditThresholdKeys, creditThresholdAmounts } = getCreditThresholds(studyprogramme, true)
|
||||
|
||||
const yearsReversed = [...years].reverse()
|
||||
|
||||
@@ -366,7 +342,10 @@ const getStudytrackStatsForStudyprogramme = async ({ studyprogramme, settings })
|
||||
studytrackOptions,
|
||||
includeGraduated: settings.graduated,
|
||||
populationTitles: tableTitles['studytracks'],
|
||||
creditTableTitles: getCreditProgressTableTitles(studyprogramme),
|
||||
creditTableTitles:
|
||||
studyprogramme === 'MH90_001'
|
||||
? tableTitles['creditProgress']['bachelor']
|
||||
: getCreditProgressTableTitles(studyprogramme),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,12 +8,10 @@ const { checkStudyGuidanceGroupsAccess, getAllStudentsUserHasInGroups } = requir
|
||||
const _ = require('lodash')
|
||||
const { User, UserElementDetails, AccessGroup, UserFaculties, sequelizeUser } = require('../models/models_user')
|
||||
const { getOrganizationAccess } = require('../util/organizationAccess')
|
||||
const { getUserIams, getAllUserAccess } = require('../util/jami')
|
||||
|
||||
const courseStatisticsGroup = 'grp-oodikone-basic-users'
|
||||
const facultyStatisticsGroup = 'grp-oodikone-users'
|
||||
const hyOneGroup = 'hy-one'
|
||||
const toskaGroup = 'grp-toska'
|
||||
const openUniGroup = 'hy-ypa-opa-dojo'
|
||||
|
||||
// Max 25 users can be stored in the cache
|
||||
const userDataCache = new LRU({
|
||||
@@ -166,32 +164,85 @@ const updateUser = async (username, fields) => {
|
||||
|
||||
const getAccessGroups = async () => await AccessGroup.findAll()
|
||||
|
||||
const updateAccessGroups = async (username, iamGroups = [], specialGroup = {}, sisId) => {
|
||||
const { jory, hyOne, superAdmin, openUni } = specialGroup
|
||||
|
||||
const userFromDb = await byUsername(username)
|
||||
const formattedUser = await formatUser(userFromDb, [], Boolean(sisId))
|
||||
|
||||
const { roles: currentAccessGroups } = formattedUser
|
||||
|
||||
// Modify accessgroups based on current groups and iamGroups
|
||||
let newAccessGroups = []
|
||||
if (currentAccessGroups.includes('studyGuidanceGroups') || (sisId && (await checkStudyGuidanceGroupsAccess(sisId))))
|
||||
newAccessGroups.push('studyGuidanceGroups')
|
||||
if (iamGroups.includes(courseStatisticsGroup)) newAccessGroups.push('courseStatistics')
|
||||
if (jory || iamGroups.includes(facultyStatisticsGroup)) newAccessGroups.push('facultyStatistics')
|
||||
if (hyOne || currentAccessGroups.includes('teachers')) newAccessGroups.push('teachers')
|
||||
if (superAdmin || currentAccessGroups.includes('admin')) newAccessGroups.push('admin')
|
||||
if (openUni) newAccessGroups.push('openUniSearch')
|
||||
|
||||
const accessGroups = await AccessGroup.findAll({
|
||||
where: {
|
||||
group_code: {
|
||||
[Op.in]: newAccessGroups,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// In the difference method "the order and references of result values are determined by the first array." https://lodash.com/docs/4.17.15#difference
|
||||
// Both directions needs to be checked in order to update roles when the access should no longer exists.
|
||||
if (
|
||||
_.difference(newAccessGroups, currentAccessGroups).length > 0 ||
|
||||
_.difference(currentAccessGroups, newAccessGroups).length > 0
|
||||
) {
|
||||
userFromDb.setAccessgroup(accessGroups.map(({ id }) => id))
|
||||
}
|
||||
|
||||
return { newAccessGroups, accessGroups }
|
||||
}
|
||||
|
||||
// newer logic
|
||||
const enrichProgrammesFromFaculties = faculties =>
|
||||
facultiesAndProgrammesForTrends.filter(f => faculties.includes(f.faculty_code)).map(f => f.programme_code)
|
||||
|
||||
const findAll = async () =>
|
||||
(
|
||||
await User.findAll({
|
||||
include: userIncludes,
|
||||
})
|
||||
).map(user => ({
|
||||
...user.get(),
|
||||
is_enabled: true,
|
||||
elementdetails: _.uniqBy([
|
||||
...user.programme.map(p => p.elementDetailCode),
|
||||
...enrichProgrammesFromFaculties(user.faculty.map(f => f.faculty_code)),
|
||||
]),
|
||||
}))
|
||||
const findAll = async () => {
|
||||
const allUsers = await User.findAll({
|
||||
include: userIncludes,
|
||||
})
|
||||
|
||||
const formatUser = async userFromDb => {
|
||||
const userAccess = await getAllUserAccess()
|
||||
|
||||
const formattedUsers = await Promise.all(
|
||||
allUsers.map(async user => {
|
||||
const { iamGroups, specialGroup } = userAccess.find(({ id }) => id === user.sisu_person_id) || {}
|
||||
|
||||
const { accessGroups } = await updateAccessGroups(user.username, iamGroups, specialGroup)
|
||||
|
||||
return {
|
||||
...user.get(),
|
||||
is_enabled: true,
|
||||
iam_groups: iamGroups || [],
|
||||
elementdetails: _.uniqBy([
|
||||
...user.programme.map(p => p.elementDetailCode),
|
||||
...enrichProgrammesFromFaculties(user.faculty.map(f => f.faculty_code)),
|
||||
]),
|
||||
accessgroup: accessGroups,
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
return formattedUsers
|
||||
}
|
||||
|
||||
const formatUser = async (userFromDb, extraRights, getStudentAccess = true) => {
|
||||
const [accessGroups, programmes, faculties] = [
|
||||
['accessgroup', 'group_code'],
|
||||
['programme', 'elementDetailCode'],
|
||||
['faculty', 'faculty_code'],
|
||||
].map(([field, code]) => userFromDb[field].map(entry => entry[code]))
|
||||
|
||||
const rights = _.uniqBy([...programmes, ...enrichProgrammesFromFaculties(faculties)])
|
||||
const rights = _.uniqBy([...programmes, ...enrichProgrammesFromFaculties(faculties)]).concat(extraRights)
|
||||
|
||||
const {
|
||||
dataValues: {
|
||||
@@ -205,9 +256,13 @@ const formatUser = async userFromDb => {
|
||||
},
|
||||
} = userFromDb
|
||||
|
||||
const studentsUserCanAccess = _.uniqBy(
|
||||
(await Promise.all([getStudentnumbersByElementdetails(rights), getAllStudentsUserHasInGroups(sisPersonId)])).flat()
|
||||
)
|
||||
const studentsUserCanAccess = getStudentAccess
|
||||
? _.uniqBy(
|
||||
(
|
||||
await Promise.all([getStudentnumbersByElementdetails(rights), getAllStudentsUserHasInGroups(sisPersonId)])
|
||||
).flat()
|
||||
)
|
||||
: []
|
||||
|
||||
// attribute naming is a bit confusing, but it's used so widely in oodikone, that it's not probably worth changing
|
||||
return {
|
||||
@@ -232,15 +287,18 @@ const getMockedUser = async ({ userToMock, mockedBy }) => {
|
||||
}
|
||||
|
||||
const userFromDb = await byUsername(userToMock)
|
||||
const formattedUser = await formatUser(userFromDb)
|
||||
userFromDb.iamGroups = await getUserIams(userFromDb.sisu_person_id)
|
||||
|
||||
const iamRights = Object.keys(await getOrganizationAccess(userFromDb)).filter(
|
||||
programmeCode => !formattedUser.rights.includes(programmeCode)
|
||||
)
|
||||
const { access = {}, specialGroup = {} } = await getOrganizationAccess(userFromDb)
|
||||
|
||||
const iamRights = Object.keys(access)
|
||||
|
||||
const formattedUser = await formatUser(userFromDb, specialGroup.kosu ? iamRights : [])
|
||||
|
||||
const toReturn = {
|
||||
...formattedUser,
|
||||
iamRights,
|
||||
specialGroup,
|
||||
mockedBy,
|
||||
}
|
||||
userDataCache.set(userToMock, toReturn)
|
||||
@@ -248,7 +306,7 @@ const getMockedUser = async ({ userToMock, mockedBy }) => {
|
||||
return toReturn
|
||||
}
|
||||
|
||||
const getUser = async ({ username, name, email, iamGroups, iamRights, sisId }) => {
|
||||
const getUser = async ({ username, name, email, iamGroups, iamRights, specialGroup, sisId }) => {
|
||||
if (userDataCache.has(username)) {
|
||||
return userDataCache.get(username)
|
||||
}
|
||||
@@ -262,41 +320,13 @@ const getUser = async ({ username, name, email, iamGroups, iamRights, sisId }) =
|
||||
email,
|
||||
language,
|
||||
sisu_person_id: sisId,
|
||||
iam_groups: iamGroups,
|
||||
last_login: new Date(),
|
||||
})
|
||||
|
||||
const userFromDb = await byUsername(username)
|
||||
const formattedUser = await formatUser(userFromDb)
|
||||
const { roles: currentAccessGroups } = formattedUser
|
||||
const formattedUser = await formatUser(userFromDb, specialGroup.kosu ? iamRights : [])
|
||||
|
||||
// Modify accessgroups based on current groups and iamGroups
|
||||
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 (iamGroups.includes(openUniGroup)) newAccessGroups.push('openUniSearch')
|
||||
// In the difference method "the order and references of result values are determined by the first array." https://lodash.com/docs/4.17.15#difference
|
||||
// Both directions needs to be checked in order to update roles when the access should no longer exists.
|
||||
if (
|
||||
_.difference(newAccessGroups, currentAccessGroups).length > 0 ||
|
||||
_.difference(currentAccessGroups, newAccessGroups).length > 0
|
||||
) {
|
||||
userFromDb.setAccessgroup(
|
||||
(
|
||||
await AccessGroup.findAll({
|
||||
where: {
|
||||
group_code: {
|
||||
[Op.in]: newAccessGroups,
|
||||
},
|
||||
},
|
||||
})
|
||||
).map(({ id }) => id)
|
||||
)
|
||||
}
|
||||
const { newAccessGroups } = await updateAccessGroups(username, iamGroups, specialGroup, sisId)
|
||||
|
||||
if (isNewUser) await sendNotificationAboutNewUser({ userId: username, userFullName: name })
|
||||
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
const axios = require('axios')
|
||||
const { importerToken } = require('../conf-backend')
|
||||
const { importerUrl, importerToken } = require('../conf-backend')
|
||||
const logger = require('./logger')
|
||||
|
||||
const importerClient = axios.create({
|
||||
headers: {
|
||||
token: importerToken,
|
||||
},
|
||||
baseURL: 'https://importer.cs.helsinki.fi/api/importer',
|
||||
baseURL: importerUrl,
|
||||
})
|
||||
|
||||
const getImporterClient = () => {
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
const axios = require('axios')
|
||||
const Sentry = require('@sentry/node')
|
||||
|
||||
const { importerToken, jamiUrl } = require('../conf-backend')
|
||||
const logger = require('./logger')
|
||||
|
||||
const jamiClient = axios.create({
|
||||
baseURL: jamiUrl,
|
||||
params: {
|
||||
token: importerToken,
|
||||
},
|
||||
})
|
||||
|
||||
const getUserIamAccess = async (user, attempt = 1) => {
|
||||
if (user.iamGroups.length === 0) return {}
|
||||
|
||||
const { id, iamGroups } = user
|
||||
|
||||
try {
|
||||
const { data: iamAccess } = await jamiClient.post('/', {
|
||||
userId: id,
|
||||
iamGroups,
|
||||
})
|
||||
|
||||
const { specialGroup } = iamAccess
|
||||
delete iamAccess.specialGroup
|
||||
|
||||
return { iamAccess, specialGroup }
|
||||
} catch (error) {
|
||||
if (attempt > 3) {
|
||||
logger.error('[Jami] error: ', error)
|
||||
Sentry.captureException(error)
|
||||
|
||||
return {}
|
||||
}
|
||||
|
||||
return getUserIamAccess(user, attempt + 1)
|
||||
}
|
||||
}
|
||||
|
||||
const getUserIams = async userId => {
|
||||
try {
|
||||
const { data } = await jamiClient.get(`/${userId}`)
|
||||
|
||||
return data.iamGroups
|
||||
} catch (error) {
|
||||
if (error.response.status !== 404) {
|
||||
logger.error('[Jami] error: ', error)
|
||||
Sentry.captureException(error)
|
||||
}
|
||||
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
const getAllUserAccess = async () => {
|
||||
const { data } = await jamiClient.get('/all-access')
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
const testJami = async () => {
|
||||
try {
|
||||
await jamiClient.get('/ping', { timeout: 4000 })
|
||||
logger.info('JAMI connected')
|
||||
} catch (error) {
|
||||
logger.error(error)
|
||||
logger.warn('JAMI not responding :(')
|
||||
logger.info('Are you sure you are using the latest JAMI image?')
|
||||
}
|
||||
}
|
||||
|
||||
testJami()
|
||||
|
||||
module.exports = {
|
||||
getUserIamAccess,
|
||||
getUserIams,
|
||||
getAllUserAccess,
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
const axios = require('axios')
|
||||
const { importerToken, jamiUrl } = require('../conf-backend')
|
||||
const logger = require('./logger')
|
||||
|
||||
const jamiClient = axios.create({
|
||||
params: {
|
||||
token: importerToken,
|
||||
},
|
||||
baseURL: jamiUrl,
|
||||
})
|
||||
|
||||
const getJamiClient = () => {
|
||||
if (!importerToken) {
|
||||
logger.error("Importer token not set, can't return client!")
|
||||
return null
|
||||
}
|
||||
return jamiClient
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getJamiClient,
|
||||
}
|
||||
@@ -1,46 +1,21 @@
|
||||
/* eslint-disable no-unused-vars */
|
||||
const _ = require('lodash')
|
||||
const Sentry = require('@sentry/node')
|
||||
|
||||
const logger = require('./logger')
|
||||
const { getJamiClient } = require('../util/jamiClient')
|
||||
const jamiClient = getJamiClient()
|
||||
|
||||
const getIAMAccessFromJami = async (user, attempt = 1) => {
|
||||
const { id, iamGroups } = user
|
||||
|
||||
try {
|
||||
const { data: iamAccess } = await jamiClient.post('/', {
|
||||
userId: id,
|
||||
iamGroups,
|
||||
})
|
||||
|
||||
return iamAccess
|
||||
} catch (error) {
|
||||
if (attempt > 3) {
|
||||
logger.error(error)
|
||||
Sentry.captureException(error)
|
||||
|
||||
return {}
|
||||
}
|
||||
|
||||
return getIAMAccessFromJami(user, attempt + 1)
|
||||
}
|
||||
}
|
||||
const { getUserIamAccess } = require('./jami')
|
||||
|
||||
const getAccessFromIAMs = async user => {
|
||||
if (user.iamGroups.length === 0) return {}
|
||||
|
||||
const { iamAccess, specialGroup } = await getUserIamAccess(user)
|
||||
|
||||
const access = {}
|
||||
if (_.isObject(iamAccess)) {
|
||||
Object.keys(iamAccess).forEach(code => {
|
||||
access[code] = iamAccess[code]
|
||||
})
|
||||
}
|
||||
|
||||
const iamAccess = await getIAMAccessFromJami(user)
|
||||
|
||||
if (!_.isObject(iamAccess)) return access
|
||||
Object.keys(iamAccess).forEach(code => {
|
||||
access[code] = iamAccess[code]
|
||||
})
|
||||
|
||||
return access
|
||||
return { access, specialGroup }
|
||||
}
|
||||
|
||||
const getOrganizationAccess = async user => {
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
const Sentry = require('@sentry/node')
|
||||
const Tracing = require('@sentry/tracing')
|
||||
|
||||
const { sentryRelease, sentryEnvironment, runningInCI } = require('../conf-backend')
|
||||
const { sentryRelease, sentryEnvironment, runningInCI, isStaging } = require('../conf-backend')
|
||||
|
||||
const initializeSentry = app => {
|
||||
if (!sentryRelease || !sentryEnvironment || runningInCI) return
|
||||
if (!sentryRelease || !sentryEnvironment || runningInCI || isStaging) return
|
||||
|
||||
Sentry.init({
|
||||
dsn: 'https://020b79f0cbb14aad94cc9d69a1ea9d52@sentry.cs.helsinki.fi/2',
|
||||
|
||||
@@ -1,25 +1,26 @@
|
||||
FROM node:14.17-alpine3.13 as base
|
||||
FROM registry.access.redhat.com/ubi8/nodejs-14-minimal
|
||||
|
||||
WORKDIR /usr/src/app
|
||||
ENV TZ="Europe/Helsinki"
|
||||
|
||||
FROM base as development
|
||||
COPY ./package* /usr/src/app/
|
||||
RUN npm ci
|
||||
COPY . /usr/src/app/
|
||||
EXPOSE 3000
|
||||
CMD ["npm", "start"]
|
||||
WORKDIR /opt/app-root/src
|
||||
|
||||
EXPOSE 5000
|
||||
|
||||
ENV NODE_ENV=production
|
||||
|
||||
ARG BASE_PATH
|
||||
ENV PUBLIC_URL=$BASE_PATH
|
||||
|
||||
FROM base as production
|
||||
ARG SENTRY_ENVIRONMENT
|
||||
ENV REACT_APP_SENTRY_ENVIRONMENT=$SENTRY_ENVIRONMENT
|
||||
ARG SENTRY_RELEASE
|
||||
ENV REACT_APP_SENTRY_RELEASE=$SENTRY_RELEASE
|
||||
ARG BUILT_AT
|
||||
ENV REACT_APP_BUILT_AT=$BUILT_AT
|
||||
COPY --chown=node:node ./package* /usr/src/app/
|
||||
RUN npm ci --production
|
||||
COPY --chown=node:node . /usr/src/app/
|
||||
RUN npm run build && npm install -g serve@12.0.0 && chown -R node:node /usr/src/app/build
|
||||
USER node
|
||||
EXPOSE 5000
|
||||
|
||||
ARG STAGING
|
||||
ENV REACT_APP_STAGING=$STAGING
|
||||
|
||||
COPY ./package* ./
|
||||
RUN npm ci --omit-dev
|
||||
COPY . .
|
||||
|
||||
RUN npm run build && npm install -g serve@12.0.0
|
||||
|
||||
CMD ["serve", "--single", "--listen", "5000", "build"]
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
FROM node:14.17-alpine3.13
|
||||
|
||||
ENV TZ="Europe/Helsinki"
|
||||
|
||||
WORKDIR /opt/app-root/src
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
COPY ./package* ./
|
||||
RUN npm ci
|
||||
COPY . .
|
||||
|
||||
CMD ["npm", "start"]
|
||||
@@ -34,7 +34,7 @@ export default {
|
||||
**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.`,
|
||||
ThesisWritersOfTheFaculty: `Sisältää kyseisenä vuonna kandidaatintutkielman tai pro gradu -tutkielman 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.
|
||||
@@ -55,25 +55,27 @@ export default {
|
||||
|
||||
Valmistumisajoista on **vähennetty lakisääteiset poissaolot**.
|
||||
|
||||
Vihreä **katkoviiva** kuvastaa tavoiteaikaa. Keltainen katkoviiva on tavoiteaika + yksi vuosi.\n
|
||||
Vihreä **vaakapalkki** merkitsee, että keskimääräinen valmistumisaika on alle tai yhtä kuin tavoiteaika, keltainen palkki merkitsee korkeintaan vuoden ylitystä
|
||||
ja punainen yli vuoden tavoiteajan ylitystä.
|
||||
|
||||
Palkissa oleva lukumäärä kertoo **montako opiskelijaa** kuhunkin valmistuneiden ryhmään kuuluu.
|
||||
Tarkastellessa valmistumisaikoja aloitusvuosittain palkissa näkyy myös **kuinka suuri osa** kyseisenä vuonna tiedekuntaan/opinto-ohjelmaan opinto-oikeuden saaneista on valmistunut.
|
||||
Palkissa oleva lukumäärä kertoo **montako opiskelijaa** kuhunkin valmistuneiden ryhmään kuuluu.
|
||||
|
||||
Vihreä **vaakapalkki** kuvaa tavoiteajassa (tai alle) valmistuneista, keltainen palkki korkeintaan vuodella tavoiteajan ylittäneitä
|
||||
ja punainen yli vuodella tavoiteajan ylittäneitä.
|
||||
|
||||
Säädöt:
|
||||
|
||||
**Breakdown**: Näyttää, kuinka moni opiskelija valmistui ajallaan, korkeintaan vuoden myöhässä tavoiteajasta tai tätä myöhemmin.
|
||||
|
||||
**Median time**: Näyttää kyseisenä vuonna/lukuvuonna valmistuneiden opiskelijoiden opintojen keston **mediaanin** kuukausina.
|
||||
|
||||
**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.
|
||||
|
||||
**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.
|
||||
|
||||
**Median time**: Näyttää kyseisenä vuonna valmistuneiden opiskelijoiden opintojen keston **mediaanin** kuukausina.
|
||||
Mediaaninäkymässä vihreä **katkoviiva** kuvastaa tavoiteaikaa. Keltainen katkoviiva on tavoiteaika + yksi vuosi.\n
|
||||
|
||||
**Mean time**: Näyttää kyseisenä vuonna valmistuneiden opiskelijoiden opintojen keston **keskiarvon** kuukausina.`,
|
||||
Tarkastellessa valmistumisaikoja aloitusvuosittain palkissa näkyy myös **kuinka suuri osa** kyseisenä vuonna tiedekuntaan/opinto-ohjelmaan opinto-oikeuden saaneista on valmistunut.
|
||||
`,
|
||||
|
||||
// Faculty Student population
|
||||
GraduatedToggle: `
|
||||
@@ -85,7 +87,7 @@ export default {
|
||||
Kuvaa tiedekunnan ohjelmissa opiskelevien opiskelijoden tilastoja. Esimerkiksi 2020-2021 aloittaneiden opintopistejakaumaa.
|
||||
|
||||
HUOM!
|
||||
- **All** Sisältää kaikki opiskelijat, joiden opinto-oikeudella on aloituspäivä kyseisenä vuonna, riippumatta siitä, onko opiskelija aloittanut opiskelemaan ko. vuonna.
|
||||
- **All** Sisältää opiskelijat, jotka siirtyneet ohjelmaan tai siitä ulos sekä aloittaneet ohjelmassa
|
||||
- **Currently enrolled** Sisältää kaikki tällä hetkellä läsnäolevat olevat opiskelija.
|
||||
- **Absent** Sisältää kaikki lakisääteisistä syistä poissaolevat opiskelijat.
|
||||
- **Started studying** Sisältää kaikki opiskelijat, jotka ovat aloittaneet opiskelemaan koulutusohjelmassa kyseisenä lukuvuonna.
|
||||
@@ -93,22 +95,18 @@ export default {
|
||||
joissa opiskelija on laiminlyönyt ilmoittautumisen tälle lukukaudelle.
|
||||
|
||||
Mukana ovat oletusarvoisesti myös vaihto-opiskelijat, ohjelmaan ja siitä pois siirtyneet opiskelijat sekä erillisopinto-oikeudella opiskelevat.
|
||||
Taulukko näyttää myös näistä opiskelijoista miesten, naisten ja suomalaisten osuudet sekä valmistuneiden määrän.
|
||||
|
||||
Yläosan valikosta on mahdollista valita tarkasteluun yhden opinto-ohjelman opiskelijat tietylle vuodelle. Sekä opiskelijoiden yleistiedot että edistyminen opintopisteittäin
|
||||
kuvaavat tällöin kyseisen opintosuunnan opiskelijoita.`,
|
||||
Taulukko näyttää myös näistä opiskelijoista miesten, naisten ja suomalaisten osuudet sekä valmistuneiden määrän. `,
|
||||
StudentProgress: `
|
||||
Kuvaa tiedekuntaan kuuluvien eri ohjemien opinto-oikeudella varustettujen opiskelijoiden etenemistä opintopisteissä mitattuna. Esimerkiksi 2021-2022 aloittaneiden opintopistejakaumaa.
|
||||
|
||||
- Bachelor: Kandidaatin opinto-oikeudella varustettujen opiskelijoiden eteneminen
|
||||
- Bachelor + Master: Kandi+maisteriopinto-oikeudella opiskelleiden ja siitä maisteriksi ohjelmiin siirtyneiden etenenminen
|
||||
- Bachelor + Master: Kandi+maisteriopinto-oikeudella opiskelleiden ja siitä maisteriksi ohjelmiin siirtyneiden etenenminen. Aloitusvuodeksi on merkitty kandiohjelman aloitusvuosi.
|
||||
Opinto-ohjelmaksi on merkitty maisteriohjelma, johon opiskelija on siirtynyt kandiksi valmistumisen jälkeen.
|
||||
- Master: Pelkällä maisterintutkinnon opinto-oikeudella varustettujen opiskelijoiden eteneminen
|
||||
- Doctor: Tohtorin ja lisensiaatin opinto-oikeudella varustettujen opiskelijoiden eteneminen
|
||||
- Doctor: Tohtorin opinto-oikeudella varustettujen opiskelijoiden eteneminen
|
||||
|
||||
HUOM! Mukana ovat myös muuhun kuin valittuun koulutusohjelmaan tai opintosuuntaan tehdyt opintopisteet.
|
||||
HUOM! Mukana ovat myös muuhun kuin valittuun koulutusohjelmaan tai opintosuuntaan tehdyt opintopisteet. Opintopisteet on laskettu opinto-oikeuden alkamisesta, eikä tällä hetkellä ota huomioon mahdollisia hyväksilukuja.
|
||||
|
||||
Mukana ovat oletusarvoisesti myös vaihto-opiskelijat, erillisopinto-oikeudella opiskelevat ja ohjelmaan siirtyneet.
|
||||
|
||||
Mikäli yläosan valikosta valittuna on jokin tietty opintosuunta, myös nämä graafit ja taulukot kuvaavat kyseisen opintosuunnan opiskelijoita.
|
||||
`,
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
export default {
|
||||
enrollmentStatus: {
|
||||
label: 'Filter students present or absent',
|
||||
label: 'Filter students present, absent or passive',
|
||||
short:
|
||||
'Suodata opiskelijoita sen mukaan ovatko he ilmoittautuneet läsnä- tai poissaoleviksi kyseiselle lukukaudelle',
|
||||
'Suodata opiskelijoita sen mukaan ovatko he joko ilmoittautuneet läsnä- tai poissaoleviksi tai jääneet passiivisiksi kyseiselle lukukaudelle. Huom. osa passiivisista opiskelijoista on jo valmistunut ko. ohjelmasta.',
|
||||
},
|
||||
studyrightStatus: {
|
||||
label: 'Rajaa opiskelijoita opinto-oikeuden statuksen mukaan',
|
||||
|
||||
@@ -17,6 +17,7 @@ export default {
|
||||
Opiskelijoiden kumulatiiviset opintopistekertymät opiskelijakohtaisina kuvaajina.
|
||||
Kun hiiren vie kuvaajien päälle, saa näkyviin jokaista kuvaajaa vastaavan opiskelijanumeron.
|
||||
Opiskelijanumeron yhteydessä olevaa ympyrää klikkaamalla siirtyy ”student statistics” puolelle ja näkee valitun opiskelijan opintotiedot.
|
||||
Timantin muotoinen ikoni kertoo opiskelijan valmistuneen valitusta koulutusohjelmasta kyseisellä hetkellä.
|
||||
|
||||
X-akselin skaalaa voi säätää harmaista painikkeista ”1m”, ”3m”, ”6m”, ”YTD”, ”1y” ja ”ALL” tai kuvaajan alla olevan sinisen näkymän harmaita ”kahvoja” siirtämällä.
|
||||
Y-akselin skaalaa voi säätää harmaista painikkeista ”small” ”medium” ja "large”`,
|
||||
@@ -49,27 +50,47 @@ export default {
|
||||
|
||||
**Cumulative**-valinnan ollessa käytössä kunkin opiskelijan kaikki aiemmat opintopisteet lasketaan mukaan opintopistemäärään. Muutoin vain pylvästä vastaavalla aikavälillä kirjatut opintopisteet otetaan huomioon.
|
||||
`,
|
||||
CoursesOfPopulation: `**Courses of class**
|
||||
CoursesOfClass: `**Courses of class**
|
||||
|
||||
Tämä taulukko sisältää näkyvissä olevien opiskelijoiden kaikki kurssisuoritukset.
|
||||
|
||||
Opiskelijat on mahdollista suodattaa kurssin perusteella käyttämällä kurssirivin vasemmassa laidassa olevaa suodatinpainiketta.
|
||||
Kurssisuodatin aukeaa ikkunan vasempaan laitaan ja tarjoaa lisää vaihtoehtoja suodatukseen. Voit myös valita monta kurssia kerralla suodattimeen.
|
||||
|
||||
Klikkaamalla kurssin nimen vieressä olevaa sinistä nuolta, siirryt ko. kurssin *Course Statistics* -näkymään.
|
||||
Klikkaamalla kurssin nimen vieressä olevaa sinistä nuolta, siirryt ko. kurssin **Course Statistics** -näkymään.
|
||||
|
||||
Taulukon tietoja voi suodattaa antamalla opiskelijoiden vähimmäismäärän kurssilla kenttään *Limit to courses where student number at least*.
|
||||
Lisäksi kurssin nimi- ja koodisarakkeissa on hakukentät *("filter here")* suodattamista varten.
|
||||
Suodattimet saa tyhjennettyä painamalla kentän vieressä olevaa ruksia.
|
||||
Lisäksi kurssin nimi **name** ja koodisarakkeissa **code** suodatinkuvaketta klikkaamalla tulee esiin hakukentät suodattamista varten.
|
||||
Suodattimet saa tyhjennettyä painamalla kentän vieressä olevaa ruksia tai valitsemall valikon alaosasta **Reset column filter**.
|
||||
|
||||
**Huom! Nämä suodattimet vaikuttavat vain tähän taulukkoon, eivät koko populatioon.**
|
||||
|
||||
**Taulukot**
|
||||
|
||||
- **pass/fail** - oletusnäkymä, joka sisältää kurssisuoritukset, hylätyt sekä yritykset. Taulukon tiedot voi järjestää sarakkeita (n, after retry, percentage, many times, per student, passed ja attempted) klikkaamalla.
|
||||
- **pass/fail w/ enrollments** - näkymä, joka sisältää suoritustiedot ilmoittautumisten kanssa. Huom.! ilmoittautumistiedot ovat saatavilla vain Sisun käyttöönoton jälkeiseltä ajalta.
|
||||
- **pass/fail** - näkymä sisältää kurssisuoritukset, hylätyt, yritykset sekä ilmoittautumistiedot. Huom.! ilmoittautumistiedot ovat saatavilla vain Sisun käyttöönoton jälkeiseltä ajalta.
|
||||
- **Total students** - sarake näyttää kaikkien opiskelijoiden määrän, mukaanlukien ilmoittautuneet ilman arvosanaa.
|
||||
- **Enrolled, no grade** - sarake näyttää niiden opiskelijoiden määrän, joilla on ilmoittautuminen muttei arvosanaa eikä hylättyä suoritusta.
|
||||
- **grades** - arvosanajakauma.
|
||||
- **when passed** - kurssisuorituksen ajankohta. Huom.! *Passed*-sarakkeen kokonaissumma ei välttämättä täsmää puolivuosittaisten lukumäärien summan kanssa johtuen tuplakirjauksista Oodissa.
|
||||
- **students** - opiskelijan arvosanat kursseittain.
|
||||
`,
|
||||
CoursesOfPopulation: `**Courses of Population**
|
||||
|
||||
Tämä taulukko sisältää näkyvissä olevien opiskelijoiden kaikki kurssisuoritukset.
|
||||
|
||||
Opiskelijat on mahdollista suodattaa kurssin perusteella käyttämällä kurssirivin vasemmassa laidassa olevaa suodatinpainiketta.
|
||||
Kurssisuodatin aukeaa ikkunan vasempaan laitaan ja tarjoaa lisää vaihtoehtoja suodatukseen. Voit myös valita monta kurssia kerralla suodattimeen.
|
||||
|
||||
Klikkaamalla kurssin nimen vieressä olevaa sinistä nuolta, siirryt ko. kurssin **Course Statistics** -näkymään.
|
||||
|
||||
Taulukon tietoja voi suodattaa antamalla opiskelijoiden vähimmäismäärän kurssilla kenttään *Limit to courses where student number at least*.
|
||||
Lisäksi kurssin nimi **name** ja koodisarakkeissa **code** suodatinkuvaketta klikkaamalla tulee esiin hakukentät suodattamista varten.
|
||||
Suodattimet saa tyhjennettyä painamalla kentän vieressä olevaa ruksia tai valitsemall valikon alaosasta **Reset column filter**.
|
||||
|
||||
**Huom! Nämä suodattimet vaikuttavat vain tähän taulukkoon, eivät koko populatioon.**
|
||||
|
||||
**Taulukot**
|
||||
- **pass/fail** - näkymä sisältää kurssisuoritukset, hylätyt, yritykset sekä ilmoittautumistiedot. Huom.! ilmoittautumistiedot ovat saatavilla vain Sisun käyttöönoton jälkeiseltä ajalta.
|
||||
- **grades** - arvosanajakauma.
|
||||
`,
|
||||
Search: `Tässä osiossa voi tarkastella koulutusohjelmakohtaisia populaatioita sisäänotto-lukuvuoden mukaisesti,
|
||||
vuosikurssi kerrallaan. Uudet koulutusohjelmat ovat pääsääntöisesti alkaneet 1.8.2017,
|
||||
@@ -83,31 +104,74 @@ export default {
|
||||
Tägien ja opintosuuntien valinnat ovat siirtyneet tästä omiksi filtereikseen.
|
||||
Voit tehdä valinnat siirryttyäsi tarkastelemaan ohjelman tilastoja.
|
||||
`,
|
||||
Students: `**Students**
|
||||
StudentsGuidanceGroups: `**Students**
|
||||
|
||||
Taulukko näyttää oletusarvoisesti vain opiskelijanumerot, mutta harmaata liukukytkintä klikkaamalla saa näkyviin opiskelijoiden nimet.
|
||||
|
||||
Klikkaamalla opiskelijanumeron vieressä olevaa sinistä henkilösymbolia, siirryt kyseisen opiskelijan "student statistic" -näkymään.
|
||||
|
||||
Klikkaamalla "Sisu"-nuolta, siirryt tarkastelemaan opiskelijan tietoja Sisu-järjestelmässä.
|
||||
|
||||
**General**
|
||||
|
||||
Klikkaamalla opiskelijanumeron vieressä olevaa sinistä nuolta, siirtyy "student statistic" näkymään valitun opiskelijan osalta.
|
||||
Valikoituja selvennyksiä:
|
||||
- **Credits**
|
||||
- **All** listaa opiskelijan suoritetut opintopisteet valitun tarkastelujakson ajalta.
|
||||
- **Since ... ** listaa opiskelijan suoritetut opintopisteet määritellystä ajankohdasta alkaen
|
||||
- **Start of studyright**: Opiskelijan valittuun ohjelmaan liittyvän opinto-oikeuden alkupäivä
|
||||
- **Started in programme**: Opiskelijan valitussa ohjelmassa aloituspäivämäärä. Kandi+maisteriopiskelijoille kandiksi valmistumispäivämäärä +1.
|
||||
|
||||
- **credits since start of studyright:** listaa opiskelijan suoritetut opintopisteet valitun tarkastelujakson / populaation ajalta.
|
||||
- **all credits:** listaa opiskelijan kaikki opintopisteet (myös aiemmat suoritukset, eli ne jotka tehty ennen valittuun tarkastelujaksoon ja populaatioon kuulumista)
|
||||
- **transferred from:** opiskelijan vanha koulutusohjelma, josta opiskelija on siirtynyt uuteen.
|
||||
- **studytracks:** opintosuunta
|
||||
`,
|
||||
StudentsClass: `**Students**
|
||||
|
||||
Taulukko näyttää oletusarvoisesti vain opiskelijanumerot, mutta harmaata liukukytkintä klikkaamalla saa näkyviin opiskelijoiden nimet.
|
||||
|
||||
**Mandatory Courses**
|
||||
Klikkaamalla opiskelijanumeron vieressä olevaa sinistä henkilösymbolia, siirryt kyseisen opiskelijan "student statistic" -näkymään.
|
||||
|
||||
Klikkaamalla "Sisu"-nuolta, siirryt tarkastelemaan opiskelijan tietoja Sisu-järjestelmässä.
|
||||
|
||||
**General**
|
||||
|
||||
Valikoituja selvennyksiä:
|
||||
- **Credits**
|
||||
- **All** listaa opiskelijan suoritetut opintopisteet valitun tarkastelujakson ajalta
|
||||
- **HOPS** listaa opiskelijan kaikki HOPSiin sijoitut opintopisteet (myös aiemmat suoritukset, eli ne jotka tehty ennen valittuun tarkastelujaksoon ja populaatioon kuulumista)
|
||||
- **Since start in programme** listaa opiskelijan ohjelmassa aloittamisen jälkeen suoritut opintopisteet
|
||||
- **Transferred from:** opiskelijan vanha koulutusohjelma, josta opiskelija on siirtynyt uuteen.
|
||||
- **Start of studyright**: Opiskelijan valittuun ohjelmaan liittyvän opinto-oikeuden alkupäivä
|
||||
- **Started in programme**: Opiskelijan ko. ohjelmassa aloituspäivämäärä. Kandi+maisteriopiskelijoille kandiksi valmistumispäivämäärä +1
|
||||
- **Semesters**: Läsnäololukukausien lukumäärä
|
||||
|
||||
**Courses**
|
||||
|
||||
Tällä välilehdellä näkyy, mitä pakollisiksi määriteltyjä kursseja opiskelija on suorittanut.
|
||||
Kurssit voi määrittää kohdassa "Study program" > "Overview" > haluttu koulutusohjelma. Scrollatessa kurssitaulukkoa alaspäin, kurssin nimen saa näkyviin viemällä kursorin halutun solun kohdalle.
|
||||
|
||||
|
||||
**Tags**
|
||||
|
||||
Opiskelijoille voi luoda tag:eja ryhmittelyä varten kohdassa "Study Program" > "Overview" > haluttu koulutusohjelma.
|
||||
|
||||
**Progress**
|
||||
|
||||
Tämä näkymä on tällä hetkellä käytössä vain kandivaiheen opinto-ohjelmissa.
|
||||
Tällä välilehdellä näkyy opintojen kehitys annettujen kriteerien mukaan akateemista vuotta kohti. Kurssi merkataan tehdyksi, jos sillä on suoritusmerkintä tai hyväksiluku
|
||||
minä tahansa lukuvuonna. Opintopistekriteerin täyttämiseen vaaditaan, että opintopisteet on suoritettu kyseisen akateemisen vuoden aikana.
|
||||
|
||||
|
||||
**Huomaa**, että klikkaamalla taulukon yläreunassa olevaa kolme pistettä sisältävää kuvaketta, voit ladata itsellesi Excel tiedoston. Tiedosto sisältää lisäksi muita sarakkeita, kuten läsnäolon lukukausittain sekä yhteystiedot.
|
||||
Yhteystietojen käyttö on kuitenkin sallittua vain opintoneuvontaan liittyvissä asiossa.
|
||||
|
||||
**Download**
|
||||
`,
|
||||
StudentsCustom: `**Students**
|
||||
|
||||
Taulukko näyttää oletusarvoisesti vain opiskelijanumerot, mutta harmaata liukukytkintä klikkaamalla saa näkyviin opiskelijoiden nimet.
|
||||
|
||||
Klikkaamalla tätä harmaata kuvaketta, saa ladattua .xlsx tiedoston, joka sisältää kaikki "General" ja "Mandatory Courses" välilehtien tiedot.
|
||||
Klikkaamalla opiskelijanumeron vieressä olevaa sinistä henkilösymbolia, siirryt kyseisen opiskelijan "student statistic" -näkymään.
|
||||
|
||||
Klikkaamalla "Sisu"-nuolta, siirryt tarkastelemaan opiskelijan tietoja Sisu-järjestelmässä.
|
||||
|
||||
**All credits** listaa opiskelijan kaikki opintopisteet (myös aiemmat suoritukset, eli ne jotka tehty ennen valittuun tarkastelujaksoon ja populaatioon kuulumista)
|
||||
|
||||
`,
|
||||
Filters: {
|
||||
Add: `Harmaata ”add” painiketta klikkaamalla aukeaa filters- eli suodatinvalikko,
|
||||
|
||||
@@ -34,6 +34,21 @@ export default {
|
||||
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ä.
|
||||
|
||||
**Transferred credits**: Sisältää opintopisteet, jotka on suoritettu kyseisenä vuonna, ja hyväksiluettu tähän koulutusohjelmaan. Mukana ovat kaikenlaisilla opinto-oikeuksilla hyväksiluetut opintopisteet.
|
||||
**Non-degree students**: Opiskelijat, joilla ei ole tutkinto-oikeutta Helsingin yliopistossa.
|
||||
|
||||
**Transferred credits**: Sisältää opintopisteet, jotka ovat suoritettu kyseisenä vuonna, ja hyväksiluettu tähän koulutusohjelmaan. Mukana ovat kaikenlaisilla opinto-oikeuksilla hyväksiluetut opintopisteet
|
||||
`,
|
||||
StudentsOfProgrammeCourses: `
|
||||
Sisältää kurssien opiskelijat.
|
||||
|
||||
**Not completed**: Sisältää opiskelijat, jotka ovat ilmoittautuneet kurssille, mutta eivät ole suorittaneet sitä ja opiskelijat, jotka ovat saaneet hylätyn arvosanan.
|
||||
|
||||
**Major students**: Sisältää opiskelijat, joilla on ensisijainen opinto-oikeus kyseiseen koulutusohjelmaan.
|
||||
|
||||
**Non-major students**: Sisältää kurssin opiskelijat, joilla EI ole ollut suoritushetkellä ensisijaista opinto-oikeutta kyseiseen koulutusohjelmaan. Sisältää
|
||||
esimerkiksi vaihto-opiskelijat ja erillis-opinto-oikeudella opiskelevat.
|
||||
|
||||
**Transferred students**: Sisältää opiskelijat, jotka ovat suorittaneet kyseisenä vuonna hyväksiluettuja opintopisteitä tähän koulutusohjelmaan. Mukana ovat kaikenlaisilla opinto-oikeuksilla hyväksiluetut opintopisteet
|
||||
`,
|
||||
GraduatedAndThesisWritersOfTheProgramme: `
|
||||
**Wrote thesis**: Sisältää kyseisenä vuonna kandidaatintutkielman tai pro gradu-tutkielman hyväksytysti suorittaneet opiskelijat. Mukana ei ole mahdollisia arvosanan korotuksia.
|
||||
@@ -41,30 +56,30 @@ export default {
|
||||
**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.
|
||||
`,
|
||||
AverageGraduationTimes: `
|
||||
Yksittäinen palkki kertoo, kuinka moni opiskelija on valmistunut kyseisenä vuonna/lukuvuonna.
|
||||
|
||||
Vihreä **vaakapalkki** kuvaa tavoiteajassa (tai alle) valmistuneita, keltainen palkki korkeintaan vuoden tavoiteajasta myöhässä valmistuneita
|
||||
ja punainen yli vuoden tavoiteajan ylittäneitä.
|
||||
|
||||
**Breakdown**: Näyttää, kuinka moni kyseisenä kalenteri- tai lukuvuotena valmistuneista opiskelijoista valmistui ajallaan, korkeintaan vuoden myöhässä tavoiteajasta tai tätä myöhemmin.
|
||||
|
||||
**Median time**: Näyttää kyseisenä kalenteri- tai lukuvuotena valmistuneiden opiskelijoiden opintojen keston **mediaanin** kuukausina.
|
||||
|
||||
**Mean time**: Näyttää kyseisenä kalenteri- tai lukuvuotena valmistuneiden opiskelijoiden opintojen keston **keskiarvon** kuukausina.
|
||||
|
||||
Valmistumisajoista on vähennetty lakisääteiset poissaolot.
|
||||
|
||||
Yksittäinen palkki kertoo, kuinka moni opiskelija on valmistunut kyseisenä vuonna.
|
||||
|
||||
Vihreä **katkoviiva** kuvastaa tavoiteaikaa. Keltainen katkoviiva on tavoiteaika + yksi vuosi.\n
|
||||
Vihreä **vaakapalkki** merkitsee, että keskimääräinen valmistumisaika on alle tai yhtä kuin tavoiteaika, keltainen palkki merkitsee korkeintaan vuoden ylitystä
|
||||
ja punainen yli vuoden tavoiteajan ylitystä.`,
|
||||
|
||||
Valmistumisajoista on vähennetty lakisääteiset poissaolot.`,
|
||||
AverageGraduationTimesStudytracks: `
|
||||
Yksittäinen palkki kertoo, kuinka moni kyseisenä **lukukautena aloittaneista** opiskelijoista on valmistunut sekä heidän prosentuaalinen osuutensa koko vuosikurssista.
|
||||
Yksittäinen palkki kertoo, kuinka moni kyseisenä **lukuvuotena ALOITTANEISTA** opiskelijoista on valmistunut.
|
||||
|
||||
**Median time**: Näyttää valmistuneiden opiskelijoiden opintojen keston **mediaanin** kuukausina.
|
||||
Vihreä **vaakapalkki** kuvaa tavoiteajassa (tai alle) valmistuneita, keltainen palkki korkeintaan vuoden tavoiteajasta myöhässä valmistuneita
|
||||
ja punainen yli vuoden tavoiteajan ylittäneitä.
|
||||
|
||||
**Mean time**: Näyttää valmistuneiden opiskelijoiden opintojen keston **keskiarvon** kuukausina.
|
||||
|
||||
Valmistumisajoista on vähennetty lakisääteiset poissaolot.
|
||||
**Breakdown**: Näyttää, kuinka moni kyseisenä lukuvuotena aloittaneista, jo valmistuneista, opiskelijoista on valmistui ajallaan, korkeintaan vuoden myöhässä tavoiteajasta tai tätä myöhemmin.
|
||||
|
||||
**Median time**: Näyttää valmistuneiden opiskelijoiden opintojen keston **mediaanin** kuukausina sekä heidän prosentuaalisen osuutensa koko vuosikurssista.
|
||||
Vihreä **katkoviiva** kuvastaa tavoiteaikaa. Keltainen katkoviiva on tavoiteaika + yksi vuosi.
|
||||
|
||||
|
||||
Vihreä **katkoviiva** kuvastaa tavoiteaikaa. Keltainen katkoviiva on tavoiteaika + yksi vuosi.\n
|
||||
Vihreä **vaakapalkki** merkitsee, että keskimääräinen valmistumisaika on alle tai yhtä kuin tavoiteaika, keltainen palkki merkitsee korkeintaan vuoden ylitystä
|
||||
ja punainen yli vuoden tavoiteajan ylitystä.`,
|
||||
Valmistumisajoista on vähennetty lakisääteiset poissaolot.`,
|
||||
ProgrammesBeforeOrAfter: `
|
||||
**Mikäli valittuna on kandiohjelma:** Sisältää maisteriohjelmat, joissa tämän kandiohjelman opiskelijat ovat aloittaneet valmistuttuaan kandeiksi. Vuosijaottelu on tehty maisteriohjelmassa aloittamisen perusteella.
|
||||
|
||||
@@ -96,13 +111,10 @@ export default {
|
||||
|
||||
Mikäli yläosan valikosta valittuna on jokin tietty opintosuunta, myös nämä graafit ja taulukot kuvaavat kyseisen opintosuunnan opiskelijoita.
|
||||
`,
|
||||
NonDegree: `
|
||||
Opiskelijat, joilla ei ole tutkinto-oikeutta Helsingin yliopistossa.
|
||||
Name: `
|
||||
Pitkät kurssinimet on lyhennetty. Näet ne kokonaan laittamalla hiiren nimen yläpuolelle.
|
||||
`,
|
||||
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
|
||||
NotCompleted: `
|
||||
Sisältää opiskelijat, jotka ovat ilmoittautuneet kurssille, mutta eivät ole suorittaneet sitä ja opiskelijat, jotka ovat saaneet hylätyn arvosanan.
|
||||
`,
|
||||
}
|
||||
|
||||
@@ -284,6 +284,8 @@ export const getTargetCreditsForProgramme = code => {
|
||||
if (code === 'MH90_001') return 180
|
||||
if (code.includes('MH')) return 120
|
||||
return 180
|
||||
// There are also doctoral degrees of 40, but probably not necessary
|
||||
// Those codes begin with 'LIS' or 'T'
|
||||
}
|
||||
|
||||
const getMonthsForDegree = code => getTargetCreditsForProgramme(code) / (60 / 12)
|
||||
@@ -352,7 +354,7 @@ export const resolveStudyPlan = (studyPlans, studyRight) => {
|
||||
.filter(e => e.element_detail.type === 20)
|
||||
.sort((a, b) => new Date(b.startdate) - new Date(a.startdate))[0] || {}
|
||||
if (!code) return null
|
||||
return studyPlans.find(p => p.programme_code === code)
|
||||
return studyPlans.find(p => p.programme_code === code && p.studyrightid === studyRight.studyrightid)
|
||||
}
|
||||
|
||||
// These are the new Bachelor's programmes in Matlu, that have BH possibility
|
||||
|
||||
@@ -0,0 +1,235 @@
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { Modal, Form, Button, TextArea, Loader } from 'semantic-ui-react'
|
||||
import qs from 'query-string'
|
||||
import {
|
||||
useGetSavedCourseListsQuery,
|
||||
useCreateCourseListMutation,
|
||||
useUpdateCourseListMutation,
|
||||
useDeleteCourseListMutation,
|
||||
} from 'redux/completedCoursesSearch'
|
||||
import SearchHistory from 'components/SearchHistory'
|
||||
|
||||
const CompletedCoursesSearch = ({ setValues, history, location }) => {
|
||||
const [modal, setModal] = useState(false)
|
||||
const [courseInput, setCourseInput] = useState('')
|
||||
const [studentInput, setStudentInput] = useState('')
|
||||
const [name, setName] = useState('')
|
||||
const courseLists = useGetSavedCourseListsQuery()
|
||||
const [searchList, setSearches] = useState(null)
|
||||
const [selectedSearchId, setSelectedSearchId] = useState('')
|
||||
const isFetchingOrLoading = courseLists.isLoading || courseLists.isFetching
|
||||
const isError = courseLists.isError || (courseLists.isSuccess && !courseLists.data)
|
||||
const [updateCourseList, { isLoading: updateIsLoading, data: updatedData }] = useUpdateCourseListMutation()
|
||||
const [createCourseList, { isLoading: createIsLoading, data: createdData }] = useCreateCourseListMutation()
|
||||
const [deleteCourseList, { isLoading: deleteIsLoading, data: deletedData }] = useDeleteCourseListMutation()
|
||||
|
||||
const parseQueryFromUrl = () => {
|
||||
const query = qs.parse(location.search)
|
||||
if (!Array.isArray(query.courseList)) query.courseList = [query.courseList]
|
||||
if (!Array.isArray(query.studentList)) query.studentList = [query.studentList]
|
||||
return query
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (courseLists.isSuccess && courseLists.data) {
|
||||
setSearches(courseLists.data)
|
||||
}
|
||||
}, [courseLists])
|
||||
|
||||
useEffect(() => {
|
||||
if (updatedData) {
|
||||
const updatedSearches = searchList.map(s => (s.id === updatedData.id ? updatedData : s))
|
||||
setSearches(updatedSearches)
|
||||
}
|
||||
}, [updatedData])
|
||||
|
||||
useEffect(() => {
|
||||
if (createdData && !createdData.error) {
|
||||
const newList = searchList.concat(createdData)
|
||||
setSearches(newList)
|
||||
}
|
||||
}, [createdData])
|
||||
|
||||
useEffect(() => {
|
||||
if (deletedData) {
|
||||
const filteredsearches = searchList.filter(s => s.id !== deletedData)
|
||||
setSearches(filteredsearches)
|
||||
}
|
||||
}, [deletedData])
|
||||
|
||||
useEffect(() => {
|
||||
setImmediate(() => {
|
||||
if (location.search) {
|
||||
const query = parseQueryFromUrl()
|
||||
setValues(query)
|
||||
}
|
||||
})
|
||||
}, [location.search])
|
||||
|
||||
const clearForm = () => {
|
||||
setCourseInput('')
|
||||
setStudentInput('')
|
||||
setSelectedSearchId('')
|
||||
}
|
||||
|
||||
const pushQueryToUrl = query => {
|
||||
setImmediate(() => {
|
||||
const searchString = qs.stringify(query)
|
||||
history.push({ search: searchString })
|
||||
})
|
||||
}
|
||||
|
||||
const handleClose = () => {
|
||||
setModal(false)
|
||||
clearForm()
|
||||
}
|
||||
|
||||
const onDelete = () => {
|
||||
deleteCourseList({ id: selectedSearchId })
|
||||
clearForm()
|
||||
}
|
||||
|
||||
const onSave = () => {
|
||||
const courseList = courseInput
|
||||
.split(/[\s,]+/)
|
||||
.map(code => code.trim().toUpperCase())
|
||||
.filter(code => code !== '')
|
||||
if (selectedSearchId !== '') {
|
||||
updateCourseList({ id: selectedSearchId, courseList })
|
||||
} else {
|
||||
createCourseList({ courseList, name })
|
||||
}
|
||||
}
|
||||
const onSelectSearch = selectedId => {
|
||||
if (!selectedId) {
|
||||
clearForm()
|
||||
return
|
||||
}
|
||||
const { id: selectedSearchId } = selectedId
|
||||
const selectedSearch = searchList?.find(search => search.id === selectedSearchId)
|
||||
if (selectedSearch) {
|
||||
setCourseInput(selectedSearch.courseList.join(', '))
|
||||
setName(selectedSearch.name)
|
||||
setSelectedSearchId(selectedSearch.id)
|
||||
}
|
||||
}
|
||||
|
||||
const onClicker = e => {
|
||||
e.preventDefault()
|
||||
|
||||
const courseList = courseInput
|
||||
.split(/[\s,]+/)
|
||||
.map(code => code.trim().toUpperCase())
|
||||
.filter(c => c !== '')
|
||||
const studentList = studentInput
|
||||
.split(/[\s,]+/)
|
||||
.map(code => code.trim())
|
||||
.filter(s => s !== '')
|
||||
.map(s => (s.length === 8 ? `0${s}` : s))
|
||||
|
||||
if (courseList.length === 0 || studentList.length === 0 || studentList[0] === null) {
|
||||
return
|
||||
}
|
||||
|
||||
setValues({
|
||||
studentList,
|
||||
courseList,
|
||||
})
|
||||
|
||||
pushQueryToUrl({ courseList, studentList })
|
||||
|
||||
handleClose()
|
||||
}
|
||||
|
||||
if (isError) return <h3>Something went wrong, please try refreshing the page.</h3>
|
||||
if (isFetchingOrLoading) return <Loader active style={{ marginTop: '15em' }} />
|
||||
|
||||
return (
|
||||
<Modal
|
||||
trigger={
|
||||
<Button size="small" color="blue" onClick={() => setModal(true)} data-cy="open-completed-courses-modal-button">
|
||||
Search completed courses of students
|
||||
</Button>
|
||||
}
|
||||
open={modal}
|
||||
onClose={handleClose}
|
||||
size="small"
|
||||
>
|
||||
<Modal.Content>
|
||||
<Form>
|
||||
<h2>Search completed courses of students</h2>
|
||||
<Form.Field>
|
||||
<em>Insert one or more student numbers, separated by a space, a newline, or a comma.</em>
|
||||
<TextArea
|
||||
value={studentInput}
|
||||
placeholder="012345678, 12345678"
|
||||
onChange={e => setStudentInput(e.target.value)}
|
||||
data-cy="student-no-input"
|
||||
/>
|
||||
</Form.Field>
|
||||
<Form.Field>
|
||||
<em>Insert one or more courses, separated by a space, a newline, or a comma.</em>
|
||||
<TextArea
|
||||
value={courseInput}
|
||||
placeholder="TKT12345, PSYK-123"
|
||||
onChange={e => setCourseInput(e.target.value)}
|
||||
data-cy="course-list-input"
|
||||
/>
|
||||
</Form.Field>
|
||||
<Form.Field>
|
||||
<Form.Field data-cy="search-name">
|
||||
<em> Insert name for this course list if you wish to save it </em>
|
||||
<Form.Input
|
||||
disabled={selectedSearchId !== ''}
|
||||
value={name}
|
||||
placeholder="name"
|
||||
onChange={e => setName(e.target.value)}
|
||||
/>
|
||||
</Form.Field>
|
||||
</Form.Field>
|
||||
<SearchHistory
|
||||
header="Saved courselists"
|
||||
items={
|
||||
searchList
|
||||
? searchList.map(list => ({
|
||||
...list,
|
||||
text: list.name,
|
||||
timestamp: list.updatedAt,
|
||||
params: { id: list.id },
|
||||
}))
|
||||
: []
|
||||
}
|
||||
updateItem={() => null}
|
||||
handleSearch={onSelectSearch}
|
||||
/>
|
||||
<Modal.Actions>
|
||||
<Button
|
||||
data-cy="save-courselist"
|
||||
disabled={!name || updateIsLoading || createIsLoading}
|
||||
loading={updateIsLoading || createIsLoading}
|
||||
floated="left"
|
||||
icon="save"
|
||||
onClick={onSave}
|
||||
content="Save"
|
||||
/>
|
||||
<Button
|
||||
disabled={!selectedSearchId || deleteIsLoading}
|
||||
negative
|
||||
floated="left"
|
||||
icon="trash"
|
||||
onClick={onDelete}
|
||||
content="Delete"
|
||||
/>
|
||||
|
||||
<Button onClick={handleClose}>Cancel</Button>
|
||||
<Button positive onClick={e => onClicker(e)} data-cy="completed-courses-search-button">
|
||||
Search
|
||||
</Button>
|
||||
</Modal.Actions>
|
||||
</Form>
|
||||
</Modal.Content>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
export default CompletedCoursesSearch
|
||||
+252
@@ -0,0 +1,252 @@
|
||||
import SortableTable from 'components/SortableTable'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { useGetCompletedCoursesQuery } from 'redux/completedCoursesSearch'
|
||||
import { Icon, Loader } from 'semantic-ui-react'
|
||||
import moment from 'moment'
|
||||
import StudentNameVisibilityToggle, { useStudentNameVisibility } from 'components/StudentNameVisibilityToggle'
|
||||
import useLanguage from 'components/LanguagePicker/useLanguage'
|
||||
import RightsNotification from 'components/RightsNotification'
|
||||
|
||||
const getColumns = (courses, showStudentNames, getTextIn) => {
|
||||
const isPassed = credit => [4, 7, 9].includes(credit)
|
||||
// 4=completed, 7=improved, 9=transferred, 10=failed
|
||||
|
||||
const getCompletion = (student, courseCode, { icon }) => {
|
||||
const completion = student.credits.find(c => c.courseCode === courseCode && isPassed(c.creditType))
|
||||
const enrollment = student.enrollments.find(e => e.courseCode === courseCode)
|
||||
if (completion === undefined) {
|
||||
if (!enrollment) {
|
||||
return icon ? null : ''
|
||||
}
|
||||
if (icon) {
|
||||
if (moment(enrollment.date) > moment().subtract(6, 'months')) {
|
||||
return <Icon fitted name="minus" color="yellow" />
|
||||
}
|
||||
return <Icon fitted name="minus" color="grey" />
|
||||
}
|
||||
|
||||
return `Latest enrollment: ${moment(enrollment.date).format('YYYY-MM-DD')}`
|
||||
}
|
||||
|
||||
const substitutionString = completion.substitution ? ` as ${completion.substitution}` : ''
|
||||
|
||||
return icon ? <Icon fitted name="check" color="green" /> : `Passed${substitutionString}`
|
||||
}
|
||||
|
||||
const getTotalPassed = s => s.credits.filter(c => isPassed(c.creditType)).length
|
||||
|
||||
const getTotalUnfinished = student => student.enrollments.length
|
||||
|
||||
const style = {
|
||||
verticalAlign: 'middle',
|
||||
textAlign: 'center',
|
||||
}
|
||||
|
||||
const studentNbrColumn = [
|
||||
{
|
||||
key: 'studentnumber',
|
||||
title: 'Student Number',
|
||||
cellProps: { style },
|
||||
getRowVal: s => s.studentNumber,
|
||||
getRowContent: s => s.studentNumber,
|
||||
},
|
||||
]
|
||||
|
||||
const getCellProps = (student, courseCode) => {
|
||||
const creditDate = student.credits.find(credit => credit.courseCode === courseCode)?.date
|
||||
const enrollmentDate = student.enrollments.find(enrollment => enrollment.courseCode === courseCode)?.date
|
||||
if (!creditDate && !enrollmentDate) {
|
||||
return { style }
|
||||
}
|
||||
const title = creditDate
|
||||
? `Passed on ${moment(creditDate).format('YYYY-MM-DD')} `
|
||||
: `Last enrollment on ${moment(enrollmentDate).format('YYYY-MM-DD')} `
|
||||
return { style, title }
|
||||
}
|
||||
|
||||
const completionStatusColumns = courses.map(course => {
|
||||
return {
|
||||
key: course.code,
|
||||
title: (
|
||||
<div key={course.code} style={{ marginLeft: '-6px', width: '8em', whiteSpace: 'normal' }}>
|
||||
<div key={`key1${course.code}`}>{course.code}</div>
|
||||
<div key={`key2${course.code}`} style={{ color: 'gray', fontWeight: 'normal' }}>
|
||||
{getTextIn(course.name)}
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
textTitle: course.code,
|
||||
cellProps: s => getCellProps(s, course.code),
|
||||
headerProps: { title: course.code },
|
||||
getRowVal: s => getCompletion(s, course.code, { icon: false }),
|
||||
getRowContent: s => getCompletion(s, course.code, { icon: true }),
|
||||
}
|
||||
})
|
||||
|
||||
const statisticColumns = [
|
||||
{
|
||||
key: 'passed',
|
||||
title: 'Passed',
|
||||
cellProps: { style },
|
||||
headerProps: { title: 'Passed' },
|
||||
getRowVal: s => getTotalPassed(s),
|
||||
getRowContent: s => getTotalPassed(s),
|
||||
child: true,
|
||||
},
|
||||
{
|
||||
key: 'unfinished',
|
||||
title: 'Unfinished',
|
||||
headerProps: { title: 'Unfinished' },
|
||||
getRowVal: s => getTotalUnfinished(s),
|
||||
getRowContent: s => getTotalUnfinished(s),
|
||||
cellProps: { style },
|
||||
child: true,
|
||||
},
|
||||
]
|
||||
|
||||
const statisticHeader = {
|
||||
key: 'statistics',
|
||||
title: <b>Totals:</b>,
|
||||
textTitle: null,
|
||||
parent: true,
|
||||
children: statisticColumns,
|
||||
}
|
||||
|
||||
const infoColumnsForCsv = [
|
||||
{
|
||||
key: 'hidden-lastname',
|
||||
title: 'Last name',
|
||||
forceToolsMode: 'none',
|
||||
getRowVal: s => s.lastName,
|
||||
headerProps: { style: { display: 'none' } },
|
||||
cellProps: { style: { display: 'none' } },
|
||||
export: true,
|
||||
},
|
||||
{
|
||||
key: 'hidden-firstnames',
|
||||
title: 'First names',
|
||||
getRowVal: s => s.firstNames,
|
||||
forceToolsMode: 'none',
|
||||
headerProps: { style: { display: 'none' } },
|
||||
cellProps: { style: { display: 'none' } },
|
||||
export: true,
|
||||
},
|
||||
{
|
||||
key: 'hidden-email',
|
||||
title: 'E-mail',
|
||||
getRowVal: s => (s.email ? s.email : ''),
|
||||
forceToolsMode: 'none',
|
||||
headerProps: { style: { display: 'none' } },
|
||||
cellProps: { style: { display: 'none' } },
|
||||
export: true,
|
||||
},
|
||||
]
|
||||
|
||||
const emailColumn = showStudentNames
|
||||
? [
|
||||
{
|
||||
key: 'email',
|
||||
title: 'Email',
|
||||
cellProps: { style },
|
||||
headerProps: { title: 'Email' },
|
||||
getRowVal: s => (s.email ? s.email : ''),
|
||||
getRowContent: s => (s.email ? s.email : ''),
|
||||
child: true,
|
||||
export: false,
|
||||
},
|
||||
]
|
||||
: []
|
||||
|
||||
const nameColumns = showStudentNames
|
||||
? [
|
||||
{
|
||||
key: 'lastname',
|
||||
title: 'Last name',
|
||||
cellProps: { style },
|
||||
getRowVal: s => s.lastName,
|
||||
getRowContent: s => s.lastName,
|
||||
export: false,
|
||||
},
|
||||
{
|
||||
key: 'firstnames',
|
||||
title: 'First names',
|
||||
cellProps: { style },
|
||||
getRowVal: s => s.firstNames,
|
||||
getRowContent: s => s.firstNames,
|
||||
export: false,
|
||||
},
|
||||
]
|
||||
: []
|
||||
|
||||
const hiddenParentColumn = {
|
||||
key: 'hiddenFiles',
|
||||
title: '',
|
||||
mergeHeader: true,
|
||||
textTitle: null,
|
||||
parent: true,
|
||||
children: infoColumnsForCsv,
|
||||
}
|
||||
|
||||
return [
|
||||
hiddenParentColumn,
|
||||
...nameColumns,
|
||||
...studentNbrColumn,
|
||||
...completionStatusColumns,
|
||||
statisticHeader,
|
||||
...emailColumn,
|
||||
]
|
||||
}
|
||||
|
||||
const CompletedCoursesSearchResults = ({ searchValues }) => {
|
||||
const { courseList, studentList } = searchValues
|
||||
const [data, setData] = useState(null)
|
||||
const showStudentNames = useStudentNameVisibility()
|
||||
const completedCoursesTable = useGetCompletedCoursesQuery({ courseList, studentList })
|
||||
const isFetchingOrLoading = completedCoursesTable.isLoading || completedCoursesTable.isFetching
|
||||
const isError = completedCoursesTable.isError || (completedCoursesTable.isSuccess && !completedCoursesTable.data)
|
||||
const { getTextIn } = useLanguage()
|
||||
|
||||
useEffect(() => {
|
||||
if (!isError && !isFetchingOrLoading) {
|
||||
setImmediate(() => {
|
||||
const data = completedCoursesTable?.data
|
||||
setData(data)
|
||||
})
|
||||
}
|
||||
}, [completedCoursesTable])
|
||||
|
||||
if (isError) return <h3>Something went wrong, please try refreshing the page.</h3>
|
||||
if (isFetchingOrLoading || data === null) return <Loader active style={{ marginTop: '15em' }} />
|
||||
|
||||
return (
|
||||
<div>
|
||||
<StudentNameVisibilityToggle />
|
||||
{data.discardedStudentNumbers?.length > 0 && (
|
||||
<RightsNotification discardedStudentNumbers={data.discardedStudentNumbers} />
|
||||
)}
|
||||
<div
|
||||
style={{ maxWidth: '100vh', overflowX: 'auto', paddingBottom: '50px' }}
|
||||
data-cy="completed-courses-table-div"
|
||||
>
|
||||
<SortableTable
|
||||
title="Completed courses search"
|
||||
getRowKey={s => s.studentNumber}
|
||||
tableProps={{
|
||||
celled: true,
|
||||
compact: 'very',
|
||||
padded: false,
|
||||
collapsing: true,
|
||||
basic: true,
|
||||
striped: true,
|
||||
singleLine: true,
|
||||
textAlign: 'center',
|
||||
}}
|
||||
columns={getColumns(data.courses, showStudentNames.visible, getTextIn)}
|
||||
data={data.students}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default CompletedCoursesSearchResults
|
||||
@@ -0,0 +1,56 @@
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { withRouter, useLocation, useHistory } from 'react-router-dom'
|
||||
import { Message, Icon } from 'semantic-ui-react'
|
||||
import useLanguage from 'components/LanguagePicker/useLanguage'
|
||||
import { useTitle } from '../../common/hooks'
|
||||
import CompletedCoursesSearch from './CompletedCoursesSearch'
|
||||
import CompletedCoursesSearchResults from './CompletedCoursesSearchResults'
|
||||
import TSA from '../../common/tsa'
|
||||
|
||||
const CompletedCourses = () => {
|
||||
useTitle('Search completed courses')
|
||||
const { language } = useLanguage()
|
||||
const [searchValues, setValues] = useState({})
|
||||
const location = useLocation()
|
||||
const history = useHistory()
|
||||
|
||||
useEffect(() => {
|
||||
if (searchValues && searchValues.courseList?.length > 0) {
|
||||
TSA.Influx.sendEvent({
|
||||
group: 'Completed Courses Tool Usage',
|
||||
name: 'completed courses tool',
|
||||
label: 'completedCoursesTool',
|
||||
value: 1,
|
||||
})
|
||||
}
|
||||
}, [searchValues])
|
||||
return (
|
||||
<div className="segmentContainer">
|
||||
<Message style={{ maxWidth: '800px', fontSize: '16px' }}>
|
||||
<Message.Header>Search completed courses</Message.Header>
|
||||
<p>
|
||||
Here you can search by a list of student and course numbers to see whether students have completed certain
|
||||
courses yet. The tool will also show if students have enrolled on the course, if they have not yet completed
|
||||
it. Course substitutions are taken into account.
|
||||
</p>
|
||||
<p>
|
||||
<Icon fitted name="check" color="green" />: Student has completed the course with a passing grade. <br />
|
||||
<Icon fitted name="minus" color="yellow" />: Student has not completed the course, but has an active
|
||||
enrollment from less than 6 months ago. <br />
|
||||
<Icon fitted name="minus" color="grey" />: Student has not completed the course, but has an enrollment from
|
||||
more than 6 months ago. <br />
|
||||
<b>Empty cell</b>: Student has no completion or enrollment for the course.
|
||||
</p>
|
||||
</Message>
|
||||
<CompletedCoursesSearch setValues={setValues} history={history} location={location} />
|
||||
|
||||
<div style={{ paddingTop: '25px' }}>
|
||||
{searchValues && searchValues.courseList?.length > 0 && searchValues.studentList?.length > 0 && (
|
||||
<CompletedCoursesSearchResults searchValues={searchValues} language={language} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default withRouter(CompletedCourses)
|
||||
@@ -628,7 +628,7 @@ const ProtoC = ({
|
||||
</Menu.Item>
|
||||
{sorterNames.map(sorterName => (
|
||||
<Menu.Item
|
||||
basic={sorter !== sorterName}
|
||||
basic={(sorter !== sorterName).toString()}
|
||||
color={sorter === sorterName ? 'blue' : 'black'}
|
||||
key={sorterName}
|
||||
active={sorter === sorterName}
|
||||
@@ -727,7 +727,7 @@ const ProtoC = ({
|
||||
</Segment>
|
||||
<RenderBelowGraph />
|
||||
<Message>
|
||||
<ReactMarkdown children={CoolDataScience.protoC} escapeHtml={false} />
|
||||
<ReactMarkdown children={CoolDataScience.protoC} />
|
||||
</Message>
|
||||
</Segment>
|
||||
)
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable babel/no-invalid-this */
|
||||
import React, { useState, useCallback, useMemo } from 'react'
|
||||
import Highcharts from 'highcharts'
|
||||
import ReactHighcharts from 'react-highcharts'
|
||||
@@ -449,7 +450,7 @@ const ProtoC = ({ programme = 'KH50_005' }) => {
|
||||
</Menu.Item>
|
||||
{sorterNames.map(sorterName => (
|
||||
<Menu.Item
|
||||
basic={sorter !== sorterName}
|
||||
basic={(sorter !== sorterName).toString()}
|
||||
color={sorter === sorterName ? 'blue' : 'black'}
|
||||
key={sorterName}
|
||||
active={sorter === sorterName}
|
||||
@@ -485,7 +486,7 @@ const ProtoC = ({ programme = 'KH50_005' }) => {
|
||||
<Message>
|
||||
{
|
||||
// eslint-disable-next-line react/no-children-prop
|
||||
<ReactMarkdown children={CoolDataScience.protoC2} escapeHtml={false} />
|
||||
<ReactMarkdown children={CoolDataScience.protoC2} />
|
||||
}
|
||||
</Message>
|
||||
</Segment>
|
||||
@@ -541,7 +542,7 @@ const ProtoC = ({ programme = 'KH50_005' }) => {
|
||||
</Menu.Item>
|
||||
{sorterNames.map(sorterName => (
|
||||
<Menu.Item
|
||||
basic={sorter !== sorterName}
|
||||
basic={(sorter !== sorterName).toString()}
|
||||
color={sorter === sorterName ? 'blue' : 'black'}
|
||||
key={sorterName}
|
||||
active={sorter === sorterName}
|
||||
@@ -594,7 +595,7 @@ const ProtoC = ({ programme = 'KH50_005' }) => {
|
||||
<Message>
|
||||
{
|
||||
// eslint-disable-next-line react/no-children-prop
|
||||
<ReactMarkdown children={CoolDataScience.protoC2} escapeHtml={false} />
|
||||
<ReactMarkdown children={CoolDataScience.protoC2} />
|
||||
}
|
||||
</Message>
|
||||
</Segment>
|
||||
|
||||
@@ -100,7 +100,7 @@ const Chart = React.memo(({ tickDates, snapshots }) => (
|
||||
return `${d.getDate()}.${d.getMonth()}.${d
|
||||
.getFullYear()
|
||||
.toString()
|
||||
.substr(-2)}`
|
||||
.substring(-2)}`
|
||||
}
|
||||
},
|
||||
tickPositions: tickDates.map(date => date.getTime()),
|
||||
@@ -292,7 +292,7 @@ const ProtoG = ({ uberdata, years, isLoading, loadingYears, getUberDispatch, get
|
||||
)}
|
||||
<Message>
|
||||
{//eslint-disable-next-line react/no-children-prop
|
||||
<ReactMarkdown children={CoolDataScience.protoC} escapeHtml={false} />
|
||||
<ReactMarkdown children={CoolDataScience.protoC} />
|
||||
}
|
||||
</Message>
|
||||
</Segment>
|
||||
|
||||
@@ -185,7 +185,7 @@ export default ({ value, changeSetting, settings, generalHelp }) => {
|
||||
<div style={{ padding: '1em' }}>
|
||||
{
|
||||
// eslint-disable-next-line react/no-children-prop
|
||||
<ReactMarkdown children={unindent(generalHelp)} escapeHtml={false} />
|
||||
<ReactMarkdown children={unindent(generalHelp)} />
|
||||
}
|
||||
</div>
|
||||
{settings.map(({ key, label, long }) => (
|
||||
@@ -196,7 +196,7 @@ export default ({ value, changeSetting, settings, generalHelp }) => {
|
||||
Valinta "<i>{label}</i>"
|
||||
</b>
|
||||
<div style={{ margin: '0.5em', fontSize: '0.9em' }}>
|
||||
<ReactMarkdown escapeHtml={false}>{unindent(long ?? '')}</ReactMarkdown>
|
||||
<ReactMarkdown>{unindent(long ?? '')}</ReactMarkdown>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -242,7 +242,7 @@ const CoursePopulation = ({
|
||||
content: {
|
||||
content: (
|
||||
<div ref={programmeRef}>
|
||||
<CustomPopulationCoursesWrapper filteredStudents={filtered} showFilter={false} />
|
||||
<CustomPopulationCoursesWrapper filteredStudents={filtered} showFilter />
|
||||
</div>
|
||||
),
|
||||
},
|
||||
@@ -372,7 +372,12 @@ const CustomPopulationCoursesWrapper = props => {
|
||||
|
||||
if (isLoading) return null
|
||||
|
||||
return <CustomPopulationCourses {...props} courses={courseStatistics} />
|
||||
return (
|
||||
<>
|
||||
<InfoBox content={infotooltips.PopulationStatistics.CoursesOfPopulation} />
|
||||
<CustomPopulationCourses {...props} courses={courseStatistics} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const mapStateToProps = ({ singleCourseStats, populations, courseSearch }) => {
|
||||
|
||||
+2
-2
@@ -71,7 +71,7 @@ const getColumns = (stats, showDetails, showGrades, userHasAccessToAllStats, alt
|
||||
},
|
||||
{
|
||||
key: 'TOTAL',
|
||||
title: 'Total Students',
|
||||
title: 'Total\nStudents',
|
||||
helpText: 'Total count of students, including enrolled students with no grade.',
|
||||
cellProps: s => ({
|
||||
style: {
|
||||
@@ -113,7 +113,7 @@ const getColumns = (stats, showDetails, showGrades, userHasAccessToAllStats, alt
|
||||
...getGradeColumns(resolveGrades(stats)),
|
||||
{
|
||||
key: 'ENROLLMENTS_MISSING_GRADE',
|
||||
title: 'Enrolled no grade',
|
||||
title: 'Enrolled,\nno grade',
|
||||
filterType: 'range',
|
||||
helpText: 'Total count of students with a valid enrollment and no passing or failing grade.',
|
||||
getRowVal: s => (s.rowObfuscated ? 5 : s.students.enrolledStudentsWithNoGrade),
|
||||
|
||||
@@ -6,7 +6,7 @@ import { useHistory } from 'react-router-dom'
|
||||
import Highcharts from 'highcharts/highstock'
|
||||
import { Button, Radio } from 'semantic-ui-react'
|
||||
import boostcanvas from 'highcharts/modules/boost-canvas'
|
||||
import _ from 'lodash/fp'
|
||||
import _ from 'lodash'
|
||||
import boost from 'highcharts/modules/boost'
|
||||
import ReactHighstock from 'react-highcharts/ReactHighstock'
|
||||
import './creditAccumulationGraphHC.css'
|
||||
@@ -31,6 +31,7 @@ const createGraphOptions = ({
|
||||
startDate,
|
||||
endDate,
|
||||
graduations,
|
||||
transfers,
|
||||
studyRightStartLine,
|
||||
}) => {
|
||||
const tooltip = {
|
||||
@@ -94,13 +95,14 @@ const createGraphOptions = ({
|
||||
min: startDate,
|
||||
max: endDate,
|
||||
plotLines: graduations
|
||||
.map(value => ({
|
||||
value,
|
||||
.map(graduation => ({
|
||||
value: graduation.value,
|
||||
color: '#a333c8',
|
||||
width: 2,
|
||||
dashStyle: 'dash',
|
||||
label: {
|
||||
text: `Graduation`,
|
||||
text: `Graduation ${graduation.studyright}`,
|
||||
fontSize: 30,
|
||||
},
|
||||
}))
|
||||
.concat(
|
||||
@@ -111,6 +113,19 @@ const createGraphOptions = ({
|
||||
dashStyle: 'dash',
|
||||
label: {
|
||||
text: `Population study start`,
|
||||
fontSize: 30,
|
||||
},
|
||||
}))
|
||||
)
|
||||
.concat(
|
||||
transfers.map(transfer => ({
|
||||
value: transfer.value,
|
||||
color: '#cbd128',
|
||||
width: 2,
|
||||
dashStyle: 'dash',
|
||||
label: {
|
||||
text: `Transfer ${transfer.studyright}`,
|
||||
fontSize: 30,
|
||||
},
|
||||
}))
|
||||
),
|
||||
@@ -138,19 +153,20 @@ const filterCourses = (
|
||||
byStudyPlanOfCode,
|
||||
cutStudyPlanCredits,
|
||||
startDate,
|
||||
customStudyStartYear
|
||||
customStudyStartYear,
|
||||
studyrightid
|
||||
) => {
|
||||
if (byStudyPlanOfCode && cutStudyPlanCredits)
|
||||
return filterCoursesByDate(
|
||||
filterCoursesByStudyPlan(
|
||||
student.studyplans.find(p => p.programme_code === byStudyPlanOfCode),
|
||||
student.studyplans.find(p => p.programme_code === byStudyPlanOfCode && p.studyrightid === studyrightid),
|
||||
student.courses
|
||||
),
|
||||
customStudyStartYear || student.studyrightStart
|
||||
)
|
||||
if (byStudyPlanOfCode)
|
||||
return filterCoursesByStudyPlan(
|
||||
student.studyplans.find(p => p.programme_code === byStudyPlanOfCode),
|
||||
student.studyplans.find(p => p.programme_code === byStudyPlanOfCode && p.studyrightid === studyrightid),
|
||||
student.courses
|
||||
)
|
||||
if (singleStudent) return student.courses
|
||||
@@ -271,6 +287,36 @@ const resolveStudyRightElement = ({ studyright_elements }) => {
|
||||
)
|
||||
}
|
||||
|
||||
const filterGraduations = (student, selectedStudyRight) => {
|
||||
const graduated = student.studyrights.filter(({ graduated }) => graduated)
|
||||
// eslint-disable-next-line camelcase
|
||||
if (!selectedStudyRight)
|
||||
return graduated.map(({ enddate, studyright_elements }) => {
|
||||
const studyrightElem = studyright_elements
|
||||
? studyright_elements.filter(ele => ele.element_detail?.type === 20)
|
||||
: []
|
||||
let elemName = ''
|
||||
if (studyrightElem.length > 0) {
|
||||
elemName = getTextIn(studyrightElem[0].element_detail?.name)
|
||||
}
|
||||
return {
|
||||
value: new Date(enddate).getTime(),
|
||||
studyright: elemName,
|
||||
}
|
||||
})
|
||||
const selectedGraduation = graduated.find(({ studyrightid }) => studyrightid === selectedStudyRight.studyrightid)
|
||||
if (!selectedGraduation) return []
|
||||
const element = selectedGraduation.studyright_elements
|
||||
? selectedGraduation.studyright_elements.filter(ele => ele.element_detail?.type === 20)
|
||||
: []
|
||||
return [
|
||||
{
|
||||
value: new Date(selectedGraduation.enddate).getTime(),
|
||||
studyright: element.length > 0 ? getTextIn(element[0].element_detail?.name) : '',
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
const createStudentCreditLines = (
|
||||
students,
|
||||
singleStudent,
|
||||
@@ -298,13 +344,40 @@ const createStudentCreditLines = (
|
||||
studyPlanProgrammeCode,
|
||||
cutStudyPlanCredits,
|
||||
startDate,
|
||||
customStudyStartYear
|
||||
customStudyStartYear,
|
||||
studyRightId
|
||||
),
|
||||
courses => [...courses].filter(({ date }) => new Date(date) <= new Date()),
|
||||
sortCoursesByDate,
|
||||
courses => courses.reduce(reduceCreditsToPoints, { credits: 0, points: [], singleStudent })
|
||||
)(student.courses)
|
||||
|
||||
const graduation =
|
||||
points.length === 0
|
||||
? null
|
||||
: student.studyrights
|
||||
.filter(({ graduated }) => graduated)
|
||||
.find(({ studyright_elements }) =>
|
||||
studyright_elements.find(
|
||||
({ element_detail }) => element_detail.code === programmeCode && element_detail.type === 20
|
||||
)
|
||||
)
|
||||
|
||||
if (graduation) {
|
||||
const graduationX = new Date(graduation.enddate).getTime()
|
||||
const index = points.findIndex(point => point.x > graduationX)
|
||||
let graduationY
|
||||
if (index <= 0) {
|
||||
graduationY = graduationX > points[0].x ? points[points.length - 1].y : points[0].y
|
||||
} else {
|
||||
const yBefore = points[index - 1].y
|
||||
const yAfter = points[index].y
|
||||
graduationY = Math.round((yBefore + yAfter) / 2)
|
||||
}
|
||||
points.push({ x: graduationX, y: graduationY, marker: { enabled: true, radius: 10, symbol: 'diamond' } })
|
||||
points.sort((a, b) => a.x - b.x)
|
||||
}
|
||||
|
||||
return {
|
||||
name: student.studentNumber,
|
||||
data: points,
|
||||
@@ -316,13 +389,13 @@ const createStudentCreditLines = (
|
||||
}
|
||||
})
|
||||
|
||||
const filterGraduations = (student, selectedStudyRight) => {
|
||||
const graduated = student.studyrights.filter(({ graduated }) => graduated)
|
||||
// eslint-disable-next-line camelcase
|
||||
if (!selectedStudyRight) return graduated.map(({ enddate }) => new Date(enddate).getTime())
|
||||
const selectedGraduation = graduated.find(({ studyrightid }) => studyrightid === selectedStudyRight.studyrightid)
|
||||
if (!selectedGraduation) return []
|
||||
return [new Date(selectedGraduation.enddate).getTime()]
|
||||
const filterTransfers = student => {
|
||||
const transferTimes = student.transfers.map(transfer => ({
|
||||
value: new Date(transfer.transferdate).getTime(),
|
||||
studyright: `From ${transfer.sourcecode} to ${transfer.targetcode}`,
|
||||
}))
|
||||
const removeOverlapping = transferTimes.filter((transfer, i, a) => a.indexOf(transfer) === i)
|
||||
return removeOverlapping
|
||||
}
|
||||
|
||||
const CreditAccumulationGraphHighCharts = ({
|
||||
@@ -369,6 +442,7 @@ const CreditAccumulationGraphHighCharts = ({
|
||||
programmeCode,
|
||||
cutStudyPlanCredits,
|
||||
customStudyStartYear,
|
||||
language,
|
||||
]
|
||||
)
|
||||
|
||||
@@ -420,8 +494,8 @@ const CreditAccumulationGraphHighCharts = ({
|
||||
return Math.min(..._.flatten(students.map(({ courses }) => courses.map(({ date }) => new Date(date).getTime()))))
|
||||
return studyRightStart
|
||||
}
|
||||
|
||||
const graduations = singleStudent ? filterGraduations(students[0], selectedStudyRight) : []
|
||||
const transfers = singleStudent ? filterTransfers(students[0]) : []
|
||||
const studyRightStartLine =
|
||||
!singleStudent && studyPlanFilterIsActive
|
||||
? [new Date(customStudyStartYear || students[0].studyrightStart).getTime()]
|
||||
@@ -440,6 +514,7 @@ const CreditAccumulationGraphHighCharts = ({
|
||||
}
|
||||
},
|
||||
graduations,
|
||||
transfers,
|
||||
studyRightStartLine,
|
||||
})
|
||||
|
||||
|
||||
@@ -92,10 +92,19 @@ const CustomOpenUniSearch = ({ setValues, savedSearches, location, history }) =>
|
||||
const courseList = input.split(/[\s,]+/).map(code => code.trim().toUpperCase())
|
||||
const query = {
|
||||
courseCode: courseList,
|
||||
startdate: moment(startdate).format('DD-MM-YYYY'),
|
||||
enddate: moment(enddate).format('DD-MM-YYYY'),
|
||||
startdate: moment(startdate).isValid()
|
||||
? moment(startdate).format('DD-MM-YYYY')
|
||||
: moment('01-08-2017', 'DD-MM-YYYY').format('DD-MM-YYYY'),
|
||||
enddate: moment(enddate).isValid() ? moment(enddate).format('DD-MM-YYYY') : moment().format('DD-MM-YYYY'),
|
||||
}
|
||||
setValues({ courseList, startdate: startdate.toISOString(), enddate: enddate.toISOString() })
|
||||
|
||||
setValues({
|
||||
courseList,
|
||||
startdate: moment(startdate).isValid()
|
||||
? moment(startdate).toISOString()
|
||||
: moment('01-08-2017', 'DD-MM-YYYY').toISOString(),
|
||||
enddate: moment(enddate).isValid() ? moment(enddate).toISOString() : moment().toISOString(),
|
||||
})
|
||||
pushQueryToUrl(query)
|
||||
handleClose()
|
||||
}
|
||||
|
||||
+7
-24
@@ -13,7 +13,6 @@ const getTableData = studentsData => {
|
||||
courseInfo: { ...studentsData[student].courseInfo },
|
||||
email: studentsData[student].email,
|
||||
secondaryEmail: studentsData[student].secondaryEmail,
|
||||
disseminationInfoAllowed: studentsData[student].disseminationInfoAllowed,
|
||||
totals: studentsData[student].totals,
|
||||
},
|
||||
],
|
||||
@@ -75,23 +74,7 @@ const getColumns = (labelsToCourses, language) => {
|
||||
},
|
||||
]
|
||||
|
||||
const getDisseminationInfoAllowed = s => {
|
||||
if (s.disseminationInfoAllowed === null) return 'unknown'
|
||||
if (s.disseminationInfoAllowed) return 'yes'
|
||||
return 'no'
|
||||
}
|
||||
|
||||
const informationColumns = [
|
||||
{
|
||||
key: 'disseminationInfoAllowed',
|
||||
title: 'Marketing Allowed',
|
||||
getRowVal: s => getDisseminationInfoAllowed(s),
|
||||
getRowContent: s => getDisseminationInfoAllowed(s),
|
||||
headerProps: { title: 'Marketing Allowed' },
|
||||
child: true,
|
||||
childOf: 'Marketing Allowed',
|
||||
cellProps: { style },
|
||||
},
|
||||
{
|
||||
key: 'email-child',
|
||||
title: 'Email',
|
||||
@@ -122,11 +105,11 @@ const getColumns = (labelsToCourses, language) => {
|
||||
const findRowValue = (s, courseCode, hidden = false) => {
|
||||
if (s.courseInfo[courseCode] === undefined) return ''
|
||||
if (s.courseInfo[courseCode].status.passed && hidden)
|
||||
return `Passed: ${moment(s.courseInfo[courseCode].status.passed).format('DD-MM-YYYY')}`
|
||||
return `Passed: ${moment(s.courseInfo[courseCode].status.passed).format('YYYY-MM-DD')}`
|
||||
if (s.courseInfo[courseCode].status.failed && hidden)
|
||||
return `Failed: ${moment(s.courseInfo[courseCode].status.failed).format('DD-MM-YYYY')}`
|
||||
return `Failed: ${moment(s.courseInfo[courseCode].status.failed).format('YYYY-MM-DD')}`
|
||||
if (s.courseInfo[courseCode].status.unfinished && hidden)
|
||||
return `Enrollment: ${moment(s.courseInfo[courseCode].status.unfinished).format('DD-MM-YYYY')}`
|
||||
return `Enrollment: ${moment(s.courseInfo[courseCode].status.unfinished).format('YYYY-MM-DD')}`
|
||||
if (s.courseInfo[courseCode].status.passed) return 'Passed'
|
||||
if (s.courseInfo[courseCode].status.failed) return 'Failed'
|
||||
if (s.courseInfo[courseCode].status.unfinished) return 'Unfinished'
|
||||
@@ -139,13 +122,13 @@ const getColumns = (labelsToCourses, language) => {
|
||||
}
|
||||
if (s.courseInfo[courseCode] === undefined) return propObj
|
||||
if (s.courseInfo[courseCode].status.passed)
|
||||
return { ...propObj, title: `Passed: ${moment(s.courseInfo[courseCode].status.passed).format('DD-MM-YYYY')}` }
|
||||
return { ...propObj, title: `Passed: ${moment(s.courseInfo[courseCode].status.passed).format('YYYY-MM-DD')}` }
|
||||
if (s.courseInfo[courseCode].status.failed)
|
||||
return { ...propObj, title: `Failed: ${moment(s.courseInfo[courseCode].status.failed).format('DD-MM-YYYY')}` }
|
||||
if (s.courseInfo[courseCode].status.passed)
|
||||
return { ...propObj, title: `Failed: ${moment(s.courseInfo[courseCode].status.failed).format('YYYY-MM-DD')}` }
|
||||
if (s.courseInfo[courseCode].status.unfinished)
|
||||
return {
|
||||
...propObj,
|
||||
title: `Enrollment: ${moment(s.courseInfo[courseCode].status.unfinished).format('DD-MM-YYYY')}`,
|
||||
title: `Enrollment: ${moment(s.courseInfo[courseCode].status.unfinished).format('YYYY-MM-DD')}`,
|
||||
}
|
||||
return propObj
|
||||
}
|
||||
|
||||
@@ -37,10 +37,10 @@ const CustomOpenUniPopulation = () => {
|
||||
<Message style={{ maxWidth: '800px', fontSize: '16px' }}>
|
||||
<Message.Header>Open uni student population</Message.Header>
|
||||
<p>
|
||||
Here you can create custom population using a list of courses. Clicking the blue custom population button will
|
||||
open a modal where you can enter a list of courses. <br />
|
||||
<Icon fitted name="check" color="green" />: Student passed the course. <br />
|
||||
<Icon fitted name="times" color="red" />: Student failed the course. <br />
|
||||
Here you can create custom population using a list of courses. Clicking the blue button will open a modal
|
||||
where you can enter a list of courses. <br />
|
||||
<Icon fitted name="check" color="green" />: Student has passed the course. <br />
|
||||
<Icon fitted name="times" color="red" />: Student has failed the course. <br />
|
||||
<Icon fitted name="minus" color="grey" />: Student has enrolled, but has not received any grade from the
|
||||
course. <br />
|
||||
<b>Empty cell</b>: Student has no enrollments for the course. <br />
|
||||
|
||||
@@ -25,6 +25,7 @@ const CustomPopulationSearch = ({
|
||||
loading,
|
||||
customPopulationSearches,
|
||||
customPopulationSearchSaving,
|
||||
setStudentsInput,
|
||||
}) => {
|
||||
const [modal, setModal] = useState(false)
|
||||
const [input, setInput] = useState('')
|
||||
@@ -54,8 +55,15 @@ const CustomPopulationSearch = ({
|
||||
clearForm()
|
||||
}
|
||||
|
||||
const parseInput = studentNumbers =>
|
||||
studentNumbers
|
||||
.split(/[\s,]+/)
|
||||
.map(code => code.trim())
|
||||
.filter(s => s !== '')
|
||||
.map(s => (s.length === 8 ? `0${s}` : s))
|
||||
|
||||
const onSave = () => {
|
||||
const studentnumberlist = input.match(/[0-9]+/g)
|
||||
const studentnumberlist = parseInput(input)
|
||||
if (selectedSearchId) {
|
||||
updateCustomPopulationSearchDispatch({ id: selectedSearchId, studentnumberlist })
|
||||
} else {
|
||||
@@ -87,7 +95,8 @@ const CustomPopulationSearch = ({
|
||||
|
||||
const onClicker = e => {
|
||||
e.preventDefault()
|
||||
const studentnumbers = input.match(/[0-9]+/g)
|
||||
const studentnumbers = parseInput(input)
|
||||
setStudentsInput(studentnumbers)
|
||||
getCustomPopulationDispatch({ studentnumberlist: studentnumbers, onProgress })
|
||||
getCustomPopulationCoursesByStudentnumbers({ studentnumberlist: studentnumbers })
|
||||
selectCustomPopulationSearchDispatch(selectedSearchId || null)
|
||||
|
||||
@@ -5,6 +5,7 @@ import { Segment, Header, Accordion, Message } from 'semantic-ui-react'
|
||||
import _ from 'lodash'
|
||||
import scrollToComponent from 'react-scroll-to-component'
|
||||
import semestersApi from 'redux/semesters'
|
||||
import RightsNotification from 'components/RightsNotification'
|
||||
import { useProgress, useTitle } from '../../common/hooks'
|
||||
import infotooltips from '../../common/InfoToolTips'
|
||||
import { getCustomPopulationSearches } from '../../redux/customPopulationSearch'
|
||||
@@ -89,12 +90,13 @@ const CustomPopulationContent = ({ students, custompop }) => {
|
||||
const { language } = useLanguage()
|
||||
const [activeIndex, setIndex] = useState([])
|
||||
const [newestIndex, setNewest] = useState(null)
|
||||
const [studentsInput, setStudentsInput] = useState(null)
|
||||
const [discardedStudentNumbers, setDiscarded] = useState([])
|
||||
const creditGainRef = useRef()
|
||||
const programmeRef = useRef()
|
||||
const coursesRef = useRef()
|
||||
const studentRef = useRef()
|
||||
const refs = [creditGainRef, programmeRef, coursesRef, studentRef]
|
||||
|
||||
const { customPopulationSearches, searchedCustomPopulationSearchId } = useSelector(
|
||||
state => state.customPopulationSearch
|
||||
)
|
||||
@@ -108,6 +110,15 @@ const CustomPopulationContent = ({ students, custompop }) => {
|
||||
studentNumbers: students.map(s => s.studentNumber),
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
setDiscarded(
|
||||
_.difference(
|
||||
studentsInput,
|
||||
students.map(s => s.studentNumber)
|
||||
)
|
||||
)
|
||||
}, [studentsInput, students])
|
||||
|
||||
useEffect(() => {
|
||||
if (newestIndex) {
|
||||
scrollToComponent(refs[newestIndex].current, { align: 'bottom' })
|
||||
@@ -179,6 +190,7 @@ const CustomPopulationContent = ({ students, custompop }) => {
|
||||
content: {
|
||||
content: (
|
||||
<div ref={coursesRef}>
|
||||
<InfoBox content={infotooltips.PopulationStatistics.CoursesOfPopulation} />
|
||||
<CustomPopulationCourses filteredStudents={students} courses={courseStats} />
|
||||
</div>
|
||||
),
|
||||
@@ -236,7 +248,10 @@ const CustomPopulationContent = ({ students, custompop }) => {
|
||||
studentnumbers you have access rights to i.e. you have rights to the programme they are in.
|
||||
</p>
|
||||
</Message>
|
||||
<CustomPopulationSearch />
|
||||
{discardedStudentNumbers?.length > 0 && !populations.pending && (
|
||||
<RightsNotification discardedStudentNumbers={discardedStudentNumbers} />
|
||||
)}
|
||||
<CustomPopulationSearch setStudentsInput={setStudentsInput} />
|
||||
{custompop.length > 0 && customPopulationFlag ? (
|
||||
<Segment className="contentSegment">{renderCustomPopulation(students)}</Segment>
|
||||
) : (
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
import React from 'react'
|
||||
import GraduationTimes from 'components/FacultyStatistics/TimesAndPaths/GraduationTimes'
|
||||
|
||||
const FacultyGraduations = ({ faculty, graduationStats, groupByStartYear, showMedian }) => {
|
||||
const groupBy = groupByStartYear ? 'byStartYear' : 'byGradYear'
|
||||
const label = groupByStartYear ? 'Start year' : 'Graduation year'
|
||||
const data = graduationStats?.data?.[groupBy].medians
|
||||
const goals = graduationStats?.data?.goals
|
||||
const goalExceptions = { ...goals?.exceptions, needed: faculty === 'H30' }
|
||||
const programmeData = graduationStats?.data?.[groupBy].programmes.medians
|
||||
const programmeNames = graduationStats?.data?.programmeNames
|
||||
const classSizes = graduationStats?.data?.classSizes
|
||||
const commonProps = { label, programmeNames, showMedian, classSizes, goalExceptions }
|
||||
|
||||
return (
|
||||
<>
|
||||
<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}
|
||||
groupBy={groupBy}
|
||||
{...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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default FacultyGraduations
|
||||
@@ -0,0 +1,153 @@
|
||||
import React from 'react'
|
||||
import { Message } from 'semantic-ui-react'
|
||||
|
||||
import FacultyProgressTable from '../FacultyStatistics/FacultyProgrammeOverview/FacultyProgressTable'
|
||||
import FacultyBarChart from '../FacultyStatistics/FacultyProgrammeOverview/FacultyBarChart'
|
||||
import sortProgrammeKeys from '../FacultyStatistics/facultyHelpers'
|
||||
|
||||
const FacultyProgress = ({ faculty, progressStats, language, getDivider }) => {
|
||||
return (
|
||||
<>
|
||||
{getDivider('Bachelor', 'BachelorStudentsOfTheFacultyByStartingYear', 'no-infobox')}
|
||||
<div className="section-container">
|
||||
<div className="graph-container">
|
||||
<FacultyBarChart
|
||||
cypress="FacultyBachelorsProgress"
|
||||
data={{
|
||||
id: faculty,
|
||||
stats: progressStats?.data.bachelorsGraphStats,
|
||||
years: progressStats?.data.years,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="table-container">
|
||||
<FacultyProgressTable
|
||||
data={progressStats?.data.bachelorsTableStats}
|
||||
programmeStats={progressStats?.data.bachelorsProgStats}
|
||||
titles={progressStats?.data.bachelorTitles}
|
||||
sortedKeys={sortProgrammeKeys(
|
||||
Object.keys(progressStats?.data.bachelorsProgStats).map(obj => [
|
||||
obj,
|
||||
progressStats?.data?.programmeNames[obj].code,
|
||||
]),
|
||||
faculty
|
||||
).map(listObj => listObj[0])}
|
||||
progressYearsVisible={Array(progressStats?.data.years.slice(1).length).fill(false)}
|
||||
programmeNames={progressStats?.data.programmeNames}
|
||||
language={language}
|
||||
cypress="FacultyBachelorsProgressTable"
|
||||
progressTitles={progressStats?.data.yearlyBachelorTitles}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{getDivider('Bachelor + Master', 'ProgressOfBachelorMaster', 'no-infobox')}
|
||||
<Message data-cy="FacultyProgrammesShownInfo">
|
||||
The starting year is the studyright start in the bachelor programme. The credits are comouted by the start date
|
||||
of the bachelor programme and at the moment, they do not include any transferred credits. Thus, in these
|
||||
statistics some students have less credits than in reality.
|
||||
</Message>
|
||||
<div className="section-container">
|
||||
<div className="graph-container">
|
||||
<FacultyBarChart
|
||||
cypress="FacultyBachelorMastersProgress"
|
||||
data={{
|
||||
id: faculty,
|
||||
stats: progressStats?.data.bcMsGraphStats,
|
||||
years: progressStats?.data.years,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="table-container">
|
||||
<FacultyProgressTable
|
||||
data={progressStats?.data.bcMsTableStats}
|
||||
programmeStats={progressStats?.data.bcMsProgStats}
|
||||
titles={progressStats?.data.bcMsTitles}
|
||||
sortedKeys={sortProgrammeKeys(
|
||||
Object.keys(progressStats?.data.bcMsProgStats).map(obj => [
|
||||
obj,
|
||||
progressStats?.data?.programmeNames[obj].code,
|
||||
]),
|
||||
faculty
|
||||
).map(listObj => listObj[0])}
|
||||
progressYearsVisible={Array(progressStats?.data.years.slice(1).length).fill(false)}
|
||||
programmeNames={progressStats?.data.programmeNames}
|
||||
language={language}
|
||||
cypress="FacultyBachelorMasterProgressTable"
|
||||
progressTitles={progressStats?.data.yearlyBcMsTitles}
|
||||
needsExtra={faculty === 'H60' ? 'NO EXTRA' : 'EXTRA HEIGHT'}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{!(faculty === 'H90') && (
|
||||
<>
|
||||
{getDivider('Master', 'MasterStudentsOfTheFacultyByStartingYear', 'no-infobox')}
|
||||
<div className="section-container">
|
||||
<div className="graph-container">
|
||||
<FacultyBarChart
|
||||
cypress="FacultyMastersProgress"
|
||||
data={{
|
||||
id: faculty,
|
||||
stats: progressStats?.data.mastersGraphStats,
|
||||
years: progressStats?.data.years,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="table-container">
|
||||
<FacultyProgressTable
|
||||
data={progressStats?.data.mastersTableStats}
|
||||
programmeStats={progressStats?.data.mastersProgStats}
|
||||
titles={progressStats?.data.mastersTitles}
|
||||
sortedKeys={sortProgrammeKeys(
|
||||
Object.keys(progressStats?.data.mastersProgStats).map(obj => [
|
||||
obj,
|
||||
progressStats?.data?.programmeNames[obj].code,
|
||||
]),
|
||||
faculty
|
||||
).map(listObj => listObj[0])}
|
||||
progressYearsVisible={Array(progressStats?.data.years.slice(1).length).fill(false)}
|
||||
programmeNames={progressStats?.data.programmeNames}
|
||||
language={language}
|
||||
cypress="FacultyMastersProgressTable"
|
||||
progressTitles={progressStats?.data.yearlyMasterTitles}
|
||||
needsExtra={faculty === 'H60' ? 'NO EXTRA' : 'EXTRA HEIGHT'}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{getDivider('Doctor', 'DoctoralStudentsOfTheFacultyByStartingYear', 'no-infobox')}
|
||||
<div className="section-container">
|
||||
<div className="graph-container">
|
||||
<FacultyBarChart
|
||||
cypress="FacultyDoctoralProgress"
|
||||
data={{
|
||||
id: faculty,
|
||||
stats: progressStats?.data.doctoralGraphStats,
|
||||
years: progressStats?.data.years,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="table-container">
|
||||
<FacultyProgressTable
|
||||
data={progressStats?.data.doctoralTableStats}
|
||||
programmeStats={progressStats?.data.doctoralProgStats}
|
||||
titles={progressStats?.data.doctoralTitles}
|
||||
sortedKeys={sortProgrammeKeys(
|
||||
Object.keys(progressStats?.data.doctoralProgStats).map(obj => [
|
||||
obj,
|
||||
progressStats?.data?.programmeNames[obj].code,
|
||||
]),
|
||||
faculty
|
||||
).map(listObj => listObj[0])}
|
||||
progressYearsVisible={Array(progressStats?.data.years.slice(1).length).fill(false)}
|
||||
programmeNames={progressStats?.data.programmeNames}
|
||||
language={language}
|
||||
cypress="FacultyDoctoralProgressTable"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default FacultyProgress
|
||||
@@ -0,0 +1,145 @@
|
||||
import React, { useState } from 'react'
|
||||
import { Divider, Header, Loader, Message } from 'semantic-ui-react'
|
||||
import {
|
||||
useGetFacultiesQuery,
|
||||
useGetFacultyProgressStatsQuery,
|
||||
useGetFacultyGraduationTimesQuery,
|
||||
} from 'redux/facultyStats'
|
||||
import { getTextIn } from 'common'
|
||||
|
||||
import FacultyGraduations from './FacultyGraduations'
|
||||
import FacultyProgress from './FacultyProgress'
|
||||
|
||||
import Toggle from '../StudyProgramme/Toggle'
|
||||
import InfoBox from '../Info/InfoBox'
|
||||
import InfotoolTips from '../../common/InfoToolTips'
|
||||
import useLanguage from '../LanguagePicker/useLanguage'
|
||||
import '../FacultyStatistics/faculty.css'
|
||||
|
||||
const FacultyView = ({ faculty }) => {
|
||||
const [graduatedGroup, setGraduatedGroup] = useState(false)
|
||||
const [showMedian, setShowMedian] = useState(false)
|
||||
const toolTips = InfotoolTips.Faculty
|
||||
const studyProgrammeFilter = 'NEW_STUDY_PROGRAMMES'
|
||||
const specials = 'SPECIAL_EXCLUDED'
|
||||
const graduated = graduatedGroup ? 'GRADUATED_EXCLUDED' : 'GRADUATED_INCLUDED'
|
||||
const { language } = useLanguage()
|
||||
|
||||
const allFaculties = useGetFacultiesQuery()
|
||||
const faculties = allFaculties?.data
|
||||
const facultyDetails = faculties && faculty && faculties.find(f => f.code === faculty)
|
||||
const facultyName = facultyDetails && getTextIn(facultyDetails.name, language)
|
||||
|
||||
const progressStats = useGetFacultyProgressStatsQuery({
|
||||
id: faculty,
|
||||
studyProgrammeFilter,
|
||||
specialGroups: specials,
|
||||
graduated,
|
||||
})
|
||||
|
||||
const graduationStats = useGetFacultyGraduationTimesQuery({ id: faculty, studyProgrammeFilter })
|
||||
|
||||
const getDivider = (title, toolTipText, content, cypress = undefined) => (
|
||||
<>
|
||||
<div className="divider">
|
||||
<Divider data-cy={`Section-${toolTipText}`} horizontal>
|
||||
{title}
|
||||
</Divider>
|
||||
</div>
|
||||
{content === 'no-infobox' ? null : <InfoBox content={content} cypress={cypress} />}
|
||||
</>
|
||||
)
|
||||
|
||||
if (allFaculties.isLoading || allFaculties.isFetching) {
|
||||
return <Loader active style={{ marginTop: '10em' }} />
|
||||
}
|
||||
|
||||
const isFetchingOrLoading =
|
||||
progressStats.isLoading || progressStats.isFetching || graduationStats.isLoading || graduationStats.isFetching
|
||||
|
||||
const isError =
|
||||
(progressStats.isError && graduationStats.isError) ||
|
||||
(progressStats.isSuccess && !progressStats.data && graduationStats.isSuccess && !graduationStats.data)
|
||||
|
||||
if (isError) return <h3>Something went wrong, please try refreshing the page.</h3>
|
||||
|
||||
return (
|
||||
<>
|
||||
<div align="center" style={{ padding: '30px' }}>
|
||||
<Header textAlign="center">{facultyName}</Header>
|
||||
<span>{faculty}</span>
|
||||
</div>
|
||||
<Message info>
|
||||
<Message.Header>This view is an abridged version of Oodikone's Faculty Overview</Message.Header>
|
||||
<p>
|
||||
In these statistics, all special studyrights have been excluded, eg. exchange students and non-degree
|
||||
students.
|
||||
</p>
|
||||
<p>
|
||||
<b>You can find more statistics on this and all other faculties of the university in main Faculty view. </b>
|
||||
The full view includes details such as: how many have graduated or started in each faculty and in its
|
||||
programmes; credits produced by the faculty; populations backgrounds and enrollment statuses. This view
|
||||
provides statistics both on the whole faculty level and a breakdown of how they are spread among the faculty's
|
||||
programmes.
|
||||
</p>
|
||||
<p>Access the full Faculty view by clicking 'Faculty' in the top navigation bar.</p>
|
||||
</Message>
|
||||
<div className="faculty-overview">
|
||||
{isFetchingOrLoading ? (
|
||||
<Loader active style={{ marginTop: '15em' }} />
|
||||
) : (
|
||||
<div className="programmes-overview">
|
||||
{progressStats.isSuccess && progressStats.data && (
|
||||
<>
|
||||
{getDivider(
|
||||
'Progress of students of the faculty ',
|
||||
'BachelorStudentsOfTheFacultyByStartingYear',
|
||||
toolTips.StudentProgress,
|
||||
'InfoFacultyProgress'
|
||||
)}
|
||||
<div className="toggle-container">
|
||||
<Toggle
|
||||
cypress="GraduatedToggle"
|
||||
toolTips={toolTips.GraduatedToggle}
|
||||
firstLabel="Graduated included"
|
||||
secondLabel="Graduated excluded"
|
||||
value={graduatedGroup}
|
||||
setValue={setGraduatedGroup}
|
||||
/>
|
||||
</div>
|
||||
<FacultyProgress
|
||||
faculty={faculty}
|
||||
progressStats={progressStats}
|
||||
language={language}
|
||||
getDivider={getDivider}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{graduationStats.isSuccess && graduationStats.data && (
|
||||
<>
|
||||
{getDivider('Average graduation times', 'AverageGraduationTimes', toolTips.AverageGraduationTimes)}
|
||||
<div className="toggle-container">
|
||||
<Toggle
|
||||
cypress="GraduationTimeToggle"
|
||||
firstLabel="Breakdown"
|
||||
secondLabel="Median times"
|
||||
value={showMedian}
|
||||
setValue={setShowMedian}
|
||||
/>
|
||||
</div>
|
||||
<FacultyGraduations
|
||||
faculty={faculty}
|
||||
graduationStats={graduationStats}
|
||||
groupByStartYear={false}
|
||||
showMedian={showMedian}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default FacultyView
|
||||
@@ -0,0 +1,194 @@
|
||||
import React, { useState } from 'react'
|
||||
import { Divider, Header, Loader, Message, Segment } from 'semantic-ui-react'
|
||||
import { useGetEvaluationStatsQuery } from 'redux/studyProgramme'
|
||||
import { getTextIn } from '../../common'
|
||||
import useLanguage from '../LanguagePicker/useLanguage'
|
||||
import Toggle from '../StudyProgramme/Toggle'
|
||||
import InfoBox from '../Info/InfoBox'
|
||||
import MedianTimeBarChart from '../StudyProgramme/MedianTimeBarChart'
|
||||
import BreakdownBarChart from '../StudyProgramme/BreakdownBarChart'
|
||||
import StackedBarChart from '../StudyProgramme/BasicOverview/StackedBarChart'
|
||||
import BarChart from '../StudyProgramme/StudytrackOverview/BarChart'
|
||||
import BasicDataTable from '../StudyProgramme/StudytrackOverview/BasicDataTable'
|
||||
import DataTable from '../StudyProgramme/BasicOverview/DataTable'
|
||||
import InfotoolTips from '../../common/InfoToolTips'
|
||||
import '../StudyProgramme/studyprogramme.css'
|
||||
|
||||
const ProgrammeView = ({ studyprogramme }) => {
|
||||
const { language } = useLanguage()
|
||||
const [showMedian, setShowMedian] = useState(false)
|
||||
const [academicYear, setAcademicYear] = useState(false)
|
||||
const [graduated, setGraduated] = useState(false)
|
||||
|
||||
const toolTips = InfotoolTips.Studyprogramme
|
||||
const yearType = academicYear ? 'ACADEMIC_YEAR' : 'CALENDAR_YEAR'
|
||||
const grad = graduated ? 'GRADUATED_EXCLUDED' : 'GRADUATED_INCLUDED'
|
||||
|
||||
const statistics = useGetEvaluationStatsQuery({
|
||||
id: studyprogramme,
|
||||
yearType,
|
||||
specialGroups: 'SPECIAL_EXCLUDED',
|
||||
graduated: grad,
|
||||
})
|
||||
|
||||
const programmeName = statistics?.data?.programmeName && getTextIn(statistics?.data?.programmeName, language)
|
||||
|
||||
const graduationData = statistics?.data?.graduations
|
||||
const progressData = statistics?.data?.progress
|
||||
|
||||
const doCombo = graduationData?.doCombo
|
||||
const timesData = graduationData?.graduationTimes
|
||||
|
||||
const getDivider = (title, toolTipText) => (
|
||||
<>
|
||||
<div className="divider">
|
||||
<Divider data-cy={`Section-${toolTipText}`} horizontal>
|
||||
{title}
|
||||
</Divider>
|
||||
</div>
|
||||
<InfoBox content={toolTips[toolTipText]} />
|
||||
</>
|
||||
)
|
||||
|
||||
const displayMedian = () => (
|
||||
<>
|
||||
{doCombo && (
|
||||
<MedianTimeBarChart
|
||||
data={graduationData?.comboTimes?.medians}
|
||||
goal={graduationData?.comboTimes?.goal}
|
||||
title="Bachelor + Master studyright"
|
||||
byStartYear={false}
|
||||
/>
|
||||
)}
|
||||
<MedianTimeBarChart
|
||||
data={graduationData?.graduationTimes?.medians}
|
||||
goal={graduationData?.graduationTimes?.goal}
|
||||
title={doCombo ? 'Master studyright' : ' '}
|
||||
byStartYear={false}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
|
||||
const displayBreakdown = () => (
|
||||
<>
|
||||
{doCombo && <BreakdownBarChart data={graduationData?.comboTimes?.medians} title="Bachelor + Master studyright" />}
|
||||
<BreakdownBarChart data={timesData?.medians} title={doCombo ? 'Master studyright' : ' '} />
|
||||
</>
|
||||
)
|
||||
|
||||
const isFetchingOrLoading = statistics.isLoading || statistics.isFetching
|
||||
|
||||
const isError = statistics.isError || (statistics.isSuccess && !statistics.data)
|
||||
|
||||
if (isError) return <h3>Something went wrong, please try refreshing the page.</h3>
|
||||
|
||||
return (
|
||||
<>
|
||||
<div align="center" style={{ padding: '30px' }}>
|
||||
<Header textAlign="center">{programmeName}</Header>
|
||||
<span>{studyprogramme}</span>
|
||||
</div>
|
||||
<div className="studyprogramme-overview">
|
||||
{isFetchingOrLoading ? (
|
||||
<Loader active style={{ marginTop: '10em' }} />
|
||||
) : (
|
||||
<Segment className="contentSegment">
|
||||
<Message info>
|
||||
<Message.Header>This view is an abridged version of Oodikone's Studyprogramme Overview</Message.Header>
|
||||
<p>
|
||||
In these statistics, all special studyrights have been excluded, eg. exchange students and non-degree
|
||||
students.
|
||||
</p>
|
||||
<p>
|
||||
<b>
|
||||
You can find more statistics on this and other studyprogrammes of the university in main
|
||||
Studyprogramme Overview.{' '}
|
||||
</b>
|
||||
The full view includes details such as: how many have graduated or started in each programme and in its
|
||||
studytracks; credits produced by the programme; population backgrounds and enrollment statuses.
|
||||
</p>
|
||||
<p>
|
||||
Access the full Studyprogramme Overview by clicking 'Studyprogramme' in the top navigation bar and then
|
||||
'Overview'. Alternatively, you can select 'Search by class' to explore studyprogrammes by starting class
|
||||
with interactive statistics and visualizations.
|
||||
</p>
|
||||
<p>
|
||||
{' '}
|
||||
Note that both views have access restrictions. If you can't access either view and feel you should have
|
||||
access, please contact oodikone@helsinki.fi.
|
||||
</p>
|
||||
</Message>
|
||||
<div>
|
||||
{getDivider(`Progress of students of the studyprogramme by starting year`, 'StudytrackProgress')}
|
||||
<Toggle
|
||||
cypress="GraduatedToggle"
|
||||
toolTips={toolTips.GraduatedToggle}
|
||||
firstLabel="Graduated included"
|
||||
secondLabel="Graduated excluded"
|
||||
value={graduated}
|
||||
setValue={setGraduated}
|
||||
/>
|
||||
<div className="section-container">
|
||||
<BarChart cypress="StudytrackProgress" data={progressData?.graphData} track={studyprogramme} />
|
||||
<BasicDataTable
|
||||
cypress="StudytrackProgress"
|
||||
data={progressData?.creditTableStats}
|
||||
track={studyprogramme}
|
||||
titles={progressData?.creditTableTitles}
|
||||
/>
|
||||
</div>
|
||||
{graduationData?.programmesBeforeOrAfterGraphStats && (
|
||||
<>
|
||||
{getDivider('Graduation times', 'AverageGraduationTimes')}
|
||||
<div className="toggle-container">
|
||||
<Toggle
|
||||
cypress="GraduationTimeToggle"
|
||||
firstLabel="Breakdown"
|
||||
secondLabel="Median time"
|
||||
value={showMedian}
|
||||
setValue={setShowMedian}
|
||||
/>
|
||||
<div className="toggle-container">
|
||||
<Toggle
|
||||
cypress="YearToggle"
|
||||
toolTips={toolTips.YearToggle}
|
||||
firstLabel="Calendar year"
|
||||
secondLabel="Academic year"
|
||||
value={academicYear}
|
||||
setValue={setAcademicYear}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className={`section-container${doCombo ? '' : '-centered'}`}>
|
||||
{showMedian ? displayMedian() : displayBreakdown()}
|
||||
</div>
|
||||
{getDivider(
|
||||
studyprogramme.includes('KH')
|
||||
? 'Primary master programme studies after this programme'
|
||||
: 'Primary bachelor programme studies before this programme',
|
||||
'ProgrammesBeforeOrAfter'
|
||||
)}
|
||||
<div className="section-container">
|
||||
<StackedBarChart
|
||||
cypress="ProgrammesBeforeOrAfter"
|
||||
wideTable
|
||||
data={graduationData?.programmesBeforeOrAfterGraphStats}
|
||||
labels={graduationData?.years}
|
||||
/>
|
||||
<DataTable
|
||||
wideTable
|
||||
data={graduationData?.programmesBeforeOrAfterTableStats}
|
||||
titles={graduationData?.programmesBeforeOrAfterTitles}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</Segment>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default ProgrammeView
|
||||
@@ -0,0 +1,24 @@
|
||||
import React from 'react'
|
||||
import { Redirect, withRouter } from 'react-router-dom'
|
||||
import { Segment } from 'semantic-ui-react'
|
||||
|
||||
import FacultyView from './FacultyView'
|
||||
import ProgrammeView from './ProgrammeView'
|
||||
|
||||
const EvaluationOverview = props => {
|
||||
const { id, level } = props.match.params
|
||||
|
||||
if (!(level === 'programme' || level === 'faculty') || !id) {
|
||||
return <Redirect to="/" />
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="segmentContainer">
|
||||
<Segment className="contentSegment">
|
||||
{level === 'programme' ? <ProgrammeView studyprogramme={id} /> : <FacultyView faculty={id} />}
|
||||
</Segment>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default withRouter(EvaluationOverview)
|
||||
@@ -118,16 +118,67 @@ const Overview = ({
|
||||
|
||||
if (isError) return <h3>Something went wrong, please try refreshing the page.</h3>
|
||||
|
||||
let creditShortTitles = []
|
||||
const creditShortTitles = ['Code', 'Total', 'Major', 'Non-major', 'Non-major other', 'Non-degree']
|
||||
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']
|
||||
}
|
||||
|
||||
const options = {
|
||||
KH: 'Bachelors',
|
||||
MH: 'Masters',
|
||||
T: 'Doctors and Licentiates',
|
||||
LIS: 'Doctors and Licentiates',
|
||||
OTHER: 'Other',
|
||||
}
|
||||
|
||||
const getChartPlotLinePlaces = programmeKeys => {
|
||||
if (programmeKeys.length === 0) return []
|
||||
let key = programmeKeys[0][1].slice(0, 2)
|
||||
if (!['KH', 'MH', 'T', 'LIS'].includes(key)) {
|
||||
key = 'OTHER'
|
||||
}
|
||||
const plotLinePlaces = [[0, options[key]]]
|
||||
for (let i = 0; i < programmeKeys.length - 1; i++) {
|
||||
if (
|
||||
(programmeKeys[i][1].startsWith('KH') && programmeKeys[i + 1][1].startsWith('MH')) ||
|
||||
(programmeKeys[i][1].startsWith('MH') && programmeKeys[i + 1][1].startsWith('KH')) ||
|
||||
(programmeKeys[i][1].startsWith('MH') && programmeKeys[i + 1][1].startsWith('T')) ||
|
||||
((programmeKeys[i][1].startsWith('T') || programmeKeys[i][1].startsWith('LIS')) &&
|
||||
(programmeKeys[i + 1][1].startsWith('KH') || programmeKeys[i + 1][1].startsWith('MH'))) ||
|
||||
((programmeKeys[i][1].startsWith('T') ||
|
||||
programmeKeys[i][1].startsWith('LIS') ||
|
||||
programmeKeys[i][1].startsWith('KH') ||
|
||||
programmeKeys[i][1].startsWith('MH')) &&
|
||||
programmeKeys[i + 1][1].startsWith('K-'))
|
||||
) {
|
||||
let key = programmeKeys[i + 1][1].slice(0, 2)
|
||||
if (!['KH', 'MH'].includes(key)) {
|
||||
const keyT = programmeKeys[i + 1][1].slice(0, 1)
|
||||
const keyLis = programmeKeys[i + 1][1].slice(0, 3)
|
||||
if (keyT === 'T') {
|
||||
key = keyT
|
||||
} else if (keyLis === 'LIS') {
|
||||
key = keyLis
|
||||
} else {
|
||||
key = 'OTHER'
|
||||
}
|
||||
}
|
||||
if (
|
||||
!programmeKeys[i + 1][1].includes(faculty.code) &&
|
||||
(programmeKeys[i + 1][1].startsWith('MH') || programmeKeys[i + 1][1].startsWith('KH'))
|
||||
) {
|
||||
plotLinePlaces.push([i + 1, `${options[key]} secondary`])
|
||||
} else {
|
||||
plotLinePlaces.push([i + 1, options[key]])
|
||||
}
|
||||
}
|
||||
}
|
||||
return plotLinePlaces
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="faculty-overview">
|
||||
<div className="toggle-container">
|
||||
@@ -169,7 +220,8 @@ const Overview = ({
|
||||
)}
|
||||
{special === 'SPECIAL_EXCLUDED' && (
|
||||
<Message data-cy="FacultyExcludeSpecialsInfo">
|
||||
Please note that the data might be inaccurate in some fields. Checking the values is in progress.
|
||||
Please note: exluding the special studyrights does not have any effects to credits produced by faculty
|
||||
-view.
|
||||
</Message>
|
||||
)}
|
||||
{basics.isSuccess && basics.data && (
|
||||
@@ -199,8 +251,18 @@ const Overview = ({
|
||||
Object.keys(basics?.data?.studentInfo.programmeTableStats).map(obj => [
|
||||
obj,
|
||||
basics?.data?.programmeNames[obj].code,
|
||||
])
|
||||
]),
|
||||
faculty.code
|
||||
).map(listObj => listObj[0])}
|
||||
plotLinePlaces={getChartPlotLinePlaces(
|
||||
sortProgrammeKeys(
|
||||
Object.keys(basics?.data?.studentInfo.programmeTableStats).map(obj => [
|
||||
obj,
|
||||
basics?.data?.programmeNames[obj].code,
|
||||
]),
|
||||
faculty.code
|
||||
)
|
||||
)}
|
||||
titles={basics?.data?.studentInfo.titles}
|
||||
sliceStart={1}
|
||||
language={language}
|
||||
@@ -211,48 +273,6 @@ const Overview = ({
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{credits.isSuccess && credits.data && (
|
||||
<>
|
||||
{getDivider(
|
||||
'Credits produced by the faculty',
|
||||
'CreditsProducedByTheFaculty',
|
||||
credits?.data?.titles,
|
||||
credits?.data?.tableStats,
|
||||
credits?.data?.programmeTableStats,
|
||||
credits?.data?.programmeNames
|
||||
)}
|
||||
<div className="section-container">
|
||||
<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).map(obj => [
|
||||
obj,
|
||||
credits?.data?.programmeNames[obj].code,
|
||||
])
|
||||
).map(listObj => listObj[0])}
|
||||
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(
|
||||
@@ -280,8 +300,18 @@ const Overview = ({
|
||||
Object.keys(basics?.data?.graduationInfo.programmeTableStats).map(obj => [
|
||||
obj,
|
||||
basics?.data?.programmeNames[obj].code,
|
||||
])
|
||||
]),
|
||||
faculty.code
|
||||
).map(listObj => listObj[0])}
|
||||
plotLinePlaces={getChartPlotLinePlaces(
|
||||
sortProgrammeKeys(
|
||||
Object.keys(basics?.data?.graduationInfo.programmeTableStats).map(obj => [
|
||||
obj,
|
||||
basics?.data?.programmeNames[obj].code,
|
||||
]),
|
||||
faculty.code
|
||||
)
|
||||
)}
|
||||
titles={basics?.data?.graduationInfo.titles}
|
||||
sliceStart={2}
|
||||
language={language}
|
||||
@@ -318,8 +348,18 @@ const Overview = ({
|
||||
Object.keys(thesisWriters?.data.programmeTableStats).map(obj => [
|
||||
obj,
|
||||
thesisWriters?.data?.programmeNames[obj].code,
|
||||
])
|
||||
]),
|
||||
faculty.code
|
||||
).map(listObj => listObj[0])}
|
||||
plotLinePlaces={getChartPlotLinePlaces(
|
||||
sortProgrammeKeys(
|
||||
Object.keys(thesisWriters?.data?.programmeTableStats).map(obj => [
|
||||
obj,
|
||||
thesisWriters?.data?.programmeNames[obj].code,
|
||||
]),
|
||||
faculty.code
|
||||
)
|
||||
)}
|
||||
titles={thesisWriters?.data?.titles}
|
||||
sliceStart={2}
|
||||
extraHeight="EXTRA HEIGHT"
|
||||
@@ -330,6 +370,58 @@ const Overview = ({
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{credits.isSuccess && credits.data && (
|
||||
<>
|
||||
{getDivider(
|
||||
'Credits produced by the faculty',
|
||||
'CreditsProducedByTheFaculty',
|
||||
credits?.data?.titles,
|
||||
credits?.data?.tableStats,
|
||||
credits?.data?.programmeTableStats,
|
||||
credits?.data?.programmeNames
|
||||
)}
|
||||
<div className="section-container">
|
||||
<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).map(obj => [
|
||||
obj,
|
||||
credits?.data?.programmeNames[obj].code,
|
||||
]),
|
||||
faculty.code
|
||||
).map(listObj => listObj[0])}
|
||||
plotLinePlaces={getChartPlotLinePlaces(
|
||||
sortProgrammeKeys(
|
||||
Object.keys(credits?.data?.programmeTableStats).map(obj => [
|
||||
obj,
|
||||
credits?.data?.programmeNames[obj].code,
|
||||
]),
|
||||
faculty.code
|
||||
)
|
||||
)}
|
||||
titles={credits?.data?.titles}
|
||||
extraHeight="EXTRA HEIGHT"
|
||||
sliceStart={2}
|
||||
language={language}
|
||||
yearsVisible={Array(credits?.data?.tableStats.length).fill(false)}
|
||||
shortNames={creditShortTitles}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
+42
-36
@@ -13,6 +13,7 @@ const FacultyProgressTable = ({
|
||||
language,
|
||||
cypress,
|
||||
progressTitles,
|
||||
needsExtra = 'NO EXTRA',
|
||||
}) => {
|
||||
const [visible, setVisible] = useState(progressYearsVisible)
|
||||
if (!data) return null
|
||||
@@ -21,6 +22,7 @@ const FacultyProgressTable = ({
|
||||
arrayToModify[yearIndex] = !visible[yearIndex]
|
||||
setVisible(arrayToModify)
|
||||
}
|
||||
|
||||
const lenOfYearArray = data.length - 1
|
||||
return (
|
||||
<div>
|
||||
@@ -34,43 +36,47 @@ const FacultyProgressTable = ({
|
||||
</Table.Header>
|
||||
|
||||
<Table.Body>
|
||||
{data?.map((yearArray, yearIndex) => (
|
||||
<React.Fragment key={`random-fragment-key-${Math.random()}`}>
|
||||
{yearIndex < lenOfYearArray ? (
|
||||
<BasicRow
|
||||
icon={visible[yearIndex] ? 'angle down' : 'angle 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={progressTitles ? progressTitles[yearIndex] : titles?.slice(2)}
|
||||
{data?.map(
|
||||
(yearArray, yearIndex) =>
|
||||
yearArray?.slice(1).some(value => value !== 0) && (
|
||||
<React.Fragment key={`random-fragment-key-${Math.random()}`}>
|
||||
{yearIndex < lenOfYearArray ? (
|
||||
<BasicRow
|
||||
icon={visible[yearIndex] ? 'angle down' : 'angle right'}
|
||||
yearArray={yearArray}
|
||||
cypress={visible[yearIndex] ? `Hide-${cypress}` : `Show-${cypress}`}
|
||||
yearIndex={yearIndex}
|
||||
toggleVisibility={() => toggleVisibility(yearIndex)}
|
||||
/>
|
||||
</Table.Cell>
|
||||
</Table.Row>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</React.Fragment>
|
||||
))}
|
||||
) : (
|
||||
<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={progressTitles ? progressTitles[yearIndex] : titles?.slice(2)}
|
||||
needsExtra={needsExtra === 'EXTRA HEIGHT'}
|
||||
/>
|
||||
</Table.Cell>
|
||||
</Table.Row>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</React.Fragment>
|
||||
)
|
||||
)}
|
||||
</Table.Body>
|
||||
</Table>
|
||||
</div>
|
||||
|
||||
+96
-29
@@ -1,7 +1,7 @@
|
||||
/* eslint-disable react/no-array-index-key */
|
||||
import PopulationLink from 'components/StudyProgramme/StudytrackOverview/PopulationLink'
|
||||
import React, { useState } from 'react'
|
||||
import { Table, Button, Icon, Label } from 'semantic-ui-react'
|
||||
import { Table, Button, Icon, Label, Popup } from 'semantic-ui-react'
|
||||
import Toggle from '../../StudyProgramme/Toggle'
|
||||
|
||||
const getStyle = idx => {
|
||||
@@ -10,10 +10,29 @@ const getStyle = idx => {
|
||||
if (idx === 18) return { borderLeftWidth: 'thick' }
|
||||
return {}
|
||||
}
|
||||
|
||||
const backgroundColors = { KH: '#ffffff', MH: '#f2f6f7', T: '#e7edee' }
|
||||
const backgroundColorsDarks = { KH: '#f9f9f9', MH: '#eeeeee', T: '#dddddd' }
|
||||
|
||||
const getTitlePopup = idx => {
|
||||
if (idx === 0) return <p>All</p>
|
||||
if ([1, 2].includes(idx)) return <p>Started studying</p>
|
||||
if ([3, 4].includes(idx)) return <p>Currently enrolled</p>
|
||||
if ([5, 6].includes(idx)) return <p>Absent</p>
|
||||
if ([7, 8].includes(idx)) return <p>Inactive</p>
|
||||
if ([9, 10].includes(idx)) return <p>Graduated</p>
|
||||
if ([11, 12].includes(idx)) return <p>Men</p>
|
||||
if ([13, 14].includes(idx)) return <p>Women</p>
|
||||
if ([15, 16].includes(idx)) return <p>Other/Unknown</p>
|
||||
if ([17, 18].includes(idx)) return <p>Finnish</p>
|
||||
return <p>Other</p>
|
||||
}
|
||||
|
||||
const FacultyStudentDataTable = ({
|
||||
tableStats,
|
||||
programmeStats,
|
||||
programmeNames,
|
||||
tableLinePlaces,
|
||||
titles,
|
||||
years,
|
||||
sortedKeys,
|
||||
@@ -29,6 +48,52 @@ const FacultyStudentDataTable = ({
|
||||
setVisible(arrayToModify)
|
||||
}
|
||||
|
||||
const getRowStyle = (idx, dark = false) => {
|
||||
if (tableLinePlaces.length >= 3 && tableLinePlaces[2][0] <= idx)
|
||||
return dark
|
||||
? { backgroundColor: backgroundColorsDarks[tableLinePlaces[2][1]] }
|
||||
: { backgroundColor: backgroundColors[tableLinePlaces[2][1]] }
|
||||
if (tableLinePlaces.length >= 3 && tableLinePlaces[2][0] > idx && tableLinePlaces[1][0] <= idx)
|
||||
return dark
|
||||
? { backgroundColor: backgroundColorsDarks[tableLinePlaces[1][1]] }
|
||||
: { backgroundColor: backgroundColors[tableLinePlaces[1][1]] }
|
||||
if (tableLinePlaces.length >= 2 && tableLinePlaces[1][0] > idx && tableLinePlaces[0][0] <= idx)
|
||||
return dark
|
||||
? { backgroundColor: backgroundColorsDarks[tableLinePlaces[0][1]] }
|
||||
: { backgroundColor: backgroundColors[tableLinePlaces[0][1]] }
|
||||
return dark
|
||||
? { backgroundColor: backgroundColorsDarks[tableLinePlaces[0][1]] }
|
||||
: { backgroundColor: backgroundColors[tableLinePlaces[0][1]] }
|
||||
}
|
||||
|
||||
const calendarYears = years.reduce((all, year) => {
|
||||
if (year === 'Total') return all
|
||||
return all.concat(Number(year.slice(0, 4)))
|
||||
}, [])
|
||||
|
||||
const getRows = (idx, programme, year) => {
|
||||
return programmeStats[programme][year].map((value, valIdx) => {
|
||||
if (!showPercentages && typeof value === 'string' && (value.includes('%') || value.includes('NA'))) return null
|
||||
return (
|
||||
<Popup
|
||||
content={getTitlePopup(valIdx)}
|
||||
trigger={
|
||||
<Table.Cell
|
||||
key={`${year}-${programme}-color-${valIdx}`}
|
||||
style={
|
||||
getStyle(valIdx + 1)?.backgroundColor
|
||||
? { borderLeftWidth: getStyle(valIdx + 1).borderLeftWidth, ...getRowStyle(idx, true) }
|
||||
: { ...getStyle(valIdx + 1), ...getRowStyle(idx) }
|
||||
}
|
||||
>
|
||||
{value}
|
||||
</Table.Cell>
|
||||
}
|
||||
/>
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="datatable">
|
||||
<Toggle
|
||||
@@ -40,7 +105,7 @@ const FacultyStudentDataTable = ({
|
||||
/>
|
||||
<Table data-cy={cypress} celled structured>
|
||||
<Table.Header>
|
||||
<Table.Row>
|
||||
<Table.Row key="FirstHeader">
|
||||
<Table.HeaderCell colSpan={!showPercentages ? 3 : 4} />
|
||||
<Table.HeaderCell colSpan={!showPercentages ? 4 : 8} style={{ borderLeftWidth: 'thick' }}>
|
||||
Status
|
||||
@@ -52,9 +117,7 @@ const FacultyStudentDataTable = ({
|
||||
Country
|
||||
</Table.HeaderCell>
|
||||
</Table.Row>
|
||||
</Table.Header>
|
||||
<Table.Header>
|
||||
<Table.Row>
|
||||
<Table.Row key="secondHeader">
|
||||
{titles.map((title, index) => (
|
||||
<Table.HeaderCell
|
||||
key={title}
|
||||
@@ -80,6 +143,7 @@ const FacultyStudentDataTable = ({
|
||||
return (
|
||||
<Table.Cell key={`${year}-faculty-cell}`}>
|
||||
<Button
|
||||
key={`${year}-studentsTableButton}`}
|
||||
as="div"
|
||||
onClick={() => toggleVisibility(yearIndex)}
|
||||
labelPosition="right"
|
||||
@@ -108,36 +172,39 @@ const FacultyStudentDataTable = ({
|
||||
})}
|
||||
</Table.Row>
|
||||
{yearsVisible[yearIndex] &&
|
||||
sortedKeys.map(programme => {
|
||||
return (
|
||||
sortedKeys.map((programme, idx) => {
|
||||
return programmeStats[programme][year].length === 0 ? null : (
|
||||
<Table.Row className="regular-row" key={`${year}-regular-row-${programme}`}>
|
||||
<Table.Cell textAlign="left" style={{ paddingLeft: '50px' }} key={`${year}-${programme}`}>
|
||||
<b>
|
||||
{programme} - {programmeNames[programme].code}
|
||||
<br />
|
||||
{programmeNames[programme][language]
|
||||
? programmeNames[programme][language]
|
||||
: programmeNames[programme].fi}
|
||||
</b>
|
||||
<Table.Cell
|
||||
textAlign="left"
|
||||
key={`${year}-${programme}`}
|
||||
style={{ paddingLeft: '50px', ...getRowStyle(idx) }}
|
||||
>
|
||||
<Popup
|
||||
content={
|
||||
programmeNames[programme][language] ? (
|
||||
<p>
|
||||
{programmeNames[programme].code} - {programmeNames[programme][language]}
|
||||
</p>
|
||||
) : (
|
||||
<p>
|
||||
{programmeNames[programme].code} - {programmeNames[programme].fi}
|
||||
</p>
|
||||
)
|
||||
}
|
||||
trigger={<b>{programme}</b>}
|
||||
/>
|
||||
{(requiredRights.rights?.includes(programmeNames[programme].code) ||
|
||||
requiredRights.isAdmin ||
|
||||
requiredRights.IAMrights?.includes(programmeNames[programme].code)) && (
|
||||
<PopulationLink studyprogramme={programmeNames[programme].code} year={year} />
|
||||
<PopulationLink
|
||||
studyprogramme={programmeNames[programme].code}
|
||||
year={year}
|
||||
years={calendarYears}
|
||||
/>
|
||||
)}
|
||||
</Table.Cell>
|
||||
{programmeStats[programme][year].map((value, valIdx) => {
|
||||
if (
|
||||
!showPercentages &&
|
||||
typeof value === 'string' &&
|
||||
(value.includes('%') || value.includes('NA'))
|
||||
)
|
||||
return null
|
||||
return (
|
||||
<Table.Cell key={`${year}-${programme}-color-${valIdx}`} style={getStyle(valIdx + 1)}>
|
||||
{value}
|
||||
</Table.Cell>
|
||||
)
|
||||
})}
|
||||
{getRows(idx, programme, year)}
|
||||
</Table.Row>
|
||||
)
|
||||
})}
|
||||
|
||||
+14
-2
@@ -8,8 +8,19 @@ 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 }) => {
|
||||
// Point width is 24 px different multipliers adjusts the height.
|
||||
const getFlexHeight = (len, needsExtra) => {
|
||||
if (len > 7 && needsExtra) return `${len * 24 * 1.5}px`
|
||||
if (len > 5 && !needsExtra) return `${len * 24 * 1.5}px`
|
||||
if (needsExtra && len <= 2) return `${len * 24 * 6}px`
|
||||
if (needsExtra && len <= 4) return `${len * 24 * 3}px`
|
||||
if (needsExtra) return `${len * 24 * 2}px`
|
||||
if (len < 2) return `${len * 24 * 5}px`
|
||||
if (len < 3) return `${len * 24 * 4}px`
|
||||
if (len <= 4) return `${len * 24 * 3}px`
|
||||
return `${len * 24}px`
|
||||
}
|
||||
const ProgrammeProgressChart = ({ data, labels, longLabels, names, language, needsExtra }) => {
|
||||
if (!data || data.length === 0)
|
||||
return (
|
||||
<>
|
||||
@@ -30,6 +41,7 @@ const ProgrammeProgressChart = ({ data, labels, longLabels, names, language }) =
|
||||
chart: {
|
||||
type: 'bar',
|
||||
marginTop: 60,
|
||||
height: getFlexHeight(labels.length, needsExtra),
|
||||
},
|
||||
credits: {
|
||||
text: 'oodikone | TOSKA',
|
||||
|
||||
+48
-9
@@ -1,5 +1,5 @@
|
||||
import React from 'react'
|
||||
import { Divider, Loader } from 'semantic-ui-react'
|
||||
import { Divider, Loader, Message } from 'semantic-ui-react'
|
||||
import { useGetFacultyProgressStatsQuery, useGetFacultyStudentStatsQuery } from 'redux/facultyStats'
|
||||
import FacultyProgressTable from './FacultyProgressTable'
|
||||
import FacultyBarChart from './FacultyBarChart'
|
||||
@@ -20,7 +20,9 @@ const getDivider = (title, toolTipText, content, cypress) => (
|
||||
{content === 'no-infobox' ? null : <InfoBox content={content} cypress={cypress} />}
|
||||
</>
|
||||
)
|
||||
|
||||
const getKey = (programmeKeys, idx) => {
|
||||
return programmeKeys[idx][1].startsWith('T') ? 'T' : programmeKeys[idx][1].slice(0, 2)
|
||||
}
|
||||
const FacultyProgrammeOverview = ({
|
||||
faculty,
|
||||
language,
|
||||
@@ -57,6 +59,22 @@ const FacultyProgrammeOverview = ({
|
||||
(studentStats.isSuccess && !studentStats.data)
|
||||
if (isError) return <h3>Something went wrong, please try refreshing the page.</h3>
|
||||
|
||||
const getTableLinePlaces = programmeKeys => {
|
||||
if (programmeKeys.length === 0) return []
|
||||
const key = getKey(programmeKeys, 0)
|
||||
const plotLinePlaces = [[0, key]]
|
||||
for (let i = 0; i < programmeKeys.length - 1; i++) {
|
||||
if (
|
||||
(programmeKeys[i][1].startsWith('KH') && programmeKeys[i + 1][1].startsWith('MH')) ||
|
||||
(programmeKeys[i][1].startsWith('MH') && programmeKeys[i + 1][1].startsWith('T'))
|
||||
) {
|
||||
const key = getKey(programmeKeys, i + 1)
|
||||
plotLinePlaces.push([i + 1, key])
|
||||
}
|
||||
}
|
||||
return plotLinePlaces
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="faculty-overview">
|
||||
<div className="toggle-container">
|
||||
@@ -99,9 +117,19 @@ const FacultyProgrammeOverview = ({
|
||||
sortedKeys={sortProgrammeKeys(
|
||||
Object.keys(studentStats?.data.programmeStats).map(obj => [
|
||||
obj,
|
||||
studentStats?.data?.programmeNames[obj].code,
|
||||
])
|
||||
studentStats?.data?.programmeNames[obj]?.code,
|
||||
]),
|
||||
faculty.code
|
||||
).map(listObj => listObj[0])}
|
||||
tableLinePlaces={getTableLinePlaces(
|
||||
sortProgrammeKeys(
|
||||
Object.keys(studentStats?.data?.programmeStats).map(obj => [
|
||||
obj,
|
||||
studentStats?.data?.programmeNames[obj].code,
|
||||
]),
|
||||
faculty.code
|
||||
)
|
||||
)}
|
||||
language={language}
|
||||
cypress="FacultyStudentStatsTable"
|
||||
requiredRights={requiredRights}
|
||||
@@ -139,7 +167,8 @@ const FacultyProgrammeOverview = ({
|
||||
Object.keys(progressStats?.data.bachelorsProgStats).map(obj => [
|
||||
obj,
|
||||
progressStats?.data?.programmeNames[obj].code,
|
||||
])
|
||||
]),
|
||||
faculty.code
|
||||
).map(listObj => listObj[0])}
|
||||
progressYearsVisible={Array(progressStats?.data.years.slice(1).length).fill(false)}
|
||||
programmeNames={progressStats?.data.programmeNames}
|
||||
@@ -150,6 +179,11 @@ const FacultyProgrammeOverview = ({
|
||||
</div>
|
||||
</div>
|
||||
{getDivider('Bachelor + Master', 'ProgressOfBachelorMaster', 'no-infobox')}
|
||||
<Message data-cy="FacultyProgrammesShownInfo">
|
||||
Please note: The starting year is the studyright start in the bachelor programme. The credits are
|
||||
computed by the start date of the bachelor programme and at the moment, they do not include any
|
||||
transferred credits. Thus, in these statistics some students have less credits than in reality.
|
||||
</Message>
|
||||
<div className="section-container">
|
||||
<div className="graph-container">
|
||||
<FacultyBarChart
|
||||
@@ -170,13 +204,15 @@ const FacultyProgrammeOverview = ({
|
||||
Object.keys(progressStats?.data.bcMsProgStats).map(obj => [
|
||||
obj,
|
||||
progressStats?.data?.programmeNames[obj].code,
|
||||
])
|
||||
]),
|
||||
faculty.code
|
||||
).map(listObj => listObj[0])}
|
||||
progressYearsVisible={Array(progressStats?.data.years.slice(1).length).fill(false)}
|
||||
programmeNames={progressStats?.data.programmeNames}
|
||||
language={language}
|
||||
cypress="FacultyBachelorMasterProgressTable"
|
||||
progressTitles={progressStats?.data.yearlyBcMsTitles}
|
||||
needsExtra={faculty.code === 'H60' ? 'NO EXTRA' : 'EXTRA HEIGHT'}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -203,19 +239,21 @@ const FacultyProgrammeOverview = ({
|
||||
Object.keys(progressStats?.data.mastersProgStats).map(obj => [
|
||||
obj,
|
||||
progressStats?.data?.programmeNames[obj].code,
|
||||
])
|
||||
]),
|
||||
faculty.code
|
||||
).map(listObj => listObj[0])}
|
||||
progressYearsVisible={Array(progressStats?.data.years.slice(1).length).fill(false)}
|
||||
programmeNames={progressStats?.data.programmeNames}
|
||||
language={language}
|
||||
cypress="FacultyMastersProgressTable"
|
||||
progressTitles={progressStats?.data.yearlyMasterTitles}
|
||||
needsExtra={faculty.code === 'H60' ? 'NO EXTRA' : 'EXTRA HEIGHT'}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{getDivider('Doctor and licentiate', 'DoctoralStudentsOfTheFacultyByStartingYear', 'no-infobox')}
|
||||
{getDivider('Doctor', 'DoctoralStudentsOfTheFacultyByStartingYear', 'no-infobox')}
|
||||
<div className="section-container">
|
||||
<div className="graph-container">
|
||||
<FacultyBarChart
|
||||
@@ -236,7 +274,8 @@ const FacultyProgrammeOverview = ({
|
||||
Object.keys(progressStats?.data.doctoralProgStats).map(obj => [
|
||||
obj,
|
||||
progressStats?.data?.programmeNames[obj].code,
|
||||
])
|
||||
]),
|
||||
faculty.code
|
||||
).map(listObj => listObj[0])}
|
||||
progressYearsVisible={Array(progressStats?.data.years.slice(1).length).fill(false)}
|
||||
programmeNames={progressStats?.data.programmeNames}
|
||||
|
||||
+35
-5
@@ -1,15 +1,23 @@
|
||||
/* 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 CollapsedStackedBar = ({
|
||||
data,
|
||||
labels,
|
||||
longLabels,
|
||||
names,
|
||||
plotLinePlaces,
|
||||
language,
|
||||
differenceData,
|
||||
extraHeight,
|
||||
}) => {
|
||||
const transpose = matrix => {
|
||||
return matrix.reduce((prev, next) => next.map((_item, i) => (prev[i] || []).concat(next[i])), [])
|
||||
}
|
||||
|
||||
if (names[0] === 'Started studying') names[0] += ' (new in faculty)'
|
||||
const needsExtra = extraHeight === 'EXTRA HEIGHT'
|
||||
const dataTranspose = transpose(data)
|
||||
.map((obj, idx) => ({ name: names[idx], data: obj, color: colors[idx] }))
|
||||
@@ -33,6 +41,27 @@ const CollapsedStackedBar = ({ data, labels, longLabels, names, language, differ
|
||||
return change
|
||||
}
|
||||
|
||||
const chartPlotLinePlaces = plotLinePlaces
|
||||
? plotLinePlaces.map(val => ({
|
||||
color: '#90A959',
|
||||
width: 1,
|
||||
value: val[0] - 0.5,
|
||||
dashStyle: 'solid',
|
||||
label: {
|
||||
text: val[1],
|
||||
style: {
|
||||
color: 'black',
|
||||
fontWeight: 'bold',
|
||||
fontSize: 14,
|
||||
position: 'absolute',
|
||||
},
|
||||
align: 'right',
|
||||
x: 0,
|
||||
y: 5,
|
||||
},
|
||||
}))
|
||||
: []
|
||||
|
||||
// Point width is 24 px different multipliers adjusts the height.
|
||||
const getFlexHeight = (len, needsExtra) => {
|
||||
if (len > 7 && needsExtra) return `${len * 24 * 1.5}px`
|
||||
@@ -73,6 +102,7 @@ const CollapsedStackedBar = ({ data, labels, longLabels, names, language, differ
|
||||
title: {
|
||||
text: '',
|
||||
},
|
||||
plotLines: chartPlotLinePlaces,
|
||||
fontSize: '24px',
|
||||
},
|
||||
yAxis: {
|
||||
@@ -83,7 +113,7 @@ const CollapsedStackedBar = ({ data, labels, longLabels, names, language, differ
|
||||
stackLabels: {
|
||||
enabled: true,
|
||||
style: {
|
||||
color: (Highcharts.defaultOptions.title.style && Highcharts.defaultOptions.title.style.color) || 'gray',
|
||||
color: 'gray',
|
||||
},
|
||||
fontSize: '24px',
|
||||
},
|
||||
@@ -96,7 +126,7 @@ const CollapsedStackedBar = ({ data, labels, longLabels, names, language, differ
|
||||
verticalAlign: 'top',
|
||||
y: -10,
|
||||
floating: true,
|
||||
backgroundColor: Highcharts.defaultOptions.legend.backgroundColor || 'white',
|
||||
backgroundColor: 'white',
|
||||
borderColor: '#CCC',
|
||||
borderWidth: 1,
|
||||
shadow: false,
|
||||
|
||||
@@ -9,6 +9,7 @@ const InteractiveDataTable = ({
|
||||
dataProgrammeStats,
|
||||
programmeNames,
|
||||
sortedKeys,
|
||||
plotLinePlaces,
|
||||
titles,
|
||||
language,
|
||||
sliceStart,
|
||||
@@ -28,14 +29,32 @@ const InteractiveDataTable = ({
|
||||
const keys = {}
|
||||
if (!(columnIndex === 0)) {
|
||||
const numbersOfYears = dataStats.length
|
||||
const data = Object.entries(dataProgrammeStats)
|
||||
const data = sortedKeys?.map(programme => [programme, dataProgrammeStats[programme]])
|
||||
const groupIndices = plotLinePlaces.length > 0 ? plotLinePlaces.map(val => val[0]) : []
|
||||
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])
|
||||
let yearlySortedKeys = []
|
||||
if (groupIndices.length > 0) {
|
||||
for (let i = 0; i < groupIndices.length; i++) {
|
||||
const ending = i === groupIndices.length - 1 ? data.length : groupIndices[i + 1]
|
||||
yearlySortedKeys = [
|
||||
...yearlySortedKeys,
|
||||
...data
|
||||
.slice(groupIndices[i], ending)
|
||||
.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]),
|
||||
]
|
||||
}
|
||||
} else {
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -132,6 +151,7 @@ const InteractiveDataTable = ({
|
||||
)
|
||||
}
|
||||
labels={sortbyColumn === 0 ? sortedKeys : keyOrder[yearIndex]}
|
||||
plotLinePlaces={plotLinePlaces}
|
||||
differenceData={Object.keys(differenceToPrevYears)?.reduce(
|
||||
(yearlyObject, programme) => ({
|
||||
...yearlyObject,
|
||||
|
||||
+38
-12
@@ -1,17 +1,8 @@
|
||||
/* eslint-disable react/no-this-in-sfc */
|
||||
import React from 'react'
|
||||
import ReactHighcharts from 'react-highcharts'
|
||||
|
||||
const BarChart = ({
|
||||
data,
|
||||
// handleClick,
|
||||
facultyGraph = true,
|
||||
// year,
|
||||
// label,
|
||||
// programmeNames,
|
||||
// language = null,
|
||||
// showMeanTime,
|
||||
// level,
|
||||
}) => {
|
||||
const BarChart = ({ data, handleClick, facultyGraph = true, year = null, label, programmeNames, language = null }) => {
|
||||
const statData = [
|
||||
{ name: 'On time', color: '#90A959', data: [] },
|
||||
{ name: 'Max. year overtime', color: '#FEE191', data: [] },
|
||||
@@ -19,6 +10,7 @@ const BarChart = ({
|
||||
]
|
||||
|
||||
let categories = []
|
||||
const codeMap = {}
|
||||
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
for (const item of data) {
|
||||
@@ -26,13 +18,29 @@ const BarChart = ({
|
||||
statData[1].data = [...statData[1].data, item.statistics.yearOver]
|
||||
statData[2].data = [...statData[2].data, item.statistics.wayOver]
|
||||
categories = [...categories, item.name]
|
||||
if (!facultyGraph) {
|
||||
codeMap[item.name || item.code] = item.code
|
||||
}
|
||||
}
|
||||
|
||||
const getHeight = () => {
|
||||
if (data.length === 1) {
|
||||
return 250
|
||||
}
|
||||
const t = data.length > 8 ? 80 : 110
|
||||
return data.length * t + 100
|
||||
}
|
||||
|
||||
const getTooltipText = (programmeId, seriesName, amount) => {
|
||||
if (!facultyGraph) {
|
||||
const code = codeMap[programmeId]
|
||||
return `<b>${
|
||||
programmeNames[code]?.[language] ? programmeNames[code]?.[language] : programmeNames[code]?.fi
|
||||
}</b><br />${code}<br /><b>${seriesName}</b>: ${amount}`
|
||||
}
|
||||
return `<b>${seriesName}</b>: ${amount}`
|
||||
}
|
||||
|
||||
const config = {
|
||||
chart: {
|
||||
type: 'bar',
|
||||
@@ -41,10 +49,20 @@ const BarChart = ({
|
||||
},
|
||||
title: { text: ' ' },
|
||||
series: statData,
|
||||
tooltip: {
|
||||
backgroundColor: 'white',
|
||||
fontSize: '25px',
|
||||
// outside: true,
|
||||
// eslint-disable-next-line
|
||||
formatter: function() {
|
||||
return getTooltipText(this.x, this.series.name, this.y)
|
||||
},
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
categories,
|
||||
title: {
|
||||
text: 'Graduation year', // facultyGraph ? label : 'Programme',
|
||||
text: facultyGraph ? label : 'Programme',
|
||||
align: 'high',
|
||||
offset: 0,
|
||||
rotation: 0,
|
||||
@@ -82,9 +100,17 @@ const BarChart = ({
|
||||
series: {
|
||||
pointWidth: data.length > 8 ? 16 : 20,
|
||||
groupPadding: 0.15,
|
||||
point: {
|
||||
events: {
|
||||
click(e) {
|
||||
handleClick(e, facultyGraph, categories[this.x])
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
if (!facultyGraph) config.title.text = `Year ${year} by ${label.toLowerCase()}`
|
||||
|
||||
return (
|
||||
<div className={`${facultyGraph ? 'faculty' : 'programmes'}-breakdown-graph`}>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user