mirror of
https://github.com/UniversityOfHelsinkiCS/oodikone.git
synced 2026-10-13 03:13:40 +03:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
218f774f5b | ||
|
|
98926ae7c2 | ||
|
|
c2583e5bc9 | ||
|
|
6bf083cc89 | ||
|
|
c88d5984ef | ||
|
|
e328258071 | ||
|
|
37a16b54e1 | ||
|
|
4729fa457c | ||
|
|
4b37fd4536 | ||
|
|
ec5cfcc6c5 | ||
|
|
b41846a51e | ||
|
|
5b866377a9 | ||
|
|
febba2752c | ||
|
|
71f733817a | ||
|
|
82d2885d99 | ||
|
|
dad7ddc1f2 | ||
|
|
ee9c2c8eb8 | ||
|
|
6c9b11a105 | ||
|
|
afa52f7e54 | ||
|
|
f2865b9145 | ||
|
|
0514f4fdfa | ||
|
|
170a2e40e1 | ||
|
|
5849e89ac6 | ||
|
|
bc6cccfe03 | ||
|
|
53d1890a4a | ||
|
|
4a93c4dce4 | ||
|
|
be79d8bb3f | ||
|
|
97df3bee41 | ||
|
|
e1c4e792f1 | ||
|
|
230d07cbde | ||
|
|
92f848bf21 | ||
|
|
ae8aeede1b | ||
|
|
2db12dcaac | ||
|
|
9672b542b8 | ||
|
|
e6a8946b65 | ||
|
|
3b0ccb5424 | ||
|
|
0c54ef578f | ||
|
|
4ee0f74361 | ||
|
|
362850c2de | ||
|
|
42c41345a0 | ||
|
|
301208cf99 | ||
|
|
b173e27f70 | ||
|
|
1bdbec963b | ||
|
|
80cf4acf01 | ||
|
|
fdd5cbd2e5 | ||
|
|
38e398598f | ||
|
|
0b01d223ae | ||
|
|
c4fd257b42 | ||
|
|
e7ee8749c8 | ||
|
|
fb21d171ff | ||
|
|
a6905313c3 | ||
|
|
7e8e5bc2e5 | ||
|
|
c84259e67a | ||
|
|
63ccd5adc1 | ||
|
|
2d7bc42a9e | ||
|
|
0c6077d287 | ||
|
|
cf24936bc9 | ||
|
|
d0134192c6 | ||
|
|
f7609dca3d | ||
|
|
4add4409eb | ||
|
|
2b5bf09514 | ||
|
|
8ea1308c77 | ||
|
|
faba796303 | ||
|
|
04715e4503 | ||
|
|
39b77e43d7 | ||
|
|
b6a99dda83 | ||
|
|
ff25524d32 | ||
|
|
d37228e68e | ||
|
|
779c1a9627 | ||
|
|
f6d8ca87f1 | ||
|
|
1903f74764 | ||
|
|
181d39a9f9 | ||
|
|
f16b211a88 | ||
|
|
71f3abcf3e | ||
|
|
32197643ef | ||
|
|
d84a02d659 | ||
|
|
5d2b52cfaf | ||
|
|
3d09abb518 | ||
|
|
a69c0b6d00 | ||
|
|
c7185bf3d7 | ||
|
|
920895c583 | ||
|
|
f277ca758d | ||
|
|
afffd17485 | ||
|
|
3b83a59b1a | ||
|
|
f92dab550d | ||
|
|
7b8a160cc9 | ||
|
|
44f40373cc | ||
|
|
2ab3dbf861 | ||
|
|
be10cefba7 | ||
|
|
3d0b6cf864 | ||
|
|
af9ceda816 | ||
|
|
ec58964032 | ||
|
|
b3f7cccae4 | ||
|
|
73ff9f585b | ||
|
|
1c81efd4b6 | ||
|
|
bdda5dcf01 | ||
|
|
cd2cd18eea | ||
|
|
a7810cf774 | ||
|
|
b17b119a59 | ||
|
|
eb60d1bc5d | ||
|
|
3c09f2186d | ||
|
|
9ef0d8b59b | ||
|
|
9aa4150b9a | ||
|
|
0c5cac1c8c | ||
|
|
a52997fbd4 | ||
|
|
b6d68f6b3d | ||
|
|
5e750a4f40 | ||
|
|
0b81a0dbf5 | ||
|
|
12996de95a | ||
|
|
7995635034 | ||
|
|
b4e176a1a6 | ||
|
|
ce982e38d0 | ||
|
|
6c1f49838b | ||
|
|
3739a17b78 | ||
|
|
a1ecc12042 | ||
|
|
e78f2d64a4 | ||
|
|
6991713112 | ||
|
|
60fc1d135d | ||
|
|
89cc24fb81 | ||
|
|
1243e09dfe | ||
|
|
0dff5d89c0 | ||
|
|
b3cc8e3054 | ||
|
|
b89f8a33f1 | ||
|
|
5279a1f6b7 | ||
|
|
6f853d0248 | ||
|
|
bf8aec914e | ||
|
|
e190a66f6e | ||
|
|
dec58cba25 | ||
|
|
a7743cfe06 | ||
|
|
7e2f6b212f | ||
|
|
eb2665b034 | ||
|
|
e5eb58aee6 | ||
|
|
cb820fd2e9 | ||
|
|
d3bc063e39 | ||
|
|
fe25985eec | ||
|
|
361d5da6a4 | ||
|
|
7fc06a5534 | ||
|
|
98bcf78fc7 | ||
|
|
fc42613625 | ||
|
|
62419f0fe4 | ||
|
|
ad6bd96af6 | ||
|
|
cad1fe590c | ||
|
|
f911ae9d1c | ||
|
|
8c05c1c2f3 | ||
|
|
b2a92d9003 | ||
|
|
44d15b5ed8 | ||
|
|
6468aaff87 | ||
|
|
cbf8663997 | ||
|
|
388a605937 | ||
|
|
80cffc5fbf | ||
|
|
0e66802c32 | ||
|
|
49da696a0b | ||
|
|
517638a44e | ||
|
|
abcd00cbad | ||
|
|
3a697b94b1 | ||
|
|
807e5fb2f6 | ||
|
|
4e74a157d7 | ||
|
|
4ffde19997 | ||
|
|
40f3713130 | ||
|
|
81d4921bf0 | ||
|
|
5a508636a0 | ||
|
|
77b7fd960d | ||
|
|
c2a1f25d8d | ||
|
|
1ec3e78ebe | ||
|
|
27c071dca4 | ||
|
|
7432f59a81 | ||
|
|
4da174c5de | ||
|
|
78f894660b | ||
|
|
3ed1b9aaaf | ||
|
|
df6175fb75 | ||
|
|
dbf176ff2c | ||
|
|
f9398f8707 | ||
|
|
fa05ca2bf8 | ||
|
|
25dcd4f009 | ||
|
|
1ac1f01c41 | ||
|
|
85a64bdd2e | ||
|
|
911f1c6156 | ||
|
|
70f937dc74 | ||
|
|
191dd7c148 | ||
|
|
caf62699f1 | ||
|
|
344dafdc88 | ||
|
|
4b79059809 | ||
|
|
24d8a157b3 | ||
|
|
05551f1f14 | ||
|
|
d075a1c24b | ||
|
|
82a9fac4cb | ||
|
|
1cb4a94772 | ||
|
|
da84c544c4 | ||
|
|
0c1959dfb8 | ||
|
|
5240a4bbe9 | ||
|
|
5277f71dd9 | ||
|
|
a1610d15a9 | ||
|
|
6a9d92e1f3 | ||
|
|
6808bfddc7 | ||
|
|
062b81d537 | ||
|
|
48f5c58a3f | ||
|
|
943b78def4 | ||
|
|
2a5f47da36 | ||
|
|
0bdd010b2b | ||
|
|
576051dae3 | ||
|
|
fc70a24b41 | ||
|
|
84504ab0d2 | ||
|
|
f2a8672c22 | ||
|
|
65c6319e87 | ||
|
|
78e3957541 | ||
|
|
37fcd15f06 | ||
|
|
3244499fc6 | ||
|
|
07c25cb9af | ||
|
|
c9592ba954 | ||
|
|
34a479c1f6 | ||
|
|
f34cc2cd44 | ||
|
|
90beeda177 | ||
|
|
2dc74af290 | ||
|
|
f488019790 | ||
|
|
7fe7557f7c | ||
|
|
feca2bbe0a | ||
|
|
179f9737be | ||
|
|
c2653c96a7 | ||
|
|
d83a36206d | ||
|
|
6cdcbb5f5f | ||
|
|
8337e9ac94 | ||
|
|
18fd991175 | ||
|
|
cb3e953be3 | ||
|
|
d45d2c5922 | ||
|
|
3f0e1d947a | ||
|
|
55a475a322 | ||
|
|
e4c703b877 | ||
|
|
f8496edcc4 | ||
|
|
59cb4638ce | ||
|
|
8a7426ae02 | ||
|
|
3d5ced89c6 | ||
|
|
6334fe5696 | ||
|
|
cf280b0a9d | ||
|
|
7f3f4f5dfe | ||
|
|
f8e13c9fea | ||
|
|
e23b843375 | ||
|
|
922f65cf81 | ||
|
|
cee1d0f6cb | ||
|
|
0a3124e90f | ||
|
|
6065329c58 | ||
|
|
d521b8a99f | ||
|
|
bb67309a7c | ||
|
|
fe190a83ea | ||
|
|
ed0bcd7fee | ||
|
|
18df30c09a | ||
|
|
17ef03d05d | ||
|
|
2bf804d7c7 | ||
|
|
04da6d92d3 | ||
|
|
2e3d22baba | ||
|
|
758d35c298 | ||
|
|
f6323229f9 | ||
|
|
28a04770bc | ||
|
|
0a417bec49 | ||
|
|
2423bd45da | ||
|
|
0155624c86 | ||
|
|
1d310b2d9a | ||
|
|
231cf8e836 | ||
|
|
f55c30b21e | ||
|
|
e9f0490ed1 | ||
|
|
f9ab98159d | ||
|
|
ec9746097b | ||
|
|
067e47a99e | ||
|
|
2e760c7473 | ||
|
|
b3449fcf2a | ||
|
|
009f3c13ae | ||
|
|
06c31b3922 | ||
|
|
e9657fec00 | ||
|
|
64f2c8f9f6 | ||
|
|
db374b6d84 | ||
|
|
9056a91857 | ||
|
|
a8d94c3707 | ||
|
|
02243fdf4b | ||
|
|
810bf65030 | ||
|
|
207711b980 | ||
|
|
dce0748b43 | ||
|
|
bd299a0b84 | ||
|
|
454bb610af | ||
|
|
134752f94c | ||
|
|
92c01d1eaa | ||
|
|
4333d6a44f | ||
|
|
34440de0d5 | ||
|
|
b3e352d51d | ||
|
|
3ca2235ca1 | ||
|
|
453586a634 | ||
|
|
4313ba2c94 | ||
|
|
295683ced1 | ||
|
|
b05b11aa06 | ||
|
|
ab7e5d01f5 | ||
|
|
e206e2b956 | ||
|
|
af6cc538da | ||
|
|
8e83da9138 | ||
|
|
7811bdd509 | ||
|
|
63dc848857 | ||
|
|
2d6a9afc80 | ||
|
|
ffb12bc89d | ||
|
|
c08db04472 | ||
|
|
63dec8a96d | ||
|
|
92b01943a0 | ||
|
|
00196be0e7 | ||
|
|
106fa5ed0f | ||
|
|
42074567e8 | ||
|
|
eed4a316da | ||
|
|
feb80d4203 | ||
|
|
89c2e95dd0 | ||
|
|
142896bc0e | ||
|
|
a5f4ab9ad2 | ||
|
|
a99fbbb6c4 | ||
|
|
24f5018861 | ||
|
|
90f6c52469 | ||
|
|
66f603e2ca | ||
|
|
937bb70b08 | ||
|
|
ae509ae8ea | ||
|
|
8beddf655a | ||
|
|
3486112d1c | ||
|
|
7cadd05c5a | ||
|
|
3b19485d44 | ||
|
|
37c000b8dc | ||
|
|
8835d38fd3 | ||
|
|
4bc5ed6558 | ||
|
|
b96d409a00 | ||
|
|
da2e24f4e6 | ||
|
|
80ca4185ed | ||
|
|
c1982596d5 | ||
|
|
f6d3728bd1 | ||
|
|
b0590afc29 | ||
|
|
703c1be7c3 | ||
|
|
416c160667 | ||
|
|
d9e1943c51 | ||
|
|
52a8cd2e75 | ||
|
|
4d712a308c | ||
|
|
b81d179f27 | ||
|
|
083aa87d0a | ||
|
|
420db2dd88 | ||
|
|
5a03c2b426 | ||
|
|
a227d65aa4 | ||
|
|
5236638322 | ||
|
|
16fd293290 | ||
|
|
2d98a776c4 | ||
|
|
be784074ee | ||
|
|
7aedb82dcf | ||
|
|
e93031427a | ||
|
|
5f98df491d | ||
|
|
8970d62413 | ||
|
|
ba5224a3b7 | ||
|
|
934f5afa2a | ||
|
|
8f331e6078 | ||
|
|
e8b51cc5e3 | ||
|
|
1bcf1baa3e | ||
|
|
9c04ca7bf1 | ||
|
|
517e67aeab | ||
|
|
13d8db6ed2 | ||
|
|
2cf2d240f6 | ||
|
|
71c297d345 | ||
|
|
4b718b3cf6 | ||
|
|
6375c816ac | ||
|
|
df081ae3a4 | ||
|
|
dba194c52e | ||
|
|
493f56c28a | ||
|
|
668c37c00c | ||
|
|
7897ef596a | ||
|
|
09b8f75a59 | ||
|
|
ef4f1518cb | ||
|
|
71a3686846 | ||
|
|
c761949d68 | ||
|
|
b9bee01681 | ||
|
|
586985c45f | ||
|
|
da4e495a33 | ||
|
|
88652020ca | ||
|
|
5bd9e7ff9f | ||
|
|
c87ea60b37 | ||
|
|
60f5c23de4 | ||
|
|
57083d03fa | ||
|
|
18f5666daf | ||
|
|
43e746de6f | ||
|
|
3cc0f89dbd | ||
|
|
a9013204d6 | ||
|
|
eb1a124292 | ||
|
|
4db0ef4897 | ||
|
|
21cbfb18d4 | ||
|
|
cfe27ed599 | ||
|
|
7dafd5d9c2 | ||
|
|
76c037dd72 | ||
|
|
053a20f285 | ||
|
|
ae33741452 | ||
|
|
5c55b58278 | ||
|
|
a692a2dc66 | ||
|
|
4e97210422 | ||
|
|
4816db4791 | ||
|
|
66af7cfe07 | ||
|
|
56f3eb98a4 | ||
|
|
3ebff5d4f9 | ||
|
|
f842364c1f | ||
|
|
3d5bc801ca | ||
|
|
d04476d874 | ||
|
|
34444db544 | ||
|
|
1c6e87ac5f | ||
|
|
a7707358d6 | ||
|
|
949b85776a | ||
|
|
41551182a4 | ||
|
|
447142c7fc | ||
|
|
085e102bd6 | ||
|
|
4de0da3b7a | ||
|
|
c0fb7208d8 | ||
|
|
47cb76d78b | ||
|
|
1d03e59691 | ||
|
|
07d1db6d36 | ||
|
|
a914b46c1c | ||
|
|
52cca4dd01 | ||
|
|
2360d05f25 | ||
|
|
f5829c0c18 | ||
|
|
587ec153eb | ||
|
|
eebabf0ba1 | ||
|
|
fc93028e57 | ||
|
|
42bbce2f57 | ||
|
|
86430e12e1 | ||
|
|
c4daf60e8c | ||
|
|
66341a1bc4 | ||
|
|
2ae0d23271 | ||
|
|
029491fb84 | ||
|
|
9a91b5fba7 | ||
|
|
b16ad3d28b | ||
|
|
2c7d9bce74 | ||
|
|
2f7343bdc0 | ||
|
|
7e474b7599 | ||
|
|
03d9ccb8aa |
+4
-117
@@ -1,119 +1,6 @@
|
||||
// ! If we don't have this file, running ESLint will fail because of the import/no-unused-modules rule
|
||||
// ! See: https://github.com/import-js/eslint-plugin-import/issues/3079
|
||||
// ! When this issue is resolved, this file can be removed
|
||||
{
|
||||
"env": {
|
||||
"commonjs": true,
|
||||
"es2021": true,
|
||||
"node": true
|
||||
},
|
||||
"parserOptions": {
|
||||
"ecmaVersion": "latest",
|
||||
"sourceType": "commonjs"
|
||||
},
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"airbnb",
|
||||
"plugin:cypress/recommended",
|
||||
"plugin:import/recommended",
|
||||
"plugin:prettier/recommended"
|
||||
],
|
||||
"overrides": [
|
||||
{
|
||||
"files": ["**/*.ts"],
|
||||
"env": {
|
||||
"es2021": true,
|
||||
"node": true
|
||||
},
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"airbnb-typescript",
|
||||
"plugin:prettier/recommended"
|
||||
],
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 2021,
|
||||
"sourceType": "module",
|
||||
"project": true
|
||||
},
|
||||
"rules": {
|
||||
"import/prefer-default-export": "off",
|
||||
"@typescript-eslint/lines-between-class-members": "off",
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
"@typescript-eslint/no-shadow": "off",
|
||||
"@typescript-eslint/return-await": "off"
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": ["services/backend/src/rapodiff/**/*"],
|
||||
"rules": {
|
||||
"no-console": "off"
|
||||
}
|
||||
}
|
||||
],
|
||||
"rules": {
|
||||
"camelcase": "off",
|
||||
"consistent-return": "off",
|
||||
"cypress/no-assigning-return-values": "off",
|
||||
"cypress/unsafe-to-chain-command": "off",
|
||||
"default-param-last": "off",
|
||||
"id-denylist": ["error", "c", "d", "e", "err", "t"],
|
||||
"import/extensions": ["error", "never"],
|
||||
"import/order": [
|
||||
"error",
|
||||
{
|
||||
"alphabetize": { "order": "asc", "caseInsensitive": true },
|
||||
"groups": [["builtin", "external"], ["internal"], ["parent"], ["sibling", "index"]],
|
||||
"pathGroups": [
|
||||
{
|
||||
"pattern": "../../../**",
|
||||
"group": "internal",
|
||||
"position": "before"
|
||||
},
|
||||
{
|
||||
"pattern": "../../**",
|
||||
"group": "internal",
|
||||
"position": "after"
|
||||
},
|
||||
{
|
||||
"pattern": "../**",
|
||||
"group": "parent",
|
||||
"position": "before"
|
||||
},
|
||||
{
|
||||
"pattern": "./**",
|
||||
"group": "sibling",
|
||||
"position": "before"
|
||||
}
|
||||
],
|
||||
"pathGroupsExcludedImportTypes": ["builtin", "external"]
|
||||
}
|
||||
],
|
||||
"no-async-promise-executor": "off",
|
||||
"no-await-in-loop": "off",
|
||||
"no-console": "error",
|
||||
"no-continue": "off",
|
||||
"no-param-reassign": "off",
|
||||
"no-plusplus": "off",
|
||||
"no-promise-executor-return": "off",
|
||||
"no-restricted-syntax": [
|
||||
"error",
|
||||
{
|
||||
"selector": "ForInStatement",
|
||||
"message": "for..in loops iterate over the entire prototype chain, which is virtually never what you want. Use Object.{keys,values,entries}, and iterate over the resulting array."
|
||||
},
|
||||
"LabeledStatement",
|
||||
"WithStatement"
|
||||
],
|
||||
"no-return-await": "off",
|
||||
"no-shadow": "off",
|
||||
"prettier/prettier": "error",
|
||||
"quotes": ["error", "single", { "avoidEscape": true }]
|
||||
},
|
||||
"settings": {
|
||||
"import/resolver": {
|
||||
"node": {
|
||||
"extensions": [".js", ".jsx", ".ts", ".tsx"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"ignorePatterns": ["build/", "dist/", "instrumented/", "node_modules/", "**/*migrations*/**"]
|
||||
"ignorePatterns": ["**/node_modules/", "**/dist/", "**/build/"]
|
||||
}
|
||||
|
||||
@@ -18,4 +18,4 @@ runs:
|
||||
run: npm run stylelint "services/frontend/src/**/*.css"
|
||||
- name: 'Run ESLint for JS and TS files'
|
||||
shell: bash
|
||||
run: npm run eslint -- --ext .js,.jsx,.ts,.tsx services --report-unused-disable-directives
|
||||
run: npm run eslint -- services
|
||||
|
||||
@@ -11,6 +11,9 @@ inputs:
|
||||
spec:
|
||||
required: true
|
||||
description: 'Path to Cypress file'
|
||||
test_name:
|
||||
required: true
|
||||
description: 'Name of the test'
|
||||
|
||||
runs:
|
||||
using: composite
|
||||
@@ -35,10 +38,10 @@ runs:
|
||||
CYPRESS_baseUrl: http://localhost:1337
|
||||
|
||||
- name: 'Upload video if tests fail'
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
if: failure()
|
||||
with:
|
||||
name: Cypress videos
|
||||
name: 'Cypress videos - ${{ inputs.test_name }}'
|
||||
path: ./cypress/videos/*
|
||||
|
||||
- name: 'Print logs on failure'
|
||||
|
||||
@@ -14,6 +14,7 @@ on:
|
||||
|
||||
jobs:
|
||||
lint_backend_and_frontend:
|
||||
name: 'Lint backend and frontend'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@@ -54,8 +55,6 @@ jobs:
|
||||
spec: cypress/e2e/Language_center.js
|
||||
- name: 'Test study guidance groups'
|
||||
spec: cypress/e2e/Study_guidance_groups.js
|
||||
- name: 'Test evaluation overview'
|
||||
spec: cypress/e2e/Evaluation_overview.js
|
||||
- name: 'Test university'
|
||||
spec: cypress/e2e/University.js
|
||||
name: ${{ matrix.name }}
|
||||
@@ -67,3 +66,4 @@ jobs:
|
||||
username: ${{ secrets.TOSKAREGISTRY_USERNAME }}
|
||||
password: ${{ secrets.TOSKAREGISTRY_PASSWORD }}
|
||||
spec: ${{ matrix.spec }}
|
||||
test_name: ${{ matrix.name }}
|
||||
|
||||
+45
-27
@@ -38,9 +38,12 @@ jobs:
|
||||
push: false
|
||||
tags: oodikone-backend:staging
|
||||
outputs: type=docker,dest=/tmp/backend.tar
|
||||
env:
|
||||
DOCKER_BUILD_SUMMARY: false
|
||||
- name: 'Upload image for other jobs'
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: backend-image
|
||||
path: /tmp/backend.tar
|
||||
|
||||
build_frontend:
|
||||
@@ -65,18 +68,37 @@ jobs:
|
||||
BASE_PATH=/oodikone
|
||||
STAGING=true
|
||||
|
||||
build_and_push_anon_sis_db:
|
||||
name: 'Build anon sis-db and push to toska hub if new migrations have been added'
|
||||
check_migration_files:
|
||||
name: Check if new migrations have been added
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
kone_db_migrations: ${{ steps.check_migrations.outputs.kone_db_migrations }}
|
||||
user_db_migrations: ${{ steps.check_migrations.outputs.user_db_migrations }}
|
||||
sis_db_migrations: ${{ steps.check_migrations.outputs.sis_db_migrations }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dorny/paths-filter@v3
|
||||
- name: Check if new migrations have been added
|
||||
id: check_migrations
|
||||
uses: dorny/paths-filter@v3
|
||||
with:
|
||||
filters: |
|
||||
migrations:
|
||||
kone_db_migrations:
|
||||
- 'services/backend/src/database/migrations_kone/**'
|
||||
- '.github/workflows/test.yaml'
|
||||
user_db_migrations:
|
||||
- 'services/backend/src/database/migrations_user/**'
|
||||
- '.github/workflows/test.yaml'
|
||||
sis_db_migrations:
|
||||
- 'updater/sis-updater-worker/src/db/migrations/**'
|
||||
- '.github/workflows/test.yaml'
|
||||
|
||||
build_and_push_anon_sis_db:
|
||||
name: 'Build anon sis-db and push to toska hub if new migrations have been added'
|
||||
needs: check_migration_files
|
||||
runs-on: ubuntu-latest
|
||||
if: needs.check_migration_files.outputs.sis_db_migrations == 'true'
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: 'Login to toska docker registry'
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
@@ -84,7 +106,6 @@ jobs:
|
||||
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: |
|
||||
docker compose --file docker-compose.ci.yml up --detach sis-updater-worker
|
||||
sleep 30 # wait for sis-updater-worker to start and run migrations
|
||||
@@ -95,22 +116,17 @@ jobs:
|
||||
needs:
|
||||
- build_backend
|
||||
- build_frontend
|
||||
- check_migration_files
|
||||
runs-on: ubuntu-latest
|
||||
if: needs.check_migration_files.outputs.kone_db_migrations == 'true'
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dorny/paths-filter@v3
|
||||
id: check_migrations
|
||||
- name: 'Download backend image'
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
filters: |
|
||||
migrations:
|
||||
- 'services/backend/src/database/migrations_kone/**'
|
||||
- '.github/workflows/test.yaml'
|
||||
- name: 'Download images'
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
path: /tmp/
|
||||
name: backend-image
|
||||
- name: 'Load built backend image'
|
||||
run: docker load --input /tmp/artifact/backend.tar
|
||||
run: docker load --input backend.tar
|
||||
- name: 'Login to toska docker registry'
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
@@ -118,10 +134,9 @@ jobs:
|
||||
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: |
|
||||
docker compose --file docker-compose.ci.yml up --detach backend
|
||||
sleep 30 # wait for sis-updater-worker to start and run migrations
|
||||
sleep 30 # wait for backend to start and run migrations
|
||||
./anonyymioodi/commit_and_push_to_toska_hub.sh kone-db
|
||||
|
||||
build_and_push_anon_user_db:
|
||||
@@ -129,7 +144,9 @@ jobs:
|
||||
needs:
|
||||
- build_backend
|
||||
- build_frontend
|
||||
- check_migration_files
|
||||
runs-on: ubuntu-latest
|
||||
if: needs.check_migration_files.outputs.user_db_migrations == 'true'
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dorny/paths-filter@v3
|
||||
@@ -139,12 +156,12 @@ jobs:
|
||||
migrations:
|
||||
- 'services/backend/src/database/migrations_user/**'
|
||||
- '.github/workflows/test.yaml'
|
||||
- name: 'Download images'
|
||||
uses: actions/download-artifact@v3
|
||||
- name: 'Download backend image'
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: /tmp/
|
||||
name: backend-image
|
||||
- name: 'Load built backend image'
|
||||
run: docker load --input /tmp/artifact/backend.tar
|
||||
run: docker load --input backend.tar
|
||||
- name: 'Login to toska docker registry'
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
@@ -152,10 +169,9 @@ jobs:
|
||||
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: |
|
||||
docker compose --file docker-compose.ci.yml up --detach backend
|
||||
sleep 30 # wait for sis-updater-worker to start and run migrations
|
||||
sleep 30 # wait for backend to start and run migrations
|
||||
./anonyymioodi/commit_and_push_to_toska_hub.sh user-db
|
||||
|
||||
# === End-to-end tests ===
|
||||
@@ -165,6 +181,8 @@ jobs:
|
||||
- build_and_push_anon_sis_db
|
||||
- build_and_push_anon_kone_db
|
||||
- build_and_push_anon_user_db
|
||||
# Always run the tests, even if some of the needed jobs failed or were skipped
|
||||
if: always()
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -195,8 +213,6 @@ jobs:
|
||||
spec: cypress/e2e/Language_center.js
|
||||
- name: 'Test study guidance groups'
|
||||
spec: cypress/e2e/Study_guidance_groups.js
|
||||
- name: 'Test evaluation overview'
|
||||
spec: cypress/e2e/Evaluation_overview.js
|
||||
- name: 'Test university'
|
||||
spec: cypress/e2e/University.js
|
||||
name: ${{ matrix.name }}
|
||||
@@ -208,6 +224,7 @@ jobs:
|
||||
username: ${{ secrets.username }}
|
||||
password: ${{ secrets.password }}
|
||||
spec: ${{ matrix.spec }}
|
||||
test_name: ${{ matrix.name }}
|
||||
|
||||
# === Other tests ===
|
||||
test_service_exit_statuses:
|
||||
@@ -216,6 +233,7 @@ jobs:
|
||||
- build_and_push_anon_sis_db
|
||||
- build_and_push_anon_kone_db
|
||||
- build_and_push_anon_user_db
|
||||
if: always()
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
@@ -27,7 +27,7 @@ jobs:
|
||||
- name: 'Install dependencies'
|
||||
run: npm ci && cd updater/sis-updater-scheduler && npm ci && cd ../sis-updater-worker && npm ci
|
||||
- name: 'Run linter'
|
||||
run: npm run eslint updater -- --report-unused-disable-directives
|
||||
run: npm run eslint -- updater
|
||||
|
||||
build_worker:
|
||||
name: 'Build Updater worker'
|
||||
@@ -42,9 +42,12 @@ jobs:
|
||||
push: false
|
||||
outputs: type=docker,dest=/tmp/worker.tar
|
||||
tags: toska/sis-updater-worker:latest
|
||||
- name: 'Store built image'
|
||||
uses: actions/upload-artifact@v3
|
||||
env:
|
||||
DOCKER_BUILD_SUMMARY: false
|
||||
- name: 'Upload image for other jobs'
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: updater-worker-image
|
||||
path: /tmp/worker.tar
|
||||
|
||||
build_scheduler:
|
||||
@@ -60,9 +63,12 @@ jobs:
|
||||
push: false
|
||||
outputs: type=docker,dest=/tmp/scheduler.tar
|
||||
tags: toska/sis-updater-scheduler:latest
|
||||
- name: 'Store built image'
|
||||
uses: actions/upload-artifact@v3
|
||||
env:
|
||||
DOCKER_BUILD_SUMMARY: false
|
||||
- name: 'Upload image for other jobs'
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: updater-scheduler-image
|
||||
path: /tmp/scheduler.tar
|
||||
|
||||
test_updater:
|
||||
@@ -81,13 +87,14 @@ jobs:
|
||||
username: ${{ secrets.TOSKAREGISTRY_USERNAME }}
|
||||
password: ${{ secrets.TOSKAREGISTRY_PASSWORD }}
|
||||
- name: 'Download built images'
|
||||
uses: actions/download-artifact@v3
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: /tmp/
|
||||
pattern: 'updater-{worker,scheduler}-image'
|
||||
merge-multiple: true
|
||||
- name: 'Load worker image'
|
||||
run: docker load --input /tmp/artifact/worker.tar
|
||||
run: docker load --input worker.tar
|
||||
- name: 'Load scheduler image'
|
||||
run: docker load --input /tmp/artifact/scheduler.tar
|
||||
run: docker load --input scheduler.tar
|
||||
- name: 'Start worker'
|
||||
run: docker compose --file docker-compose.ci.yml up --detach --no-recreate sis-updater-worker
|
||||
- name: 'Run hourly jobs'
|
||||
@@ -128,6 +135,9 @@ jobs:
|
||||
context: updater/sis-updater-worker
|
||||
containerfiles: |
|
||||
updater/sis-updater-worker/Dockerfile
|
||||
build-args: |
|
||||
SENTRY_RELEASE=${{ github.sha }}
|
||||
SENTRY_ENVIRONMENT=production
|
||||
|
||||
- name: Push to quay.io
|
||||
uses: redhat-actions/push-to-registry@v2
|
||||
@@ -155,6 +165,9 @@ jobs:
|
||||
context: updater/sis-updater-scheduler
|
||||
containerfiles: |
|
||||
updater/sis-updater-scheduler/Dockerfile
|
||||
build-args: |
|
||||
SENTRY_RELEASE=${{ github.sha }}
|
||||
SENTRY_ENVIRONMENT=production
|
||||
|
||||
- name: Push to quay.io
|
||||
uses: redhat-actions/push-to-registry@v2
|
||||
|
||||
+3
-3
@@ -1,7 +1,7 @@
|
||||
/* eslint-disable global-require */
|
||||
/* eslint-disable import/extensions */
|
||||
/* eslint-disable @typescript-eslint/no-require-imports */
|
||||
/* eslint-disable import/no-commonjs */
|
||||
/* eslint-disable import/no-extraneous-dependencies */
|
||||
|
||||
/* eslint-disable import/no-unused-modules */
|
||||
const { defineConfig } = require('cypress')
|
||||
|
||||
module.exports = defineConfig({
|
||||
|
||||
+655
-271
File diff suppressed because it is too large
Load Diff
@@ -1,514 +0,0 @@
|
||||
/// <reference types="cypress" />
|
||||
|
||||
const { getEmptyYears } = require('../support/commands')
|
||||
|
||||
describe('Evaluation overview', () => {
|
||||
describe('Programme view', () => {
|
||||
beforeEach(() => {
|
||||
cy.init('/evaluationoverview/programme/KH50_001', 'admin')
|
||||
})
|
||||
|
||||
it('Page opens correctly', () => {
|
||||
cy.contains('Matemaattisten tieteiden kandiohjelma')
|
||||
cy.contains('KH50_001')
|
||||
cy.contains("This view is an abridged version of Oodikone's Studyprogramme Overview")
|
||||
})
|
||||
|
||||
it('Progress data is shown correctly with graduated included', () => {
|
||||
cy.get('[data-cy="Graph-StudytrackProgress"]').within(() => {
|
||||
const totalStats = ['12.9%', '6.9%', '9.7%', '9.2%', '3.7%', '6.0%', '51.6%']
|
||||
for (const stat of totalStats) {
|
||||
cy.contains(stat)
|
||||
}
|
||||
})
|
||||
|
||||
const years = getEmptyYears(true)
|
||||
|
||||
const tableContents = [
|
||||
...years.map(year => [year, 0, 0, 0, 0, 0, 0, 0, 0]),
|
||||
['2023 - 2024', 8, 8, 0, 0, 0, 0, 0, 0],
|
||||
['2022 - 2023', 26, 9, 9, 4, 3, 0, 1, 0],
|
||||
['2021 - 2022', 32, 8, 3, 8, 8, 5, 0, 0],
|
||||
['2020 - 2021', 27, 2, 1, 3, 6, 2, 4, 9],
|
||||
['2019 - 2020', 33, 1, 0, 2, 1, 0, 3, 26],
|
||||
['2018 - 2019', 44, 0, 1, 1, 2, 0, 3, 37],
|
||||
['2017 - 2018', 47, 0, 1, 3, 0, 1, 2, 40],
|
||||
['Total', 217, 28, 15, 21, 20, 8, 13, 112],
|
||||
]
|
||||
|
||||
cy.checkTableStats(tableContents, 'StudytrackProgress')
|
||||
})
|
||||
|
||||
it('Progress data is shown correctly with graduated excluded', () => {
|
||||
cy.get('[data-cy=GraduatedToggle]').click()
|
||||
cy.get('[data-cy="Graph-StudytrackProgress"]').within(() => {
|
||||
const totalStats = ['30.8%', '15.4%', '22.0%', '20.9%', '4.4%', '5.5%']
|
||||
for (const stat of totalStats) {
|
||||
cy.contains(stat)
|
||||
}
|
||||
})
|
||||
|
||||
const years = getEmptyYears(true)
|
||||
|
||||
const tableContents = [
|
||||
...years.map(year => [year, 0, 0, 0, 0, 0, 0, 0, 0]),
|
||||
['2023 - 2024', 8, 8, 0, 0, 0, 0, 0, 0],
|
||||
['2022 - 2023', 24, 9, 8, 4, 3, 0, 0, 0],
|
||||
['2021 - 2022', 31, 8, 3, 8, 8, 4, 0, 0],
|
||||
['2020 - 2021', 11, 2, 1, 2, 5, 0, 0, 1],
|
||||
['2019 - 2020', 7, 1, 0, 2, 1, 0, 0, 3],
|
||||
['2018 - 2019', 5, 0, 1, 1, 2, 0, 0, 1],
|
||||
['2017 - 2018', 5, 0, 1, 3, 0, 0, 1, 0],
|
||||
['Total', 91, 28, 14, 20, 19, 4, 1, 5],
|
||||
]
|
||||
|
||||
cy.checkTableStats(tableContents, 'StudytrackProgress')
|
||||
})
|
||||
|
||||
it('Graduation times breakdown data is shown correctly', () => {
|
||||
cy.get('[data-cy=graduation-times-graph-breakdown]').within(() => {
|
||||
cy.contains('Graduation year')
|
||||
cy.contains('2021')
|
||||
cy.get('[aria-label="2021, 8. Overtime."]')
|
||||
cy.get('[aria-label="2021, 24. Max. year overtime."]')
|
||||
cy.get('[aria-label="2021, 14. On time."]').trigger('mouseover')
|
||||
cy.contains('Graduated On time: 14 students')
|
||||
})
|
||||
|
||||
cy.get('[data-cy=YearToggle]').click()
|
||||
|
||||
cy.get('[data-cy=graduation-times-graph-breakdown]').within(() => {
|
||||
cy.contains('Graduation year')
|
||||
cy.contains('2021 - 2022')
|
||||
cy.get('[aria-label="2021 - 2022, 13. Overtime."]')
|
||||
cy.get('[aria-label="2021 - 2022, 21. Max. year overtime."]')
|
||||
cy.get('[aria-label="2021 - 2022, 21. On time."]').trigger('mouseover')
|
||||
cy.contains('Graduated On time: 21 students')
|
||||
})
|
||||
})
|
||||
|
||||
it('Graduation times median time data is shown correctly', () => {
|
||||
cy.get('[data-cy=GraduationTimeToggle]').click()
|
||||
|
||||
cy.get('[data-cy=graduation-times-graph]').within(() => {
|
||||
cy.contains('Graduation year')
|
||||
cy.contains('2021')
|
||||
cy.contains('46 graduated').trigger('mouseover')
|
||||
cy.contains('46 students graduated in year 2021')
|
||||
cy.contains('median study time: 41 months')
|
||||
cy.contains('14 graduated on time')
|
||||
cy.contains('24 graduated max year overtime')
|
||||
cy.contains('8 graduated over year late')
|
||||
})
|
||||
|
||||
cy.get('[data-cy=YearToggle]').click()
|
||||
|
||||
cy.get('[data-cy=graduation-times-graph]').within(() => {
|
||||
cy.contains('Graduation year')
|
||||
cy.contains('2021 - 2022')
|
||||
cy.contains('55 graduated').trigger('mouseover')
|
||||
cy.contains('55 students graduated in year 2021 - 2022')
|
||||
cy.contains('median study time: 40 months')
|
||||
cy.contains('21 graduated on time')
|
||||
cy.contains('21 graduated max year overtime')
|
||||
cy.contains('13 graduated over year late')
|
||||
})
|
||||
})
|
||||
|
||||
it("'Programmes before or after' data is shown correctly", () => {
|
||||
cy.get('[data-cy=Graph-ProgrammesBeforeOrAfter]').within(() => {
|
||||
cy.contains('Matematiikan ja tilastotieteen maisteriohjelma')
|
||||
cy.contains('Tietojenkäsittelytieteen maisteriohjelma')
|
||||
cy.contains('Datatieteen maisteriohjelma')
|
||||
cy.contains('2021')
|
||||
cy.contains('32')
|
||||
cy.contains('19')
|
||||
})
|
||||
|
||||
let emptyYears = new Array(getEmptyYears(false).length).fill(0)
|
||||
|
||||
let tableContents = [
|
||||
['MH50_001', 'MAST', 'Matematiikan ja tilastotieteen maisteriohjelma', 0, 0, 1, 9, 32, 19, 15, ...emptyYears],
|
||||
['MH50_002', 'LSI', 'Life Science Informatics -maisteriohjelma', 0, 0, 0, 1, 2, 1, 1, ...emptyYears],
|
||||
[
|
||||
'MH50_003',
|
||||
'TCM',
|
||||
'Teoreettisten ja laskennallisten menetelmien maisteriohjelma',
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
2,
|
||||
1,
|
||||
...emptyYears,
|
||||
],
|
||||
['MH50_009', 'CSM', 'Tietojenkäsittelytieteen maisteriohjelma', 0, 0, 0, 0, 2, 3, 2, ...emptyYears],
|
||||
['MH50_010', 'DATA', 'Datatieteen maisteriohjelma', 0, 0, 0, 1, 5, 10, 1, ...emptyYears],
|
||||
['MH70_009', 'ECON', 'Taloustieteen maisteriohjelma', 0, 0, 0, 0, 4, 7, 4, ...emptyYears],
|
||||
]
|
||||
|
||||
cy.checkTableStats(tableContents, 'undefined')
|
||||
|
||||
cy.get('[data-cy=YearToggle]').click()
|
||||
|
||||
cy.get('[data-cy=Graph-ProgrammesBeforeOrAfter]').within(() => {
|
||||
cy.contains('Matematiikan ja tilastotieteen maisteriohjelma')
|
||||
cy.contains('Tietojenkäsittelytieteen maisteriohjelma')
|
||||
cy.contains('Datatieteen maisteriohjelma')
|
||||
cy.contains('2021 - 2022')
|
||||
cy.contains('28')
|
||||
cy.contains('16')
|
||||
})
|
||||
|
||||
emptyYears = new Array(getEmptyYears(true).length).fill(0)
|
||||
|
||||
tableContents = [
|
||||
['MH50_001', 'MAST', 'Matematiikan ja tilastotieteen maisteriohjelma', 0, 0, 4, 24, 28, 16, 4, ...emptyYears],
|
||||
['MH50_002', 'LSI', 'Life Science Informatics -maisteriohjelma', 0, 0, 1, 0, 3, 1, 0, ...emptyYears],
|
||||
[
|
||||
'MH50_003',
|
||||
'TCM',
|
||||
'Teoreettisten ja laskennallisten menetelmien maisteriohjelma',
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
1,
|
||||
2,
|
||||
0,
|
||||
...emptyYears,
|
||||
],
|
||||
['MH50_009', 'CSM', 'Tietojenkäsittelytieteen maisteriohjelma', 0, 0, 0, 0, 4, 3, 0, ...emptyYears],
|
||||
['MH50_010', 'DATA', 'Datatieteen maisteriohjelma', 0, 0, 1, 0, 12, 4, 0, ...emptyYears],
|
||||
['MH70_009', 'ECON', 'Taloustieteen maisteriohjelma', 0, 0, 0, 4, 6, 5, 0, ...emptyYears],
|
||||
]
|
||||
|
||||
cy.checkTableStats(tableContents, 'undefined')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Faculty view', () => {
|
||||
beforeEach(() => {
|
||||
cy.init('/evaluationoverview/faculty/H50', 'admin')
|
||||
})
|
||||
|
||||
it('Page opens correctly', () => {
|
||||
cy.contains('Matemaattis-luonnontieteellinen tiedekunta')
|
||||
cy.contains('H50')
|
||||
cy.contains("This view is an abridged version of Oodikone's Faculty Overview")
|
||||
})
|
||||
|
||||
describe('Progress stats', () => {
|
||||
describe('Bachelor stats', () => {
|
||||
it('graph with graduated included is shown correctly', () => {
|
||||
cy.get('[data-cy="Graph-FacultyBachelorsProgress"]').within(() => {
|
||||
const totalStats = ['12.2%', '5.3%', '7.5%', '8.2%', '5.3%', '6.3%', '55.2%']
|
||||
for (const stat of totalStats) {
|
||||
cy.contains(stat)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
it('graph with graduated excluded is shown correctly', () => {
|
||||
cy.get('[data-cy="GraduatedToggle"]').click()
|
||||
cy.get('[data-cy="Graph-FacultyBachelorsProgress"]').within(() => {
|
||||
const totalStats = ['30.5%', '12.5%', '18.0%', '19.5%', '7.0%', '3.1%', '9.4%']
|
||||
for (const stat of totalStats) {
|
||||
cy.contains(stat)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
it('table with graduated included is shown correctly', () => {
|
||||
cy.get('[data-cy="Table-FacultyBachelorsProgress"]')
|
||||
const years = getEmptyYears(true)
|
||||
const tableContents = [
|
||||
...years.map(year => [year, 0, 0, 0, 0, 0, 0, 0, 0]),
|
||||
['2023 - 2024', 14, 13, 0, 0, 0, 0, 0, 1],
|
||||
['2022 - 2023', 32, 11, 10, 6, 4, 0, 1, 0],
|
||||
['2021 - 2022', 44, 10, 4, 9, 11, 7, 1, 2],
|
||||
['2020 - 2021', 38, 3, 1, 3, 7, 3, 8, 13],
|
||||
['2019 - 2020', 50, 2, 0, 2, 1, 4, 5, 36],
|
||||
['2018 - 2019', 75, 0, 1, 1, 2, 2, 3, 66],
|
||||
['2017 - 2018', 66, 0, 1, 3, 1, 1, 2, 58],
|
||||
['Total', 319, 39, 17, 24, 26, 17, 20, 176],
|
||||
]
|
||||
cy.checkTableStats(tableContents, 'FacultyBachelorsProgress')
|
||||
})
|
||||
|
||||
it('table with graduated excluded is shown correctly', () => {
|
||||
cy.get('[data-cy="GraduatedToggle"]').click()
|
||||
cy.get('[data-cy="Table-FacultyBachelorsProgress"]')
|
||||
const years = getEmptyYears(true)
|
||||
const tableContents = [
|
||||
...years.map(year => [year, 0, 0, 0, 0, 0, 0, 0, 0]),
|
||||
['2023 - 2024', 14, 13, 0, 0, 0, 0, 0, 1],
|
||||
['2022 - 2023', 30, 11, 9, 6, 4, 0, 0, 0],
|
||||
['2021 - 2022', 41, 10, 4, 9, 11, 6, 0, 1],
|
||||
['2020 - 2021', 14, 3, 1, 2, 6, 0, 1, 1],
|
||||
['2019 - 2020', 14, 2, 0, 2, 1, 3, 2, 4],
|
||||
['2018 - 2019', 6, 0, 1, 1, 2, 0, 0, 2],
|
||||
['2017 - 2018', 9, 0, 1, 3, 1, 0, 1, 3],
|
||||
['Total', 128, 39, 16, 23, 25, 9, 4, 12],
|
||||
]
|
||||
cy.checkTableStats(tableContents, 'FacultyBachelorsProgress')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Bachelor + master stats', () => {
|
||||
it('graph with graduated included is shown correctly', () => {
|
||||
cy.get('[data-cy=Graph-FacultyBachelorMastersProgress]').within(() => {
|
||||
const totalStats = ['21.3%', '8.4%', '10.1%', '14.0%', '12.4%', '6.7%', '27.0%']
|
||||
for (const stat of totalStats) {
|
||||
cy.contains(stat)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
it('graph with graduated excluded is shown correctly', () => {
|
||||
cy.get('[data-cy="GraduatedToggle"]').click()
|
||||
cy.get('[data-cy=Graph-FacultyBachelorMastersProgress]').within(() => {
|
||||
const totalStats = ['28.7%', '11.6%', '14.0%', '18.6%', '15.5%', '3.1%', '8.5%']
|
||||
for (const stat of totalStats) {
|
||||
cy.contains(stat)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
it('table with graduated included is shown correctly', () => {
|
||||
cy.get('[data-cy="Table-FacultyBachelorMastersProgress"]')
|
||||
const years = getEmptyYears(true)
|
||||
const tableContents = [
|
||||
...years.map(year => [year, 0, 0, 0, 0, 0, 0, 0, 0]),
|
||||
['2023 - 2024', 0, 0, 0, 0, 0, 0, 0, 0],
|
||||
['2022 - 2023', 2, 2, 0, 0, 0, 0, 0, 0],
|
||||
['2021 - 2022', 4, 2, 0, 0, 0, 1, 0, 1],
|
||||
['2020 - 2021', 23, 13, 2, 4, 0, 4, 0, 0],
|
||||
['2019 - 2020', 32, 7, 7, 4, 7, 3, 3, 1],
|
||||
['2018 - 2019', 63, 10, 2, 5, 13, 7, 4, 22],
|
||||
['2017 - 2018', 54, 4, 4, 5, 5, 7, 5, 24],
|
||||
['Total', 178, 38, 15, 18, 25, 22, 12, 48],
|
||||
]
|
||||
cy.checkTableStats(tableContents, 'FacultyBachelorMastersProgress')
|
||||
})
|
||||
|
||||
it('table with graduated excluded is shown correctly', () => {
|
||||
cy.get('[data-cy="GraduatedToggle"]').click()
|
||||
cy.get('[data-cy="Table-FacultyBachelorMastersProgress"]')
|
||||
const years = getEmptyYears(true)
|
||||
const tableContents = [
|
||||
...years.map(year => [year, 0, 0, 0, 0, 0, 0, 0, 0]),
|
||||
['2023 - 2024', 0, 0, 0, 0, 0, 0, 0, 0],
|
||||
['2022 - 2023', 2, 2, 0, 0, 0, 0, 0, 0],
|
||||
['2021 - 2022', 4, 2, 0, 0, 0, 1, 0, 1],
|
||||
['2020 - 2021', 21, 13, 2, 4, 0, 2, 0, 0],
|
||||
['2019 - 2020', 31, 7, 7, 4, 7, 3, 2, 1],
|
||||
['2018 - 2019', 41, 9, 2, 5, 12, 7, 1, 5],
|
||||
['2017 - 2018', 30, 4, 4, 5, 5, 7, 1, 4],
|
||||
['Total', 129, 37, 15, 18, 24, 20, 4, 11],
|
||||
]
|
||||
cy.checkTableStats(tableContents, 'FacultyBachelorMastersProgress')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Master stats', () => {
|
||||
it('graph with graduated included is shown correctly', () => {
|
||||
cy.get('[data-cy=Graph-FacultyMastersProgress]').within(() => {
|
||||
const totalStats = ['16.1%', '12.9%', '6.5%', '12.9%', '9.7%', '16.1%', '25.8%']
|
||||
for (const stat of totalStats) {
|
||||
cy.contains(stat)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
it('graph with graduated excluded is shown correctly', () => {
|
||||
cy.get('[data-cy="GraduatedToggle"]').click()
|
||||
cy.get('[data-cy=Graph-FacultyMastersProgress]').within(() => {
|
||||
const totalStats = ['23.8%', '19.0%', '9.5%', '9.5%', '9.5%', '14.3%', '14.3%']
|
||||
for (const stat of totalStats) {
|
||||
cy.contains(stat)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
it('table with graduated included is shown correctly', () => {
|
||||
cy.get('[data-cy="Table-FacultyMastersProgress"]')
|
||||
const years = getEmptyYears(true)
|
||||
const tableContents = [
|
||||
...years.map(year => [year, 0, 0, 0, 0, 0, 0, 0, 0]),
|
||||
['2023 - 2024', 4, 4, 0, 0, 0, 0, 0, 0],
|
||||
['2022 - 2023', 5, 0, 1, 1, 2, 0, 0, 1],
|
||||
['2021 - 2022', 8, 1, 2, 0, 2, 1, 1, 1],
|
||||
['2020 - 2021', 3, 0, 0, 0, 0, 0, 2, 1],
|
||||
['2019 - 2020', 7, 0, 0, 1, 0, 1, 1, 4],
|
||||
['2018 - 2019', 2, 0, 0, 0, 0, 1, 0, 1],
|
||||
['2017 - 2018', 2, 0, 1, 0, 0, 0, 1, 0],
|
||||
['Total', 31, 5, 4, 2, 4, 3, 5, 8],
|
||||
]
|
||||
cy.checkTableStats(tableContents, 'FacultyMastersProgress')
|
||||
})
|
||||
|
||||
it('table with graduated excluded is shown correctly', () => {
|
||||
cy.get('[data-cy="GraduatedToggle"]').click()
|
||||
cy.get('[data-cy="Table-FacultyMastersProgress"]')
|
||||
const years = getEmptyYears(true)
|
||||
const tableContents = [
|
||||
...years.map(year => [year, 0, 0, 0, 0, 0, 0, 0, 0]),
|
||||
['2023 - 2024', 4, 4, 0, 0, 0, 0, 0, 0],
|
||||
['2022 - 2023', 5, 0, 1, 1, 2, 0, 0, 1],
|
||||
['2021 - 2022', 5, 1, 2, 0, 0, 0, 1, 1],
|
||||
['2020 - 2021', 0, 0, 0, 0, 0, 0, 0, 0],
|
||||
['2019 - 2020', 3, 0, 0, 1, 0, 1, 1, 0],
|
||||
['2018 - 2019', 2, 0, 0, 0, 0, 1, 0, 1],
|
||||
['2017 - 2018', 2, 0, 1, 0, 0, 0, 1, 0],
|
||||
['Total', 21, 5, 4, 2, 2, 2, 3, 3],
|
||||
]
|
||||
cy.checkTableStats(tableContents, 'FacultyMastersProgress')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Doctor stats', () => {
|
||||
it('graph with graduated included is shown correctly', () => {
|
||||
cy.get('[data-cy=Graph-FacultyDoctoralProgress]').within(() => {
|
||||
const totalStats = ['6.0%', '18.0%', '16.0%', '14.0%', '46.0%']
|
||||
for (const stat of totalStats) {
|
||||
cy.contains(stat)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
it('graph with graduated excluded is shown correctly', () => {
|
||||
cy.get('[data-cy="GraduatedToggle"]').click()
|
||||
cy.get('[data-cy=Graph-FacultyDoctoralProgress]').within(() => {
|
||||
const totalStats = ['9.1%', '24.2%', '12.1%', '15.2%', '39.4%']
|
||||
for (const stat of totalStats) {
|
||||
cy.contains(stat)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
it('table with graduated included is shown correctly', () => {
|
||||
cy.get('[data-cy="Table-FacultyDoctoralProgress"]')
|
||||
const years = getEmptyYears(true)
|
||||
const tableContents = [
|
||||
...years.map(year => [year, 0, 0, 0, 0, 0, 0]),
|
||||
['2023 - 2024', 1, 1, 0, 0, 0, 0],
|
||||
['2022 - 2023', 9, 1, 7, 1, 0, 0],
|
||||
['2021 - 2022', 3, 1, 1, 0, 0, 1],
|
||||
['2020 - 2021', 5, 0, 0, 2, 1, 2],
|
||||
['2019 - 2020', 11, 0, 0, 1, 4, 6],
|
||||
['2018 - 2019', 14, 0, 1, 4, 2, 7],
|
||||
['2017 - 2018', 7, 0, 0, 0, 0, 7],
|
||||
['Total', 50, 3, 9, 8, 7, 23],
|
||||
]
|
||||
cy.checkTableStats(tableContents, 'FacultyDoctoralProgress')
|
||||
})
|
||||
|
||||
it('table with graduated excluded is shown correctly', () => {
|
||||
cy.get('[data-cy="GraduatedToggle"]').click()
|
||||
cy.get('[data-cy="Table-FacultyDoctoralProgress"]')
|
||||
const years = getEmptyYears(true)
|
||||
const tableContents = [
|
||||
...years.map(year => [year, 0, 0, 0, 0, 0, 0]),
|
||||
['2023 - 2024', 1, 1, 0, 0, 0, 0],
|
||||
['2022 - 2023', 9, 1, 7, 1, 0, 0],
|
||||
['2021 - 2022', 3, 1, 1, 0, 0, 1],
|
||||
['2020 - 2021', 4, 0, 0, 2, 0, 2],
|
||||
['2019 - 2020', 9, 0, 0, 1, 4, 4],
|
||||
['2018 - 2019', 5, 0, 0, 0, 1, 4],
|
||||
['2017 - 2018', 2, 0, 0, 0, 0, 2],
|
||||
['Total', 33, 3, 8, 4, 5, 13],
|
||||
]
|
||||
cy.checkTableStats(tableContents, 'FacultyDoctoralProgress')
|
||||
})
|
||||
})
|
||||
|
||||
it('Years in the tables can be clicked to show programme level breakdown', () => {
|
||||
cy.get('[data-cy="Table-FacultyBachelorsProgress"]').within(() => {
|
||||
cy.contains('2021 - 2022').click()
|
||||
cy.contains('31.3%').trigger('mouseover', { force: true })
|
||||
cy.contains('Matemaattisten tieteiden kandiohjelma')
|
||||
cy.contains('MAT - KH50_001')
|
||||
cy.contains('0 Credits: 0')
|
||||
cy.contains('1 ≤ Credits < 45: 10')
|
||||
cy.contains('45 ≤ Credits < 90: 9')
|
||||
cy.contains('90 ≤ Credits < 135: 11')
|
||||
cy.contains('135 ≤ Credits < 180: 2')
|
||||
cy.contains('180 ≤ Credits: 0')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('Graduation times stats', () => {
|
||||
it('All the correct sections are displayed', () => {
|
||||
const checkSections = breakdown => {
|
||||
const sections = ['bachelor', 'bcMsCombo', 'master', 'doctor']
|
||||
for (const section of sections) {
|
||||
cy.get(`[data-cy=Section-${section}]`).within(() => {
|
||||
cy.get('div.graduations-chart-container').within(() => {
|
||||
cy.get(`div.faculty-${breakdown ? 'breakdown-' : ''}graph`)
|
||||
cy.get('div.graduations-message').contains("Click a bar to view that year's programme level breakdown")
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
checkSections(true)
|
||||
cy.get('[data-cy=GraduationTimeToggle]').click()
|
||||
checkSections(false)
|
||||
})
|
||||
|
||||
it('Graduation times breakdown data is shown correctly', () => {
|
||||
cy.get('[data-cy=Section-bachelor]').within(() => {
|
||||
cy.contains('Graduation year')
|
||||
cy.contains('2022')
|
||||
|
||||
cy.get('[aria-label="2022, 29. On time."]').trigger('mouseover')
|
||||
cy.contains('On time: 29')
|
||||
|
||||
cy.get('[aria-label="2022, 29. On time."]').trigger('click')
|
||||
cy.contains('Year 2022 by graduation year')
|
||||
|
||||
cy.get('[aria-label="MAT, 22. On time."]').trigger('mouseover')
|
||||
cy.contains('Matemaattisten tieteiden kandiohjelma')
|
||||
cy.contains('KH50_001')
|
||||
cy.contains('On time: 22')
|
||||
|
||||
cy.get('[aria-label="MFKK, 7. On time."]').trigger('mouseover')
|
||||
cy.contains('Matematiikan, fysiikan ja kemian opettajan kandiohjelma')
|
||||
cy.contains('KH50_004')
|
||||
cy.contains('On time: 7')
|
||||
})
|
||||
})
|
||||
|
||||
it('Graduation times median time data is shown correctly', () => {
|
||||
cy.get('[data-cy=GraduationTimeToggle]').click()
|
||||
|
||||
cy.get('[data-cy=Section-bachelor]').within(() => {
|
||||
cy.contains('Graduation year')
|
||||
cy.contains('2021')
|
||||
|
||||
cy.contains('70 graduated').trigger('mouseover')
|
||||
cy.contains('70 students graduated in year 2021')
|
||||
cy.contains('median study time: 39.5 months')
|
||||
cy.contains('28 graduated on time')
|
||||
cy.contains('32 graduated max year overtime')
|
||||
cy.contains('10 graduated over year late')
|
||||
|
||||
cy.contains('70 graduated').trigger('click')
|
||||
cy.contains('Year 2021 by graduation year')
|
||||
|
||||
cy.get('[aria-label="MAT, 41."]').trigger('mouseover')
|
||||
cy.contains('Matemaattisten tieteiden kandiohjelma')
|
||||
cy.contains('KH50_001')
|
||||
cy.contains('47 students graduated in year 2021')
|
||||
cy.contains('median study time: 41 months')
|
||||
cy.contains('14 graduated on time')
|
||||
cy.contains('25 graduated max year overtime')
|
||||
cy.contains('8 graduated over year late')
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -23,7 +23,7 @@ describe('Faculty overview', () => {
|
||||
})
|
||||
|
||||
it('Credits produced by faculty are shown', () => {
|
||||
cy.get('[data-cy="Section-CreditsProducedByTheFaculty"]').should('be.visible')
|
||||
cy.get('[data-cy="Section-creditsProducedByTheFaculty"]').should('be.visible')
|
||||
cy.get('[data-cy="Graph-CreditsProducedByTheFaculty"]').should('be.visible')
|
||||
})
|
||||
|
||||
@@ -75,14 +75,14 @@ describe('Faculty overview', () => {
|
||||
})
|
||||
|
||||
it('Students of the faculty infobox works', () => {
|
||||
cy.get('[data-cy="StudentsOfTheFaculty-info-content"]').should('not.exist')
|
||||
cy.get('[data-cy="StudentsOfTheFaculty-open-info"]').click()
|
||||
cy.get('[data-cy="StudentsOfTheFaculty-info-content"]').should('be.visible')
|
||||
cy.get('[data-cy="StudentsOfTheFaculty-close-info"]').click()
|
||||
cy.get('[data-cy="StudentsOfTheFaculty-info-content"]').should('not.exist')
|
||||
cy.get('[data-cy="studentsOfTheFaculty-info-content"]').should('not.exist')
|
||||
cy.get('[data-cy="studentsOfTheFaculty-open-info"]').click()
|
||||
cy.get('[data-cy="studentsOfTheFaculty-info-content"]').should('be.visible')
|
||||
cy.get('[data-cy="studentsOfTheFaculty-close-info"]').click()
|
||||
cy.get('[data-cy="studentsOfTheFaculty-info-content"]').should('not.exist')
|
||||
})
|
||||
|
||||
it('Data can be exported to Excel files', () => {
|
||||
it('Data can be exported to Excel files', { retries: 3 }, () => {
|
||||
const sections = [
|
||||
'StudentsOfTheFaculty',
|
||||
'GraduatedOfTheFaculty',
|
||||
@@ -91,7 +91,7 @@ describe('Faculty overview', () => {
|
||||
]
|
||||
sections.forEach(section => {
|
||||
cy.get(`[data-cy="Graph-${section}"]`).within(() => {
|
||||
cy.get('[aria-label="View chart menu, Chart"]').click()
|
||||
cy.get('[aria-label="View chart menu, Chart"]').click({ force: true })
|
||||
cy.contains('li.highcharts-menu-item', 'Download XLS').click()
|
||||
const downloadedFile = `oodikone_${section}_H50_${timestamp}.xls`
|
||||
cy.readFile(path.join(downloadsFolder, downloadedFile))
|
||||
@@ -234,7 +234,7 @@ describe('Faculty overview', () => {
|
||||
|
||||
cy.get('[data-cy="Section-bachelor"]').within(() => {
|
||||
cy.get('div[class="faculty-graph"]')
|
||||
cy.contains('24 graduated (64.9 % of class)').trigger('mouseover')
|
||||
cy.contains('24 graduated').trigger('mouseover')
|
||||
cy.contains('From class of 2020 - 2021, 24/37 students have graduated')
|
||||
})
|
||||
|
||||
|
||||
+26
-18
@@ -18,7 +18,6 @@ const clearSingleDropdownSelection = dataCyAttribute => {
|
||||
}
|
||||
|
||||
const testRangeFilter = (parentEl, min, max, expected) => {
|
||||
cy.clock(MOCKED_DATE, ['Date'])
|
||||
cy.cs(parentEl)
|
||||
.cs('range-selector-min')
|
||||
.find('input')
|
||||
@@ -29,13 +28,17 @@ const testRangeFilter = (parentEl, min, max, expected) => {
|
||||
.find('input')
|
||||
.invoke('val')
|
||||
.then(initialMax => {
|
||||
cy.cs(parentEl).cs('range-selector-min').find('input').type(`{selectall}{backspace}${min}`).blur()
|
||||
cy.cs(parentEl).cs('range-selector-max').find('input').type(`{selectall}{backspace}${max}`).blur()
|
||||
cy.cs(parentEl).cs('range-selector-min').find('input').clear()
|
||||
cy.cs(parentEl).cs('range-selector-min').find('input').type(min)
|
||||
cy.cs(parentEl).cs('range-selector-max').find('input').clear()
|
||||
cy.cs(parentEl).cs('range-selector-max').find('input').type(max)
|
||||
|
||||
checkFilteringResult(expected)
|
||||
|
||||
cy.cs(parentEl).cs('range-selector-min').find('input').type(`{selectall}{backspace}${initialMin}`).blur()
|
||||
cy.cs(parentEl).cs('range-selector-max').find('input').type(`{selectall}{backspace}${initialMax}`).blur()
|
||||
cy.cs(parentEl).cs('range-selector-min').find('input').clear()
|
||||
cy.cs(parentEl).cs('range-selector-min').find('input').type(initialMin)
|
||||
cy.cs(parentEl).cs('range-selector-max').find('input').clear()
|
||||
cy.cs(parentEl).cs('range-selector-max').find('input').type(initialMax)
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -83,27 +86,27 @@ describe("Population statistics with a master's programme", () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('Studyright type filter', () => {
|
||||
describe('Study right type filter', () => {
|
||||
it('is visible', () => {
|
||||
cy.contains('Studyright type')
|
||||
cy.contains('Study right type')
|
||||
})
|
||||
|
||||
it('is set to all by default', () => {
|
||||
runTestStepWithPreAndPostParts('studyright-type', () => {
|
||||
const card = cy.cs('studyright-type-filter-card')
|
||||
runTestStepWithPreAndPostParts('studyRightTypeFilter', () => {
|
||||
const card = cy.cs('studyRightTypeFilter-filter-card')
|
||||
card.get('[data-cy="all"] input').should('be.checked')
|
||||
})
|
||||
})
|
||||
|
||||
it('works', () => {
|
||||
runTestStepWithPreAndPostParts('studyright-type', () => {
|
||||
runTestStepWithPreAndPostParts('studyRightTypeFilter', () => {
|
||||
const expectedBaMaStudents = 24
|
||||
const expectedMasterOnlyStudents = 2
|
||||
cy.cs('studyright-type-filter-card').cs('bama').click()
|
||||
cy.cs('studyRightTypeFilter-filter-card').cs('bama').click()
|
||||
checkFilteringResult(expectedBaMaStudents)
|
||||
cy.cs('studyright-type-filter-card').cs('master').click()
|
||||
cy.cs('studyRightTypeFilter-filter-card').cs('master').click()
|
||||
checkFilteringResult(expectedMasterOnlyStudents)
|
||||
cy.cs('studyright-type-filter-card').cs('all').click()
|
||||
cy.cs('studyRightTypeFilter-filter-card').cs('all').click()
|
||||
checkFilteringResult(defaultAmountOfStudents)
|
||||
})
|
||||
})
|
||||
@@ -169,6 +172,7 @@ describe('Population Statistics', () => {
|
||||
})
|
||||
|
||||
it('Age filter works', { retries: 3 }, () => {
|
||||
cy.clock(MOCKED_DATE, ['Date'])
|
||||
runTestStepWithPreAndPostParts('Age', () => {
|
||||
testRangeFilter('Age-filter-card', 23, 30, 21)
|
||||
})
|
||||
@@ -228,6 +232,7 @@ describe('Population Statistics', () => {
|
||||
})
|
||||
|
||||
it('Filter combinations work', () => {
|
||||
cy.clock(MOCKED_DATE, ['Date'])
|
||||
runTestStepWithPreAndPostParts('GraduatedFromProgramme', () => {
|
||||
runTestStepWithPreAndPostParts('Age', () => {
|
||||
const getCard = () => cy.cs('GraduatedFromProgramme-filter-card')
|
||||
@@ -249,8 +254,8 @@ describe('Population Statistics', () => {
|
||||
checkFilteringResult(defaultAmountOfStudents)
|
||||
})
|
||||
|
||||
it('Studyright type filter is not visible', () => {
|
||||
cy.contains('Studyright type').should('not.exist')
|
||||
it('Study right type filter is not visible', () => {
|
||||
cy.contains('Study right type').should('not.exist')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -284,13 +289,14 @@ describe('Course Statistics', () => {
|
||||
it('Programme filter works', () => {
|
||||
runTestStepWithPreAndPostParts('Programme', () => {
|
||||
const card = cy.cs('Programme-filter-card')
|
||||
const programmeDropdown = card.cs('Programme-filter-dropdown').selectFromDropdown(2)
|
||||
checkFilteringResult(30)
|
||||
const programmeDropdown = card.cs('Programme-filter-dropdown').selectFromDropdown(1)
|
||||
checkFilteringResult(33)
|
||||
programmeDropdown.get('i.delete').click()
|
||||
})
|
||||
})
|
||||
|
||||
it('Age filter works', () => {
|
||||
it('Age filter works', { retries: 3 }, () => {
|
||||
cy.clock(MOCKED_DATE, ['Date'])
|
||||
runTestStepWithPreAndPostParts('Age', () => {
|
||||
testRangeFilter('Age-filter-card', 21, 30, 36)
|
||||
})
|
||||
@@ -322,6 +328,7 @@ describe('Course Statistics', () => {
|
||||
})
|
||||
|
||||
it('Filter combinations work', () => {
|
||||
cy.clock(MOCKED_DATE, ['Date'])
|
||||
runTestStepWithPreAndPostParts('Grade', () => {
|
||||
runTestStepWithPreAndPostParts('Age', () => {
|
||||
cy.cs('gradeFilter-5').click()
|
||||
@@ -348,6 +355,7 @@ describe('Custom Population Statistics', () => {
|
||||
})
|
||||
|
||||
it('Age filter works', () => {
|
||||
cy.clock(MOCKED_DATE, ['Date'])
|
||||
runTestStepWithPreAndPostParts('Age', () => {
|
||||
testRangeFilter('Age-filter-card', 24, 28, 5)
|
||||
})
|
||||
|
||||
@@ -16,13 +16,14 @@ const chooseSemester = (semester, fromOrTo) => {
|
||||
})
|
||||
}
|
||||
|
||||
const checkNumbers = (numbers, numberOfColumns) => {
|
||||
const checkNumbers = (numbers, numberOfColumns, mode) => {
|
||||
const offset = mode === 'semesters' ? 2 : 1
|
||||
cy.get('table > tbody > tr:first').within(() => {
|
||||
cy.get('td').should('have.length', numberOfColumns)
|
||||
cy.get('td').eq(0).contains('All courses total')
|
||||
numbers.forEach((number, index) => {
|
||||
cy.get('td')
|
||||
.eq(index + 1)
|
||||
.eq(index + offset)
|
||||
.contains(number)
|
||||
})
|
||||
})
|
||||
@@ -33,7 +34,7 @@ const checkStyle = (styles, numberOfColumns) => {
|
||||
cy.get('td').should('have.length', numberOfColumns)
|
||||
styles.forEach((style, index) => {
|
||||
cy.get('td')
|
||||
.eq(index + 1)
|
||||
.eq(index + 2)
|
||||
.should('have.attr', 'style')
|
||||
.and('include', style)
|
||||
})
|
||||
@@ -64,7 +65,7 @@ describe('When language center is opened', () => {
|
||||
cy.get('table > tbody > tr:first').within(() => {
|
||||
cy.get('td').should('have.length', 15)
|
||||
cy.get('td').eq(0).contains('All courses total')
|
||||
const numbers = [36, 9, 33, 751, 15, 40, 6, 49, 1, 24, 1059, 53, 0, 2076]
|
||||
const numbers = [36, 9, 34, 755, 15, 40, 6, 48, 1, 24, 1059, 49, 0, 2076]
|
||||
numbers.forEach((number, index) => {
|
||||
cy.get('td')
|
||||
.eq(index + 1)
|
||||
@@ -75,12 +76,12 @@ describe('When language center is opened', () => {
|
||||
|
||||
it('Faculties tab "exceeding" button works', () => {
|
||||
cy.get('[data-cy="difference-button"]').click()
|
||||
checkNumbers([...new Array(12).fill(0), 1, 218], 15)
|
||||
checkNumbers([...new Array(12).fill(0), 1, 227], 15)
|
||||
})
|
||||
|
||||
it('Faculties tab semester selector changes numbers', () => {
|
||||
chooseSemester('Syksy 2020', 'from')
|
||||
checkNumbers([28, 6, 27, 491, 4, 10, 2, 34, 0, 9, 535, 38, 0, 1184], 15)
|
||||
checkNumbers([28, 6, 28, 495, 4, 10, 2, 33, 0, 9, 535, 34, 0, 1184], 15)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -92,7 +93,7 @@ describe('When language center is opened', () => {
|
||||
})
|
||||
|
||||
it('Semester tab shows numbers', () => {
|
||||
checkNumbers([69, 26, 298, 58, 343, 98, 438, 138, 310, 90, 123, 74, 10, 1, 2076], 16)
|
||||
checkNumbers([69, 26, 298, 58, 343, 98, 438, 138, 310, 90, 123, 74, 10, 1, 2076], 17, 'semesters')
|
||||
})
|
||||
|
||||
it('Coloring mode works on semester tab', () => {
|
||||
@@ -101,10 +102,10 @@ describe('When language center is opened', () => {
|
||||
|
||||
checkStyle(
|
||||
expectedAlphas.map(alpha => `background-color: rgba(0, 170, 0, ${alpha})`),
|
||||
16
|
||||
17
|
||||
)
|
||||
cy.get('table > tbody > tr:first').within(() => {
|
||||
cy.get('td').eq(15).should('have.attr', 'style').and('not.include', 'background-color')
|
||||
cy.get('td').eq(16).should('have.attr', 'style').and('not.include', 'background-color')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,65 +1,82 @@
|
||||
/// <reference types="cypress" />
|
||||
// Now "Class statistics" in UI
|
||||
|
||||
describe('Population Statistics tests', () => {
|
||||
const pathToMathBSc2020 =
|
||||
'/populations?months=49&semesters=FALL&semesters=SPRING&studyRights=%7B"programme"%3A"KH50_001"%7D&year=2020'
|
||||
const pathToMathMSc2020 =
|
||||
'/populations?months=49&semesters=FALL&semesters=SPRING&studyRights=%7B"programme"%3A"MH50_001"%7D&year=2020'
|
||||
describe('when using basic user', () => {
|
||||
const selectStudyProgramme = programme => {
|
||||
cy.contains('Select study programme')
|
||||
cy.get('[data-cy=select-study-programme]').click().children().contains(programme).click()
|
||||
}
|
||||
|
||||
const selectStudyTrack = studyTrack => {
|
||||
cy.contains('Select study track')
|
||||
cy.get('[data-cy=select-study-track]').click().children().contains(studyTrack).click()
|
||||
}
|
||||
|
||||
describe('Population statistics tests', () => {
|
||||
const getPath = programme => {
|
||||
return `/populations?months=49&semesters=FALL&semesters=SPRING&studyRights=%7B"programme"%3A"${programme}"%7D&year=2020`
|
||||
}
|
||||
|
||||
const pathToMathBSc2020 = getPath('KH50_001')
|
||||
const pathToMathMSc2020 = getPath('MH50_001')
|
||||
|
||||
describe('When using basic user', () => {
|
||||
beforeEach(() => {
|
||||
cy.init('/populations')
|
||||
})
|
||||
|
||||
it('Population statistics search infobox works', () => {
|
||||
cy.get('[data-cy="PopulationSearch-info-content"]').should('not.exist')
|
||||
cy.get('[data-cy="PopulationSearch-open-info"]').click()
|
||||
cy.get('[data-cy="PopulationSearch-info-content"]').should('be.visible')
|
||||
cy.get('[data-cy="PopulationSearch-info-content"]').should(
|
||||
'contain',
|
||||
'lukuvuosi, jolloin opiskelija on ilmoittautunut'
|
||||
)
|
||||
cy.get('[data-cy="PopulationSearch-close-info"]').click()
|
||||
cy.get('[data-cy="PopulationSearch-info-content"]').should('not.exist')
|
||||
})
|
||||
describe('Population search', () => {
|
||||
it('Info box works', () => {
|
||||
cy.get('[data-cy="PopulationSearch-info-content"]').should('not.exist')
|
||||
cy.get('[data-cy="PopulationSearch-open-info"]').click()
|
||||
cy.get('[data-cy="PopulationSearch-info-content"]').should('be.visible')
|
||||
cy.get('[data-cy="PopulationSearch-info-content"]').should('contain', 'Tässä osiossa voi tarkastella')
|
||||
cy.get('[data-cy="PopulationSearch-close-info"]').click()
|
||||
cy.get('[data-cy="PopulationSearch-info-content"]').should('not.exist')
|
||||
})
|
||||
|
||||
it('Population statistics search form is usable', () => {
|
||||
cy.contains('See class').should('be.disabled')
|
||||
cy.contains('Search for class')
|
||||
cy.contains('Class of')
|
||||
.parent()
|
||||
.within(() => {
|
||||
cy.get('.form-control').as('enrollmentSelect')
|
||||
it('Form is usable', () => {
|
||||
cy.contains('Search for class')
|
||||
cy.contains('See class').should('be.disabled')
|
||||
cy.contains('Class of')
|
||||
.parent()
|
||||
.within(() => {
|
||||
cy.get('.form-control').as('enrollmentSelect')
|
||||
})
|
||||
|
||||
cy.get('@enrollmentSelect')
|
||||
.its(`${[0]}.value`)
|
||||
.then(beforeVal => {
|
||||
cy.get('@enrollmentSelect').click()
|
||||
cy.get('.yearSelectInput .rdtPrev').click({ force: true })
|
||||
cy.get('.yearSelectInput table').contains('2018-2019').click({ force: true })
|
||||
cy.get('@enrollmentSelect').should('not.have.value', beforeVal)
|
||||
})
|
||||
|
||||
cy.contains('No study tracks available for this programme')
|
||||
selectStudyProgramme('Matematiikan ja tilastotieteen maisteriohjelma')
|
||||
cy.contains('See class').should('be.enabled')
|
||||
selectStudyTrack('Matematiikka ja soveltava matematiikka')
|
||||
cy.contains('See class').should('be.enabled')
|
||||
})
|
||||
|
||||
describe('Correct population is shown for programme', () => {
|
||||
it('without study tracks', () => {
|
||||
cy.contains('Select study programme')
|
||||
selectStudyProgramme('Matemaattisten tieteiden kandiohjelma')
|
||||
cy.contains('See class').click()
|
||||
cy.contains('Matemaattisten tieteiden kandiohjelma 2017 - 2018')
|
||||
cy.contains('class size 47 students')
|
||||
})
|
||||
|
||||
cy.get('@enrollmentSelect')
|
||||
.its(`${[0]}.value`)
|
||||
.then(beforeVal => {
|
||||
cy.get('@enrollmentSelect').click()
|
||||
cy.get('.yearSelectInput .rdtPrev').click({ force: true })
|
||||
cy.get('.yearSelectInput table').contains('2018-2019').click({ force: true })
|
||||
cy.get('@enrollmentSelect').should('not.have.value', beforeVal)
|
||||
it('with study tracks', () => {
|
||||
selectStudyProgramme('Matematiikan ja tilastotieteen maisteriohjelma')
|
||||
selectStudyTrack('Matematiikka ja soveltava matematiikka')
|
||||
cy.contains('See class').click()
|
||||
cy.contains('Matematiikan ja tilastotieteen maisteriohjelma 2017 - 2018')
|
||||
cy.contains('studytrack MAST-MSM')
|
||||
cy.contains('class size 1 students')
|
||||
})
|
||||
|
||||
cy.contains('Select study programme')
|
||||
cy.get('[data-cy=select-study-programme]')
|
||||
.click()
|
||||
.children()
|
||||
.contains('Matematiikan ja tilastotieteen maisteriohjelma')
|
||||
.click()
|
||||
cy.contains('See class').should('be.enabled')
|
||||
})
|
||||
|
||||
it('Searching for population really shows population', () => {
|
||||
cy.contains('Select study programme')
|
||||
cy.get('[data-cy=select-study-programme]')
|
||||
.click()
|
||||
.children()
|
||||
.contains('Matemaattisten tieteiden kandiohjelma')
|
||||
.click()
|
||||
cy.contains('See class').click()
|
||||
cy.contains('Matemaattisten tieteiden kandiohjelma 2017 - 2018')
|
||||
cy.contains('class size 47 students')
|
||||
})
|
||||
})
|
||||
|
||||
it('Population statistics is usable on general level', () => {
|
||||
@@ -81,18 +98,61 @@ describe('Population Statistics tests', () => {
|
||||
cy.contains('td', 'MAT11001').siblings().find('i.level.up').click()
|
||||
cy.wait('@coursePage')
|
||||
cy.url().should('include', '/coursestatistics')
|
||||
cy.contains('MAT11001, 57033, AYMAT11001, A57033 Johdatus yliopistomatematiikkaan')
|
||||
cy.contains('MAT11001 Johdatus yliopistomatematiikkaan')
|
||||
cy.contains('AYMAT11001 Avoin yo: Johdatus yliopistomatematiikkaan')
|
||||
cy.contains('57033 Johdatus yliopistomatematiikkaan')
|
||||
cy.contains('A57033 Avoin yo: Johdatus yliopistomatematiikkaan')
|
||||
})
|
||||
|
||||
it('Courses of class curriculum selection works', () => {
|
||||
cy.visit(pathToMathBSc2020)
|
||||
cy.contains('Courses of class').click()
|
||||
cy.get('[data-cy=curriculum-picker]').contains('2020 - 2023')
|
||||
cy.get('[data-cy=curriculum-picker]').contains('2020–2023')
|
||||
cy.get('[data-cy=toggle-group-module-MAT-tyo]')
|
||||
cy.get('[data-cy=curriculum-picker]').click().contains('2023 - 2026').click()
|
||||
cy.get('[data-cy=curriculum-picker]').click().contains('2023–2026').click()
|
||||
cy.get('[data-cy=toggle-group-module-MAT-tyo]').should('not.exist')
|
||||
})
|
||||
|
||||
it('New fetch of courses data is done when curriculum is changed', () => {
|
||||
cy.visit(pathToMathBSc2020)
|
||||
cy.contains('Courses of class').click()
|
||||
cy.intercept('/api/v2/populationstatistics/courses').as('courseData')
|
||||
cy.get('[data-cy=curriculum-picker]').click().contains('2020–2023').click()
|
||||
cy.wait('@courseData').then(({ response }) => {
|
||||
expect(response.body).to.have.property('allStudents')
|
||||
expect(response.body).to.have.property('coursestatistics')
|
||||
expect(response.body.allStudents).to.equal(27)
|
||||
expect(response.body.coursestatistics.some(stat => stat.course.code === 'DIGI-100')).to.equal(true)
|
||||
})
|
||||
cy.get('[data-cy=curriculum-picker]').click().contains('2023–2026').click()
|
||||
cy.wait('@courseData').then(({ response }) => {
|
||||
expect(response.body).to.have.property('allStudents')
|
||||
expect(response.body).to.have.property('coursestatistics')
|
||||
expect(response.body.allStudents).to.equal(27)
|
||||
expect(response.body.coursestatistics.some(stat => stat.course.code === 'DIGI-100')).to.equal(false)
|
||||
})
|
||||
})
|
||||
|
||||
it('New fetch of courses data is done when filtered students change', () => {
|
||||
cy.visit(pathToMathBSc2020)
|
||||
cy.contains('Courses of class').click()
|
||||
cy.intercept('/api/v2/populationstatistics/courses').as('courseData')
|
||||
cy.wait('@courseData').then(({ response }) => {
|
||||
expect(response.body).to.have.property('allStudents')
|
||||
expect(response.body).to.have.property('coursestatistics')
|
||||
expect(response.body.allStudents).to.equal(27)
|
||||
})
|
||||
cy.get('[data-cy=GraduatedFromProgramme-filter-card]').within(() => {
|
||||
cy.get('[data-cy=GraduatedFromProgramme-header]').click()
|
||||
cy.get('[data-cy=option-graduated-true]').click()
|
||||
cy.wait('@courseData').then(({ response }) => {
|
||||
expect(response.body).to.have.property('allStudents')
|
||||
expect(response.body).to.have.property('coursestatistics')
|
||||
expect(response.body.allStudents).to.equal(16)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it("Empty 'tags' tab has a link to the page where tags can be created", { retries: 2 }, () => {
|
||||
cy.visit(pathToMathBSc2020)
|
||||
cy.contains('Students (27)')
|
||||
@@ -254,7 +314,7 @@ describe('Population Statistics tests', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('when using admin', () => {
|
||||
describe('When using admin', () => {
|
||||
it('Student list checking works as intended', () => {
|
||||
cy.init(pathToMathBSc2020, 'admin')
|
||||
const existing = '433237'
|
||||
@@ -283,7 +343,7 @@ describe('Population Statistics tests', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('when using IAM user', () => {
|
||||
describe('When using IAM user', () => {
|
||||
beforeEach(() => {
|
||||
cy.init(pathToMathBSc2020, 'onlyiamrights')
|
||||
cy.contains('Matemaattisten tieteiden kandiohjelma 2020 - 2021')
|
||||
|
||||
@@ -91,7 +91,9 @@ describe('Students tests', () => {
|
||||
cy.get('.level').click()
|
||||
})
|
||||
cy.url().should('include', '/coursestatistics')
|
||||
cy.contains('MAT12004, 57046, AYMAT12004 Tilastollinen päättely I')
|
||||
cy.contains('MAT12004 Tilastollinen päättely I')
|
||||
cy.contains('AYMAT12004 Avoin yo: Tilastollinen päättely I')
|
||||
cy.contains('57046 Johdatus tilastolliseen päättelyyn')
|
||||
})
|
||||
|
||||
it('Has correct Sisu link', () => {
|
||||
@@ -247,7 +249,7 @@ describe('Students tests', () => {
|
||||
it('When a study plan is selected, the time frame of the credit graph is updated', () => {
|
||||
cy.clock(new Date('2024-08-30').getTime(), ['Date'])
|
||||
cy.visit('/students/550789')
|
||||
cy.contains('.highcharts-container text', '2 Aug 2020')
|
||||
cy.contains('.highcharts-container text', '1 Aug 2020')
|
||||
cy.contains('.highcharts-container text', '30 Aug 2024')
|
||||
cy.contains('table tbody tr', 'Kulttuurien tutkimuksen kandiohjelma (01.08.2020–30.06.2023)').within(() => {
|
||||
cy.get('td').eq(0).click()
|
||||
|
||||
@@ -132,7 +132,7 @@ describe('Study guidance group tests', () => {
|
||||
const firstRowHeadings = [
|
||||
'Student number',
|
||||
'Credits',
|
||||
'Start of studyright',
|
||||
'Start of study right',
|
||||
'Started in programme',
|
||||
'Semesters present',
|
||||
'Graduation date',
|
||||
|
||||
@@ -9,21 +9,21 @@ const deleteTag = name => {
|
||||
cy.contains('td', name).should('not.exist')
|
||||
}
|
||||
|
||||
describe('Studyprogramme overview', () => {
|
||||
describe('Study programme overview', () => {
|
||||
/* Basic information overview -tests */
|
||||
describe('Basic information -view works for basic user', () => {
|
||||
describe('Basic information view works for basic user', () => {
|
||||
beforeEach(() => {
|
||||
cy.init('/study-programme')
|
||||
cy.contains('a', 'Matemaattisten tieteiden kandiohjelma').click({ force: true })
|
||||
})
|
||||
|
||||
// If the backend breaks for one of the sections, the section header is not rendered and this will fail
|
||||
it('Basic information -tab loads', () => {
|
||||
cy.get('[data-cy=Section-StudentsOfTheStudyprogramme]')
|
||||
cy.get('[data-cy=Section-CreditsProducedByTheStudyprogramme]')
|
||||
cy.get('[data-cy=Section-GraduatedAndThesisWritersOfTheProgramme]')
|
||||
cy.get('[data-cy=Section-ProgrammesBeforeOrAfter]')
|
||||
cy.get('[data-cy=Section-AverageGraduationTimes]')
|
||||
it('Basic information tab loads', () => {
|
||||
cy.get('[data-cy=Section-studentsOfTheStudyProgramme]')
|
||||
cy.get('[data-cy=Section-creditsProducedByTheStudyProgramme]')
|
||||
cy.get('[data-cy=Section-graduatedAndThesisWritersOfTheProgramme]')
|
||||
cy.get('[data-cy=Section-programmesBeforeOrAfter]')
|
||||
cy.get('[data-cy=Section-averageGraduationTimes]')
|
||||
})
|
||||
|
||||
it('Basic information contains correct students', () => {
|
||||
@@ -40,7 +40,7 @@ describe('Studyprogramme overview', () => {
|
||||
[2017, 41, 47, 0, 0, 0],
|
||||
]
|
||||
|
||||
cy.checkTableStats(tableContents, 'StudentsOfTheStudyprogramme')
|
||||
cy.checkTableStats(tableContents, 'StudentsOfTheStudyProgramme')
|
||||
})
|
||||
|
||||
it('Basic information contains correct credits', () => {
|
||||
@@ -51,12 +51,12 @@ describe('Studyprogramme overview', () => {
|
||||
[2022, 3235, 3205, 0, 30, 209],
|
||||
[2021, 5133, 5108, 0, 25, 428],
|
||||
[2020, 5801, 5796, 0, 5, 94],
|
||||
[2019, 5295, 5295, 0, 0, 162],
|
||||
[2019, 5305, 5305, 0, 0, 162],
|
||||
[2018, 3442, 3432, 0, 10, 21],
|
||||
[2017, 1211, 1211, 0, 0, 189],
|
||||
]
|
||||
|
||||
cy.checkTableStats(tableContents, 'CreditsProducedByTheStudyprogramme')
|
||||
cy.checkTableStats(tableContents, 'CreditsProducedByTheStudyProgramme')
|
||||
})
|
||||
|
||||
it("Toggling 'Show special categories' on displays additional information", () => {
|
||||
@@ -68,12 +68,12 @@ describe('Studyprogramme overview', () => {
|
||||
[2022, 3235, 3205, 0, 30, 0, 0, 209],
|
||||
[2021, 5133, 5108, 0, 25, 0, 0, 428],
|
||||
[2020, 5801, 5796, 0, 5, 0, 0, 94],
|
||||
[2019, 5295, 5295, 0, 0, 0, 0, 162],
|
||||
[2019, 5305, 5305, 0, 0, 0, 0, 162],
|
||||
[2018, 3442, 3432, 0, 10, 0, 0, 21],
|
||||
[2017, 1211, 1211, 0, 0, 0, 0, 189],
|
||||
]
|
||||
|
||||
cy.checkTableStats(tableContents, 'CreditsProducedByTheStudyprogramme')
|
||||
cy.checkTableStats(tableContents, 'CreditsProducedByTheStudyProgramme')
|
||||
})
|
||||
|
||||
it('Basic information contains correct thesis writers and graduates', () => {
|
||||
@@ -93,7 +93,7 @@ describe('Studyprogramme overview', () => {
|
||||
cy.checkTableStats(tableContents, 'GraduatedAndThesisWritersOfTheProgramme')
|
||||
})
|
||||
|
||||
it('Special studyrights can be excluded and basic data changes accordingly', () => {
|
||||
it('Special study rights can be excluded and basic data changes accordingly', () => {
|
||||
cy.get('[data-cy=StudentToggle]').click()
|
||||
const years = getEmptyYears()
|
||||
const studentTableContents = [
|
||||
@@ -108,7 +108,7 @@ describe('Studyprogramme overview', () => {
|
||||
[2017, 41, 47, 0],
|
||||
]
|
||||
|
||||
cy.checkTableStats(studentTableContents, 'StudentsOfTheStudyprogramme')
|
||||
cy.checkTableStats(studentTableContents, 'StudentsOfTheStudyProgramme')
|
||||
|
||||
const graduatedTableContents = [
|
||||
// [Year, Graduated, Wrote thesis]
|
||||
@@ -140,7 +140,7 @@ describe('Studyprogramme overview', () => {
|
||||
['2018 - 2019', 40, 45, 0, 0, 0],
|
||||
['2017 - 2018', 41, 47, 0, 0, 0],
|
||||
]
|
||||
cy.checkTableStats(studentTableContents, 'StudentsOfTheStudyprogramme')
|
||||
cy.checkTableStats(studentTableContents, 'StudentsOfTheStudyProgramme')
|
||||
|
||||
const creditTableContents = [
|
||||
...years.map(year => [year, 0, 0, 0, 0, 0]),
|
||||
@@ -149,23 +149,23 @@ describe('Studyprogramme overview', () => {
|
||||
['2021 - 2022', 4092, 4042, 0, 50, 198],
|
||||
['2020 - 2021', 5420, 5415, 0, 5, 321],
|
||||
['2019 - 2020', 6043, 6043, 0, 0, 101],
|
||||
['2018 - 2019', 4846, 4841, 0, 5, 107],
|
||||
['2018 - 2019', 4856, 4851, 0, 5, 107],
|
||||
['2017 - 2018', 2350, 2345, 0, 5, 26],
|
||||
]
|
||||
|
||||
cy.checkTableStats(creditTableContents, 'CreditsProducedByTheStudyprogramme')
|
||||
cy.checkTableStats(creditTableContents, 'CreditsProducedByTheStudyProgramme')
|
||||
cy.get('[data-cy=YearToggle]').click()
|
||||
})
|
||||
|
||||
it('Basic information graphs render', () => {
|
||||
cy.get('[data-cy=Graph-StudentsOfTheStudyprogramme')
|
||||
cy.get('[data-cy=Graph-StudentsOfTheStudyProgramme')
|
||||
.should('contain', 'Started studying')
|
||||
.should('contain', 'Accepted')
|
||||
.should('contain', 'Graduated')
|
||||
.should('contain', 'Transferred away')
|
||||
.should('contain', 'Transferred to')
|
||||
|
||||
cy.get('[data-cy=Graph-CreditsProducedByTheStudyprogramme')
|
||||
cy.get('[data-cy=Graph-CreditsProducedByTheStudyProgramme')
|
||||
.should('contain', 'Degree students')
|
||||
.should('contain', 'Transferred')
|
||||
.should('contain', 5796)
|
||||
@@ -210,7 +210,7 @@ describe('Studyprogramme overview', () => {
|
||||
|
||||
cy.get('[data-cy=GraduationTimeToggle]').click()
|
||||
cy.get('[data-cy=graduation-times-graphMaster]').within(() => {
|
||||
cy.contains('Master studyright')
|
||||
cy.contains('Master study right')
|
||||
cy.contains('Graduation year')
|
||||
cy.contains('2021')
|
||||
cy.contains('2 graduated').trigger('mouseover')
|
||||
@@ -222,7 +222,7 @@ describe('Studyprogramme overview', () => {
|
||||
})
|
||||
|
||||
cy.get('[data-cy=graduation-times-graphBachelor]').within(() => {
|
||||
cy.contains('Bachelor + master studyright')
|
||||
cy.contains('Bachelor + master study right')
|
||||
cy.contains('Graduation year')
|
||||
cy.contains('2023')
|
||||
cy.contains('11 graduated').trigger('mouseover')
|
||||
@@ -235,22 +235,22 @@ describe('Studyprogramme overview', () => {
|
||||
})
|
||||
})
|
||||
|
||||
/* Studytrack overview -tests */
|
||||
describe('Studytrack overview works for basic user', () => {
|
||||
/* Study track overview tests */
|
||||
describe('Study track overview works for basic user', () => {
|
||||
beforeEach(() => {
|
||||
cy.init('/study-programme')
|
||||
cy.contains('a', 'Matemaattisten tieteiden kandiohjelma').click()
|
||||
cy.get('.attached').contains('Studytracks and class statistics').click()
|
||||
cy.get('.attached').contains('Study tracks and class statistics').click()
|
||||
})
|
||||
|
||||
// If the backend breaks for one of the sections, the section header is not rendered and this will fail
|
||||
it('Studytracks and class statistics -tab loads', () => {
|
||||
cy.get('[data-cy=Section-StudytrackOverview]')
|
||||
cy.get('[data-cy=Section-StudytrackProgress]')
|
||||
cy.get('[data-cy=Section-AverageGraduationTimesStudytracks]')
|
||||
it('Study tracks and class statistics -tab loads', () => {
|
||||
cy.get('[data-cy=Section-studyTrackOverview]')
|
||||
cy.get('[data-cy=Section-studyTrackProgress]')
|
||||
cy.get('[data-cy=Section-averageGraduationTimesStudyTracks]')
|
||||
})
|
||||
|
||||
it('Students of the studyprogramme are shown correctly', () => {
|
||||
it('Students of the study programme are shown correctly', () => {
|
||||
const tableContents = [
|
||||
// [Year, All, Started studying, Present, Absent, Inactive, Graduated, Men, Women, Other/Unknown, Finland, Other]
|
||||
['2023 - 2024', 8, 8, 0, 0, 8, 0, 5, 3, 0, 8, 0],
|
||||
@@ -263,11 +263,11 @@ describe('Studyprogramme overview', () => {
|
||||
['Total', 229, 197, 0, 0, 95, 134, 147, 82, 0, 222, 7],
|
||||
]
|
||||
|
||||
cy.checkTableStats(tableContents, 'StudytrackOverview')
|
||||
cy.checkTableStats(tableContents, 'StudyTrackOverview')
|
||||
})
|
||||
|
||||
it('Years in the students table can be expanded and study track data will be shown', () => {
|
||||
cy.get('[data-cy=Table-StudytrackOverview]').within(() => {
|
||||
cy.get('[data-cy=Table-StudyTrackOverview]').within(() => {
|
||||
cy.get('tbody tr.header-row')
|
||||
.eq(3)
|
||||
.within(() => {
|
||||
@@ -302,7 +302,7 @@ describe('Studyprogramme overview', () => {
|
||||
})
|
||||
|
||||
it('Links to class statistics page work', () => {
|
||||
cy.get('[data-cy=Table-StudytrackOverview]').within(() => {
|
||||
cy.get('[data-cy=Table-StudyTrackOverview]').within(() => {
|
||||
cy.get('tbody tr.header-row')
|
||||
.eq(1)
|
||||
.within(() => {
|
||||
@@ -314,8 +314,8 @@ describe('Studyprogramme overview', () => {
|
||||
cy.contains('class size 26 students')
|
||||
})
|
||||
|
||||
it('Links to class statistics page with all years combined work', () => {
|
||||
cy.get('[data-cy=Table-StudytrackOverview]').within(() => {
|
||||
it('Links to class statistics page with all years combined work', { retries: 2 }, () => {
|
||||
cy.get('[data-cy=Table-StudyTrackOverview]').within(() => {
|
||||
cy.get('td.total-row-cell a').click()
|
||||
})
|
||||
|
||||
@@ -324,7 +324,7 @@ describe('Studyprogramme overview', () => {
|
||||
})
|
||||
|
||||
it('Links to class statistics page with study track info included work', () => {
|
||||
cy.get('[data-cy=Table-StudytrackOverview]').within(() => {
|
||||
cy.get('[data-cy=Table-StudyTrackOverview]').within(() => {
|
||||
cy.get('tbody tr.header-row')
|
||||
.eq(3)
|
||||
.within(() => {
|
||||
@@ -357,29 +357,37 @@ describe('Studyprogramme overview', () => {
|
||||
['Total', 229, 28, 18, 24, 21, 10, 14, 114],
|
||||
]
|
||||
|
||||
cy.checkTableStats(tableContents, 'StudytrackProgress')
|
||||
cy.checkTableStats(tableContents, 'StudyTrackProgress')
|
||||
})
|
||||
|
||||
it('Studytrack overview graphs render', () => {
|
||||
cy.get('[data-cy=Graph-StudytrackProgress]')
|
||||
it('Study track overview graphs render', () => {
|
||||
cy.get('[data-cy=Graph-StudyTrackProgress]')
|
||||
.should('contain', 'Less than 30 credits')
|
||||
.should('contain', '30–60 credits')
|
||||
.should('contain', 'At least 180 credits')
|
||||
.should('contain', '49.8%') // The percentage for total, at least 180 credits, to check that the graph renders
|
||||
|
||||
cy.get('[data-cy=Graph-StudytrackProgress]').contains('49.8%').trigger('mouseover', { force: true })
|
||||
cy.get('[data-cy=Graph-StudyTrackProgress]').contains('49.8%').trigger('mouseover', { force: true })
|
||||
cy.contains('At least 180 credits: 114')
|
||||
|
||||
cy.get("[data-cy='graduation-times-graph-breakdownBachelor']").within(() => {
|
||||
cy.get("[data-cy='Section-KH50_001']").within(() => {
|
||||
cy.contains('Start year')
|
||||
cy.contains('2020 - 2021')
|
||||
cy.get('[aria-label="2020 - 2021, 15. On time."]').trigger('mouseover')
|
||||
cy.contains('Graduated On time: 15 students')
|
||||
cy.contains('On time: 15')
|
||||
cy.contains("Click a bar to view that year's study track level breakdown")
|
||||
cy.get('[aria-label="2019 - 2020, 22. On time."]').click()
|
||||
cy.contains("Click a bar to view that year's study track level breakdown").should('not.exist')
|
||||
cy.contains('Year 2019 - 2020 by start year')
|
||||
cy.get('[aria-label="MAT-MAT, 14. On time."]').trigger('mouseover')
|
||||
cy.contains('Matematiikka')
|
||||
cy.contains('MAT-MAT')
|
||||
cy.contains('On time: 14')
|
||||
})
|
||||
|
||||
cy.get('[data-cy=GraduationTimeToggle]').click()
|
||||
|
||||
cy.get('[data-cy=graduation-times-graphBachelor]').within(() => {
|
||||
cy.get("[data-cy='Section-KH50_001']").within(() => {
|
||||
cy.contains('Start year')
|
||||
cy.contains('2020 - 2021')
|
||||
cy.contains('19 graduated').trigger('mouseover')
|
||||
@@ -391,14 +399,16 @@ describe('Studyprogramme overview', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('Studytrack can be changed', () => {
|
||||
describe('Study track can be changed', () => {
|
||||
beforeEach(() => {
|
||||
cy.get('.studytrack-selector').contains('All students of the programme, KH50_001').click()
|
||||
cy.get('.studytrack-selector .visible.menu').contains('Matematiikka, MAT-MAT').click()
|
||||
})
|
||||
|
||||
it('Students of the study track are shown correctly', () => {
|
||||
cy.get("[data-cy='Section-StudytrackOverview']").contains('Students of the studytrack MAT-MAT by starting year')
|
||||
cy.get("[data-cy='Section-studyTrackOverview']").contains(
|
||||
'Students of the study track MAT-MAT by starting year'
|
||||
)
|
||||
const tableContents = [
|
||||
// [Year, All, Started studying, Present, Absent, Inactive, Graduated, Men, Women, Other/Unknown, Finland, Other]
|
||||
['2022 - 2023', 3, 3, 0, 0, 1, 2, 2, 1, 0, 3, 0],
|
||||
@@ -409,11 +419,11 @@ describe('Studyprogramme overview', () => {
|
||||
['2017 - 2018', 28, 24, 0, 0, 1, 27, 15, 13, 0, 28, 0],
|
||||
['Total', 89, 73, 0, 0, 6, 83, 48, 41, 0, 87, 2],
|
||||
]
|
||||
cy.checkTableStats(tableContents, 'StudytrackOverview')
|
||||
cy.checkTableStats(tableContents, 'StudyTrackOverview')
|
||||
})
|
||||
|
||||
it('Links to class statistics page with study track info included work', () => {
|
||||
cy.get('[data-cy=Table-StudytrackOverview]').within(() => {
|
||||
cy.get('[data-cy=Table-StudyTrackOverview]').within(() => {
|
||||
cy.get('tbody tr.regular-row')
|
||||
.eq(2)
|
||||
.within(() => {
|
||||
@@ -428,7 +438,7 @@ describe('Studyprogramme overview', () => {
|
||||
})
|
||||
|
||||
it('Info message about missing progress stats is displayed', () => {
|
||||
cy.contains('.divider', 'Progress of students of the studytrack MAT-MAT by starting year')
|
||||
cy.contains('.divider', 'Progress of students of the study track MAT-MAT by starting year')
|
||||
cy.contains(
|
||||
'.message',
|
||||
'Currently progress data is only available for all students of the study programme. Please select ”All students of the programme” to view the progress data.'
|
||||
@@ -436,7 +446,7 @@ describe('Studyprogramme overview', () => {
|
||||
})
|
||||
|
||||
it('Average graduation times are displayed correctly', () => {
|
||||
cy.get("[data-cy='Section-AverageGraduationTimesStudytracks']")
|
||||
cy.get("[data-cy='Section-averageGraduationTimesStudyTracks']")
|
||||
cy.get("[data-cy='graduation-times-graph-breakdownBachelor']").within(() => {
|
||||
cy.contains('Start year')
|
||||
cy.contains('2020 - 2021')
|
||||
@@ -656,7 +666,7 @@ describe('Studyprogramme overview', () => {
|
||||
|
||||
it('content loads', () => {
|
||||
cy.contains('h3', 'Select curriculum to edit:')
|
||||
cy.get('[data-cy=curriculum-picker]').contains('2023 - 2026')
|
||||
cy.get('[data-cy=curriculum-picker]').contains('2023–2026')
|
||||
cy.contains('Change visibility of degree courses and select criteria for academic years')
|
||||
cy.contains('form', 'First year (12 months) last set: 0')
|
||||
cy.get('table').within(() => {
|
||||
@@ -756,7 +766,7 @@ describe('Studyprogramme overview', () => {
|
||||
|
||||
it('can access programme and correct tabs are visible', () => {
|
||||
cy.contains('Basic information')
|
||||
cy.contains('Studytracks and class statistics')
|
||||
cy.contains('Study tracks and class statistics')
|
||||
|
||||
cy.contains('Update statistics').should('not.exist')
|
||||
cy.contains('Degree courses').should('not.exist')
|
||||
@@ -765,19 +775,19 @@ describe('Studyprogramme overview', () => {
|
||||
it('can access basic information', () => {
|
||||
cy.contains('Basic information').click()
|
||||
|
||||
cy.get('[data-cy=Section-StudentsOfTheStudyprogramme]')
|
||||
cy.get('[data-cy=Section-CreditsProducedByTheStudyprogramme]')
|
||||
cy.get('[data-cy=Section-GraduatedAndThesisWritersOfTheProgramme]')
|
||||
cy.get('[data-cy=Section-ProgrammesBeforeOrAfter]')
|
||||
cy.get('[data-cy=Section-AverageGraduationTimes]')
|
||||
cy.get('[data-cy=Section-studentsOfTheStudyProgramme]')
|
||||
cy.get('[data-cy=Section-creditsProducedByTheStudyProgramme]')
|
||||
cy.get('[data-cy=Section-graduatedAndThesisWritersOfTheProgramme]')
|
||||
cy.get('[data-cy=Section-programmesBeforeOrAfter]')
|
||||
cy.get('[data-cy=Section-averageGraduationTimes]')
|
||||
})
|
||||
|
||||
it('can access studytracks', () => {
|
||||
cy.get('.attached').contains('Studytracks and class statistics').click()
|
||||
it('can access study tracks', () => {
|
||||
cy.get('.attached').contains('Study tracks and class statistics').click()
|
||||
|
||||
cy.get('[data-cy=Section-StudytrackOverview]')
|
||||
cy.get('[data-cy=Section-StudytrackProgress]')
|
||||
cy.get('[data-cy=Section-AverageGraduationTimesStudytracks]')
|
||||
cy.get('[data-cy=Section-studyTrackOverview]')
|
||||
cy.get('[data-cy=Section-studyTrackProgress]')
|
||||
cy.get('[data-cy=Section-averageGraduationTimesStudyTracks]')
|
||||
})
|
||||
|
||||
it("doesn't see other tabs", () => {
|
||||
|
||||
+51
-114
@@ -1,159 +1,96 @@
|
||||
/// <reference types="cypress" />
|
||||
|
||||
const levels = ['Bachelors', 'BachelorMasters', 'Masters', 'Doctoral']
|
||||
const progressLevels = ['Bachelors', 'BachelorMasters', 'Masters', 'Doctoral']
|
||||
|
||||
const checkProgressGraphs = () => {
|
||||
levels.forEach(level => cy.get(`[data-cy=Graph-Faculty${level}Progress]`))
|
||||
const checkProgressBarCharts = () => {
|
||||
progressLevels.forEach(level => cy.get(`[data-cy=Faculty${level}ProgressBarChart]`))
|
||||
}
|
||||
|
||||
const checkProgressTables = () => {
|
||||
levels.forEach(level => cy.get(`[data-cy=Table-Faculty${level}Progress]`))
|
||||
progressLevels.forEach(level => cy.get(`[data-cy=Faculty${level}ProgressTable]`))
|
||||
}
|
||||
|
||||
const checkAverageGraduationTimesGraphs = breakdown => {
|
||||
const levels = ['bachelor', 'bcMsCombo', 'master', 'doctor']
|
||||
levels.forEach(level => {
|
||||
cy.get(`[data-cy=Section-${level}] .graduations-chart-container .faculty-${breakdown ? 'breakdown-' : ''}graph`)
|
||||
const graduationTimesLevels = ['bachelor', 'master', 'doctor'] // ? Bachelor + master seems to be missing in test data
|
||||
|
||||
const checkAverageGraduationTimesBreakdownBarCharts = () => {
|
||||
graduationTimesLevels.forEach(level => {
|
||||
cy.get(`[data-cy=${level}BreakdownBarChart]`)
|
||||
})
|
||||
}
|
||||
|
||||
const checkProgressStatsFacultyLevelBreakdown = () => {
|
||||
cy.get('[data-cy=Table-FacultyBachelorsProgress]').within(() => {
|
||||
cy.contains('2021 - 2022').click()
|
||||
cy.contains('29.5%').trigger('mouseover', { force: true })
|
||||
cy.contains('Matemaattis-luonnontieteellinen tiedekunta')
|
||||
cy.contains('H50 - H50')
|
||||
cy.contains('0 Credits: 0')
|
||||
cy.contains('1 ≤ Credits < 45: 13')
|
||||
cy.contains('45 ≤ Credits < 90: 10')
|
||||
cy.contains('90 ≤ Credits < 135: 16')
|
||||
cy.contains('135 ≤ Credits < 180: 3')
|
||||
cy.contains('180 ≤ Credits: 2')
|
||||
})
|
||||
}
|
||||
|
||||
const checkAverageGraduationTimesFacultyLevelBreakdown = breakdown => {
|
||||
cy.get('[data-cy=Section-bachelor] .graduations-chart-container').within(() => {
|
||||
cy.contains('.graduations-message', "Click a bar to view that year's faculty level breakdown")
|
||||
cy.contains(breakdown ? '51' : '141 graduated').click()
|
||||
cy.contains('.graduations-message', "Click a bar to view that year's faculty level breakdown").should('not.exist')
|
||||
cy.contains(`.programmes-${breakdown ? 'breakdown-' : ''}graph`, 'Year 2021 by graduation year')
|
||||
const checkAverageGraduationTimesMedianBarCharts = () => {
|
||||
graduationTimesLevels.forEach(level => {
|
||||
cy.get(`[data-cy=${level}MedianBarChart]`)
|
||||
})
|
||||
}
|
||||
|
||||
describe('University view', () => {
|
||||
beforeEach(() => {
|
||||
cy.init('/university')
|
||||
cy.contains('University-level view')
|
||||
cy.contains('University')
|
||||
cy.contains('Programme MH90_001 (Veterinary medicine bachelor + licentiate) is currently excluded.')
|
||||
})
|
||||
|
||||
describe('Progress stats section', () => {
|
||||
it('has all the correct progress graphs', () => {
|
||||
checkProgressGraphs()
|
||||
describe('Faculty progress tab', () => {
|
||||
it('contains all the correct progress bar charts', () => {
|
||||
checkProgressBarCharts()
|
||||
})
|
||||
|
||||
it('has all the correct progress tables', () => {
|
||||
it('contains all the correct progress tables', () => {
|
||||
checkProgressTables()
|
||||
})
|
||||
|
||||
it("'Graduated included/excluded' toggle works", () => {
|
||||
it("'Graduated included / Graduated excluded' toggle works", () => {
|
||||
cy.get('[data-cy=GraduatedToggle]').click()
|
||||
checkProgressGraphs()
|
||||
checkProgressBarCharts()
|
||||
checkProgressTables()
|
||||
})
|
||||
|
||||
it("'All studyrights/Special studyrights excluded' toggle works", () => {
|
||||
cy.get('[data-cy=StudentToggle]').click()
|
||||
checkProgressGraphs()
|
||||
it("'All study rights / Special study rights excluded' toggle works", () => {
|
||||
cy.get('[data-cy=StudyRightToggle]').click()
|
||||
checkProgressBarCharts()
|
||||
checkProgressTables()
|
||||
})
|
||||
|
||||
it('Years in the tables can be clicked to show faculty level breakdown', () => {
|
||||
// Filtering out special study rights to make the data match with the Evaluation overview page
|
||||
cy.get('[data-cy=StudentToggle]').click()
|
||||
checkProgressStatsFacultyLevelBreakdown()
|
||||
it('years in the tables can be clicked to show faculty level breakdown', () => {
|
||||
cy.get('[data-cy=StudyRightToggle]').click()
|
||||
cy.get('[data-cy=FacultyBachelorsProgressTableShowButton3]').click()
|
||||
cy.contains('29.5%').trigger('mouseover', { force: true })
|
||||
cy.contains('Matemaattis-luonnontieteellinen tiedekunta')
|
||||
cy.contains('H50')
|
||||
cy.contains('0 Credits: 0')
|
||||
cy.contains('1 ≤ Credits < 45: 13')
|
||||
cy.contains('45 ≤ Credits < 90: 10')
|
||||
cy.contains('90 ≤ Credits < 135: 16')
|
||||
cy.contains('135 ≤ Credits < 180: 3')
|
||||
cy.contains('180 ≤ Credits: 2')
|
||||
})
|
||||
|
||||
it('info boxes contain correct information', () => {
|
||||
cy.get('[data-cy=FacultyProgressInfoBoxButton]').click()
|
||||
cy.get('[data-cy=FacultyProgressInfoBoxContent]').contains('Kuvaa tiedekuntaan kuuluvien')
|
||||
cy.get('[data-cy=FacultyBachelorMastersProgressInfoBoxButton]').click()
|
||||
cy.get('[data-cy=FacultyBachelorMastersProgressInfoBoxContent]').contains('The starting year is the')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Average graduation times section', () => {
|
||||
it('graphs exist', () => {
|
||||
checkAverageGraduationTimesGraphs(true)
|
||||
describe('Faculty graduations tab', () => {
|
||||
beforeEach(() => {
|
||||
cy.get('[data-cy=FacultyGraduationsTab]').click()
|
||||
})
|
||||
|
||||
it('Faculty level breakdown can be seen by clicking a bar in the breakdown graph', () => {
|
||||
checkAverageGraduationTimesFacultyLevelBreakdown(true)
|
||||
it('has all the correct median time bar charts', () => {
|
||||
checkAverageGraduationTimesBreakdownBarCharts()
|
||||
})
|
||||
|
||||
it("'Breakdown/Median times' toggle works", () => {
|
||||
cy.get('[data-cy=GraduationTimeToggle]').click()
|
||||
checkAverageGraduationTimesGraphs(false)
|
||||
checkAverageGraduationTimesMedianBarCharts()
|
||||
})
|
||||
|
||||
it('Faculty level breakdown can be seen by clicking a bar in the median times graph', () => {
|
||||
cy.get('[data-cy=GraduationTimeToggle]').click()
|
||||
checkAverageGraduationTimesFacultyLevelBreakdown(false)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// It's better to have these tests defined here than in the 'Evaluation overview' tests as they use the same data as the 'University view' tests
|
||||
describe("'Evaluation overview' university page", () => {
|
||||
beforeEach(() => {
|
||||
cy.init('/evaluationoverview/university')
|
||||
cy.contains('University-level view')
|
||||
})
|
||||
|
||||
it('page opens', () => {
|
||||
cy.contains("This view is a combined version of Oodikone's Faculty Evaluation Overview")
|
||||
})
|
||||
|
||||
it('has links to faculty evaluation overview pages', () => {
|
||||
cy.get('.facultyLinkBox span').should('have.length', 12)
|
||||
cy.contains('.facultyLinkBox span a', 'H50 Matemaattis-luonnontieteellinen tiedekunta').click()
|
||||
cy.url().should('include', '/evaluationoverview/faculty/H50')
|
||||
})
|
||||
|
||||
describe('Progress stats section', () => {
|
||||
it('has all the correct progress graphs', () => {
|
||||
checkProgressGraphs()
|
||||
})
|
||||
|
||||
it('has all the correct progress tables', () => {
|
||||
checkProgressTables()
|
||||
})
|
||||
|
||||
it("'Graduated included/excluded' toggle works", () => {
|
||||
cy.get('[data-cy=GraduatedToggle]').click()
|
||||
checkProgressGraphs()
|
||||
checkProgressTables()
|
||||
})
|
||||
|
||||
it("'All studyrights/Special studyrights excluded' toggle doesn't exist", () => {
|
||||
cy.get('.toggle-container [data-cy=StudentToggle]').should('not.exist')
|
||||
})
|
||||
|
||||
it('Years in the tables can be clicked to show faculty level breakdown', () => {
|
||||
checkProgressStatsFacultyLevelBreakdown()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Average graduation times section', () => {
|
||||
it('graphs exist', () => {
|
||||
checkAverageGraduationTimesGraphs(true)
|
||||
})
|
||||
|
||||
it('Faculty level breakdown can be seen by clicking a bar in the breakdown graph', () => {
|
||||
checkAverageGraduationTimesFacultyLevelBreakdown(true)
|
||||
})
|
||||
|
||||
it("'Breakdown/Median times' toggle works", () => {
|
||||
cy.get('[data-cy=GraduationTimeToggle]').click()
|
||||
checkAverageGraduationTimesGraphs(false)
|
||||
})
|
||||
|
||||
it('Faculty level breakdown can be seen by clicking a bar in the median times graph', () => {
|
||||
cy.get('[data-cy=GraduationTimeToggle]').click()
|
||||
checkAverageGraduationTimesFacultyLevelBreakdown(false)
|
||||
it('info boxes contain correct information', () => {
|
||||
cy.get('[data-cy=AverageGraduationTimesInfoBoxButton]').click()
|
||||
cy.get('[data-cy=AverageGraduationTimesInfoBoxContent]').contains('Opiskelijoiden keskimääräiset valmistumisajat')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
+59
-41
@@ -1,47 +1,60 @@
|
||||
/// <reference types="cypress" />
|
||||
|
||||
const visibleLinks = {
|
||||
norights: ['University', 'Faculties', 'Special populations', 'Give feedback', 'Logout'],
|
||||
onlycoursestatistics: ['University', 'Courses', 'Special populations', 'Give feedback', 'Logout'],
|
||||
norights: ['University', 'Faculties', 'Special populations', 'Feedback'],
|
||||
onlycoursestatistics: ['University', 'Courses', 'Special populations', 'Feedback'],
|
||||
}
|
||||
|
||||
visibleLinks.basic = [...visibleLinks.onlycoursestatistics, 'Faculties', 'Programmes', 'Students']
|
||||
visibleLinks.admin = [...visibleLinks.basic, 'Teachers', 'Users', 'Updater']
|
||||
visibleLinks.admin = [...visibleLinks.basic, 'Teachers', 'Admin']
|
||||
|
||||
const hasVisibleItems = visibleItems => {
|
||||
cy.get('[data-cy=nav-bar]').children().should('have.length', visibleItems)
|
||||
}
|
||||
|
||||
const containsLinks = links => {
|
||||
cy.get('[data-cy=nav-bar]').within(() => {
|
||||
for (const link of links) {
|
||||
cy.contains(link)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const userButtonWorks = (username, mocking = false) => {
|
||||
cy.get('[data-cy=nav-bar-user-button]').click()
|
||||
cy.contains(mocking ? `Mocking as ${username}` : `Logged in as ${username}`)
|
||||
cy.contains('Language')
|
||||
cy.contains('suomi')
|
||||
cy.contains('English')
|
||||
cy.contains('svenska')
|
||||
cy.contains(mocking ? 'Stop mocking' : 'Log out')
|
||||
}
|
||||
|
||||
describe('Users tests', () => {
|
||||
describe('Using user with just grp-oodikone-user, no other rights', () => {
|
||||
describe('Using as user with just grp-oodikone-user, no other rights', () => {
|
||||
it('shows correct tabs', () => {
|
||||
cy.init('', 'norights')
|
||||
cy.get('[data-cy=navBar]').children().should('have.length', 7)
|
||||
cy.get('[data-cy=navBar]').within(() => {
|
||||
for (const link of visibleLinks.norights) {
|
||||
cy.contains(link)
|
||||
}
|
||||
})
|
||||
hasVisibleItems(7)
|
||||
containsLinks(visibleLinks.norights)
|
||||
userButtonWorks('norights')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Using as coursestatistics user', () => {
|
||||
it('shows correct tabs', () => {
|
||||
cy.init('', 'onlycoursestatistics')
|
||||
cy.get('[data-cy=navBar]').children().should('have.length', 7)
|
||||
cy.get('[data-cy=navBar]').within(() => {
|
||||
for (const link of visibleLinks.onlycoursestatistics) {
|
||||
cy.contains(link)
|
||||
}
|
||||
})
|
||||
hasVisibleItems(7)
|
||||
containsLinks(visibleLinks.onlycoursestatistics)
|
||||
userButtonWorks('onlycoursestatistics')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Using as basic user', () => {
|
||||
it('shows correct tabs', () => {
|
||||
cy.init('')
|
||||
cy.get('[data-cy=navBar]').children().should('have.length', 10)
|
||||
cy.get('[data-cy=navBar]').within(() => {
|
||||
for (const link of visibleLinks.basic) {
|
||||
cy.contains(link)
|
||||
}
|
||||
})
|
||||
hasVisibleItems(10)
|
||||
containsLinks(visibleLinks.basic)
|
||||
userButtonWorks('basic')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -51,27 +64,32 @@ describe('Users tests', () => {
|
||||
})
|
||||
|
||||
it('should see more stuff than others', () => {
|
||||
cy.get('[data-cy=navBar]').children().should('have.length', 13)
|
||||
cy.get('[data-cy=navBar]').within(() => {
|
||||
for (const link of visibleLinks.admin) {
|
||||
cy.contains(link)
|
||||
}
|
||||
})
|
||||
hasVisibleItems(13)
|
||||
containsLinks(visibleLinks.admin)
|
||||
userButtonWorks('admin')
|
||||
})
|
||||
|
||||
it("mocking normal user shows only the mocked user's programmes", () => {
|
||||
cy.contains('mocking').should('not.exist')
|
||||
cy.cs('user-edit-button-basic').click()
|
||||
cy.get('i.spy').click()
|
||||
cy.contains('mocking as basic')
|
||||
cy.contains('Programmes').click().siblings().contains('Class statistics').click()
|
||||
cy.contains('label', 'Study programme')
|
||||
.siblings()
|
||||
.within(() => {
|
||||
cy.get("div[role='option']").should('have.length', 2)
|
||||
cy.get("div[role='option']").eq(0).contains('Matemaattisten tieteiden kandiohjelma')
|
||||
cy.get("div[role='option']").eq(1).contains('Matematiikan ja tilastotieteen maisteriohjelma')
|
||||
})
|
||||
describe('can mock as other users', () => {
|
||||
beforeEach(() => {
|
||||
cy.cs('user-edit-button-basic').click()
|
||||
cy.get('i.spy').click()
|
||||
})
|
||||
|
||||
it('user button shows mocked user', () => {
|
||||
userButtonWorks('basic', true)
|
||||
})
|
||||
|
||||
it("only the mocked user's programmes are visible", () => {
|
||||
cy.get('[data-cy=nav-bar-button-studyProgramme]').click()
|
||||
cy.get('[data-cy=nav-bar-button-class]').click()
|
||||
cy.contains('label', 'Study programme')
|
||||
.siblings()
|
||||
.within(() => {
|
||||
cy.get("div[role='option']").should('have.length', 2)
|
||||
cy.get("div[role='option']").eq(0).contains('Matemaattisten tieteiden kandiohjelma')
|
||||
cy.get("div[role='option']").eq(1).contains('Matematiikan ja tilastotieteen maisteriohjelma')
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
/**
|
||||
* @type {Cypress.PluginConfig}
|
||||
*/
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
module.exports = (on, config) => {
|
||||
// `on` is used to hook into various events Cypress emits
|
||||
// `config` is the resolved Cypress config
|
||||
|
||||
@@ -17,12 +17,8 @@
|
||||
require('@cypress/code-coverage/support')
|
||||
require('./commands')
|
||||
|
||||
// This error is benign and seems to be only present in Cypress
|
||||
const resizeObserverLoopErrRe = /^[^(ResizeObserver loop limit exceeded)]/
|
||||
|
||||
Cypress.on('uncaught:exception', err => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
Cypress.on('uncaught:exception', error => {
|
||||
/* returning false here prevents Cypress from failing the test */
|
||||
if (resizeObserverLoopErrRe.test(err.message)) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
@@ -46,7 +46,7 @@ services:
|
||||
<<: *defaults
|
||||
command: >
|
||||
sh -c 'redis-server --appendonly yes && redis-cli flushall'
|
||||
image: redis:6.2.14
|
||||
image: redis:6.2.16
|
||||
ports:
|
||||
- 6379:6379
|
||||
|
||||
|
||||
@@ -85,7 +85,7 @@ services:
|
||||
<<: *defaults
|
||||
command: 'redis-server --appendonly yes'
|
||||
container_name: redis
|
||||
image: redis:6.2.14
|
||||
image: redis:6.2.16
|
||||
ports:
|
||||
- '6379:6379'
|
||||
volumes:
|
||||
|
||||
+1
-1
@@ -76,7 +76,7 @@ services:
|
||||
<<: *defaults
|
||||
command: 'redis-server --appendonly yes'
|
||||
container_name: redis
|
||||
image: redis:6.2.14
|
||||
image: redis:6.2.16
|
||||
ports:
|
||||
- '6379:6379'
|
||||
volumes:
|
||||
|
||||
@@ -13,7 +13,7 @@ Roles are defined [here](../services/backend/src/config/roles.ts). Criteria for
|
||||
- studyGuidanceGroups
|
||||
- teachers
|
||||
|
||||
The roles `openUniSearch`, `teachers` and `studyGuidanceGroups` simply enable certain views (_Open uni student population_, _Teachers_ and _Guidance groups_ respectively).
|
||||
The roles `openUniSearch` and `studyGuidanceGroups` simply enable certain views (_Open uni student population_ and _Guidance groups_ respectively).
|
||||
|
||||
### courseStatistics
|
||||
|
||||
@@ -23,9 +23,15 @@ The roles `openUniSearch`, `teachers` and `studyGuidanceGroups` simply enable ce
|
||||
|
||||
- Can see statistics of every course and year
|
||||
|
||||
### katselemusViewer
|
||||
### katselmusViewer
|
||||
|
||||
- Enables the _Evaluation overview_ view
|
||||
- Enables the _Evaluation overview_ view (deprecated)
|
||||
|
||||
### teachers
|
||||
|
||||
- By default, the role only allows seeing the statistics of course providers (study programmes) they have full rights to (see [Programmes](#programmes))
|
||||
- This role always needs to be given and removed manually
|
||||
- If the user is a member of the `hy-dekaanit` or `hy-varadekaanit-opetus` IAM group (or is an admin), they can see leaderboards, information about individual teachers, and statistics of all course providers. They don't need to be given the `teachers` role manually.
|
||||
|
||||
### Other roles
|
||||
|
||||
@@ -67,7 +73,6 @@ A user must be a member of either the `grp-oodikone-basic-users` or `grp-oodikon
|
||||
| grp-oodikone-users | facultyStatistics |
|
||||
| grp-toska | admin |
|
||||
| grp-katselmus-\* | katselmusViewer |
|
||||
| hy-one | teachers |
|
||||
| hy-ypa-opa-dojo | openUniSearch |
|
||||
|
||||
`grp-oodikone-basic-users`: basically the whole staff (hy-\*-allstaff)
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 71 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 68 KiB |
@@ -42,6 +42,8 @@ A collection of useful tools, documentation and other resources.
|
||||
|
||||
## User interface
|
||||
|
||||
- [Material UI documentation](https://mui.com/material-ui/getting-started/)
|
||||
- [Material Icons](https://mui.com/material-ui/material-icons/)
|
||||
- [Semantic UI React documentation](https://react.semantic-ui.com)
|
||||
|
||||
<!--
|
||||
|
||||
@@ -0,0 +1,177 @@
|
||||
import pluginJs from '@eslint/js'
|
||||
import globals from 'globals'
|
||||
import pluginCypress from 'eslint-plugin-cypress/flat'
|
||||
import pluginImport from 'eslint-plugin-import'
|
||||
import pluginPrettierRecommended from 'eslint-plugin-prettier/recommended'
|
||||
import pluginReact from 'eslint-plugin-react'
|
||||
import pluginReactHooks from 'eslint-plugin-react-hooks'
|
||||
import tsEslint from 'typescript-eslint'
|
||||
|
||||
/** @type {import('eslint').Linter.Config[]} */
|
||||
export default [
|
||||
{
|
||||
files: ['**/*.{js,ts,jsx,tsx}'],
|
||||
},
|
||||
{
|
||||
ignores: ['eslint.config.mjs', '**/build/', '**/dist/', '**/node_modules/'],
|
||||
},
|
||||
{
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.browser,
|
||||
...globals.node,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
linterOptions: {
|
||||
reportUnusedDisableDirectives: 'error',
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['**/*.{ts,tsx}'],
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
project: [
|
||||
'services/frontend/tsconfig.json',
|
||||
'services/frontend/tsconfig.node.json',
|
||||
'services/backend/tsconfig.json',
|
||||
'services/shared/tsconfig.json',
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
pluginJs.configs.recommended,
|
||||
...tsEslint.configs.recommendedTypeChecked,
|
||||
...tsEslint.configs.stylisticTypeChecked,
|
||||
pluginCypress.configs.recommended,
|
||||
pluginReact.configs.flat.recommended,
|
||||
pluginReact.configs.flat['jsx-runtime'],
|
||||
pluginImport.flatConfigs.recommended,
|
||||
pluginImport.flatConfigs.typescript,
|
||||
pluginPrettierRecommended, // This must be the last plugin so it can override other configs
|
||||
{
|
||||
plugins: {
|
||||
'react-hooks': pluginReactHooks,
|
||||
},
|
||||
},
|
||||
{
|
||||
rules: {
|
||||
camelcase: 'off', // TODO: Enable this rule (at least for frontend)
|
||||
'class-methods-use-this': 'error',
|
||||
'consistent-return': 'error',
|
||||
'id-denylist': ['error', 'c', 'd', 'e', 'err', 't'],
|
||||
'import/no-commonjs': 'error',
|
||||
'import/no-default-export': 'error',
|
||||
'import/no-extraneous-dependencies': ['error', { devDependencies: false }],
|
||||
'import/no-unused-modules': ['error', { missingExports: true, unusedExports: true }],
|
||||
'import/order': [
|
||||
'error',
|
||||
{
|
||||
alphabetize: { order: 'asc', caseInsensitive: true },
|
||||
groups: [['builtin', 'external'], ['internal'], ['parent'], ['sibling', 'index']],
|
||||
},
|
||||
],
|
||||
'no-alert': 'error',
|
||||
'no-async-promise-executor': 'error',
|
||||
'no-await-in-loop': 'error',
|
||||
'no-console': 'error',
|
||||
'no-implied-eval': 'error',
|
||||
'no-param-reassign': ['error', { props: false }],
|
||||
'no-promise-executor-return': 'error',
|
||||
'no-return-assign': 'error',
|
||||
'no-return-await': 'error',
|
||||
'object-shorthand': ['error', 'always'],
|
||||
'prefer-const': 'error',
|
||||
'prefer-destructuring': ['error', { VariableDeclarator: { object: true } }],
|
||||
quotes: ['error', 'single', { avoidEscape: true, allowTemplateLiterals: false }],
|
||||
'react/display-name': 'off', // TODO: Delete this override
|
||||
'react/function-component-definition': [
|
||||
'error',
|
||||
{ namedComponents: 'arrow-function', unnamedComponents: 'arrow-function' },
|
||||
],
|
||||
'react/jsx-filename-extension': ['error', { allow: 'as-needed', extensions: ['.jsx', '.tsx'] }],
|
||||
'react/jsx-sort-props': ['error', { reservedFirst: false }],
|
||||
'react/no-array-index-key': 'error',
|
||||
'react/no-this-in-sfc': 'error',
|
||||
'react/no-unescaped-entities': 'off',
|
||||
'react/no-unknown-property': 'off', // TODO: Delete this override
|
||||
'react/prefer-stateless-function': 'error',
|
||||
'react/prop-types': 'off',
|
||||
...pluginReactHooks.configs.recommended.rules,
|
||||
'react-hooks/exhaustive-deps': 'off', // TODO: Delete this override
|
||||
'@typescript-eslint/array-type': 'off',
|
||||
'@typescript-eslint/consistent-indexed-object-style': 'off',
|
||||
'@typescript-eslint/consistent-type-definitions': 'off',
|
||||
'@typescript-eslint/no-empty-function': 'off',
|
||||
'@typescript-eslint/no-empty-object-type': ['error', { allowInterfaces: 'with-single-extends' }],
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
'@typescript-eslint/no-misused-promises': 'off', // Most of these errors come from Express route handlers that are handled correctly by express-async-errors
|
||||
'@typescript-eslint/no-unsafe-argument': 'off',
|
||||
'@typescript-eslint/no-unsafe-assignment': 'off',
|
||||
'@typescript-eslint/no-unsafe-call': 'off',
|
||||
'@typescript-eslint/no-unsafe-member-access': 'off',
|
||||
'@typescript-eslint/no-unsafe-return': 'off',
|
||||
'@typescript-eslint/no-unused-vars': [
|
||||
'error',
|
||||
{ argsIgnorePattern: '^_', caughtErrors: 'none', ignoreRestSiblings: true, varsIgnorePattern: '^_' },
|
||||
],
|
||||
'@typescript-eslint/restrict-template-expressions': 'off',
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['**/*.{js,jsx}'],
|
||||
rules: {
|
||||
...tsEslint.configs.disableTypeChecked.rules,
|
||||
'dot-notation': 'error',
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['services/backend/src/rapodiff/**/*'],
|
||||
rules: {
|
||||
'no-console': 'off',
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['cypress/**/*.js', 'services/backend/**/*.js', 'updater/**/*.js'],
|
||||
languageOptions: {
|
||||
sourceType: 'commonjs',
|
||||
},
|
||||
rules: {
|
||||
'@typescript-eslint/no-require-imports': 'off',
|
||||
'import/no-commonjs': 'off',
|
||||
'import/no-unused-modules': 'off',
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['services/backend/**/*.{js,ts}', 'updater/**/*.js'],
|
||||
rules: {
|
||||
// TODO: Most of these overrides should probably be removed
|
||||
'consistent-return': 'off',
|
||||
'import/no-default-export': 'off',
|
||||
'import/no-unused-modules': 'off',
|
||||
'no-await-in-loop': 'off',
|
||||
'no-param-reassign': 'off',
|
||||
'no-promise-executor-return': 'off',
|
||||
'no-return-await': 'off',
|
||||
},
|
||||
},
|
||||
{
|
||||
settings: {
|
||||
'import/internal-regex': '^@/shared/',
|
||||
'import/resolver': {
|
||||
typescript: {
|
||||
project: [
|
||||
'services/frontend/tsconfig.json',
|
||||
'services/frontend/tsconfig.node.json',
|
||||
'services/backend/tsconfig.json',
|
||||
'services/shared/tsconfig.json',
|
||||
],
|
||||
},
|
||||
},
|
||||
react: {
|
||||
version: '18.3',
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
Generated
+878
-849
File diff suppressed because it is too large
Load Diff
+17
-19
@@ -7,7 +7,7 @@
|
||||
"cypress:run": "cypress run --browser chrome",
|
||||
"instrument": "./scripts/instrument.sh",
|
||||
"eslint": "eslint",
|
||||
"lint:all": "eslint --ext .js,.jsx,.ts,.tsx services/ updater/ --report-unused-disable-directives",
|
||||
"lint:all": "eslint services/ updater/",
|
||||
"stylelint": "stylelint",
|
||||
"actionlint": "node-actionlint",
|
||||
"prettier": "prettier",
|
||||
@@ -38,29 +38,27 @@
|
||||
},
|
||||
"homepage": "https://github.com/UniversityOfHelsinkiCS/oodikone#readme",
|
||||
"devDependencies": {
|
||||
"@cypress/code-coverage": "^3.12.39",
|
||||
"@cypress/code-coverage": "^3.13.6",
|
||||
"@eslint/js": "^9.14.0",
|
||||
"@istanbuljs/schema": "^0.1.3",
|
||||
"@typescript-eslint/eslint-plugin": "^7.16.1",
|
||||
"@typescript-eslint/parser": "^7.16.1",
|
||||
"cypress": "^13.2.0",
|
||||
"eslint": "^8.56.0",
|
||||
"eslint-config-airbnb": "^19.0.4",
|
||||
"eslint-config-airbnb-typescript": "^18.0.0",
|
||||
"cypress": "^13.15.2",
|
||||
"eslint": "^9.14.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-import-resolver-custom-alias": "^1.3.2",
|
||||
"eslint-plugin-cypress": "^3.0.0",
|
||||
"eslint-plugin-import": "^2.29.1",
|
||||
"eslint-plugin-jsx-a11y": "^6.8.0",
|
||||
"eslint-import-resolver-typescript": "^3.6.3",
|
||||
"eslint-plugin-cypress": "^4.1.0",
|
||||
"eslint-plugin-import": "^2.31.0",
|
||||
"eslint-plugin-prettier": "^5.2.1",
|
||||
"eslint-plugin-react": "^7.33.2",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"husky": "^9.0.0",
|
||||
"lint-staged": "^15.0.0",
|
||||
"eslint-plugin-react": "^7.37.2",
|
||||
"eslint-plugin-react-hooks": "^5.0.0",
|
||||
"globals": "^15.12.0",
|
||||
"husky": "^9.1.6",
|
||||
"lint-staged": "^15.2.10",
|
||||
"node-actionlint": "^1.2.2",
|
||||
"nyc": "^17.0.0",
|
||||
"nyc": "^17.1.0",
|
||||
"prettier": "3.3.3",
|
||||
"stylelint": "^16.2.1",
|
||||
"stylelint-config-standard": "^36.0.0"
|
||||
"stylelint": "^16.10.0",
|
||||
"stylelint-config-standard": "^36.0.1",
|
||||
"typescript-eslint": "^8.13.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"lodash": "^4.17.21",
|
||||
|
||||
+15
-3
@@ -5,18 +5,30 @@
|
||||
{
|
||||
"groupName": "all non-major dependencies",
|
||||
"groupSlug": "all-minor-patch",
|
||||
"matchPackagePatterns": ["*"],
|
||||
"matchUpdateTypes": ["minor", "patch"]
|
||||
"matchUpdateTypes": ["minor", "patch"],
|
||||
"matchCurrentVersion": "!/^0/",
|
||||
"automerge": true,
|
||||
"matchPackageNames": ["*"]
|
||||
},
|
||||
{
|
||||
"matchDatasources": ["docker"],
|
||||
"matchPackageNames": ["node"],
|
||||
"versionCompatibility": "^(?<version>[^-]+)(?<compatibility>-.*)?$",
|
||||
"versioning": "node"
|
||||
},
|
||||
{
|
||||
"matchDatasources": ["docker"],
|
||||
"matchPackageNames": ["node"],
|
||||
"matchUpdateTypes": ["minor", "patch"],
|
||||
"enabled": false
|
||||
}
|
||||
],
|
||||
"rebaseWhen": "never",
|
||||
"vulnerabilityAlerts": {
|
||||
"labels": ["security"]
|
||||
}
|
||||
},
|
||||
"addLabels": ["dependencies"],
|
||||
"rangeStrategy": "bump",
|
||||
"schedule": ["before 5:00am"],
|
||||
"timezone": "Europe/Helsinki"
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM registry.access.redhat.com/ubi9/nodejs-20-minimal AS build
|
||||
FROM registry.access.redhat.com/ubi9/nodejs-22-minimal AS build
|
||||
|
||||
ENV TZ="Europe/Helsinki"
|
||||
|
||||
@@ -28,7 +28,7 @@ RUN npm run build
|
||||
|
||||
|
||||
|
||||
FROM registry.access.redhat.com/ubi9/nodejs-20-minimal
|
||||
FROM registry.access.redhat.com/ubi9/nodejs-22-minimal
|
||||
|
||||
ENV TZ="Europe/Helsinki"
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM node:20-alpine
|
||||
FROM node:22-alpine
|
||||
|
||||
ENV TZ="Europe/Helsinki"
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
require('./src/util/sentry') // Sentry must be the first import
|
||||
require('./src/app')
|
||||
const logger = require('./src/util/logger')
|
||||
|
||||
|
||||
Generated
+1490
-362
File diff suppressed because it is too large
Load Diff
@@ -5,47 +5,46 @@
|
||||
"build": "tsc"
|
||||
},
|
||||
"dependencies": {
|
||||
"@sentry/node": "^5.30.0",
|
||||
"@sentry/tracing": "^6.18.2",
|
||||
"@sentry/node": "^8.37.1",
|
||||
"@sentry/profiling-node": "^8.37.1",
|
||||
"axios": "^0.28.1",
|
||||
"bluebird": "^3.7.2",
|
||||
"bullmq": "^5.0.0",
|
||||
"compression": "^1.7.4",
|
||||
"bullmq": "^5.25.4",
|
||||
"compression": "^1.7.5",
|
||||
"cors": "^2.8.5",
|
||||
"cron": "^3.0.0",
|
||||
"express": "^4.19.2",
|
||||
"cron": "^3.1.9",
|
||||
"express": "^4.21.1",
|
||||
"express-async-errors": "^3.1.1",
|
||||
"lodash": "^4.17.21",
|
||||
"lru-cache": "^11.0.0",
|
||||
"lru-cache": "^11.0.2",
|
||||
"moment": "^2.30.1",
|
||||
"moment-timezone": "^0.5.37",
|
||||
"moment-timezone": "^0.5.46",
|
||||
"morgan": "^1.10.0",
|
||||
"oodikone-shared": "file:../oodikone-shared",
|
||||
"papaparse": "^5.4.1",
|
||||
"pg": "^8.11.5",
|
||||
"redis": "^3.1.2",
|
||||
"pg": "^8.13.1",
|
||||
"redis": "^4.7.0",
|
||||
"reflect-metadata": "^0.2.2",
|
||||
"sequelize": "^6.37.3",
|
||||
"sequelize": "^6.37.5",
|
||||
"sequelize-typescript": "^2.1.6",
|
||||
"umzug": "^3.8.1",
|
||||
"umzug": "^3.8.2",
|
||||
"unfuck-utf8-headers-middleware": "^1.0.1",
|
||||
"winston": "^3.8.2",
|
||||
"winston": "^3.17.0",
|
||||
"winston-gelf-transporter": "^1.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/compression": "^1.7.5",
|
||||
"@types/cors": "^2.8.17",
|
||||
"@types/express": "^4.17.21",
|
||||
"@types/lodash": "^4.17.6",
|
||||
"@types/lodash": "^4.17.13",
|
||||
"@types/morgan": "^1.9.9",
|
||||
"@types/node": "^20.14.2",
|
||||
"@types/validator": "^13.12.0",
|
||||
"tsx": "^4.16.2",
|
||||
"typescript": "^5.5.3"
|
||||
"@types/node": "^22.9.0",
|
||||
"@types/validator": "^13.12.2",
|
||||
"tsx": "^4.19.2",
|
||||
"typescript": "^5.6.3"
|
||||
},
|
||||
"overrides": {
|
||||
"winston-gelf-transporter": {
|
||||
"gelf-pro": "^1.3.4"
|
||||
"gelf-pro": "^1.3.12"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,10 +3,6 @@ const isDev = process.env.NODE_ENV === 'development'
|
||||
const isStaging = process.env.REACT_APP_STAGING === 'true'
|
||||
const isProduction = !isStaging && process.env.NODE_ENV === 'production'
|
||||
|
||||
// Sentry
|
||||
const sentryRelease = process.env.SENTRY_RELEASE || ''
|
||||
const sentryEnvironment = process.env.SENTRY_ENVIRONMENT || ''
|
||||
const sentryDSN = process.env.SENTRY_DSN || ''
|
||||
const runningInCI = process.env.CI === 'true'
|
||||
|
||||
// IAM group
|
||||
@@ -84,9 +80,6 @@ module.exports = {
|
||||
SIS_DB_URL,
|
||||
SIS_UPDATER_URL,
|
||||
runningInCI,
|
||||
sentryRelease,
|
||||
sentryEnvironment,
|
||||
sentryDSN,
|
||||
isProduction,
|
||||
pateToken,
|
||||
jamiUrl,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Linter was not having any of this so neeeded to add resolveJsonModule:true to tsconfig
|
||||
// Linter was not having any of this so needed to add resolveJsonModule:true to tsconfig
|
||||
// export const { facultyCodes, ignoredFacultyCodes, magicFacultyCode } = require('../environment/organizationConfig.json')
|
||||
|
||||
export { facultyCodes, ignoredFacultyCodes, magicFacultyCode } from '../environment/organizationConfig.json'
|
||||
|
||||
@@ -118,7 +118,7 @@ const sequelizeKone = new Sequelize(conf.DB_URL_KONE!, {
|
||||
],
|
||||
})
|
||||
|
||||
sequelizeKone.query(`SET SESSION search_path to ${conf.DB_SCHEMA_KONE}`)
|
||||
void sequelizeKone.query(`SET SESSION search_path to ${conf.DB_SCHEMA_KONE}`)
|
||||
|
||||
const sequelizeUser = new Sequelize(conf.DB_URL_USER!, {
|
||||
logging: false,
|
||||
|
||||
@@ -8,8 +8,8 @@ import { computeLanguageCenterData, LANGUAGE_CENTER_REDIS_KEY } from './services
|
||||
import { findStudentsCloseToGraduation, CLOSE_TO_GRADUATION_REDIS_KEY } from './services/populations/closeToGraduation'
|
||||
import { redisClient } from './services/redis'
|
||||
import { getCurrentSemester } from './services/semesters'
|
||||
import { combinedStudyprogrammes, isRelevantProgramme } from './services/studyProgramme/studyProgrammeHelpers'
|
||||
import { updateBasicView, updateStudytrackView } from './services/studyProgramme/studyProgrammeUpdates'
|
||||
import { combinedStudyProgrammes, isRelevantProgramme } from './services/studyProgramme/studyProgrammeHelpers'
|
||||
import { updateBasicView, updateStudyTrackView } from './services/studyProgramme/studyProgrammeUpdates'
|
||||
import { findAndSaveTeachers } from './services/teachers/top'
|
||||
import { deleteOutdatedUsers } from './services/userService'
|
||||
import logger from './util/logger'
|
||||
@@ -25,7 +25,10 @@ const schedule = (cronTime: string, onTick: () => void) => {
|
||||
export const refreshCloseToGraduating = async () => {
|
||||
logger.info('Refreshing students close to graduating')
|
||||
const updatedData = await findStudentsCloseToGraduation()
|
||||
await redisClient.setAsync(CLOSE_TO_GRADUATION_REDIS_KEY, JSON.stringify(updatedData))
|
||||
await redisClient.set(
|
||||
CLOSE_TO_GRADUATION_REDIS_KEY,
|
||||
JSON.stringify({ ...updatedData, lastUpdated: new Date().toISOString() })
|
||||
)
|
||||
logger.info('Students close to graduating updated!')
|
||||
}
|
||||
|
||||
@@ -45,7 +48,7 @@ export const refreshFaculties = async () => {
|
||||
export const refreshLanguageCenterData = async () => {
|
||||
logger.info('Refreshing language center data')
|
||||
const freshData = await computeLanguageCenterData()
|
||||
await redisClient.setAsync(LANGUAGE_CENTER_REDIS_KEY, JSON.stringify(freshData))
|
||||
await redisClient.set(LANGUAGE_CENTER_REDIS_KEY, JSON.stringify(freshData))
|
||||
logger.info('Language center data refreshed!')
|
||||
}
|
||||
|
||||
@@ -61,10 +64,10 @@ const refreshProgrammesAndFaculties = async () => {
|
||||
|
||||
export const refreshProgramme = async (code: string) => {
|
||||
await updateBasicView(code, '')
|
||||
await updateStudytrackView(code, '')
|
||||
const combinedProgramme = combinedStudyprogrammes[code] || ''
|
||||
await updateStudyTrackView(code, '')
|
||||
const combinedProgramme = combinedStudyProgrammes[code] || ''
|
||||
await updateBasicView(code, combinedProgramme)
|
||||
await updateStudytrackView(code, combinedProgramme)
|
||||
await updateStudyTrackView(code, combinedProgramme)
|
||||
}
|
||||
|
||||
export const refreshProgrammes = async () => {
|
||||
@@ -83,7 +86,7 @@ export const refreshProgrammes = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
const refreshTeacherLeaderboard = async () => {
|
||||
export const refreshTeacherLeaderboard = async () => {
|
||||
logger.info('Refreshing statistics for teacher leaderboard')
|
||||
const currentSemestersYearCode = (await getCurrentSemester()).getDataValue('yearcode')
|
||||
await findAndSaveTeachers(currentSemestersYearCode, currentSemestersYearCode - 1)
|
||||
|
||||
@@ -40,15 +40,15 @@ const accessLogger = morgan((tokens, req: Request, res: Response): undefined =>
|
||||
|
||||
const message = [
|
||||
`${user.mockedBy ? '(mocking) ' : ''}${user.name}`,
|
||||
`${(tokens['response-time'](req, res) || '0').split('.')[0]} ms`.padEnd(8, ' '),
|
||||
`${(tokens['response-time'](req, res) ?? '0').split('.')[0]} ms`.padEnd(8, ' '),
|
||||
tokens.status(req, res),
|
||||
tokens.method(req, res),
|
||||
decodeURIComponent(tokens.url(req, res) as string),
|
||||
decodeURIComponent(tokens.url(req, res)!),
|
||||
].join(' ')
|
||||
|
||||
const fullStudyProgrammeRights = getFullStudyProgrammeRights(user.programmeRights)
|
||||
const iamBasedRights = user.programmeRights.filter(right => right.isIamBased)
|
||||
const usingIamRights = iamBasedRights.some(right => (tokens.url(req, res) as string).includes(right.code))
|
||||
const usingIamRights = iamBasedRights.some(right => tokens.url(req, res)!.includes(right.code))
|
||||
const onlyIamRights = !user.isAdmin && fullStudyProgrammeRights.length === 0
|
||||
|
||||
logger.info(message, {
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
import { NextFunction, Request, Response } from 'express'
|
||||
|
||||
import { Role } from '../types'
|
||||
import { Role } from '../shared/types'
|
||||
|
||||
export const roles = (requiredRoles: Role[]) => async (req: Request, res: Response, next: NextFunction) => {
|
||||
const { roles } = req.user
|
||||
if (requiredRoles.some(role => roles.includes(role)) || roles.includes('admin')) {
|
||||
return next()
|
||||
export const roles =
|
||||
(requiredRoles: Role[], requiredIamGroups?: string[]) => (req: Request, res: Response, next: NextFunction) => {
|
||||
const { roles, iamGroups } = req.user
|
||||
if (
|
||||
requiredRoles.some(role => roles.includes(role)) ||
|
||||
requiredIamGroups?.some(group => iamGroups.includes(group)) ||
|
||||
roles.includes('admin')
|
||||
) {
|
||||
return next()
|
||||
}
|
||||
|
||||
res.status(403).json({ error: 'Missing required roles or IAM groups' })
|
||||
}
|
||||
|
||||
res.status(403).json({ error: 'Missing required roles' })
|
||||
}
|
||||
|
||||
@@ -25,42 +25,28 @@ const getUser = async (
|
||||
iamAccess: IamAccess
|
||||
) => {
|
||||
if (showAsUser && specialGroup?.superAdmin) {
|
||||
return await getMockedUser({ userToMock: showAsUser as string, mockedBy: username as string })
|
||||
return await getMockedUser({ userToMock: showAsUser, mockedBy: username })
|
||||
}
|
||||
return await getUserToska({
|
||||
username: username as string,
|
||||
name: name as string,
|
||||
email: email as string,
|
||||
username,
|
||||
name,
|
||||
email,
|
||||
iamGroups,
|
||||
specialGroup,
|
||||
sisId: sisId as string,
|
||||
sisId,
|
||||
iamAccess,
|
||||
})
|
||||
}
|
||||
|
||||
type Headers = {
|
||||
host?: string
|
||||
'user-agent'?: string
|
||||
accept?: string
|
||||
'accept-language'?: string
|
||||
'accept-encoding'?: string
|
||||
referer?: string
|
||||
displayname?: string
|
||||
hygroupcn?: string
|
||||
hypersonsisuid?: string
|
||||
mail?: string
|
||||
remote_user?: string
|
||||
'shib-session-id'?: string
|
||||
shib_logout_url?: string
|
||||
uid?: string
|
||||
'x-show-as-user'?: string
|
||||
connection?: string
|
||||
cookie?: string
|
||||
'sec-fetch-dest'?: string
|
||||
'sec-fetch-mode'?: string
|
||||
'sec-fetch-site'?: string
|
||||
'if-none-match'?: string
|
||||
priority?: string
|
||||
}
|
||||
|
||||
const toskaUserMiddleware = async (req: Request, _res: Response, next: NextFunction) => {
|
||||
@@ -87,8 +73,8 @@ const toskaUserMiddleware = async (req: Request, _res: Response, next: NextFunct
|
||||
)
|
||||
}
|
||||
|
||||
const iamGroups = parseIamGroups(hygroupcn as string)
|
||||
const { iamAccess = {}, specialGroup = {} } = await getOrganizationAccess(sisId as string, iamGroups)
|
||||
const iamGroups = parseIamGroups(hygroupcn!)
|
||||
const { iamAccess = {}, specialGroup = {} } = await getOrganizationAccess(sisId!, iamGroups)
|
||||
const iamRights = Object.keys(iamAccess)
|
||||
|
||||
if (!hasRequiredIamGroup(iamGroups, iamRights)) {
|
||||
@@ -107,7 +93,7 @@ const toskaUserMiddleware = async (req: Request, _res: Response, next: NextFunct
|
||||
Sentry.setUser({ username: user.mockedBy ?? username! })
|
||||
|
||||
req.user = user
|
||||
req.logoutUrl = logoutUrl as string
|
||||
req.logoutUrl = logoutUrl!
|
||||
|
||||
next()
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
/* eslint-disable import/no-cycle */
|
||||
import { InferAttributes } from 'sequelize'
|
||||
import {
|
||||
BelongsTo,
|
||||
@@ -50,11 +49,10 @@ export class SISStudyRight extends Model<InferAttributes<SISStudyRight>> {
|
||||
@Column(DataType.BOOLEAN)
|
||||
cancelled!: boolean
|
||||
|
||||
@ForeignKey(() => Student)
|
||||
@Column(DataType.STRING)
|
||||
studentNumber!: string
|
||||
|
||||
@BelongsTo(() => Student)
|
||||
@BelongsTo(() => Student, { foreignKey: 'studentNumber', targetKey: 'studentnumber' })
|
||||
student!: Student
|
||||
|
||||
@ForeignKey(() => StudyrightExtent)
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
/* eslint-disable import/no-cycle */
|
||||
import { InferAttributes } from 'sequelize'
|
||||
import {
|
||||
BelongsTo,
|
||||
@@ -12,7 +11,8 @@ import {
|
||||
UpdatedAt,
|
||||
} from 'sequelize-typescript'
|
||||
|
||||
import { DegreeProgrammeType, Name, Phase, StudyTrack } from '../types'
|
||||
import { Name } from '../shared/types'
|
||||
import { DegreeProgrammeType, Phase, StudyTrack } from '../types'
|
||||
import { SISStudyRight } from './SISStudyRight'
|
||||
|
||||
@Table({
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
/* eslint-disable import/no-cycle */
|
||||
import { InferAttributes } from 'sequelize'
|
||||
import {
|
||||
BelongsTo,
|
||||
@@ -14,7 +13,7 @@ import {
|
||||
UpdatedAt,
|
||||
} from 'sequelize-typescript'
|
||||
|
||||
import { Name } from '../types'
|
||||
import { Name } from '../shared/types'
|
||||
import { CourseProvider } from './courseProvider'
|
||||
import { CourseType } from './courseType'
|
||||
import { Credit } from './credit'
|
||||
@@ -57,15 +56,6 @@ export class Course extends Model<InferAttributes<Course>> {
|
||||
@Column(DataType.JSONB)
|
||||
name!: Name
|
||||
|
||||
@Column(DataType.DATE)
|
||||
latest_instance_date!: Date
|
||||
|
||||
@Column(DataType.DATE)
|
||||
startdate!: Date
|
||||
|
||||
@Column(DataType.DATE)
|
||||
enddate!: Date
|
||||
|
||||
@Column(DataType.DATE)
|
||||
max_attainment_date!: Date
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
/* eslint-disable import/no-cycle */
|
||||
import { InferAttributes } from 'sequelize'
|
||||
import { Column, CreatedAt, DataType, ForeignKey, Model, PrimaryKey, Table, UpdatedAt } from 'sequelize-typescript'
|
||||
import { Column, CreatedAt, DataType, ForeignKey, Model, Table, UpdatedAt } from 'sequelize-typescript'
|
||||
|
||||
import { Course } from './course'
|
||||
import { Organization } from './organization'
|
||||
@@ -9,18 +8,8 @@ import { Organization } from './organization'
|
||||
underscored: true,
|
||||
modelName: 'course_provider',
|
||||
tableName: 'course_providers',
|
||||
indexes: [
|
||||
{
|
||||
unique: true,
|
||||
fields: ['coursecode', 'organizationcode'],
|
||||
},
|
||||
],
|
||||
})
|
||||
export class CourseProvider extends Model<InferAttributes<CourseProvider>> {
|
||||
@PrimaryKey
|
||||
@Column(DataType.STRING)
|
||||
composite!: string
|
||||
|
||||
@ForeignKey(() => Course)
|
||||
@Column(DataType.STRING)
|
||||
coursecode!: string
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
/* eslint-disable import/no-cycle */
|
||||
import { InferAttributes } from 'sequelize'
|
||||
import { Column, CreatedAt, DataType, HasMany, Model, PrimaryKey, Table, UpdatedAt } from 'sequelize-typescript'
|
||||
|
||||
import { Name } from '../types'
|
||||
import { Name } from '../shared/types'
|
||||
import { Course } from './course'
|
||||
|
||||
@Table({
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
/* eslint-disable import/no-cycle */
|
||||
import { InferAttributes } from 'sequelize'
|
||||
import {
|
||||
BelongsTo,
|
||||
|
||||
@@ -1,16 +1,5 @@
|
||||
/* eslint-disable import/no-cycle */
|
||||
import { InferAttributes } from 'sequelize'
|
||||
import {
|
||||
Column,
|
||||
CreatedAt,
|
||||
DataType,
|
||||
ForeignKey,
|
||||
Model,
|
||||
PrimaryKey,
|
||||
Table,
|
||||
Unique,
|
||||
UpdatedAt,
|
||||
} from 'sequelize-typescript'
|
||||
import { Column, CreatedAt, DataType, ForeignKey, Model, Table, UpdatedAt } from 'sequelize-typescript'
|
||||
|
||||
import { Credit } from './credit'
|
||||
import { Teacher } from './teacher'
|
||||
@@ -21,11 +10,6 @@ import { Teacher } from './teacher'
|
||||
tableName: 'credit_teachers',
|
||||
})
|
||||
export class CreditTeacher extends Model<InferAttributes<CreditTeacher>> {
|
||||
@PrimaryKey
|
||||
@Unique
|
||||
@Column(DataType.STRING)
|
||||
composite!: string
|
||||
|
||||
@ForeignKey(() => Credit)
|
||||
@Column(DataType.STRING)
|
||||
credit_id!: string
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
/* eslint-disable import/no-cycle */
|
||||
import { InferAttributes } from 'sequelize'
|
||||
import { Column, CreatedAt, DataType, Model, PrimaryKey, Table, UpdatedAt } from 'sequelize-typescript'
|
||||
|
||||
import { CreditTypeCode, Name } from '../types'
|
||||
import { Name } from '../shared/types'
|
||||
import { CreditTypeCode } from '../types'
|
||||
|
||||
@Table({
|
||||
underscored: true,
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
/* eslint-disable import/no-cycle */
|
||||
import { InferAttributes } from 'sequelize'
|
||||
import { Column, CreatedAt, DataType, Model, Table, UpdatedAt } from 'sequelize-typescript'
|
||||
|
||||
import { Name } from '../types'
|
||||
import { Name } from '../shared/types'
|
||||
|
||||
@Table({
|
||||
underscored: true,
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
/* eslint-disable import/no-cycle */
|
||||
import { InferAttributes } from 'sequelize'
|
||||
import {
|
||||
BelongsTo,
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
/* eslint-disable import/no-cycle */
|
||||
import { CreditTypeCode } from '../types'
|
||||
import { Course } from './course'
|
||||
import { CourseProvider } from './courseProvider'
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
/* eslint-disable import/no-cycle */
|
||||
import { CreationOptional, InferAttributes, InferCreationAttributes } from 'sequelize'
|
||||
import { AutoIncrement, BelongsTo, Column, DataType, ForeignKey, Model, PrimaryKey, Table } from 'sequelize-typescript'
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
/* eslint-disable import/no-cycle */
|
||||
import { CreationOptional, InferAttributes, InferCreationAttributes } from 'sequelize'
|
||||
import { AutoIncrement, BelongsTo, Column, DataType, ForeignKey, Model, PrimaryKey, Table } from 'sequelize-typescript'
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
/* eslint-disable import/no-cycle */
|
||||
import { InferAttributes } from 'sequelize'
|
||||
import {
|
||||
BelongsToMany,
|
||||
@@ -12,7 +11,7 @@ import {
|
||||
UpdatedAt,
|
||||
} from 'sequelize-typescript'
|
||||
|
||||
import { Name } from '../types'
|
||||
import { Name } from '../shared/types'
|
||||
import { Course, CourseProvider, ProgrammeModule, SISStudyRight } from '.'
|
||||
|
||||
@Table({
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
/* eslint-disable import/no-cycle */
|
||||
import { InferAttributes } from 'sequelize'
|
||||
import {
|
||||
BelongsTo,
|
||||
@@ -13,7 +12,8 @@ import {
|
||||
UpdatedAt,
|
||||
} from 'sequelize-typescript'
|
||||
|
||||
import { DegreeProgrammeType, Name } from '../types'
|
||||
import { Name } from '../shared/types'
|
||||
import { DegreeProgrammeType } from '../types'
|
||||
import { Organization } from './organization'
|
||||
import { ProgrammeModuleChild } from './programmeModuleChild'
|
||||
|
||||
@@ -70,6 +70,9 @@ export class ProgrammeModule extends Model<InferAttributes<ProgrammeModule>> {
|
||||
@Column(DataType.STRING)
|
||||
degreeProgrammeType!: DegreeProgrammeType | null
|
||||
|
||||
@Column(DataType.INTEGER)
|
||||
minimumCredits!: number | null
|
||||
|
||||
@CreatedAt
|
||||
@Column(DataType.DATE)
|
||||
createdAt!: Date
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
/* eslint-disable import/no-cycle */
|
||||
import { InferAttributes } from 'sequelize'
|
||||
import { Column, CreatedAt, DataType, ForeignKey, Model, PrimaryKey, Table, UpdatedAt } from 'sequelize-typescript'
|
||||
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
/* eslint-disable import/no-cycle */
|
||||
import { InferAttributes } from 'sequelize'
|
||||
import { Column, CreatedAt, DataType, Model, PrimaryKey, Table, UpdatedAt } from 'sequelize-typescript'
|
||||
|
||||
import { Name } from '../types'
|
||||
import { Name } from '../shared/types'
|
||||
|
||||
@Table({
|
||||
underscored: true,
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
/* eslint-disable import/no-cycle */
|
||||
import { InferAttributes } from 'sequelize'
|
||||
import { Column, CreatedAt, DataType, HasMany, Model, PrimaryKey, Table, UpdatedAt } from 'sequelize-typescript'
|
||||
|
||||
@@ -27,7 +26,7 @@ export class Student extends Model<InferAttributes<Student>> {
|
||||
@HasMany(() => Enrollment, { foreignKey: 'studentnumber', sourceKey: 'studentnumber' })
|
||||
enrollments!: Enrollment[]
|
||||
|
||||
@HasMany(() => SISStudyRight)
|
||||
@HasMany(() => SISStudyRight, { foreignKey: 'studentNumber', sourceKey: 'studentnumber' })
|
||||
studyRights!: SISStudyRight[]
|
||||
|
||||
@HasMany(() => Studyplan, { foreignKey: 'studentnumber', sourceKey: 'studentnumber' })
|
||||
@@ -81,6 +80,9 @@ export class Student extends Model<InferAttributes<Student>> {
|
||||
@Column(DataType.STRING)
|
||||
sis_person_id!: string
|
||||
|
||||
@Column(DataType.BOOLEAN)
|
||||
hasPersonalIdentityCode!: boolean
|
||||
|
||||
@CreatedAt
|
||||
@Column(DataType.DATE)
|
||||
createdAt!: Date
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
/* eslint-disable import/no-cycle */
|
||||
import { InferAttributes } from 'sequelize'
|
||||
import {
|
||||
BelongsTo,
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
/* eslint-disable import/no-cycle */
|
||||
import { InferAttributes } from 'sequelize'
|
||||
import { Column, CreatedAt, DataType, Model, PrimaryKey, Table, UpdatedAt } from 'sequelize-typescript'
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
/* eslint-disable import/no-cycle */
|
||||
import { InferAttributes } from 'sequelize'
|
||||
import { BelongsToMany, Column, CreatedAt, DataType, Model, PrimaryKey, Table, UpdatedAt } from 'sequelize-typescript'
|
||||
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
/* eslint-disable import/no-cycle */
|
||||
import { CreationOptional, InferAttributes, InferCreationAttributes } from 'sequelize'
|
||||
import { AutoIncrement, Column, DataType, Default, Model, PrimaryKey, Table, Unique } from 'sequelize-typescript'
|
||||
|
||||
import { Language, Role } from '../types'
|
||||
import { Language, Role } from '../shared/types'
|
||||
|
||||
@Table({
|
||||
underscored: true,
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
const { orderBy } = require('lodash')
|
||||
|
||||
// dbConnections must be imported to avoid Sequelize errors even if it's not used
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { dbConnections } = require('../database/connection')
|
||||
const { computeCreditsProduced } = require('../services/providerCredits')
|
||||
const { parseCsv, printProgressBar } = require('./helpers')
|
||||
@@ -68,7 +69,7 @@ const diff = (rapoData, okData, code) => {
|
||||
}
|
||||
}
|
||||
|
||||
const process = async data => {
|
||||
const processProgrammes = async data => {
|
||||
let programmeCounter = 0
|
||||
const rapoProgrammeData = formatData(data.slice(1))
|
||||
const allProgrammeCodes = [...new Set(Object.keys(rapoProgrammeData))]
|
||||
@@ -87,7 +88,7 @@ const process = async data => {
|
||||
|
||||
const programmeCreditsDiff = async fileName => {
|
||||
console.log('Running studyprogramme credits diff')
|
||||
await parseCsv(fileName, process)
|
||||
await parseCsv(fileName, processProgrammes)
|
||||
console.log('Diff completed.')
|
||||
}
|
||||
|
||||
|
||||
@@ -36,14 +36,8 @@ import university from './routes/university'
|
||||
import updater from './routes/updater'
|
||||
import usersToska from './routes/users'
|
||||
import usersFd from './routes/usersFd'
|
||||
import initializeSentry from './util/sentry'
|
||||
|
||||
const routes = (app: Express, url: string) => {
|
||||
initializeSentry(app)
|
||||
|
||||
app.use(Sentry.Handlers.requestHandler())
|
||||
app.use(Sentry.Handlers.tracingHandler())
|
||||
|
||||
app.use(cors({ credentials: true, origin: frontUrl }))
|
||||
app.use(express.json())
|
||||
app.use(compression())
|
||||
@@ -71,7 +65,7 @@ const routes = (app: Express, url: string) => {
|
||||
app.use(`${url}/faculties`, faculties)
|
||||
app.use(`${url}/university`, university)
|
||||
app.use(`${url}/updater`, auth.roles(['admin']), updater)
|
||||
app.use(`${url}/teachers`, auth.roles(['teachers']), teachers)
|
||||
app.use(`${url}/teachers`, teachers)
|
||||
if (serviceProvider === 'toska') {
|
||||
app.use(`${url}/users`, usersToska)
|
||||
} else {
|
||||
@@ -84,11 +78,10 @@ const routes = (app: Express, url: string) => {
|
||||
app.use(`${url}/close-to-graduation`, auth.roles(['fullSisuAccess', 'studyGuidanceGroups']), closeToGraduation)
|
||||
app.use(`${url}/study-programme-pins`, studyProgrammePins)
|
||||
app.use(`${url}/curriculum-periods`, curriculumPeriods)
|
||||
app.get('*', async (_, res) => {
|
||||
const results = { error: 'unknown endpoint' }
|
||||
res.status(404).json(results)
|
||||
app.get('*', (_, res) => {
|
||||
res.status(404).json({ error: 'unknown endpoint' })
|
||||
})
|
||||
app.use(Sentry.Handlers.errorHandler())
|
||||
Sentry.setupExpressErrorHandler(app)
|
||||
app.use(errorHandler)
|
||||
}
|
||||
|
||||
|
||||
@@ -2,39 +2,44 @@ import axios from 'axios'
|
||||
import { Request, Response, Router } from 'express'
|
||||
|
||||
import { isDev } from '../config'
|
||||
import { Release } from '../shared/types'
|
||||
|
||||
const router = Router()
|
||||
|
||||
type ChangeLogData = {
|
||||
description: string
|
||||
time: Date
|
||||
title: string
|
||||
}
|
||||
|
||||
const changelog: { data?: ChangeLogData[] } = {}
|
||||
const changelog: { data?: Release[] } = {}
|
||||
|
||||
router.get('/', async (_req: Request, res: Response) => {
|
||||
if (changelog.data) {
|
||||
return res.status(200).send(changelog.data)
|
||||
}
|
||||
if (isDev) {
|
||||
const fakeChangeLogData: ChangeLogData[] = [
|
||||
const fakeRelease: Release[] = [
|
||||
{
|
||||
description: "### Fake release\nLet's not spam the GitHub API in development!",
|
||||
title: 'Fake title for fake release',
|
||||
time: new Date(),
|
||||
description: '**Feature 1**\n- Added a fancy new feature \n\n**Feature 2**\n- Fixed a bug\n- Fixed another bug',
|
||||
title: 'Release 1',
|
||||
time: new Date().toISOString(),
|
||||
},
|
||||
{
|
||||
description: "Let's not spam the GitHub API in development!",
|
||||
title: 'Release 2',
|
||||
time: new Date().toISOString(),
|
||||
},
|
||||
{
|
||||
description: 'This release should not be visible on the frontpage',
|
||||
title: 'Release 3',
|
||||
time: new Date().toISOString(),
|
||||
},
|
||||
]
|
||||
return res.status(200).json(fakeChangeLogData)
|
||||
return res.status(200).json(fakeRelease)
|
||||
}
|
||||
const response = await axios.get('https://api.github.com/repos/UniversityOfHelsinkiCS/oodikone/releases')
|
||||
const newChangelogData = response.data.map((release: Record<string, any>) => ({
|
||||
const releasesFromAPI: Release[] = response.data.map((release: Record<string, any>) => ({
|
||||
description: release.body,
|
||||
time: release.created_at,
|
||||
time: release.published_at,
|
||||
title: release.name,
|
||||
}))
|
||||
changelog.data = newChangelogData
|
||||
res.status(200).send(newChangelogData)
|
||||
changelog.data = releasesFromAPI
|
||||
res.status(200).json(releasesFromAPI)
|
||||
})
|
||||
|
||||
export default router
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import crypto from 'crypto'
|
||||
import { Request, Response, Router } from 'express'
|
||||
|
||||
import { serviceProvider } from '../config'
|
||||
import { getCourseYearlyStats } from '../services/courses'
|
||||
import { getCoursesByNameAndOrCode, getCoursesByCodes } from '../services/courses/courseFinders'
|
||||
import { CourseWithSubsId } from '../types'
|
||||
import { getFullStudyProgrammeRights, hasFullAccessToStudentData, validateParamLength } from '../util'
|
||||
import logger from '../util/logger'
|
||||
|
||||
const router = Router()
|
||||
|
||||
@@ -54,7 +56,7 @@ router.get('/v3/courseyearlystats', async (req: GetCourseYearlyStatsRequest, res
|
||||
const { roles, programmeRights } = req.user
|
||||
|
||||
const userHasFullAccessToStudentData = hasFullAccessToStudentData(roles)
|
||||
const userHasCorrectRole = userHasFullAccessToStudentData || roles.includes('courseStatistics')
|
||||
const userHasCorrectRole = userHasFullAccessToStudentData === true || roles.includes('courseStatistics')
|
||||
const fullStudyProgrammeRights = getFullStudyProgrammeRights(programmeRights)
|
||||
|
||||
// If user has rights to see at least one programme, then they are allowed to see all of them
|
||||
@@ -70,11 +72,16 @@ router.get('/v3/courseyearlystats', async (req: GetCourseYearlyStatsRequest, res
|
||||
const combineSubstitutions = req.query.combineSubstitutions !== 'false'
|
||||
const separate = req.query.separate === 'true'
|
||||
|
||||
if (serviceProvider === 'fd')
|
||||
logger.info('Debugging c.y.s.: got through authorization and collected req query params.')
|
||||
|
||||
// Student numbers should be obfuscated to all other users except admins,
|
||||
// fullSisuAccess users, and users with rights to any specific study programmes
|
||||
const anonymize = !userHasFullAccessToStudentData && fullStudyProgrammeRights.length === 0
|
||||
const anonymizationSalt = anonymize ? crypto.randomBytes(12).toString('hex') : null
|
||||
const results = await getCourseYearlyStats(codes, separate, anonymizationSalt, combineSubstitutions)
|
||||
if (serviceProvider === 'fd')
|
||||
logger.info('Debugging c.y.s.: getCourseYearlyStats returned and route should return result.')
|
||||
res.json(results)
|
||||
})
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ interface GetSearchRequest extends Request {
|
||||
}
|
||||
|
||||
router.get('/', async (req: GetSearchRequest, res: Response) => {
|
||||
const courseCodes = JSON.parse(req.query?.courselist as string) || []
|
||||
const courseCodes = JSON.parse(req.query?.courselist) || []
|
||||
const startDate = req.query?.startdate !== 'null' ? new Date(req.query.startdate) : new Date(2017, 7, 1, 0, 0, 0)
|
||||
const endDate = req.query?.enddate !== 'null' ? new Date(req.query.enddate) : new Date()
|
||||
if (req.query?.enddate === 'null') {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Request, Response, Router } from 'express'
|
||||
|
||||
import * as auth from '../middleware/auth'
|
||||
import { getDegreeProgrammesOfFaculty } from '../services/faculty/faculty'
|
||||
import { getDegreeProgrammesOfFaculty, getFacultyCodeById } from '../services/faculty/faculty'
|
||||
import { combineFacultyBasics } from '../services/faculty/facultyBasics'
|
||||
import { getFacultyCredits } from '../services/faculty/facultyCredits'
|
||||
import { countGraduationTimes } from '../services/faculty/facultyGraduationTimes'
|
||||
@@ -47,7 +47,8 @@ router.get(
|
||||
'/:id/basicstats',
|
||||
auth.roles(['facultyStatistics', 'katselmusViewer']),
|
||||
async (req: GetStatsRequest, res: Response) => {
|
||||
const code = req.params.id
|
||||
const facultyId = req.params.id
|
||||
const code = await getFacultyCodeById(facultyId)
|
||||
if (!code) {
|
||||
return res.status(422).end()
|
||||
}
|
||||
@@ -82,7 +83,8 @@ router.get(
|
||||
'/:id/creditstats',
|
||||
auth.roles(['facultyStatistics', 'katselmusViewer']),
|
||||
async (req: GetCreditStatsRequest, res: Response) => {
|
||||
const code = req.params.id
|
||||
const facultyId = req.params.id
|
||||
const code = await getFacultyCodeById(facultyId)
|
||||
const { year_type: yearType } = req.query
|
||||
const stats = await getFacultyCredits(code, yearType === 'ACADEMIC_YEAR')
|
||||
return res.json(stats)
|
||||
@@ -93,7 +95,8 @@ router.get(
|
||||
'/:id/thesisstats',
|
||||
auth.roles(['facultyStatistics', 'katselmusViewer']),
|
||||
async (req: GetStatsRequest, res: Response) => {
|
||||
const code = req.params.id
|
||||
const facultyId = req.params.id
|
||||
const code = await getFacultyCodeById(facultyId)
|
||||
if (!code) {
|
||||
return res.status(422).end()
|
||||
}
|
||||
@@ -127,7 +130,8 @@ router.get(
|
||||
'/:id/graduationtimes',
|
||||
auth.roles(['facultyStatistics', 'katselmusViewer']),
|
||||
async (req: GetGraduationStatsRequest, res: Response) => {
|
||||
const code = req.params.id
|
||||
const facultyId = req.params.id
|
||||
const code = await getFacultyCodeById(facultyId)
|
||||
if (!code) {
|
||||
return res.status(422).end()
|
||||
}
|
||||
@@ -162,7 +166,8 @@ router.get(
|
||||
'/:id/progressstats',
|
||||
auth.roles(['facultyStatistics', 'katselmusViewer']),
|
||||
async (req: GetProgressStatsRequest, res: Response) => {
|
||||
const code = req.params.id
|
||||
const facultyId = req.params.id
|
||||
const code = await getFacultyCodeById(facultyId)
|
||||
if (!code) {
|
||||
return res.status(422).end()
|
||||
}
|
||||
@@ -192,7 +197,8 @@ router.get(
|
||||
'/:id/studentstats',
|
||||
auth.roles(['facultyStatistics', 'katselmusViewer']),
|
||||
async (req: GetStudentStatsRequest, res: Response) => {
|
||||
const code = req.params.id
|
||||
const facultyId = req.params.id
|
||||
const code = await getFacultyCodeById(facultyId)
|
||||
if (!code) {
|
||||
return res.status(422).end()
|
||||
}
|
||||
@@ -227,7 +233,8 @@ router.get(
|
||||
'/:id/update_basicview',
|
||||
auth.roles(['facultyStatistics', 'katselmusViewer']),
|
||||
async (req: GetUpdateBasicViewRequest, res: Response) => {
|
||||
const code = req.params.id
|
||||
const facultyId = req.params.id
|
||||
const code = await getFacultyCodeById(facultyId)
|
||||
if (!code) {
|
||||
return res.status(422).end()
|
||||
}
|
||||
@@ -247,7 +254,8 @@ router.get(
|
||||
'/:id/update_progressview',
|
||||
auth.roles(['facultyStatistics', 'katselmusViewer']),
|
||||
async (req: Request, res: Response) => {
|
||||
const code = req.params.id
|
||||
const facultyId = req.params.id
|
||||
const code = await getFacultyCodeById(facultyId)
|
||||
if (!code) {
|
||||
return res.status(422).end()
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Request, Router } from 'express'
|
||||
import { Request, Response, Router } from 'express'
|
||||
|
||||
import { sendFeedbackToToska } from '../services/mailService'
|
||||
import { ApplicationError } from '../util/customErrors'
|
||||
@@ -12,7 +12,7 @@ interface EmailRequest extends Request {
|
||||
}
|
||||
}
|
||||
|
||||
router.post('/email', async (req: EmailRequest) => {
|
||||
router.post('/email', async (req: EmailRequest, res: Response) => {
|
||||
const { content } = req.body
|
||||
const { user } = req
|
||||
|
||||
@@ -26,6 +26,7 @@ router.post('/email', async (req: EmailRequest) => {
|
||||
})
|
||||
|
||||
logger.info(`${user.userId} succesfully sent feedback to Toska`)
|
||||
return res.status(200).end()
|
||||
})
|
||||
|
||||
export default router
|
||||
|
||||
@@ -5,7 +5,7 @@ import { ApplicationError } from '../util/customErrors'
|
||||
|
||||
const router = Router()
|
||||
|
||||
router.get('/', async (req: Request, res: Response) => {
|
||||
router.get('/', (req: Request, res: Response) => {
|
||||
const { user, logoutUrl } = req
|
||||
|
||||
if (!user) {
|
||||
|
||||
@@ -12,7 +12,7 @@ import { bottlenecksOf } from '../services/populations/bottlenecksOf'
|
||||
import { optimizedStatisticsOf } from '../services/populations/optimizedStatisticsOf'
|
||||
import { populationStudentsMerger, populationCourseStatsMerger } from '../services/statMerger'
|
||||
import { findByTag, findByCourseAndSemesters } from '../services/students'
|
||||
import { mapToProviders } from '../shared/util/mapToProviders'
|
||||
import { mapToProviders } from '../shared/util'
|
||||
import { GenderCode, ParsedCourse, Unarray, Unification, UnifyStatus } from '../types'
|
||||
import { getFullStudyProgrammeRights, hasFullAccessToStudentData } from '../util'
|
||||
import { ApplicationError } from '../util/customErrors'
|
||||
@@ -88,7 +88,7 @@ router.post('/v2/populationstatistics/courses', async (req: PostPopulationStatis
|
||||
|
||||
if (req.body.years) {
|
||||
const upperYearBound = new Date().getFullYear() + 1
|
||||
const multicoursestatPromises = Promise.all(
|
||||
const multiCourseStatPromises = Promise.all(
|
||||
req.body.years.map(year => {
|
||||
if (req.body.selectedStudentsByYear) {
|
||||
req.body.selectedStudents = encrypted
|
||||
@@ -101,8 +101,8 @@ router.post('/v2/populationstatistics/courses', async (req: PostPopulationStatis
|
||||
return coursestatistics
|
||||
})
|
||||
)
|
||||
const multicoursestats = await multicoursestatPromises
|
||||
const result = populationCourseStatsMerger(multicoursestats)
|
||||
const multiCourseStats = await multiCourseStatPromises
|
||||
const result = populationCourseStatsMerger(multiCourseStats)
|
||||
if (result.error) {
|
||||
return res.status(400).json(result)
|
||||
}
|
||||
@@ -118,8 +118,10 @@ router.post('/v2/populationstatistics/courses', async (req: PostPopulationStatis
|
||||
try {
|
||||
const result = await bottlenecksOf(query, null, encrypted)
|
||||
return res.json(result)
|
||||
} catch (error: any) {
|
||||
Sentry.captureException(new Error(error.message))
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof Error) {
|
||||
Sentry.captureException(error)
|
||||
}
|
||||
return res.status(400).end()
|
||||
}
|
||||
})
|
||||
@@ -156,8 +158,10 @@ router.post(
|
||||
studentNumberList
|
||||
)
|
||||
return res.json(result)
|
||||
} catch (error: any) {
|
||||
Sentry.captureException(new Error(error.message))
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof Error) {
|
||||
Sentry.captureException(error)
|
||||
}
|
||||
return res.status(400).end()
|
||||
}
|
||||
}
|
||||
@@ -200,8 +204,10 @@ router.post(
|
||||
studentNumbers
|
||||
)
|
||||
return res.json(result)
|
||||
} catch (error: any) {
|
||||
Sentry.captureException(new Error(error.message))
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof Error) {
|
||||
Sentry.captureException(error)
|
||||
}
|
||||
return res.status(400).end()
|
||||
}
|
||||
}
|
||||
@@ -248,7 +254,7 @@ router.get('/v3/populationstatistics', async (req: GetPopulationStatisticsReques
|
||||
|
||||
if (req.query.years) {
|
||||
const upperYearBound = new Date().getFullYear() + 1
|
||||
const multipopulationstudentPromises = Promise.all(
|
||||
const multiPopulationStudentPromises = Promise.all(
|
||||
req.query.years.map(year => {
|
||||
const newMonths = (upperYearBound - Number(year)) * 12
|
||||
const populationStudents = optimizedStatisticsOf({
|
||||
@@ -260,10 +266,9 @@ router.get('/v3/populationstatistics', async (req: GetPopulationStatisticsReques
|
||||
return populationStudents
|
||||
})
|
||||
)
|
||||
const multipopulationstudents = await multipopulationstudentPromises
|
||||
|
||||
const result = populationStudentsMerger(multipopulationstudents)
|
||||
const multiPopulationStudents = await multiPopulationStudentPromises
|
||||
|
||||
const result = populationStudentsMerger(multiPopulationStudents)
|
||||
res.json(filterPersonalTags(result, userId))
|
||||
} else {
|
||||
const result: any = await optimizedStatisticsOf({
|
||||
@@ -427,10 +432,9 @@ router.post('/v3/populationstatisticsbystudentnumbers', async (req: PostByStuden
|
||||
const filteredStudentNumbers = hasFullAccessToStudentData(roles)
|
||||
? studentnumberlist
|
||||
: intersection(studentnumberlist, studentsUserCanAccess)
|
||||
const studyProgrammeCode =
|
||||
tags?.studyProgramme && tags?.studyProgramme.includes('+')
|
||||
? tags?.studyProgramme.split('+')[0]
|
||||
: tags?.studyProgramme
|
||||
const studyProgrammeCode = tags?.studyProgramme?.includes('+')
|
||||
? tags?.studyProgramme.split('+')[0]
|
||||
: tags?.studyProgramme
|
||||
|
||||
const result = await optimizedStatisticsOf(
|
||||
{
|
||||
|
||||
@@ -6,14 +6,6 @@ import { ApplicationError } from '../util/customErrors'
|
||||
|
||||
const router = Router()
|
||||
|
||||
const filterStudentTags = (student: Awaited<ReturnType<typeof withStudentNumber>>, userId: string) => {
|
||||
if (!student) return null
|
||||
return {
|
||||
...student,
|
||||
tags: (student.tags ?? []).filter(({ tag }) => !tag.personal_user_id || tag.personal_user_id === userId),
|
||||
}
|
||||
}
|
||||
|
||||
interface GetStudentsRequest extends Request {
|
||||
query: {
|
||||
searchTerm: string
|
||||
@@ -59,11 +51,15 @@ router.get('/:studentNumber', async (req: GetStudentRequest, res: Response) => {
|
||||
} = req
|
||||
|
||||
if (!hasFullAccessToStudentData(roles) && !studentsUserCanAccess.includes(studentNumber)) {
|
||||
throw new ApplicationError('Error finding student', 400)
|
||||
return res
|
||||
.status(403)
|
||||
.json({ error: `User does not have permission to view data for student number ${studentNumber}.` })
|
||||
}
|
||||
const student = await withStudentNumber(studentNumber)
|
||||
const filteredTags = filterStudentTags(student, id)
|
||||
res.json(filteredTags)
|
||||
const student = await withStudentNumber(studentNumber, id)
|
||||
if (!student) {
|
||||
return res.status(404).json({ error: `Student not found with student number ${studentNumber}.` })
|
||||
}
|
||||
res.json(student)
|
||||
})
|
||||
|
||||
export default router
|
||||
|
||||
@@ -5,20 +5,20 @@ import {
|
||||
setBasicStats,
|
||||
getGraduationStats,
|
||||
setGraduationStats,
|
||||
getStudytrackStats,
|
||||
setStudytrackStats,
|
||||
getStudyTrackStats,
|
||||
setStudyTrackStats,
|
||||
} from '../services/analyticsService'
|
||||
import { getCreditsProduced } from '../services/providerCredits'
|
||||
import { getProgrammeName } from '../services/studyProgramme'
|
||||
import { getBasicStatsForStudytrack } from '../services/studyProgramme/studyProgrammeBasics'
|
||||
import {
|
||||
getStudyProgrammeCoursesForStudyTrack,
|
||||
getStudyProgrammeStatsForColorizedCoursesTable,
|
||||
} from '../services/studyProgramme/studyProgrammeCourses'
|
||||
import { getGraduationStatsForStudytrack } from '../services/studyProgramme/studyProgrammeGraduations'
|
||||
import { updateBasicView, updateStudytrackView } from '../services/studyProgramme/studyProgrammeUpdates'
|
||||
import { getStudyRightsInProgramme } from '../services/studyProgramme/studyRightFinders'
|
||||
import { getGraduationStatsForStudyTrack } from '../services/studyProgramme/studyProgrammeGraduations'
|
||||
import { updateBasicView, updateStudyTrackView } from '../services/studyProgramme/studyProgrammeUpdates'
|
||||
import { getStudyRightsInProgramme, getStudyTracksForProgramme } from '../services/studyProgramme/studyRightFinders'
|
||||
import { getStudyTrackStatsForStudyProgramme } from '../services/studyProgramme/studyTrackStats'
|
||||
import { Graduated, SpecialGroups, YearType } from '../types'
|
||||
import logger from '../util/logger'
|
||||
import { logInfoForGrafana } from '../util/logInfoForGrafana'
|
||||
|
||||
@@ -43,8 +43,8 @@ router.get('/creditstats', async (req: GetCreditStatsRequest, res: Response) =>
|
||||
|
||||
interface GetStatsRequest extends Request {
|
||||
query: {
|
||||
year_type: string
|
||||
special_groups: string
|
||||
year_type: YearType
|
||||
special_groups: SpecialGroups
|
||||
combined_programme: string
|
||||
}
|
||||
}
|
||||
@@ -56,7 +56,7 @@ router.get('/:id/basicstats', async (req: GetStatsRequest, res: Response) => {
|
||||
return res.status(422).end()
|
||||
}
|
||||
|
||||
logInfoForGrafana(code, combinedProgramme)
|
||||
void logInfoForGrafana(code, combinedProgramme)
|
||||
const data = await getBasicStats(code, combinedProgramme, yearType, specialGroups)
|
||||
if (data) {
|
||||
return res.json(data)
|
||||
@@ -88,7 +88,7 @@ router.get('/:id/graduationstats', async (req: GetStatsRequest, res: Response) =
|
||||
return res.json(data)
|
||||
}
|
||||
|
||||
const updatedStats = await getGraduationStatsForStudytrack({
|
||||
const updatedStats = await getGraduationStatsForStudyTrack({
|
||||
studyProgramme: code,
|
||||
combinedProgramme,
|
||||
settings: {
|
||||
@@ -116,7 +116,7 @@ router.get('/:id/coursestats', async (req: GetCourseStatsRequest, res: Response)
|
||||
const combinedProgramme = req.query?.combined_programme
|
||||
const date = new Date()
|
||||
date.setHours(23, 59, 59, 999)
|
||||
logInfoForGrafana(code, combinedProgramme)
|
||||
void logInfoForGrafana(code, combinedProgramme)
|
||||
try {
|
||||
const data = await getStudyProgrammeCoursesForStudyTrack(date.getTime(), code, showByYear, combinedProgramme)
|
||||
return res.json(data)
|
||||
@@ -127,8 +127,8 @@ router.get('/:id/coursestats', async (req: GetCourseStatsRequest, res: Response)
|
||||
|
||||
interface GetStudyTrackStatsRequest extends Request {
|
||||
query: {
|
||||
graduated: string
|
||||
special_groups: string
|
||||
graduated: Graduated
|
||||
special_groups: SpecialGroups
|
||||
combined_programme: string
|
||||
}
|
||||
}
|
||||
@@ -140,8 +140,8 @@ router.get('/:id/studytrackstats', async (req: GetStudyTrackStatsRequest, res: R
|
||||
return res.status(422).end()
|
||||
}
|
||||
|
||||
logInfoForGrafana(code, combinedProgramme)
|
||||
const data = await getStudytrackStats(code, combinedProgramme, graduated, specialGroups)
|
||||
void logInfoForGrafana(code, combinedProgramme)
|
||||
const data = await getStudyTrackStats(code, combinedProgramme, graduated, specialGroups)
|
||||
if (data) {
|
||||
return res.json(data)
|
||||
}
|
||||
@@ -157,7 +157,7 @@ router.get('/:id/studytrackstats', async (req: GetStudyTrackStatsRequest, res: R
|
||||
studyRightsOfProgramme,
|
||||
})
|
||||
if (updated) {
|
||||
await setStudytrackStats(updated, graduated, specialGroups)
|
||||
await setStudyTrackStats(updated, graduated, specialGroups)
|
||||
}
|
||||
return res.json(updated)
|
||||
})
|
||||
@@ -172,6 +172,19 @@ router.get('/:id/colorizedtablecoursestats', async (req: Request, res: Response)
|
||||
}
|
||||
})
|
||||
|
||||
router.get('/:id/studytracks', async (req: Request, res: Response) => {
|
||||
const code = req.params.id
|
||||
if (!code) {
|
||||
return res.status(422).end()
|
||||
}
|
||||
try {
|
||||
const data = await getStudyTracksForProgramme(code)
|
||||
return res.json(data)
|
||||
} catch (error) {
|
||||
logger.error({ message: `Failed to get study tracks for study programme ${code}`, meta: `${error}` })
|
||||
}
|
||||
})
|
||||
|
||||
interface GetUpdateViewRequest extends Request {
|
||||
query: {
|
||||
combined_programme: string
|
||||
@@ -201,7 +214,7 @@ router.get('/:id/update_studytrackview', async (req: GetUpdateViewRequest, res:
|
||||
return res.status(400).json({ error: 'Missing code' })
|
||||
}
|
||||
try {
|
||||
const result = await updateStudytrackView(code, combinedProgramme)
|
||||
const result = await updateStudyTrackView(code, combinedProgramme)
|
||||
return res.json(result)
|
||||
} catch (error) {
|
||||
const message = `Failed to update study track stats for programme ${code}${combinedProgramme ? `+${combinedProgramme}` : ''}`
|
||||
@@ -210,73 +223,4 @@ router.get('/:id/update_studytrackview', async (req: GetUpdateViewRequest, res:
|
||||
}
|
||||
})
|
||||
|
||||
interface GetEvaluationStatsRequest extends Request {
|
||||
query: {
|
||||
graduated: string
|
||||
year_type: string
|
||||
special_groups: string
|
||||
}
|
||||
}
|
||||
|
||||
router.get('/:id/evaluationstats', async (req: GetEvaluationStatsRequest, res: Response) => {
|
||||
const code = req.params.id
|
||||
const { graduated, year_type: yearType, special_groups: specialGroups } = req.query
|
||||
if (!code) {
|
||||
return res.status(422).end()
|
||||
}
|
||||
// Statistics for Tilannekuvalomake view
|
||||
const combinedProgramme = ''
|
||||
let gradData = await getGraduationStats(code, combinedProgramme, yearType, specialGroups)
|
||||
if (!gradData) {
|
||||
const updatedStats = await getGraduationStatsForStudytrack({
|
||||
studyProgramme: code,
|
||||
combinedProgramme,
|
||||
settings: {
|
||||
isAcademicYear: yearType === 'ACADEMIC_YEAR',
|
||||
includeAllSpecials: specialGroups === 'SPECIAL_INCLUDED',
|
||||
},
|
||||
})
|
||||
if (updatedStats) {
|
||||
await setGraduationStats(updatedStats, yearType, specialGroups)
|
||||
gradData = updatedStats
|
||||
}
|
||||
}
|
||||
|
||||
let progressData = await getStudytrackStats(code, combinedProgramme, graduated, specialGroups)
|
||||
if (!progressData) {
|
||||
const studyRightsOfProgramme = await getStudyRightsInProgramme(code, false, true)
|
||||
const updated = await getStudyTrackStatsForStudyProgramme({
|
||||
studyProgramme: code,
|
||||
combinedProgramme,
|
||||
settings: {
|
||||
graduated: graduated === 'GRADUATED_INCLUDED',
|
||||
specialGroups: specialGroups === 'SPECIAL_INCLUDED',
|
||||
},
|
||||
studyRightsOfProgramme,
|
||||
})
|
||||
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,
|
||||
status: gradData?.status,
|
||||
lastUpdated: gradData.lastUpdated,
|
||||
graduations: gradData,
|
||||
creditCounts: progressData?.creditCounts,
|
||||
creditCountsCombo: progressData?.creditCountsCombo,
|
||||
years: progressData?.years,
|
||||
}
|
||||
|
||||
return res.json(data)
|
||||
})
|
||||
|
||||
export default router
|
||||
|
||||
@@ -19,7 +19,7 @@ router.get('/', async (req: StudyProgrammeCriteriaRequest, res: Response) => {
|
||||
if (studyProgramme !== '' && !studyProgramme) {
|
||||
return res.status(422).end()
|
||||
}
|
||||
const studyProgrammeCriteria = await getCriteria(studyProgramme as string)
|
||||
const studyProgrammeCriteria = await getCriteria(studyProgramme)
|
||||
return res.json(studyProgrammeCriteria)
|
||||
})
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
TagFromFrontend,
|
||||
StudentTagFromFrontend,
|
||||
} from '../services/tags'
|
||||
import { Role } from '../types'
|
||||
import { Role } from '../shared/types'
|
||||
import { getFullStudyProgrammeRights, hasFullAccessToStudentData } from '../util'
|
||||
|
||||
const router = Router()
|
||||
@@ -69,7 +69,6 @@ interface PostTagRequest extends Request {
|
||||
|
||||
router.post('/tags', async (req: PostTagRequest, res: Response) => {
|
||||
const {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
tag: { studytrack, tagname, year, personal_user_id },
|
||||
} = req.body
|
||||
const { roles, id, programmeRights } = req.user
|
||||
|
||||
@@ -1,20 +1,27 @@
|
||||
import { Request, Response, Router } from 'express'
|
||||
|
||||
import { serviceProvider } from '../config'
|
||||
import * as auth from '../middleware/auth'
|
||||
import { getProvidersOfFaculty, isFaculty } from '../services/organizations'
|
||||
import { getTeachersBySearchTerm, getTeacherStatistics, getYearlyStatistics } from '../services/teachers'
|
||||
import { CategoryID, getTeacherStats, findAndSaveTeachers, getCategoriesAndYears } from '../services/teachers/top'
|
||||
import { mapToProviders } from '../shared/util/mapToProviders'
|
||||
import { mapToProviders } from '../shared/util'
|
||||
import { getFullStudyProgrammeRights, splitByEmptySpace } from '../util'
|
||||
|
||||
const router = Router()
|
||||
|
||||
const iamGroupsGivingFullAccess = ['hy-dekaanit', 'hy-varadekaanit-opetus']
|
||||
|
||||
const fullAccessAuth = () =>
|
||||
serviceProvider === 'toska' ? auth.roles([], iamGroupsGivingFullAccess) : auth.roles(['teachers'])
|
||||
|
||||
interface GetTeachersRequest extends Request {
|
||||
query: {
|
||||
searchTerm: string
|
||||
}
|
||||
}
|
||||
|
||||
router.get('/', async (req: GetTeachersRequest, res: Response) => {
|
||||
router.get('/', fullAccessAuth(), async (req: GetTeachersRequest, res: Response) => {
|
||||
const { searchTerm } = req.query
|
||||
if (!searchTerm) {
|
||||
return res.status(400).json({ error: 'Search term missing' })
|
||||
@@ -38,30 +45,37 @@ interface GetTopTeachersRequest extends Request {
|
||||
}
|
||||
}
|
||||
|
||||
router.get('/top', async (req: GetTopTeachersRequest, res: Response) => {
|
||||
router.get('/top', fullAccessAuth(), async (req: GetTopTeachersRequest, res: Response) => {
|
||||
const { yearcode, category = CategoryID.ALL } = req.query
|
||||
if (!yearcode) {
|
||||
return res.status(422).send('Missing required yearcode query param')
|
||||
}
|
||||
|
||||
const result = await getTeacherStats(category, Number(yearcode))
|
||||
res.json(result)
|
||||
if (result) {
|
||||
return res.json(result)
|
||||
}
|
||||
await findAndSaveTeachers(Number(yearcode), Number(yearcode))
|
||||
const updatedStats = await getTeacherStats(category, Number(yearcode))
|
||||
res.json(updatedStats)
|
||||
})
|
||||
|
||||
interface PostTopTeachersRequest extends Request {
|
||||
body: {
|
||||
startyearcode: string
|
||||
endyearcode: string
|
||||
startyearcode?: string
|
||||
endyearcode?: string
|
||||
}
|
||||
}
|
||||
|
||||
router.post('/top', async (req: PostTopTeachersRequest, res: Response) => {
|
||||
router.post('/top', auth.roles(['admin']), async (req: PostTopTeachersRequest, res: Response) => {
|
||||
const { startyearcode, endyearcode } = req.body
|
||||
const endYear = endyearcode ? Number(endyearcode) : undefined
|
||||
const startYear = startyearcode ? Number(startyearcode) : undefined
|
||||
res.status(200).end()
|
||||
await findAndSaveTeachers(Number(endyearcode), Number(startyearcode))
|
||||
await findAndSaveTeachers(endYear, startYear)
|
||||
})
|
||||
|
||||
router.get('/top/categories', async (_req: Request, res: Response) => {
|
||||
router.get('/top/categories', fullAccessAuth(), async (_req: Request, res: Response) => {
|
||||
const result = await getCategoriesAndYears()
|
||||
res.json(result)
|
||||
})
|
||||
@@ -75,7 +89,7 @@ interface GetTeacherStatsRequest extends Request {
|
||||
}
|
||||
|
||||
router.get('/stats', async (req: GetTeacherStatsRequest, res: Response) => {
|
||||
const { roles, programmeRights } = req.user
|
||||
const { roles, programmeRights, iamGroups } = req.user
|
||||
const fullStudyProgrammeRights = getFullStudyProgrammeRights(programmeRights)
|
||||
|
||||
const { providers, semesterStart, semesterEnd } = req.query
|
||||
@@ -84,7 +98,13 @@ router.get('/stats', async (req: GetTeacherStatsRequest, res: Response) => {
|
||||
}
|
||||
|
||||
const providerRights = mapToProviders(fullStudyProgrammeRights)
|
||||
if (!(providers.every(provider => providerRights.includes(provider)) || roles.includes('admin'))) {
|
||||
if (
|
||||
!(
|
||||
(roles.includes('teachers') && providers.every(provider => providerRights.includes(provider))) ||
|
||||
roles.includes('admin') ||
|
||||
iamGroups.some(group => iamGroupsGivingFullAccess.includes(group))
|
||||
)
|
||||
) {
|
||||
return res.status(403).send('You do not have permission to see this data')
|
||||
}
|
||||
|
||||
@@ -112,7 +132,7 @@ interface GetTeacherByIdRequest extends Request {
|
||||
}
|
||||
}
|
||||
|
||||
router.get('/:id', async (req: GetTeacherByIdRequest, res: Response) => {
|
||||
router.get('/:id', fullAccessAuth(), async (req: GetTeacherByIdRequest, res: Response) => {
|
||||
const { id } = req.params
|
||||
const result = await getTeacherStatistics(id)
|
||||
if (!result) {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
// const router = require('express').Router()
|
||||
import { Request, Response, Router } from 'express'
|
||||
import { cloneDeep } from 'lodash'
|
||||
|
||||
@@ -16,6 +15,7 @@ import {
|
||||
} from '../services/faculty/facultyService'
|
||||
import { combineFacultyStudentProgress, FacultyProgressData } from '../services/faculty/facultyStudentProgress'
|
||||
import { getMedian } from '../services/studyProgramme/studyProgrammeHelpers'
|
||||
import { NameWithCode } from '../shared/types'
|
||||
import { Graduated } from '../types'
|
||||
import logger from '../util/logger'
|
||||
|
||||
@@ -24,7 +24,7 @@ const router = Router()
|
||||
const degreeNames = ['bachelor', 'bachelorMaster', 'master', 'doctor'] as const
|
||||
|
||||
const getProgrammeNames = (faculties: Organization[]) => {
|
||||
return faculties.reduce<Record<string, { code: string; en?: string; fi?: string; sv?: string }>>((obj, faculty) => {
|
||||
return faculties.reduce<Record<string, NameWithCode>>((obj, faculty) => {
|
||||
const { name, code } = faculty.dataValues
|
||||
obj[faculty.code] = { code, ...name }
|
||||
return obj
|
||||
|
||||
@@ -65,13 +65,6 @@ router.post('/update/v2/customlist/:type', async (req: PostCustomListRequest, re
|
||||
}
|
||||
})
|
||||
|
||||
router.post('/update/v2/courses', async (req: Request, res: Response) => {
|
||||
const response = await updateSISCoursesByCourseCode(req.body)
|
||||
if (response) {
|
||||
res.status(200).json('Update SIS courses scheduled')
|
||||
}
|
||||
})
|
||||
|
||||
router.get('/update/v2/programmes', async (_req: Request, res: Response) => {
|
||||
const response = await updateSISProgrammes()
|
||||
if (response) {
|
||||
@@ -87,25 +80,27 @@ router.get('/refresh_redis_cache', async (req: Request, res: Response) => {
|
||||
}
|
||||
})
|
||||
|
||||
router.post('/refresh_statistic_v2', async (req: Request, res: Response) => {
|
||||
logger.info(`${req.user.username} requested refresh of statistics`)
|
||||
jobMaker.statistics()
|
||||
res.status(200).json('Teacher and study programme statistics refreshed')
|
||||
router.post('/refresh-teacher-leaderboard', (req: Request, res: Response) => {
|
||||
logger.info(
|
||||
`${req.user.username} requested refresh of teacher leaderboard for the current and previous academic year`
|
||||
)
|
||||
jobMaker.teacherLeaderboard()
|
||||
res.status(200).json('Teacher leaderboard for the current and previous academic year refreshed')
|
||||
})
|
||||
|
||||
router.post('/refresh_study_programmes_v2', async (req: Request, res: Response) => {
|
||||
logger.info(`${req.user.username} requested refresh of study programmes`)
|
||||
refreshProgrammes()
|
||||
await refreshProgrammes()
|
||||
res.status(200).json('Added job for refreshing study programme overviews')
|
||||
})
|
||||
|
||||
router.post('/refresh_faculties_v2', async (req: Request, res: Response) => {
|
||||
logger.info(`${req.user.username} requested refresh of faculties`)
|
||||
refreshFaculties()
|
||||
await refreshFaculties()
|
||||
res.status(200).json('Added job for refreshing faculties')
|
||||
})
|
||||
|
||||
router.post('/refresh_language_center_data', async (req: Request, res: Response) => {
|
||||
router.post('/refresh_language_center_data', (req: Request, res: Response) => {
|
||||
logger.info(`${req.user.username} requested refresh of language center data`)
|
||||
if (!languageCenterViewEnabled)
|
||||
res.status(418).json({ error: 'The language center functionality is not activated in your environment.' })
|
||||
@@ -113,7 +108,7 @@ router.post('/refresh_language_center_data', async (req: Request, res: Response)
|
||||
res.status(200).json('Added job for refreshing language center data')
|
||||
})
|
||||
|
||||
router.post('/refresh-close-to-graduation', async (req: Request, res: Response) => {
|
||||
router.post('/refresh-close-to-graduation', (req: Request, res: Response) => {
|
||||
logger.info(`${req.user.username} requested refresh of close to graduation data`)
|
||||
jobMaker.closeToGraduation()
|
||||
res.status(200).json('Added job for refreshing close to graduation data')
|
||||
|
||||
@@ -5,7 +5,7 @@ import * as auth from '../middleware/auth'
|
||||
import { sendNotificationAboutAccessToUser, previewNotificationAboutAccessToUser } from '../services/mailService'
|
||||
import * as userService from '../services/userService'
|
||||
import { LANGUAGE_CODES } from '../shared/language'
|
||||
import { Language } from '../types'
|
||||
import { Language } from '../shared/types'
|
||||
import logger from '../util/logger'
|
||||
|
||||
const router = Router()
|
||||
@@ -15,7 +15,7 @@ router.get('/', auth.roles(['admin']), async (_req: Request, res: Response) => {
|
||||
res.json(results)
|
||||
})
|
||||
|
||||
router.get('/access_groups', auth.roles(['admin']), async (_req: Request, res: Response) => {
|
||||
router.get('/access_groups', auth.roles(['admin']), (_req: Request, res: Response) => {
|
||||
res.json(roles)
|
||||
})
|
||||
|
||||
|
||||
@@ -23,16 +23,46 @@ router.get('/from-sisu-by-eppn/:newUserEppn', auth.roles(['admin']), async (req:
|
||||
res.json(person)
|
||||
})
|
||||
|
||||
interface User {
|
||||
id: string
|
||||
first_name: string
|
||||
last_name: string
|
||||
call_name: string
|
||||
eppn: string
|
||||
email_address: string
|
||||
student_number: string
|
||||
phone_number: string
|
||||
document_state: string
|
||||
}
|
||||
|
||||
interface AddUserRequest extends Request {
|
||||
body: {
|
||||
user: any // TODO: Funidata, what is the type of user?
|
||||
user: User
|
||||
}
|
||||
}
|
||||
|
||||
interface DeleteUserRequest extends Request {
|
||||
body: {
|
||||
userId: string
|
||||
}
|
||||
}
|
||||
|
||||
router.post('/add', auth.roles(['admin']), async (req: AddUserRequest, res: Response) => {
|
||||
const { user } = req.body
|
||||
const person = await userService.addNewUser(user)
|
||||
res.json(person)
|
||||
const oldUser = await userService.findByUsername(user.eppn)
|
||||
if (oldUser) return res.status(400).json({ error: 'Trying to add a user already present in the user database.' })
|
||||
const success = await userService.addNewUser(user)
|
||||
res.json(success)
|
||||
})
|
||||
|
||||
router.delete('/delete', auth.roles(['admin']), async (req: DeleteUserRequest, res: Response) => {
|
||||
const { userId } = req.body
|
||||
try {
|
||||
await userService.deleteUserById(userId)
|
||||
res.json({ deleted: userId })
|
||||
} catch (error) {
|
||||
return res.status(400).json({ error: 'Deletion was unsuccessful with the user id included in the request' })
|
||||
}
|
||||
})
|
||||
|
||||
export default router
|
||||
|
||||
@@ -1,44 +1,49 @@
|
||||
import moment from 'moment'
|
||||
import { facultyCodes, ignoredFacultyCodes } from '../config/organizationConstants'
|
||||
import { redisClient } from './redis'
|
||||
|
||||
// Only new bachelor, masters and doctoral programmes get their data updated in redis every night, use redis for them
|
||||
const isUpdatedNewProgramme = (code: string) => code.includes('KH') || code.includes('MH') || /^(T)[0-9]{6}$/.test(code)
|
||||
import { facultyCodes, ignoredFacultyCodes } from '../config/organizationConstants'
|
||||
import { Graduated, SpecialGroups, YearType } from '../types'
|
||||
import { redisClient } from './redis'
|
||||
import { isRelevantProgramme } from './studyProgramme/studyProgrammeHelpers'
|
||||
|
||||
const filteredFacultyCodes = facultyCodes.filter(item => !ignoredFacultyCodes.includes(item))
|
||||
const isFaculty = (code: string) => filteredFacultyCodes.includes(code)
|
||||
|
||||
const createRedisKeyForBasicStats = (id: string, yearType: string, specialGroups: string) => {
|
||||
const createRedisKeyForBasicStats = (id: string, yearType: YearType, specialGroups: SpecialGroups) => {
|
||||
return `BASIC_STATS_${id}_${yearType}_${specialGroups}`
|
||||
}
|
||||
const createRedisKeyForCreditStats = (id: string, yearType: string, specialGroups: string) => {
|
||||
const createRedisKeyForCreditStats = (id: string, yearType: YearType, specialGroups: SpecialGroups) => {
|
||||
return `CREDIT_STATS_${id}_${yearType}_${specialGroups}`
|
||||
}
|
||||
const createRedisKeyForGraduationStats = (id: string, yearType: string, specialGroups: string) => {
|
||||
const createRedisKeyForGraduationStats = (id: string, yearType: YearType, specialGroups: SpecialGroups) => {
|
||||
return `GRADUATION_STATS_${id}_${yearType}_${specialGroups}`
|
||||
}
|
||||
const createRedisKeyForStudytrackStats = (id: string, graduated: string, specialGroups: string) => {
|
||||
const createRedisKeyForStudyTrackStats = (id: string, graduated: Graduated, specialGroups: SpecialGroups) => {
|
||||
return `STUDYTRACK_STATS_${id}_${graduated}_${specialGroups}`
|
||||
}
|
||||
|
||||
const shouldSaveToRedis = (id: unknown) => {
|
||||
if (typeof id !== 'string') return false
|
||||
if (isRelevantProgramme(id)) return true
|
||||
const splitId = id.split('-')
|
||||
return splitId.length === 2 && isRelevantProgramme(splitId[0]) && isRelevantProgramme(splitId[1])
|
||||
}
|
||||
|
||||
export const getBasicStats = async (
|
||||
id: string,
|
||||
combinedProgramme: string | null,
|
||||
yearType: string,
|
||||
specialGroups: string
|
||||
yearType: YearType,
|
||||
specialGroups: SpecialGroups
|
||||
) => {
|
||||
if (!isUpdatedNewProgramme(id)) {
|
||||
return null
|
||||
}
|
||||
const searchkey = combinedProgramme ? `${id}-${combinedProgramme}` : id
|
||||
const redisKey = createRedisKeyForBasicStats(searchkey, yearType, specialGroups)
|
||||
const dataFromRedis = await redisClient.getAsync(redisKey)
|
||||
const searchKey = combinedProgramme ? `${id}-${combinedProgramme}` : id
|
||||
const redisKey = createRedisKeyForBasicStats(searchKey, yearType, specialGroups)
|
||||
const dataFromRedis = await redisClient.get(redisKey)
|
||||
if (!dataFromRedis) {
|
||||
return null
|
||||
}
|
||||
return JSON.parse(dataFromRedis)
|
||||
}
|
||||
|
||||
export const setBasicStats = async (data, yearType: string, specialGroups: string) => {
|
||||
export const setBasicStats = async (data, yearType: YearType, specialGroups: SpecialGroups) => {
|
||||
const { id } = data
|
||||
const redisKey = createRedisKeyForBasicStats(id, yearType, specialGroups)
|
||||
const dataToRedis = {
|
||||
@@ -46,10 +51,10 @@ export const setBasicStats = async (data, yearType: string, specialGroups: strin
|
||||
status: 'DONE',
|
||||
lastUpdated: moment().format(),
|
||||
}
|
||||
if (!isUpdatedNewProgramme(id)) {
|
||||
if (!shouldSaveToRedis(id)) {
|
||||
return dataToRedis
|
||||
}
|
||||
const setOperationStatus = await redisClient.setAsync(redisKey, JSON.stringify(dataToRedis))
|
||||
const setOperationStatus = await redisClient.set(redisKey, JSON.stringify(dataToRedis))
|
||||
if (setOperationStatus !== 'OK') {
|
||||
return null
|
||||
}
|
||||
@@ -62,7 +67,7 @@ export const getCreditStats = async (providerCode: string, isAcademicYear: boole
|
||||
isAcademicYear ? 'ACADEMIC_YEAR' : 'CALENDAR_YEAR',
|
||||
specialGroups ? 'SPECIAL_INCLUDED' : 'SPECIAL_EXCLUDED'
|
||||
)
|
||||
const dataFromRedis = await redisClient.getAsync(redisKey)
|
||||
const dataFromRedis = await redisClient.get(redisKey)
|
||||
if (!dataFromRedis) {
|
||||
return null
|
||||
}
|
||||
@@ -81,10 +86,10 @@ export const setCreditStats = async (data, isAcademicYear: boolean, specialGroup
|
||||
status: 'DONE',
|
||||
lastUpdated: moment().format(),
|
||||
}
|
||||
if (!isUpdatedNewProgramme(data.id) && !isFaculty(data.id)) {
|
||||
if (!isRelevantProgramme(id) && !isFaculty(id)) {
|
||||
return dataToRedis
|
||||
}
|
||||
const setOperationStatus = await redisClient.setAsync(redisKey, JSON.stringify(dataToRedis))
|
||||
const setOperationStatus = await redisClient.set(redisKey, JSON.stringify(dataToRedis))
|
||||
if (setOperationStatus !== 'OK') {
|
||||
return null
|
||||
}
|
||||
@@ -94,22 +99,19 @@ export const setCreditStats = async (data, isAcademicYear: boolean, specialGroup
|
||||
export const getGraduationStats = async (
|
||||
id: string,
|
||||
combinedProgramme: string | null,
|
||||
yearType: string,
|
||||
specialGroups: string
|
||||
yearType: YearType,
|
||||
specialGroups: SpecialGroups
|
||||
) => {
|
||||
if (!isUpdatedNewProgramme(id)) {
|
||||
return null
|
||||
}
|
||||
const searchkey = combinedProgramme ? `${id}-${combinedProgramme}` : id
|
||||
const redisKey = createRedisKeyForGraduationStats(searchkey, yearType, specialGroups)
|
||||
const dataFromRedis = await redisClient.getAsync(redisKey)
|
||||
const searchKey = combinedProgramme ? `${id}-${combinedProgramme}` : id
|
||||
const redisKey = createRedisKeyForGraduationStats(searchKey, yearType, specialGroups)
|
||||
const dataFromRedis = await redisClient.get(redisKey)
|
||||
if (!dataFromRedis) {
|
||||
return null
|
||||
}
|
||||
return JSON.parse(dataFromRedis)
|
||||
}
|
||||
|
||||
export const setGraduationStats = async (data, yearType: string, specialGroups: string) => {
|
||||
export const setGraduationStats = async (data, yearType: YearType, specialGroups: SpecialGroups) => {
|
||||
const { id } = data
|
||||
const redisKey = createRedisKeyForGraduationStats(id, yearType, specialGroups)
|
||||
const dataToRedis = {
|
||||
@@ -117,46 +119,43 @@ export const setGraduationStats = async (data, yearType: string, specialGroups:
|
||||
status: 'DONE',
|
||||
lastUpdated: moment().format(),
|
||||
}
|
||||
if (!isUpdatedNewProgramme(id)) {
|
||||
if (!shouldSaveToRedis(id)) {
|
||||
return dataToRedis
|
||||
}
|
||||
const setOperationStatus = await redisClient.setAsync(redisKey, JSON.stringify(dataToRedis))
|
||||
const setOperationStatus = await redisClient.set(redisKey, JSON.stringify(dataToRedis))
|
||||
if (setOperationStatus !== 'OK') {
|
||||
return null
|
||||
}
|
||||
return dataToRedis
|
||||
}
|
||||
|
||||
export const getStudytrackStats = async (
|
||||
export const getStudyTrackStats = async (
|
||||
id: string,
|
||||
combinedProgramme: string | null,
|
||||
graduated: string,
|
||||
specialGroups: string
|
||||
graduated: Graduated,
|
||||
specialGroups: SpecialGroups
|
||||
) => {
|
||||
if (!isUpdatedNewProgramme(id)) {
|
||||
return null
|
||||
}
|
||||
const searchkey = combinedProgramme ? `${id}-${combinedProgramme}` : id
|
||||
const redisKey = createRedisKeyForStudytrackStats(searchkey, graduated, specialGroups)
|
||||
const dataFromRedis = await redisClient.getAsync(redisKey)
|
||||
const searchKey = combinedProgramme ? `${id}-${combinedProgramme}` : id
|
||||
const redisKey = createRedisKeyForStudyTrackStats(searchKey, graduated, specialGroups)
|
||||
const dataFromRedis = await redisClient.get(redisKey)
|
||||
if (!dataFromRedis) {
|
||||
return null
|
||||
}
|
||||
return JSON.parse(dataFromRedis)
|
||||
}
|
||||
|
||||
export const setStudytrackStats = async (data, graduated: string, specialGroups: string) => {
|
||||
export const setStudyTrackStats = async (data, graduated: Graduated, specialGroups: SpecialGroups) => {
|
||||
const { id } = data
|
||||
const redisKey = createRedisKeyForStudytrackStats(id, graduated, specialGroups)
|
||||
const redisKey = createRedisKeyForStudyTrackStats(id, graduated, specialGroups)
|
||||
const dataToRedis = {
|
||||
...data,
|
||||
status: 'DONE',
|
||||
lastUpdated: moment().format(),
|
||||
}
|
||||
if (!isUpdatedNewProgramme(id)) {
|
||||
if (!shouldSaveToRedis(id)) {
|
||||
return dataToRedis
|
||||
}
|
||||
const setOperationStatus = await redisClient.setAsync(redisKey, JSON.stringify(dataToRedis))
|
||||
const setOperationStatus = await redisClient.set(redisKey, JSON.stringify(dataToRedis))
|
||||
if (setOperationStatus !== 'OK') {
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -15,7 +15,6 @@ interface StudentCredit {
|
||||
interface StudentEnrollment {
|
||||
date: Date
|
||||
courseCode: string
|
||||
state: EnrollmentState
|
||||
substitution: string | null
|
||||
}
|
||||
|
||||
@@ -84,7 +83,7 @@ const getCredits = async (
|
||||
? null
|
||||
: courses.find(course => course.substitutions.includes(credit.course_code))?.code
|
||||
return {
|
||||
courseCode: originalCode || credit.course_code,
|
||||
courseCode: originalCode ?? credit.course_code,
|
||||
substitution: originalCode ? credit.course_code : null,
|
||||
studentNumber: credit.student_studentnumber,
|
||||
creditType: credit.credittypecode,
|
||||
@@ -95,9 +94,9 @@ const getCredits = async (
|
||||
}
|
||||
|
||||
const getEnrollments = async (courses: Courses, fullCourseCodes: string[], studentNumbers: string[]) => {
|
||||
const enrollments: Array<Pick<Enrollment, 'course_code' | 'enrollment_date_time' | 'studentnumber' | 'state'>> =
|
||||
const enrollments: Array<Pick<Enrollment, 'course_code' | 'enrollment_date_time' | 'studentnumber'>> =
|
||||
await Enrollment.findAll({
|
||||
attributes: ['course_code', 'enrollment_date_time', 'studentnumber', 'state'],
|
||||
attributes: ['course_code', 'enrollment_date_time', 'studentnumber'],
|
||||
where: {
|
||||
course_code: {
|
||||
[Op.in]: fullCourseCodes,
|
||||
@@ -113,11 +112,10 @@ const getEnrollments = async (courses: Courses, fullCourseCodes: string[], stude
|
||||
const formattedEnrollments = enrollments.map(enrollment => {
|
||||
const originalCode = courses.find(course => course.substitutions.includes(enrollment.course_code))?.code
|
||||
return {
|
||||
courseCode: originalCode || enrollment.course_code,
|
||||
courseCode: originalCode ?? enrollment.course_code,
|
||||
substitution: originalCode ? enrollment.course_code : null,
|
||||
studentNumber: enrollment.studentnumber,
|
||||
date: enrollment.enrollment_date_time,
|
||||
state: enrollment.state,
|
||||
}
|
||||
})
|
||||
return formattedEnrollments
|
||||
@@ -201,7 +199,6 @@ export const getCompletedCourses = async (studentNumbers: string[], courseCodes:
|
||||
studentCredits[enrollment.studentNumber].enrollments.push({
|
||||
date: enrollment.date,
|
||||
courseCode: enrollment.courseCode,
|
||||
state: enrollment.state,
|
||||
substitution: enrollment.substitution,
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { EnrollmentState, Name } from '../../types'
|
||||
import { Name } from '../../shared/types'
|
||||
import { EnrollmentState } from '../../types'
|
||||
import { lengthOf, percentageOf } from '../../util'
|
||||
|
||||
const fall: string[] = []
|
||||
@@ -21,26 +22,16 @@ const initializePassingSemesters = () => {
|
||||
return passingSemesters
|
||||
}
|
||||
|
||||
const getEnrolledStudents = (enrollments: DynamicEnrollments) => {
|
||||
if (enrollments) {
|
||||
return Object.keys(enrollments)
|
||||
.filter(key => key !== EnrollmentState.ENROLLED && key !== 'semesters')
|
||||
.reduce((acc, key) => [...acc, ...[...enrollments[key]].map(studentNumber => studentNumber)], [] as string[])
|
||||
const getFilteredEnrolledNoGrade = (enrollments: DynamicEnrollments, allStudents: string[]) => {
|
||||
const enrolledStudents = [...(enrollments[EnrollmentState.ENROLLED] ?? [])]
|
||||
const result: Record<string, boolean> = {}
|
||||
for (const student of enrolledStudents) {
|
||||
if (allStudents.includes(student)) {
|
||||
continue
|
||||
}
|
||||
result[student] = true
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
const getFilteredEnrolledNoGrade = (
|
||||
enrollments: DynamicEnrollments,
|
||||
enrolledStudents: string[],
|
||||
allStudents: string[]
|
||||
) => {
|
||||
if (enrollments[EnrollmentState.ENROLLED]) {
|
||||
return [...enrollments[EnrollmentState.ENROLLED]]
|
||||
.filter(student => !enrolledStudents.includes(student) && !allStudents.includes(student))
|
||||
.reduce((acc, student) => ({ ...acc, [student]: true }), {} as Record<string, boolean>)
|
||||
}
|
||||
return {} as Record<string, boolean>
|
||||
return result
|
||||
}
|
||||
|
||||
type DynamicEnrollments = {
|
||||
@@ -57,8 +48,6 @@ type Stats = {
|
||||
students: number
|
||||
passed: number
|
||||
failed: number
|
||||
failedMany: number
|
||||
retryPassed: number
|
||||
attempts: number
|
||||
improvedPassedGrade: number
|
||||
percentage: number | undefined
|
||||
@@ -76,8 +65,6 @@ type Students = {
|
||||
all: Record<string, boolean>
|
||||
passed: Record<string, boolean>
|
||||
failed: Record<string, boolean>
|
||||
retryPassed: Record<string, boolean>
|
||||
failedMany: Record<string, boolean>
|
||||
improvedPassedGrade: Record<string, boolean>
|
||||
markedToSemester: Record<string, boolean>
|
||||
enrolledNoGrade: Record<string, boolean>
|
||||
@@ -126,8 +113,6 @@ export class CourseStatsCounter {
|
||||
all: {},
|
||||
passed: {},
|
||||
failed: {},
|
||||
retryPassed: {},
|
||||
failedMany: {},
|
||||
improvedPassedGrade: {},
|
||||
markedToSemester: {},
|
||||
enrolledNoGrade: {},
|
||||
@@ -136,8 +121,6 @@ export class CourseStatsCounter {
|
||||
students: 0,
|
||||
passed: 0,
|
||||
failed: 0,
|
||||
failedMany: 0,
|
||||
retryPassed: 0,
|
||||
attempts: 0,
|
||||
improvedPassedGrade: 0,
|
||||
percentage: undefined,
|
||||
@@ -216,13 +199,11 @@ export class CourseStatsCounter {
|
||||
|
||||
private removeFromFailed(studentNumber: string) {
|
||||
delete this.students.failed[studentNumber]
|
||||
delete this.students.failedMany[studentNumber]
|
||||
}
|
||||
|
||||
private markPassingGrade(studentNumber: string) {
|
||||
this.students.passed[studentNumber] = true
|
||||
if (this.failedBefore(studentNumber)) {
|
||||
this.students.retryPassed[studentNumber] = true
|
||||
this.removeFromFailed(studentNumber)
|
||||
}
|
||||
}
|
||||
@@ -235,12 +216,8 @@ export class CourseStatsCounter {
|
||||
|
||||
private markFailedGrade(studentNumber: string) {
|
||||
if (this.passedBefore(studentNumber)) {
|
||||
this.students.retryPassed[studentNumber] = true
|
||||
this.removeFromFailed(studentNumber)
|
||||
} else {
|
||||
if (this.failedBefore(studentNumber)) {
|
||||
this.students.failedMany[studentNumber] = true
|
||||
}
|
||||
this.students.failed[studentNumber] = true
|
||||
}
|
||||
}
|
||||
@@ -275,17 +252,14 @@ export class CourseStatsCounter {
|
||||
|
||||
public getFinalStats() {
|
||||
const { stats, students } = this
|
||||
const enrolledStudents = getEnrolledStudents(this.enrollments)
|
||||
const allStudents = Object.keys(students.all).map(student => student)
|
||||
const filteredEnrolledNoGrade = getFilteredEnrolledNoGrade(this.enrollments, enrolledStudents, allStudents)
|
||||
const allStudents = Object.keys(students.all)
|
||||
const filteredEnrolledNoGrade = getFilteredEnrolledNoGrade(this.enrollments, allStudents)
|
||||
|
||||
students.all = { ...students.all, ...filteredEnrolledNoGrade }
|
||||
students.enrolledNoGrade = filteredEnrolledNoGrade
|
||||
stats.students = lengthOf(students.all)
|
||||
stats.passed = lengthOf(students.passed)
|
||||
stats.failed = lengthOf(students.failed)
|
||||
stats.failedMany = lengthOf(students.failedMany)
|
||||
stats.retryPassed = lengthOf(students.retryPassed)
|
||||
stats.improvedPassedGrade = lengthOf(students.improvedPassedGrade)
|
||||
stats.percentage = percentageOf(stats.passed, stats.students)
|
||||
stats.passedOfPopulation = percentageOf(stats.passed, this.studentsInTotal)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { EnrollmentState, Name, PassedCategory } from '../../types'
|
||||
import { Name } from '../../shared/types'
|
||||
import { FormattedProgramme, OrganizationDetails } from './helpers'
|
||||
|
||||
type Programme = {
|
||||
@@ -34,13 +34,8 @@ type GroupAttempts = {
|
||||
}
|
||||
|
||||
type GroupStudents = {
|
||||
categories: {
|
||||
passedFirst: string[]
|
||||
passedEventually: string[]
|
||||
neverPassed: string[]
|
||||
}
|
||||
grades: { [studentNumber: string]: { grade: string; passed: boolean } }
|
||||
studentnumbers: string[]
|
||||
studentNumbers: string[]
|
||||
}
|
||||
|
||||
type Group = {
|
||||
@@ -49,14 +44,13 @@ type Group = {
|
||||
coursecode: string
|
||||
attempts: GroupAttempts
|
||||
students: GroupStudents
|
||||
enrollments: { studentNumber: string; state: string; enrollmentDateTime: Date }[]
|
||||
allEnrollments: { studentNumber: string; state: string; enrollmentDateTime: Date }[]
|
||||
yearcode: number
|
||||
enrollments: { studentNumber: string; enrollmentDateTime: Date }[]
|
||||
allEnrollments: { studentNumber: string; enrollmentDateTime: Date }[]
|
||||
yearCode: number
|
||||
}
|
||||
|
||||
type Student = {
|
||||
earliestAttainment: Date
|
||||
category: PassedCategory
|
||||
code: number
|
||||
}
|
||||
|
||||
@@ -117,17 +111,12 @@ export class CourseYearlyStatsCounter {
|
||||
},
|
||||
},
|
||||
students: {
|
||||
categories: {
|
||||
passedFirst: [],
|
||||
passedEventually: [],
|
||||
neverPassed: [],
|
||||
},
|
||||
grades: {},
|
||||
studentnumbers: [],
|
||||
studentNumbers: [],
|
||||
},
|
||||
enrollments: [],
|
||||
allEnrollments: [],
|
||||
yearcode: yearCode,
|
||||
yearCode,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -147,7 +136,6 @@ export class CourseYearlyStatsCounter {
|
||||
if (!this.facultyStats[yearCode]) {
|
||||
this.initFacultyYear(yearCode)
|
||||
}
|
||||
|
||||
if (
|
||||
facultyCode &&
|
||||
!this.facultyStats[yearCode].faculties[facultyCode] &&
|
||||
@@ -163,7 +151,6 @@ export class CourseYearlyStatsCounter {
|
||||
this.programmes[code].passed[yearCode] = []
|
||||
this.programmes[code].credits[yearCode] = 0
|
||||
}
|
||||
|
||||
if (facultyCode && !this.facultyStats[yearCode].allStudents.includes(studentNumber)) {
|
||||
this.facultyStats[yearCode].allStudents.push(studentNumber)
|
||||
this.facultyStats[yearCode].faculties[facultyCode].students.push(studentNumber)
|
||||
@@ -172,7 +159,6 @@ export class CourseYearlyStatsCounter {
|
||||
this.programmes[code].passed[yearCode].push(studentNumber)
|
||||
this.programmes[code].credits[yearCode] += credits
|
||||
}
|
||||
|
||||
if (facultyCode && passed && !this.facultyStats[yearCode].allPassed.includes(studentNumber)) {
|
||||
this.facultyStats[yearCode].allPassed.push(studentNumber)
|
||||
this.facultyStats[yearCode].faculties[facultyCode].passed.push(studentNumber)
|
||||
@@ -211,7 +197,6 @@ export class CourseYearlyStatsCounter {
|
||||
|
||||
public markEnrollmentToGroup(
|
||||
studentNumber: string,
|
||||
state: string,
|
||||
enrollmentDateTime: Date,
|
||||
groupCode: number,
|
||||
groupName: string | Name,
|
||||
@@ -221,22 +206,13 @@ export class CourseYearlyStatsCounter {
|
||||
if (!this.groups[groupCode]) {
|
||||
this.initGroup(groupCode, groupName, courseCode, yearCode)
|
||||
}
|
||||
const enrollment = { studentNumber, state, enrollmentDateTime }
|
||||
const enrollment = { studentNumber, enrollmentDateTime }
|
||||
this.groups[groupCode].allEnrollments.push(enrollment)
|
||||
const oldEnrollment = this.groups[groupCode].enrollments.find(
|
||||
enrollment => enrollment.studentNumber === studentNumber
|
||||
)
|
||||
if (!oldEnrollment) {
|
||||
return this.groups[groupCode].enrollments.push({ studentNumber, state, enrollmentDateTime })
|
||||
}
|
||||
if (oldEnrollment.state === EnrollmentState.ENROLLED || oldEnrollment.state === EnrollmentState.CONFIRMED) {
|
||||
return
|
||||
}
|
||||
if (state !== EnrollmentState.ENROLLED && state !== EnrollmentState.CONFIRMED) {
|
||||
return
|
||||
}
|
||||
if (![EnrollmentState.ENROLLED, EnrollmentState.CONFIRMED].includes(state as EnrollmentState)) {
|
||||
return
|
||||
return this.groups[groupCode].enrollments.push({ studentNumber, enrollmentDateTime })
|
||||
}
|
||||
this.groups[groupCode].enrollments = this.groups[groupCode].enrollments
|
||||
.filter(enrollment => enrollment.studentNumber !== studentNumber)
|
||||
@@ -272,56 +248,25 @@ export class CourseYearlyStatsCounter {
|
||||
this.groups[groupCode].students.grades[studentNumber] = { grade, passed }
|
||||
}
|
||||
|
||||
public markCreditToStudentCategories(
|
||||
studentNumber: string,
|
||||
passed: boolean,
|
||||
attainmentDate: Date,
|
||||
groupCode: number
|
||||
) {
|
||||
public markCreditToStudentCategories(studentNumber: string, attainmentDate: Date, groupCode: number) {
|
||||
if (!this.students.has(studentNumber)) {
|
||||
this.addNewStudent(studentNumber, passed, attainmentDate, groupCode)
|
||||
this.addOrUpdateStudent(studentNumber, attainmentDate, groupCode)
|
||||
} else {
|
||||
this.updateExistingStudent(studentNumber, passed, attainmentDate, groupCode)
|
||||
this.updateExistingStudent(studentNumber, attainmentDate, groupCode)
|
||||
}
|
||||
}
|
||||
|
||||
private addNewStudent(studentNumber: string, passed: boolean, attainmentDate: Date, groupCode: number) {
|
||||
const category = passed ? 'passedFirst' : 'neverPassed'
|
||||
private addOrUpdateStudent(studentNumber: string, attainmentDate: Date, groupCode: number) {
|
||||
this.students.set(studentNumber, {
|
||||
earliestAttainment: attainmentDate,
|
||||
category,
|
||||
code: groupCode,
|
||||
})
|
||||
}
|
||||
|
||||
private updateExistingStudent(studentNumber: string, passed: boolean, attainmentDate: Date, groupCode: number) {
|
||||
private updateExistingStudent(studentNumber: string, attainmentDate: Date, groupCode: number) {
|
||||
const student = this.students.get(studentNumber)
|
||||
if (attainmentDate < student!.earliestAttainment) {
|
||||
this.updateCategoryForEarlierAttainment(studentNumber, passed, attainmentDate, groupCode, student!)
|
||||
} else if (student!.category === 'neverPassed' && passed) {
|
||||
this.students.set(studentNumber, { ...student!, category: 'passedEventually' })
|
||||
}
|
||||
}
|
||||
|
||||
private updateCategoryForEarlierAttainment(
|
||||
studentNumber: string,
|
||||
passed: boolean,
|
||||
attainmentDate: Date,
|
||||
groupCode: number,
|
||||
student: Student
|
||||
) {
|
||||
if (passed) {
|
||||
this.students.set(studentNumber, {
|
||||
earliestAttainment: attainmentDate,
|
||||
category: 'passedFirst',
|
||||
code: groupCode,
|
||||
})
|
||||
} else if (student.category === 'passedFirst') {
|
||||
this.students.set(studentNumber, {
|
||||
earliestAttainment: attainmentDate,
|
||||
category: 'passedEventually',
|
||||
code: groupCode,
|
||||
})
|
||||
this.addOrUpdateStudent(studentNumber, attainmentDate, groupCode)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -341,8 +286,7 @@ export class CourseYearlyStatsCounter {
|
||||
|
||||
private parseGroupStatistics(anonymizationSalt: string | null) {
|
||||
for (const [studentNumber, data] of this.students) {
|
||||
this.groups[data.code].students.categories[data.category].push(studentNumber)
|
||||
this.groups[data.code].students.studentnumbers.push(studentNumber)
|
||||
this.groups[data.code].students.studentNumbers.push(studentNumber)
|
||||
}
|
||||
|
||||
const groupStatistics = Object.values(this.groups).map(({ ...rest }) => {
|
||||
@@ -360,7 +304,7 @@ export class CourseYearlyStatsCounter {
|
||||
...rest,
|
||||
students: { ...students, grades },
|
||||
}
|
||||
if (anonymizationSalt && normalStats.students.studentnumbers.length < 6) {
|
||||
if (anonymizationSalt && normalStats.students.studentNumbers.length < 6) {
|
||||
this.obfuscated = true
|
||||
const gradeSpread = {} as Grades
|
||||
for (const grade of Object.keys(normalStats.attempts.grades)) {
|
||||
@@ -378,14 +322,9 @@ export class CourseYearlyStatsCounter {
|
||||
},
|
||||
grades: gradeSpread,
|
||||
},
|
||||
yearcode: rest.yearcode,
|
||||
yearCode: rest.yearCode,
|
||||
students: {
|
||||
categories: {
|
||||
passedFirst: [] as string[],
|
||||
passedEventually: [] as string[],
|
||||
neverPassed: [] as string[],
|
||||
},
|
||||
studentnumbers: [] as string[],
|
||||
studentNumbers: [] as string[],
|
||||
},
|
||||
}
|
||||
return obfuscatedStats
|
||||
|
||||
@@ -95,7 +95,7 @@ export const getEnrollmentsForCourses = async (codes: string[], unification: Uni
|
||||
[Op.in]: codes,
|
||||
},
|
||||
enrollment_date_time: { [Op.gte]: new Date('2021-05-31') },
|
||||
state: [EnrollmentState.ENROLLED, EnrollmentState.CONFIRMED],
|
||||
state: EnrollmentState.ENROLLED,
|
||||
is_open: getIsOpen(unification),
|
||||
},
|
||||
})
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Op } from 'sequelize'
|
||||
|
||||
import { Name, Unification } from '../../types'
|
||||
import { Name } from '../../shared/types'
|
||||
import { Unification } from '../../types'
|
||||
|
||||
export type FormattedProgramme = {
|
||||
code: string
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
/* eslint-disable @typescript-eslint/naming-convention */
|
||||
import crypto from 'crypto'
|
||||
import { Op } from 'sequelize'
|
||||
|
||||
import { serviceProvider } from '../../config'
|
||||
import { Course, Credit, Enrollment, Organization, SISStudyRightElement } from '../../models'
|
||||
import { Name, Unification } from '../../types'
|
||||
import { Name } from '../../shared/types'
|
||||
import { EnrollmentState, Unification } from '../../types'
|
||||
import { isOpenUniCourseCode } from '../../util'
|
||||
import logger from '../../util/logger'
|
||||
import { getSortRank } from '../../util/sortRank'
|
||||
import { CourseYearlyStatsCounter } from './courseYearlyStatsCounter'
|
||||
import {
|
||||
@@ -99,7 +101,6 @@ type FormattedEnrollment = {
|
||||
semesterCode: number
|
||||
semesterName: Name
|
||||
courseCode: string
|
||||
state: string
|
||||
enrollmentDateTime: Date
|
||||
studentNumber: string
|
||||
programmes: Array<Record<string, any>>
|
||||
@@ -114,7 +115,6 @@ const parseEnrollment = (
|
||||
const {
|
||||
studentnumber: studentNumber,
|
||||
semester,
|
||||
state,
|
||||
enrollment_date_time: enrollmentDateTime,
|
||||
course_code: courseCode,
|
||||
} = enrollment
|
||||
@@ -128,7 +128,6 @@ const parseEnrollment = (
|
||||
semesterCode,
|
||||
semesterName,
|
||||
courseCode,
|
||||
state,
|
||||
enrollmentDateTime,
|
||||
studentNumber,
|
||||
programmes: studyRightElements.map(formatStudyRightElement),
|
||||
@@ -150,6 +149,10 @@ const getYearlyStatsOfNew = async (
|
||||
combineSubstitutions: boolean,
|
||||
studentNumberToSrElementsMap: Record<string, SISStudyRightElement[]>
|
||||
) => {
|
||||
if (serviceProvider === 'fd')
|
||||
logger.info(
|
||||
`Debugging c.y.s.: entered getYearlyStatsOfNew with courseCode ${courseCode} and unification ${unification}`
|
||||
)
|
||||
const courseForSubs = await Course.findOne({
|
||||
where: { code: courseCode },
|
||||
})
|
||||
@@ -173,6 +176,11 @@ const getYearlyStatsOfNew = async (
|
||||
}),
|
||||
])
|
||||
|
||||
if (serviceProvider === 'fd')
|
||||
logger.info(
|
||||
`Debugging c.y.s., getYearlyStatsOfNew, courseCode ${courseCode} unification ${unification}: got credits ${credits.length}, enrollments ${enrollments.length} and course ${course ? 'yes' : 'no'}`
|
||||
)
|
||||
|
||||
const counter = new CourseYearlyStatsCounter()
|
||||
|
||||
for (const credit of credits) {
|
||||
@@ -218,32 +226,56 @@ const getYearlyStatsOfNew = async (
|
||||
credits
|
||||
)
|
||||
counter.markCreditToGroup(studentNumber, passed, grade, groupCode, groupName, courseCode, yearCode)
|
||||
counter.markCreditToStudentCategories(studentNumber, passed, attainmentDate, groupCode)
|
||||
counter.markCreditToStudentCategories(studentNumber, attainmentDate, groupCode)
|
||||
}
|
||||
|
||||
enrollments.forEach(enrollment => {
|
||||
const { studentNumber, semesterCode, semesterName, yearCode, yearName, courseCode, state, enrollmentDateTime } =
|
||||
const { studentNumber, semesterCode, semesterName, yearCode, yearName, courseCode, enrollmentDateTime } =
|
||||
parseEnrollment(enrollment, anonymizationSalt, studentNumberToSrElementsMap)
|
||||
|
||||
const groupCode = separate ? semesterCode : yearCode
|
||||
const groupName = separate ? semesterName : yearName
|
||||
|
||||
counter.markEnrollmentToGroup(studentNumber, state, enrollmentDateTime, groupCode, groupName, courseCode, yearCode)
|
||||
counter.markEnrollmentToGroup(studentNumber, enrollmentDateTime, groupCode, groupName, courseCode, yearCode)
|
||||
})
|
||||
if (serviceProvider === 'fd')
|
||||
logger.info(
|
||||
`Debugging c.y.s., getYearlyStatsOfNew, code ${courseCode} unification ${unification}: finished looping through credits.`
|
||||
)
|
||||
|
||||
const statistics = counter.getFinalStatistics(anonymizationSalt)
|
||||
|
||||
if (serviceProvider === 'fd')
|
||||
logger.info(
|
||||
`Debugging c.y.s., getYearlyStatsOfNew, code ${courseCode} unification ${unification}: got final statistics.`
|
||||
)
|
||||
|
||||
let substitutionCourses: Course[] | undefined
|
||||
if (combineSubstitutions && courseForSubs?.substitutions && courseForSubs.substitutions.length > 0) {
|
||||
substitutionCourses = await Course.findAll({
|
||||
where: {
|
||||
code: {
|
||||
[Op.in]: codes,
|
||||
},
|
||||
},
|
||||
attributes: ['code', 'name'],
|
||||
})
|
||||
}
|
||||
|
||||
if (serviceProvider === 'fd')
|
||||
logger.info(
|
||||
`Debugging c.y.s., getYearlyStatsOfNew, code ${courseCode} unification ${unification}: got substitutionCourses ${substitutionCourses?.length} items.`
|
||||
)
|
||||
|
||||
return {
|
||||
...statistics,
|
||||
coursecode: courseCode,
|
||||
alternatives: codes,
|
||||
alternatives: substitutionCourses ?? [{ code: courseForSubs!.code, name: courseForSubs!.name }],
|
||||
name: course?.name,
|
||||
}
|
||||
}
|
||||
|
||||
export const maxYearsToCreatePopulationFrom = async (courseCodes: string[], unification: Unification) => {
|
||||
const is_open = getIsOpen(unification)
|
||||
|
||||
const maxAttainmentDate = new Date(
|
||||
Math.max(
|
||||
...(
|
||||
@@ -270,7 +302,7 @@ export const maxYearsToCreatePopulationFrom = async (courseCodes: string[], unif
|
||||
attainment_date: {
|
||||
[Op.gt]: attainmentThreshold,
|
||||
},
|
||||
is_open,
|
||||
is_open: getIsOpen(unification),
|
||||
},
|
||||
order: [['attainment_date', 'ASC']],
|
||||
})
|
||||
@@ -311,9 +343,17 @@ export const getCourseYearlyStats = async (
|
||||
})
|
||||
const enrollments = await Enrollment.findAll({
|
||||
attributes: ['studentnumber'],
|
||||
where: { course_code: { [Op.in]: courseCodes } },
|
||||
where: {
|
||||
course_code: {
|
||||
[Op.in]: courseCodes,
|
||||
},
|
||||
state: EnrollmentState.ENROLLED,
|
||||
},
|
||||
})
|
||||
|
||||
if (serviceProvider === 'fd')
|
||||
logger.info(`Debugging c.y.s.: got credits ${credits.length} items and enrollments ${enrollments.length} items`)
|
||||
|
||||
const studentNumbers = {}
|
||||
|
||||
credits.forEach(credit => {
|
||||
@@ -325,6 +365,11 @@ export const getCourseYearlyStats = async (
|
||||
|
||||
const studentNumberToSrElementsMap = await getStudentNumberToSrElementsMap(Object.keys(studentNumbers))
|
||||
|
||||
if (serviceProvider === 'fd')
|
||||
logger.info(
|
||||
`Debugging c.y.s.: got a studentNumberToSrElementsMap of size ${Object.keys(studentNumberToSrElementsMap).length}`
|
||||
)
|
||||
|
||||
const statsRegular = await Promise.all(
|
||||
courseCodes.map(async courseCode => {
|
||||
const unifyStats = await getYearlyStatsOfNew(
|
||||
@@ -356,6 +401,8 @@ export const getCourseYearlyStats = async (
|
||||
})
|
||||
)
|
||||
|
||||
if (serviceProvider === 'fd') logger.info('Debugging c.y.s.: finished creating statsRegular')
|
||||
|
||||
return statsRegular
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import { groupBy, orderBy } from 'lodash'
|
||||
import moment from 'moment'
|
||||
import { InferAttributes, QueryTypes } from 'sequelize'
|
||||
|
||||
import { serviceProvider } from '../../config'
|
||||
import { programmeCodes } from '../../config/programmeCodes'
|
||||
import { dbConnections } from '../../database/connection'
|
||||
import { Organization, ProgrammeModule } from '../../models'
|
||||
@@ -54,7 +55,9 @@ export const getDegreeProgrammesOfOrganization = async (organizationId: string,
|
||||
const programmesWithProgIds = programmesOfOrganization.map(programme => ({
|
||||
...programme,
|
||||
progId:
|
||||
programme.code in programmeCodes ? programmeCodes[programme.code as keyof typeof programmeCodes] : programme.code,
|
||||
programme.code in programmeCodes && serviceProvider !== 'fd'
|
||||
? programmeCodes[programme.code as keyof typeof programmeCodes]
|
||||
: programme.code,
|
||||
}))
|
||||
const programmesGroupedByCode = groupBy(orderBy(programmesWithProgIds, ['valid_from'], ['desc']), prog => prog.code)
|
||||
const curriculumPeriods = await getCurriculumPeriods()
|
||||
@@ -106,3 +109,16 @@ export const getDegreeProgrammesOfFaculty = async (facultyCode: string, onlyCurr
|
||||
}
|
||||
return getDegreeProgrammesOfOrganization(organization.id, onlyCurrentProgrammes)
|
||||
}
|
||||
|
||||
export const getFacultyCodeById = async (facultyId: string) => {
|
||||
const organization = await Organization.findOne({
|
||||
attributes: ['code'],
|
||||
where: {
|
||||
id: facultyId,
|
||||
},
|
||||
})
|
||||
if (!organization) {
|
||||
throw new Error(`The organization with the id ${facultyId} was not found.`)
|
||||
}
|
||||
return organization.code
|
||||
}
|
||||
|
||||
@@ -106,7 +106,7 @@ const calculateCombinedStats = async (
|
||||
} else {
|
||||
for (let i = 0; i < tableStatsForProgramme.length; i++) {
|
||||
for (let j = 1; j < tableStatsForProgramme[i].length; j++) {
|
||||
;(combinedTableStats[i][j] as number) += tableStatsForProgramme[i][j] as number
|
||||
;(combinedTableStats[i][j] as number) += tableStatsForProgramme[i][j]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,32 +1,33 @@
|
||||
import { Name } from '../../shared/types'
|
||||
import { getCreditsProduced } from '../providerCredits'
|
||||
import { isRelevantProgramme } from '../studyProgramme/studyProgrammeHelpers'
|
||||
import { getProgrammes } from './facultyService'
|
||||
import { getDegreeProgrammesOfFaculty } from './faculty'
|
||||
|
||||
/**
|
||||
* Returns credits produced by the programmes of the faculty but
|
||||
* also the credits where the provider is the faculty itself
|
||||
*/
|
||||
export const getFacultyCredits = async (facultyCode: string, isAcademicYear: boolean) => {
|
||||
const allProgrammes = (await getProgrammes(facultyCode)).data
|
||||
const programmes = allProgrammes.filter(({ code }) => isRelevantProgramme(code))
|
||||
const programmes = (await getDegreeProgrammesOfFaculty(facultyCode, true)).filter(({ code }) =>
|
||||
isRelevantProgramme(code)
|
||||
)
|
||||
const facultyCredits = await getCreditsProduced(facultyCode, isAcademicYear)
|
||||
|
||||
const stats = {
|
||||
[facultyCode]: facultyCredits,
|
||||
codes: [facultyCode],
|
||||
programmeNames: {},
|
||||
programmeNames: {} as Record<string, { code: string } & Name>,
|
||||
ids: [facultyCode],
|
||||
}
|
||||
|
||||
for (const programme of programmes) {
|
||||
const programmeCode = programme.code
|
||||
const programmeCredits = await getCreditsProduced(programmeCode, isAcademicYear)
|
||||
stats[programme.progId] = programmeCredits
|
||||
stats.codes.push(programmeCode)
|
||||
stats.ids.push(programme.progId)
|
||||
stats.programmeNames[programme.progId] = {
|
||||
code: programmeCode,
|
||||
...programmes.find(programme => programme.code === programmeCode).name,
|
||||
for (const { code, progId, name } of programmes) {
|
||||
const programmeCredits = await getCreditsProduced(code, isAcademicYear)
|
||||
stats[progId] = programmeCredits
|
||||
stats.codes.push(code)
|
||||
stats.ids.push(progId)
|
||||
stats.programmeNames[progId] = {
|
||||
code,
|
||||
...name,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { cloneDeep, omit } from 'lodash'
|
||||
|
||||
import { DegreeProgrammeType, Name } from '../../types'
|
||||
import { getGraduationStats, getStudytrackStats, setGraduationStats, setStudytrackStats } from '../analyticsService'
|
||||
import { getGraduationStatsForStudytrack, GraduationTimes } from '../studyProgramme/studyProgrammeGraduations'
|
||||
import { Name } from '../../shared/types'
|
||||
import { DegreeProgrammeType } from '../../types'
|
||||
import { getGraduationStats, getStudyTrackStats, setGraduationStats, setStudyTrackStats } from '../analyticsService'
|
||||
import { getGraduationStatsForStudyTrack, GraduationTimes } from '../studyProgramme/studyProgrammeGraduations'
|
||||
import { getMedian } from '../studyProgramme/studyProgrammeHelpers'
|
||||
import { getStudyRightsInProgramme } from '../studyProgramme/studyRightFinders'
|
||||
import {
|
||||
@@ -25,7 +26,7 @@ export const programmeTypes = {
|
||||
} as const
|
||||
|
||||
const getStatsByGraduationYear = async (facultyProgrammes: ProgrammesOfOrganization) => {
|
||||
const newStats: Array<Awaited<ReturnType<typeof getGraduationStatsForStudytrack>>> = []
|
||||
const newStats: Array<Awaited<ReturnType<typeof getGraduationStatsForStudyTrack>>> = []
|
||||
const medians: Record<string, LevelGraduationStats[]> = {}
|
||||
const programmes: { medians: Record<string, Record<string, ProgrammeStats>> } = { medians: {} }
|
||||
|
||||
@@ -38,7 +39,7 @@ const getStatsByGraduationYear = async (facultyProgrammes: ProgrammesOfOrganizat
|
||||
newStats.push(statsFromRedis)
|
||||
continue
|
||||
}
|
||||
const updatedStats = await getGraduationStatsForStudytrack({
|
||||
const updatedStats = await getGraduationStatsForStudyTrack({
|
||||
studyProgramme: programme.code,
|
||||
combinedProgramme: '',
|
||||
settings: {
|
||||
@@ -136,7 +137,7 @@ const getStatsByStartYear = async (facultyProgrammes: ProgrammesOfOrganization)
|
||||
if (programme.degreeProgrammeType == null || !(programme.degreeProgrammeType in programmeTypes)) {
|
||||
continue
|
||||
}
|
||||
const statsFromRedis = await getStudytrackStats(programme.code, '', 'GRADUATED_INCLUDED', 'SPECIAL_EXCLUDED')
|
||||
const statsFromRedis = await getStudyTrackStats(programme.code, '', 'GRADUATED_INCLUDED', 'SPECIAL_EXCLUDED')
|
||||
if (statsFromRedis) {
|
||||
newStats.push(statsFromRedis)
|
||||
continue
|
||||
@@ -152,7 +153,7 @@ const getStatsByStartYear = async (facultyProgrammes: ProgrammesOfOrganization)
|
||||
},
|
||||
studyRightsOfProgramme,
|
||||
})
|
||||
await setStudytrackStats(updatedStats, 'GRADUATED_INCLUDED', 'SPECIAL_EXCLUDED')
|
||||
await setStudyTrackStats(updatedStats, 'GRADUATED_INCLUDED', 'SPECIAL_EXCLUDED')
|
||||
newStats.push(updatedStats)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
import moment from 'moment'
|
||||
|
||||
import { DegreeProgrammeType, Graduated, Name, ProgrammeFilter, SpecialGroups, YearType } from '../../types'
|
||||
import { Name } from '../../shared/types'
|
||||
import { DegreeProgrammeType, Graduated, ProgrammeFilter, SpecialGroups, YearType } from '../../types'
|
||||
import { redisClient } from '../redis'
|
||||
import { getDegreeProgrammesOfFaculty } from './faculty'
|
||||
import { FacultyProgressData } from './facultyStudentProgress'
|
||||
|
||||
const createRedisKeyForFacultyProgrammes = (id: string, programmeFilter: ProgrammeFilter) => {
|
||||
return `FACULTY_PROGRAMMES_${id}_${programmeFilter}`
|
||||
}
|
||||
const createRedisKeyForBasicStats = (
|
||||
id: string,
|
||||
yearType: YearType,
|
||||
@@ -48,41 +45,6 @@ const removeGraduationTimes = (data: GraduationData) => {
|
||||
)
|
||||
}
|
||||
|
||||
export const setFacultyProgrammes = async (id: string, data, programmeFilter: ProgrammeFilter) => {
|
||||
const redisKey = createRedisKeyForFacultyProgrammes(id, programmeFilter)
|
||||
const dataToRedis = {
|
||||
data,
|
||||
status: 'DONE',
|
||||
lastUpdated: moment().format(),
|
||||
}
|
||||
const setOperationStatus = await redisClient.setAsync(redisKey, JSON.stringify(dataToRedis))
|
||||
if (setOperationStatus !== 'OK') {
|
||||
return null
|
||||
}
|
||||
return dataToRedis
|
||||
}
|
||||
|
||||
const getFacultyProgrammesFromRedis = async (id: string, programmeFilter: ProgrammeFilter) => {
|
||||
const redisKey = createRedisKeyForFacultyProgrammes(id, programmeFilter)
|
||||
const dataFromRedis = await redisClient.getAsync(redisKey)
|
||||
if (!dataFromRedis) {
|
||||
return null
|
||||
}
|
||||
return JSON.parse(dataFromRedis)
|
||||
}
|
||||
|
||||
export const getProgrammes = async (code: string, programmeFilter: ProgrammeFilter = 'NEW_STUDY_PROGRAMMES') => {
|
||||
const programmes = await getFacultyProgrammesFromRedis(code, programmeFilter)
|
||||
if (programmes) {
|
||||
return programmes
|
||||
}
|
||||
let updatedProgrammes: any = await getDegreeProgrammesOfFaculty(code, programmeFilter === 'NEW_STUDY_PROGRAMMES')
|
||||
if (updatedProgrammes) {
|
||||
updatedProgrammes = await setFacultyProgrammes(code, updatedProgrammes, programmeFilter)
|
||||
}
|
||||
return updatedProgrammes
|
||||
}
|
||||
|
||||
type GraduationInfo = {
|
||||
graphStats: {
|
||||
name: 'Bachelors' | 'Masters' | 'Doctors' | 'Others'
|
||||
@@ -124,7 +86,7 @@ export const setBasicStats = async (
|
||||
status: 'DONE',
|
||||
lastUpdated: moment().format(),
|
||||
}
|
||||
const setOperationStatus = await redisClient.setAsync(redisKey, JSON.stringify(dataToRedis))
|
||||
const setOperationStatus = await redisClient.set(redisKey, JSON.stringify(dataToRedis))
|
||||
if (setOperationStatus !== 'OK') {
|
||||
return null
|
||||
}
|
||||
@@ -138,7 +100,7 @@ export const getBasicStats = async (
|
||||
specialGroups: SpecialGroups
|
||||
) => {
|
||||
const redisKey = createRedisKeyForBasicStats(id, yearType, programmeFilter, specialGroups)
|
||||
const dataFromRedis = await redisClient.getAsync(redisKey)
|
||||
const dataFromRedis = await redisClient.get(redisKey)
|
||||
if (!dataFromRedis) {
|
||||
return null
|
||||
}
|
||||
@@ -175,7 +137,7 @@ export const setThesisWritersStats = async (
|
||||
status: 'DONE',
|
||||
lastUpdated: moment().format(),
|
||||
}
|
||||
const setOperationStatus = await redisClient.setAsync(redisKey, JSON.stringify(dataToRedis))
|
||||
const setOperationStatus = await redisClient.set(redisKey, JSON.stringify(dataToRedis))
|
||||
if (setOperationStatus !== 'OK') return null
|
||||
return dataToRedis
|
||||
}
|
||||
@@ -187,7 +149,7 @@ export const getThesisWritersStats = async (
|
||||
specialGroups: SpecialGroups
|
||||
) => {
|
||||
const redisKey = createRedisKeyForThesiswriters(id, yearType, programmeFilter, specialGroups)
|
||||
const dataFromRedis = await redisClient.getAsync(redisKey)
|
||||
const dataFromRedis = await redisClient.get(redisKey)
|
||||
if (!dataFromRedis) {
|
||||
return null
|
||||
}
|
||||
@@ -224,20 +186,16 @@ export const setGraduationStats = async (data: GraduationData, programmeFilter:
|
||||
status: 'DONE',
|
||||
lastUpdated: moment().format(),
|
||||
}
|
||||
const setOperationStatus = await redisClient.setAsync(redisKey, JSON.stringify(dataToRedis))
|
||||
const setOperationStatus = await redisClient.set(redisKey, JSON.stringify(dataToRedis))
|
||||
if (setOperationStatus !== 'OK') {
|
||||
return null
|
||||
}
|
||||
return dataToRedis
|
||||
}
|
||||
|
||||
export const getGraduationStats = async (
|
||||
id: string,
|
||||
programmeFilter: ProgrammeFilter,
|
||||
keepGraduationTimes: boolean = false
|
||||
) => {
|
||||
export const getGraduationStats = async (id: string, programmeFilter: ProgrammeFilter, keepGraduationTimes = false) => {
|
||||
const redisKey = createRedisKeyForGraduationTimeStats(id, programmeFilter)
|
||||
const dataFromRedis = await redisClient.getAsync(redisKey)
|
||||
const dataFromRedis = await redisClient.get(redisKey)
|
||||
if (!dataFromRedis) {
|
||||
return null
|
||||
}
|
||||
@@ -260,7 +218,7 @@ export const setFacultyProgressStats = async (
|
||||
status: 'DONE',
|
||||
lastUpdated: moment().format(),
|
||||
}
|
||||
const setOperationStatus = await redisClient.setAsync(redisKey, JSON.stringify(dataToRedis))
|
||||
const setOperationStatus = await redisClient.set(redisKey, JSON.stringify(dataToRedis))
|
||||
if (setOperationStatus !== 'OK') {
|
||||
return null
|
||||
}
|
||||
@@ -269,7 +227,7 @@ export const setFacultyProgressStats = async (
|
||||
|
||||
export const getFacultyProgressStats = async (id: string, specialGroups: SpecialGroups, graduated: Graduated) => {
|
||||
const redisKey = createRedisKeyForFacultyProgress(id, specialGroups, graduated)
|
||||
const dataFromRedis = await redisClient.getAsync(redisKey)
|
||||
const dataFromRedis = await redisClient.get(redisKey)
|
||||
if (!dataFromRedis) {
|
||||
return null
|
||||
}
|
||||
@@ -301,7 +259,7 @@ export const setFacultyStudentStats = async (
|
||||
status: 'DONE',
|
||||
lastUpdated: moment().format(),
|
||||
}
|
||||
const setOperationStatus = await redisClient.setAsync(redisKey, JSON.stringify(dataToRedis))
|
||||
const setOperationStatus = await redisClient.set(redisKey, JSON.stringify(dataToRedis))
|
||||
if (setOperationStatus !== 'OK') {
|
||||
return null
|
||||
}
|
||||
@@ -310,7 +268,7 @@ export const setFacultyStudentStats = async (
|
||||
|
||||
export const getFacultyStudentStats = async (id: string, specialGroups: SpecialGroups, graduated: Graduated) => {
|
||||
const redisKey = createRedisKeyForFacultyStudents(id, specialGroups, graduated)
|
||||
const dataFromRedis = await redisClient.getAsync(redisKey)
|
||||
const dataFromRedis = await redisClient.get(redisKey)
|
||||
if (!dataFromRedis) {
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import moment from 'moment'
|
||||
|
||||
import { rootOrgId } from '../../config'
|
||||
import { Graduated, SpecialGroups, Unarray } from '../../types'
|
||||
import { getStudytrackStats, setStudytrackStats } from '../analyticsService'
|
||||
import { getStudyTrackStats, setStudyTrackStats } from '../analyticsService'
|
||||
import { getYearsArray } from '../studyProgramme/studyProgrammeHelpers'
|
||||
import { getStudyRightsInProgramme } from '../studyProgramme/studyRightFinders'
|
||||
import { getStudyTrackStatsForStudyProgramme } from '../studyProgramme/studyTrackStats'
|
||||
@@ -114,7 +114,7 @@ export const combineFacultyStudentProgress = async (
|
||||
) {
|
||||
continue
|
||||
}
|
||||
const statsFromRedis = await getStudytrackStats(studyProgramme, null, graduated, specialGroups)
|
||||
const statsFromRedis = await getStudyTrackStats(studyProgramme, null, graduated, specialGroups)
|
||||
if (statsFromRedis) {
|
||||
statsOfProgrammes.push(statsFromRedis)
|
||||
} else {
|
||||
@@ -128,7 +128,7 @@ export const combineFacultyStudentProgress = async (
|
||||
studyRightsOfProgramme,
|
||||
})
|
||||
statsOfProgrammes.push(updatedStats)
|
||||
setStudytrackStats(updatedStats, graduated, specialGroups)
|
||||
await setStudyTrackStats(updatedStats, graduated, specialGroups)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Unarray } from '../../types'
|
||||
import { getStudytrackStats, setStudytrackStats } from '../analyticsService'
|
||||
import { getStudyTrackStats, setStudyTrackStats } from '../analyticsService'
|
||||
import { getPercentage, tableTitles } from '../studyProgramme/studyProgrammeHelpers'
|
||||
import { getStudyRightsInProgramme } from '../studyProgramme/studyRightFinders'
|
||||
import { getStudyTrackStatsForStudyProgramme } from '../studyProgramme/studyTrackStats'
|
||||
@@ -65,7 +65,7 @@ export const combineFacultyStudents = async (
|
||||
const newStats: StudyTrackStats[] = []
|
||||
|
||||
for (const studyProgramme of programmeCodes) {
|
||||
const statsFromRedis = await getStudytrackStats(studyProgramme, null, graduated, specialGroups)
|
||||
const statsFromRedis = await getStudyTrackStats(studyProgramme, null, graduated, specialGroups)
|
||||
if (statsFromRedis) {
|
||||
newStats.push(statsFromRedis)
|
||||
if (!years.length) {
|
||||
@@ -82,7 +82,7 @@ export const combineFacultyStudents = async (
|
||||
},
|
||||
studyRightsOfProgramme,
|
||||
})
|
||||
setStudytrackStats(updatedStats, graduated, specialGroups)
|
||||
await setStudyTrackStats(updatedStats, graduated, specialGroups)
|
||||
if (!years.length) {
|
||||
years = updatedStats.years
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import { cloneDeep } from 'lodash'
|
||||
|
||||
import { DegreeProgrammeType } from '../../types'
|
||||
import { getGraduationStats, setGraduationStats } from '../analyticsService'
|
||||
import { getGraduationStatsForStudytrack } from '../studyProgramme/studyProgrammeGraduations'
|
||||
import { getGraduationStatsForStudyTrack } from '../studyProgramme/studyProgrammeGraduations'
|
||||
import { getDegreeProgrammesOfFaculty, ProgrammesOfOrganization } from './faculty'
|
||||
|
||||
type ProgrammeName = {
|
||||
@@ -25,7 +25,7 @@ const calculateCombinedStats = async (
|
||||
]
|
||||
const programmeTableStats: Record<string, Array<Array<string | number>>> = {}
|
||||
|
||||
const newStats: Array<Awaited<ReturnType<typeof getGraduationStatsForStudytrack>>> = []
|
||||
const newStats: Array<Awaited<ReturnType<typeof getGraduationStatsForStudyTrack>>> = []
|
||||
const combinedTableStats: Array<Array<string | number>> = []
|
||||
let years: Array<string | number> = []
|
||||
const programmeNames: Record<string, ProgrammeName> = {}
|
||||
@@ -47,7 +47,7 @@ const calculateCombinedStats = async (
|
||||
newStats.push(statsFromRedis)
|
||||
continue
|
||||
}
|
||||
const updatedStats = await getGraduationStatsForStudytrack({
|
||||
const updatedStats = await getGraduationStatsForStudyTrack({
|
||||
studyProgramme: programme.code,
|
||||
combinedProgramme: '',
|
||||
settings: {
|
||||
|
||||
@@ -6,7 +6,6 @@ import { combineFacultyBasics } from './facultyBasics'
|
||||
import { countGraduationTimes } from './facultyGraduationTimes'
|
||||
import {
|
||||
setBasicStats,
|
||||
setFacultyProgrammes,
|
||||
setFacultyProgressStats,
|
||||
setFacultyStudentStats,
|
||||
setGraduationStats,
|
||||
@@ -63,7 +62,6 @@ export const updateFacultyOverview = async (facultyCode: string, statsType: Stat
|
||||
|
||||
export const updateFacultyProgressOverview = async (facultyCode: string) => {
|
||||
const onlyNew = await getDegreeProgrammesOfFaculty(facultyCode, true)
|
||||
const newProgrammes = await setFacultyProgrammes(facultyCode, onlyNew, 'NEW_STUDY_PROGRAMMES')
|
||||
|
||||
for (const graduated of graduatedOptions) {
|
||||
for (const specialGroups of specialGroupOptions) {
|
||||
@@ -71,7 +69,7 @@ export const updateFacultyProgressOverview = async (facultyCode: string) => {
|
||||
await setFacultyStudentStats(updateFacultyStudentStats, specialGroups, graduated)
|
||||
const updateFacultyProgressStats = await combineFacultyStudentProgress(
|
||||
facultyCode,
|
||||
newProgrammes?.data,
|
||||
onlyNew,
|
||||
specialGroups,
|
||||
graduated
|
||||
)
|
||||
|
||||
@@ -219,10 +219,10 @@ const computeLanguageCenterData = async () => {
|
||||
}
|
||||
|
||||
const getLanguageCenterData = async () => {
|
||||
const dataOnRedis = await redisClient.getAsync(LANGUAGE_CENTER_REDIS_KEY)
|
||||
const dataOnRedis = await redisClient.get(LANGUAGE_CENTER_REDIS_KEY)
|
||||
if (dataOnRedis) return JSON.parse(dataOnRedis)
|
||||
const freshData = await computeLanguageCenterData()
|
||||
redisClient.setAsync(LANGUAGE_CENTER_REDIS_KEY, JSON.stringify(freshData))
|
||||
await redisClient.set(LANGUAGE_CENTER_REDIS_KEY, JSON.stringify(freshData))
|
||||
return freshData
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user