mirror of
https://github.com/UniversityOfHelsinkiCS/oodikone.git
synced 2026-10-13 03:13:40 +03:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
59b4aad1ab | ||
|
|
de3e0f5e6b | ||
|
|
7ede62760e | ||
|
|
d2fdeeb830 | ||
|
|
220952153f | ||
|
|
bace3c80af | ||
|
|
d9457bf855 | ||
|
|
2d6d12f95f | ||
|
|
93a3014209 | ||
|
|
3d6bc23658 | ||
|
|
1906827760 | ||
|
|
4b8f389dd3 | ||
|
|
5d69a9b53b | ||
|
|
5c0c267606 | ||
|
|
00c7e45672 | ||
|
|
b28d43c7f8 | ||
|
|
c0f15c9dd9 | ||
|
|
64240c9f6f | ||
|
|
cbbcdd0e40 | ||
|
|
dc7a6805a7 | ||
|
|
0321dbed62 | ||
|
|
88a46a8f23 | ||
|
|
5c26f04436 | ||
|
|
e80fa25fb3 | ||
|
|
12f11183cf | ||
|
|
abfefa8c3e | ||
|
|
eb82eee286 | ||
|
|
5ff9b3b788 | ||
|
|
f4d73b649f | ||
|
|
4136c0d3b5 | ||
|
|
2b8cf76053 | ||
|
|
e23c60cb38 | ||
|
|
676804b770 | ||
|
|
2785742934 | ||
|
|
f82638763f | ||
|
|
7253ba1e17 | ||
|
|
e2847a68ea | ||
|
|
687e231e65 | ||
|
|
0629614ed8 | ||
|
|
843e04f154 | ||
|
|
d7c3423a3f | ||
|
|
8be238d4cc | ||
|
|
ecbaa35802 | ||
|
|
732e14cb2d | ||
|
|
e98ef1b94f | ||
|
|
e254de4df3 | ||
|
|
1d169ee70a | ||
|
|
0380cab715 | ||
|
|
4391c06f00 | ||
|
|
36a3044b16 | ||
|
|
db08330cb9 | ||
|
|
0a071ee071 | ||
|
|
2354b696a5 | ||
|
|
f6db0f2fe8 | ||
|
|
e40c219e78 | ||
|
|
2e7ca4d960 | ||
|
|
c3fd874559 | ||
|
|
80768b4d15 | ||
|
|
ef3aa1faf2 | ||
|
|
7444567d64 | ||
|
|
a559dab709 | ||
|
|
34feb51d7b | ||
|
|
a206a1a9b3 | ||
|
|
9a18de83f9 | ||
|
|
d1723e7ad6 | ||
|
|
e1adc24ccd | ||
|
|
1668f9581b | ||
|
|
58b0b97e3a | ||
|
|
90479e5fbf | ||
|
|
7461ace443 | ||
|
|
7490395f99 | ||
|
|
a16195d47a | ||
|
|
c4084faf89 | ||
|
|
09a9958272 | ||
|
|
e4325f67bd | ||
|
|
91145e3a88 | ||
|
|
f4920de7d2 | ||
|
|
d2f96b4657 | ||
|
|
21d252f1c7 | ||
|
|
06f63110b9 | ||
|
|
9bbd67f7a3 | ||
|
|
fb465ec17e | ||
|
|
76f06a6481 | ||
|
|
929c022bf8 | ||
|
|
d32aba88fc | ||
|
|
e9d482f99c | ||
|
|
496fd6bb15 | ||
|
|
016d8da641 | ||
|
|
37d12ac65f | ||
|
|
1b42b51e6e | ||
|
|
36230033ef | ||
|
|
dd1e143367 | ||
|
|
d44ee6738f | ||
|
|
1e8c6a2c79 | ||
|
|
3833720a22 | ||
|
|
90ae26903e | ||
|
|
b68806e6ed | ||
|
|
3a030a163d | ||
|
|
cd726716f5 | ||
|
|
627cd52b5e | ||
|
|
01afe1607c | ||
|
|
fd497422f8 | ||
|
|
fdcc445086 | ||
|
|
36d7b3f7f3 | ||
|
|
b8d8e7df87 | ||
|
|
7c2f2344b8 | ||
|
|
33691621d8 | ||
|
|
93ed56dd38 | ||
|
|
54f47b8607 | ||
|
|
db2649be6f | ||
|
|
17a07de1f0 | ||
|
|
3aa6892bff | ||
|
|
92405c3561 | ||
|
|
cfc2658078 | ||
|
|
209feebb5d | ||
|
|
0fb7a94eb1 | ||
|
|
e80c0aaf85 | ||
|
|
8dda45da48 | ||
|
|
e5a85500b3 | ||
|
|
a4b8844c77 | ||
|
|
7696e5bcd8 | ||
|
|
486561789b | ||
|
|
28fc185a2a | ||
|
|
db59d0e138 | ||
|
|
1687f0a0c6 | ||
|
|
6ac6ad687e | ||
|
|
5e1e71b559 | ||
|
|
66b5e9a65d | ||
|
|
30b1a31bb3 | ||
|
|
d6765bd7d6 | ||
|
|
5ce3337977 | ||
|
|
c5e24c7409 | ||
|
|
34f029da9e | ||
|
|
e95414806c | ||
|
|
456b420321 | ||
|
|
0eb87ea44a | ||
|
|
454fabeb1d | ||
|
|
d3f022adc5 | ||
|
|
8efe3d2aa3 | ||
|
|
d3003d5797 | ||
|
|
0dd5f32f96 | ||
|
|
85b307b306 | ||
|
|
73eadbd1b1 | ||
|
|
b294c560e4 | ||
|
|
455d9dc532 | ||
|
|
df1049a436 | ||
|
|
ac22df8ced | ||
|
|
1fd987221d | ||
|
|
45985f2c44 | ||
|
|
159e11a508 | ||
|
|
5ff68533d7 | ||
|
|
61f20173a3 | ||
|
|
987f1ec42b | ||
|
|
3f4c39600b | ||
|
|
88ef490b1b | ||
|
|
dbc8b684c2 | ||
|
|
7e135d4643 | ||
|
|
06a8760701 | ||
|
|
5eaa035f1d | ||
|
|
ae1f0ceeb7 | ||
|
|
3ca82286b4 | ||
|
|
63add43c0f | ||
|
|
72440609b4 | ||
|
|
c946cd2405 | ||
|
|
96d316ebf5 | ||
|
|
b3eb7dceac | ||
|
|
ee02d002ba | ||
|
|
34448d27d0 | ||
|
|
39ce64edce | ||
|
|
0d210a607b | ||
|
|
f1a6302928 | ||
|
|
359db8f365 | ||
|
|
387a3e0ef2 | ||
|
|
4c957b347b | ||
|
|
10d3d5aeed | ||
|
|
6e365d4a23 | ||
|
|
68292a45d0 | ||
|
|
f0088cf7a0 | ||
|
|
b8469a242e | ||
|
|
8c5d5f1ee9 | ||
|
|
80759cacee | ||
|
|
4df2cc78ff | ||
|
|
8b34f7a5f9 | ||
|
|
e81d6eb877 | ||
|
|
0c47d7c21c | ||
|
|
175996235d | ||
|
|
42409d555e | ||
|
|
23b1a02d42 | ||
|
|
1be53368c6 | ||
|
|
52e0d28a4d | ||
|
|
83114a26f9 | ||
|
|
bdc09f86e6 | ||
|
|
6dd9679267 | ||
|
|
5fb7168d17 | ||
|
|
6ac4b6fb3d | ||
|
|
1a8de4344c | ||
|
|
1493725699 | ||
|
|
6cff94e2ca | ||
|
|
d51a70e259 | ||
|
|
7f84c0b9f0 | ||
|
|
df555ca93f | ||
|
|
b68b98b62c | ||
|
|
640dc741a4 | ||
|
|
5d02b839ee | ||
|
|
ae60a38445 | ||
|
|
83d4bd26c4 | ||
|
|
481b351276 | ||
|
|
23cb8c8ee5 | ||
|
|
9bcdd81772 | ||
|
|
53e443142d | ||
|
|
da72107eb9 | ||
|
|
d2cbb89eb9 | ||
|
|
3bf0c60378 | ||
|
|
59a9854ed5 | ||
|
|
b70bc09359 | ||
|
|
8ef82d01b1 | ||
|
|
ac683175a8 | ||
|
|
5e86967627 | ||
|
|
d971bb993f | ||
|
|
2328f2deff | ||
|
|
ddeb65abef | ||
|
|
0871d8ecbd | ||
|
|
3b8aec2fef | ||
|
|
71dc06728f | ||
|
|
9484ecb456 | ||
|
|
25319d1c8f | ||
|
|
e7fd54b443 | ||
|
|
aed2bc154b | ||
|
|
09a991669c | ||
|
|
41325bfa12 | ||
|
|
17a1d3718f | ||
|
|
8b9c5b8789 | ||
|
|
e11609c335 | ||
|
|
3a5b3c2d65 | ||
|
|
33916312b5 | ||
|
|
dab8a2679f | ||
|
|
390334c820 | ||
|
|
356ffc953e | ||
|
|
a2f3a5de61 | ||
|
|
635a3fbcbc | ||
|
|
7ebcb86924 | ||
|
|
eb1781e6af | ||
|
|
6df983642b | ||
|
|
634c4c9a97 | ||
|
|
9371c7d2ab | ||
|
|
f6678739ad | ||
|
|
2882dd25c6 | ||
|
|
ed3166d08f | ||
|
|
39340108f2 | ||
|
|
94f037b6af | ||
|
|
090e81a432 | ||
|
|
6e8ae9b320 | ||
|
|
0db094655e | ||
|
|
c437ed4e96 | ||
|
|
ed5a778e50 | ||
|
|
13633ce11a | ||
|
|
eab47657e6 | ||
|
|
35267e8b6d | ||
|
|
15cc9cef4a | ||
|
|
f5e1e7267f | ||
|
|
c6eaa18a1f | ||
|
|
63e16e4332 | ||
|
|
074e41de59 | ||
|
|
46b6686f4b | ||
|
|
ede94708d5 | ||
|
|
f20953c8d2 | ||
|
|
1e728593ea | ||
|
|
21f7fcc6d2 | ||
|
|
20c026aa56 | ||
|
|
24eb824163 | ||
|
|
2065afe852 | ||
|
|
d60480d1ae | ||
|
|
1be0f9090e | ||
|
|
f9dad0a183 | ||
|
|
597f1f0c0b | ||
|
|
c26728d24a | ||
|
|
4c1e11692a | ||
|
|
132a21e113 | ||
|
|
324fcb71a9 | ||
|
|
55111201e6 | ||
|
|
067040d92f | ||
|
|
0d857d716a | ||
|
|
ec9b0b22b0 | ||
|
|
32cf9ed185 | ||
|
|
1702e086a3 | ||
|
|
8246818a94 | ||
|
|
e1ee00a331 | ||
|
|
6e22cb5e58 | ||
|
|
f4ebb3d547 | ||
|
|
5683fbc3cb | ||
|
|
0c78f1c7e2 | ||
|
|
5051bbdded | ||
|
|
c7b86856fb | ||
|
|
afcf72e29f | ||
|
|
a83482179a | ||
|
|
1e7a198dec | ||
|
|
41646985e4 | ||
|
|
80f6c7b1fb | ||
|
|
cc9465f141 | ||
|
|
d56c472e77 | ||
|
|
68e5daae13 | ||
|
|
b259f30093 | ||
|
|
804f5df55f | ||
|
|
96ec7000fa | ||
|
|
bf6d6af2ec | ||
|
|
264aaa25f2 | ||
|
|
98eaddca5b | ||
|
|
e8881fb8be | ||
|
|
2824ede5f2 | ||
|
|
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
|
||||
|
||||
@@ -70,3 +70,4 @@ environments/
|
||||
|
||||
# Target of shared symlink
|
||||
services/**/src/shared
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ services:
|
||||
PGDATA: /data
|
||||
POSTGRES_DB: kone-db
|
||||
POSTGRES_HOST_AUTH_METHOD: trust
|
||||
image: postgres:15.8
|
||||
image: postgres:15.10
|
||||
|
||||
sis-db:
|
||||
<<: *defaults
|
||||
@@ -18,7 +18,7 @@ services:
|
||||
PGDATA: /data
|
||||
POSTGRES_DB: sis-db
|
||||
POSTGRES_HOST_AUTH_METHOD: trust
|
||||
image: postgres:15.8
|
||||
image: postgres:15.10
|
||||
shm_size: 2gb
|
||||
|
||||
sis-importer-db:
|
||||
@@ -28,7 +28,7 @@ services:
|
||||
PGDATA: /data
|
||||
POSTGRES_DB: sis-importer-db
|
||||
POSTGRES_HOST_AUTH_METHOD: trust
|
||||
image: postgres:15.8
|
||||
image: postgres:15.10
|
||||
|
||||
user-db:
|
||||
<<: *defaults
|
||||
@@ -37,4 +37,4 @@ services:
|
||||
PGDATA: /data
|
||||
POSTGRES_DB: user-db
|
||||
POSTGRES_HOST_AUTH_METHOD: trust
|
||||
image: postgres:15.8
|
||||
image: postgres:15.10
|
||||
|
||||
+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({
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/// <reference types="cypress" />
|
||||
|
||||
const openCompletedCoursesModal = () => {
|
||||
cy.get('button').contains('Search completed courses of students').click()
|
||||
cy.contains('button', 'Search completed courses of students').click()
|
||||
}
|
||||
|
||||
const hasLanded = () => {
|
||||
@@ -15,22 +15,40 @@ const openSearch = () => {
|
||||
cy.init('/completedcoursessearch')
|
||||
hasLanded()
|
||||
}
|
||||
|
||||
const selectSavedCourselist = name => {
|
||||
cy.get('[data-cy="history-search"]').children().eq(0).type(name).type('{enter}')
|
||||
cy.get('[data-cy=history-search]').click()
|
||||
cy.get('[data-cy=history-search]').type(name)
|
||||
cy.get('[data-cy=history-search]').type('{downarrow}')
|
||||
cy.get('[data-cy=history-search]').type('{enter}')
|
||||
}
|
||||
|
||||
const deleteAllSearches = () => {
|
||||
cy.contains('Saved courselists')
|
||||
.get('.dropdown')
|
||||
.then(dropdown => {
|
||||
const searchItems = dropdown.find('div[role=option] > span[class=text]')
|
||||
for (let i = 0; i < searchItems.length; i++) {
|
||||
if (searchItems[i].textContent.includes('TEST-')) {
|
||||
cy.get('[data-cy="history-search"]').children().eq(0).type(searchItems[i].textContent).type('{enter}')
|
||||
cy.get('button').contains('Delete').click()
|
||||
const createCourseList = (courseCodes, courseListName) => {
|
||||
openSearch()
|
||||
openCompletedCoursesModal()
|
||||
cy.get('[data-cy=course-list-input]').type(courseCodes.join('\n'))
|
||||
cy.get('[data-cy=search-name]').type(courseListName)
|
||||
cy.get('[data-cy=save-courselist]').click()
|
||||
}
|
||||
|
||||
const generateCourseListName = () => `TEST-course-list-${new Date().getTime()}`
|
||||
|
||||
const deleteAllPreviousSearches = () => {
|
||||
cy.get('[data-cy=history-search]').click()
|
||||
|
||||
cy.get('.MuiAutocomplete-popper li').then($options => {
|
||||
if ($options.length > 0) {
|
||||
cy.wrap($options).each(($option, index, $list) => {
|
||||
cy.wrap($option).click()
|
||||
cy.get('[data-cy=delete-courselist]').click()
|
||||
if (index < $list.length - 1) {
|
||||
cy.get('[data-cy=history-search]').click()
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
} else {
|
||||
cy.contains('You have no previous searches.')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
describe('When search modal is opened', () => {
|
||||
@@ -39,8 +57,7 @@ describe('When search modal is opened', () => {
|
||||
hasLanded()
|
||||
cy.url().should('include', '/completedcoursessearch')
|
||||
cy.contains('Search completed courses of students')
|
||||
cy.get('[data-cy="open-completed-courses-modal-button"]').click()
|
||||
deleteAllSearches()
|
||||
cy.get('[data-cy=open-completed-courses-modal-button]').click()
|
||||
})
|
||||
|
||||
it('Modal opens correctly', () => {
|
||||
@@ -49,26 +66,33 @@ describe('When search modal is opened', () => {
|
||||
openCompletedCoursesModal()
|
||||
cy.contains('Search completed courses of students')
|
||||
cy.contains('Insert one or more student numbers, separated by a space, a newline, a comma, or a semicolon.')
|
||||
cy.contains('Insert one or more courses, separated by a space, a newline, or a comma.')
|
||||
cy.contains('Insert one or more courses, separated by a space, a newline, a comma, or a semicolon.')
|
||||
cy.contains('Insert name for this course list if you wish to save it')
|
||||
})
|
||||
|
||||
it('Modal gets the correct course codes and student numbers from the URL', () => {
|
||||
cy.init('/completedcoursessearch?courseList=TKT10001&courseList=TKT10002&studentList=433237&studentList=457144')
|
||||
hasLanded()
|
||||
openCompletedCoursesModal()
|
||||
cy.contains('[data-cy=student-no-input]', '433237, 457144')
|
||||
cy.contains('[data-cy=course-list-input]', 'TKT10001, TKT10002')
|
||||
})
|
||||
|
||||
describe('When a search is executed with invalid input', () => {
|
||||
it('Returns empty table and notification', () => {
|
||||
cy.init('/completedcoursessearch')
|
||||
hasLanded()
|
||||
openCompletedCoursesModal()
|
||||
cy.get('[data-cy="student-no-input"]').type('1')
|
||||
cy.get('[data-cy="course-list-input"]').type('1')
|
||||
cy.get('[data-cy="completed-courses-search-button"]').click()
|
||||
cy.get('[data-cy="rights-notification"]').should(
|
||||
cy.get('[data-cy=student-no-input]').type('1')
|
||||
cy.get('[data-cy=course-list-input]').type('1')
|
||||
cy.get('[data-cy=completed-courses-search-button]').click()
|
||||
cy.get('[data-cy=rights-notification]').should(
|
||||
'contain.text',
|
||||
'The following students information could not be displayed'
|
||||
'The information for the following students could not be displayed'
|
||||
)
|
||||
cy.get('[data-cy="rights-notification"]').should('contain.text', '1')
|
||||
cy.get('[data-cy="completed-courses-table-div"]').should('contain.text', 'Completed courses search')
|
||||
cy.get('[data-cy="completed-courses-table-div"]').should('contain.text', 'Student number')
|
||||
cy.get('[data-cy="completed-courses-table-div"]').should('not.contain', '1')
|
||||
cy.contains('[data-cy=rights-notification]', '1')
|
||||
cy.contains('[data-cy=completed-courses-table-div]', 'Student number')
|
||||
cy.contains('[data-cy=completed-courses-table-div]', '1').should('not.exist')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -78,9 +102,9 @@ describe('When search modal is opened', () => {
|
||||
hasLanded()
|
||||
openCompletedCoursesModal()
|
||||
cy.fixture('completedCoursesData').then(({ studentSet1, coursesSet1 }) => {
|
||||
cy.get('[data-cy="student-no-input"]').type(studentSet1.join('\n'))
|
||||
cy.get('[data-cy="course-list-input"]').type(coursesSet1.join('\n'))
|
||||
cy.get('[data-cy="completed-courses-search-button"]').click()
|
||||
cy.get('[data-cy=student-no-input]').type(studentSet1.join('\n'))
|
||||
cy.get('[data-cy=course-list-input]').type(coursesSet1.join('\n'))
|
||||
cy.get('[data-cy=completed-courses-search-button]').click()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -95,63 +119,61 @@ describe('When search modal is opened', () => {
|
||||
|
||||
it('Finds correct students and courses', () => {
|
||||
cy.fixture('completedCoursesData').then(({ studentSet1, coursesSet1 }) => {
|
||||
cy.get('[data-cy="completed-courses-table-div"]').should('contain.text', studentSet1[0])
|
||||
cy.get('[data-cy="completed-courses-table-div"]').should('contain.text', studentSet1[1])
|
||||
cy.get('[data-cy="completed-courses-table-div"]').should('contain.text', studentSet1[2])
|
||||
cy.get('[data-cy="completed-courses-table-div"]').should('contain.text', studentSet1[3])
|
||||
cy.get('[data-cy="completed-courses-table-div"]').should('contain.text', studentSet1[4])
|
||||
cy.get('[data-cy="completed-courses-table-div"]').should('contain.text', coursesSet1[0])
|
||||
cy.get('[data-cy="completed-courses-table-div"]').should('contain.text', coursesSet1[1])
|
||||
cy.get('[data-cy="completed-courses-table-div"]').should('contain.text', coursesSet1[2])
|
||||
cy.get('[data-cy="completed-courses-table-div"]').should('contain.text', coursesSet1[3])
|
||||
for (const studentNumber of studentSet1) {
|
||||
cy.contains('[data-cy=completed-courses-table-div]', studentNumber)
|
||||
}
|
||||
for (const courseCode of coursesSet1) {
|
||||
cy.contains('[data-cy=completed-courses-table-div]', courseCode)
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
const courseListNames = ['TEST-courselist1', 'TEST-courselist2']
|
||||
|
||||
describe('Courselist saving-related functions work', () => {
|
||||
beforeEach(() => {
|
||||
openSearch()
|
||||
openCompletedCoursesModal()
|
||||
cy.get('[data-cy="course-list-input"]').type('CSM14204\nTKT10004')
|
||||
cy.get('[data-cy="search-name"]').type(courseListNames[0])
|
||||
cy.get('button').contains('Save').click()
|
||||
cy.get('body').then($body => {
|
||||
if ($body.find('[data-cy=history-search]').length > 0) {
|
||||
deleteAllPreviousSearches()
|
||||
} else {
|
||||
cy.contains('You have no previous searches.')
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
it('Course list can be saved', () => {
|
||||
selectSavedCourselist(courseListNames[0])
|
||||
const courseList = generateCourseListName()
|
||||
const courses = ['CSM14204', 'TKT10004']
|
||||
createCourseList(courses, courseList)
|
||||
openSearch()
|
||||
openCompletedCoursesModal()
|
||||
selectSavedCourselist(courseListNames[0])
|
||||
cy.get('[data-cy="course-list-input"]').contains('TKT10004').contains('CSM14204')
|
||||
selectSavedCourselist(courseList)
|
||||
cy.contains('[data-cy=course-list-input]', courses.join(', '))
|
||||
})
|
||||
|
||||
it('Course list can be deleted', () => {
|
||||
const courseList = generateCourseListName()
|
||||
createCourseList(['CSM14204', 'TKT10004'], courseList)
|
||||
openSearch()
|
||||
openCompletedCoursesModal()
|
||||
cy.get('[data-cy="search-name"]').type(courseListNames[1])
|
||||
cy.get('button').contains('Save').click()
|
||||
openSearch()
|
||||
openCompletedCoursesModal()
|
||||
selectSavedCourselist(courseListNames[1])
|
||||
cy.get('button').contains('Delete').click()
|
||||
cy.get('[data-cy="history-search"]')
|
||||
.type(courseListNames[1])
|
||||
.type('{enter}')
|
||||
.should('not.contain', courseListNames[1])
|
||||
selectSavedCourselist(courseList)
|
||||
cy.get('[data-cy=delete-courselist]').click()
|
||||
cy.contains('You have no previous searches.')
|
||||
})
|
||||
|
||||
it('Course list can be updated', () => {
|
||||
const courseList = generateCourseListName()
|
||||
createCourseList(['CSM14204', 'TKT10004'], courseList)
|
||||
openSearch()
|
||||
openCompletedCoursesModal()
|
||||
selectSavedCourselist(courseListNames[0])
|
||||
cy.get('[data-cy="course-list-input"]').type(',TKT10001')
|
||||
cy.get('button').contains('Save').click()
|
||||
selectSavedCourselist(courseList)
|
||||
cy.get('[data-cy=course-list-input]').type(',TKT10001')
|
||||
cy.get('[data-cy=save-courselist]').click()
|
||||
openSearch()
|
||||
openCompletedCoursesModal()
|
||||
selectSavedCourselist(courseListNames[0])
|
||||
cy.get('[data-cy="course-list-input"]').contains('TKT10001')
|
||||
selectSavedCourselist(courseList)
|
||||
cy.contains('[data-cy=course-list-input]', 'CSM14204, TKT10004, TKT10001')
|
||||
})
|
||||
})
|
||||
|
||||
+880
-310
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')
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
+236
-284
@@ -5,331 +5,283 @@ const path = require('path')
|
||||
const timestamp = new Date().toLocaleDateString('en-CA', { year: 'numeric', month: '2-digit', day: '2-digit' }) // YYYY-MM-DD, taking the local time zone into account
|
||||
const downloadsFolder = Cypress.config('downloadsFolder')
|
||||
|
||||
describe('Faculty overview', () => {
|
||||
describe('Faculty can be selected', () => {
|
||||
it('Faculties are listed and one can be chosen', () => {
|
||||
describe('Faculty statistics', () => {
|
||||
describe('Faculty list', () => {
|
||||
beforeEach(() => {
|
||||
cy.init('/faculties')
|
||||
cy.get('[data-cy=select-faculty]').contains('a', 'Teologinen tiedekunta')
|
||||
cy.contains('a', 'H99').should('not.exist')
|
||||
})
|
||||
|
||||
it('contains faculty names and faculty codes', () => {
|
||||
cy.contains('Faculties')
|
||||
cy.contains('Teologinen tiedekunta')
|
||||
cy.contains('H10')
|
||||
cy.contains('Oikeustieteellinen tiedekunta')
|
||||
cy.contains('H20')
|
||||
cy.contains('Lääketieteellinen tiedekunta')
|
||||
cy.contains('H30')
|
||||
cy.contains('Humanistinen tiedekunta')
|
||||
cy.contains('H40')
|
||||
cy.contains('Matemaattis-luonnontieteellinen tiedekunta')
|
||||
cy.contains('H50')
|
||||
cy.contains('Farmasian tiedekunta')
|
||||
cy.contains('H55')
|
||||
cy.contains('Bio- ja ympäristötieteellinen tiedekunta')
|
||||
cy.contains('H57')
|
||||
cy.contains('Kasvatustieteellinen tiedekunta')
|
||||
cy.contains('H60')
|
||||
cy.contains('Valtiotieteellinen tiedekunta')
|
||||
cy.contains('H70')
|
||||
cy.contains('Svenska social- och kommunalhögskolan')
|
||||
cy.contains('H74')
|
||||
cy.contains('Maatalous-metsätieteellinen tiedekunta')
|
||||
cy.contains('H80')
|
||||
cy.contains('Eläinlääketieteellinen tiedekunta')
|
||||
cy.contains('H90')
|
||||
})
|
||||
|
||||
it('contains a working link to faculty page', () => {
|
||||
cy.contains('Faculties')
|
||||
cy.contains('a', 'Teologinen tiedekunta').click()
|
||||
cy.contains('.header', 'Teologinen tiedekunta')
|
||||
cy.location('pathname').should('eq', '/faculties/hy-org-1000000580')
|
||||
cy.contains('Teologinen tiedekunta')
|
||||
cy.contains('H10')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Basic information: admin user', () => {
|
||||
beforeEach(() => {
|
||||
cy.init('/faculties', 'admin')
|
||||
cy.contains('a', 'Eläinlääketieteellinen tiedekunta').click()
|
||||
describe('Basic information tab', () => {
|
||||
describe('Admin user', () => {
|
||||
beforeEach(() => {
|
||||
cy.init('/faculties', 'admin')
|
||||
cy.contains('a', 'Eläinlääketieteellinen tiedekunta').click()
|
||||
})
|
||||
|
||||
it('Correct tabs are shown', () => {
|
||||
cy.get('[data-cy="FacultyTabs"]').should('contain', 'Basic information')
|
||||
cy.get('[data-cy="FacultyTabs"]').should('contain', 'Students by starting year')
|
||||
cy.get('[data-cy="FacultyTabs"]').should('contain', 'Graduation times')
|
||||
cy.get('[data-cy="FacultyTabs"]').should('contain', 'Update statistics')
|
||||
})
|
||||
})
|
||||
|
||||
it('Credits produced by faculty are shown', () => {
|
||||
cy.get('[data-cy="Section-CreditsProducedByTheFaculty"]').should('be.visible')
|
||||
cy.get('[data-cy="Graph-CreditsProducedByTheFaculty"]').should('be.visible')
|
||||
})
|
||||
describe('Basic user', () => {
|
||||
beforeEach(() => {
|
||||
cy.init('/faculties')
|
||||
cy.contains('a', 'Matemaattis-luonnontieteellinen tiedekunta').click()
|
||||
})
|
||||
|
||||
it('Update statistics tab is shown', () => {
|
||||
cy.get('[data-cy="FacultySegmentContainer"]').should('contain', 'Update statistics')
|
||||
describe('Info boxes', () => {
|
||||
it('Students of the faculty', () => {
|
||||
cy.get('[data-cy=StudentsOfTheFacultyInfoBoxButton]').click()
|
||||
cy.get('[data-cy=StudentsOfTheFacultyInfoBoxContent]').contains('Taulukon luvut on')
|
||||
})
|
||||
|
||||
it('Graduated of the faculty', () => {
|
||||
cy.get('[data-cy=GraduatedOfTheFacultyInfoBoxButton]').click()
|
||||
cy.get('[data-cy=GraduatedOfTheFacultyInfoBoxContent]').contains('Sisältää kyseisenä')
|
||||
})
|
||||
|
||||
it('Thesis writers of the faculty', () => {
|
||||
cy.get('[data-cy=ThesisWritersOfTheFacultyInfoBoxButton]').click()
|
||||
cy.get('[data-cy=ThesisWritersOfTheFacultyInfoBoxContent]').contains('Sisältää kyseisenä')
|
||||
})
|
||||
|
||||
it('Credits produced by the faculty', () => {
|
||||
cy.get('[data-cy=CreditsProducedByTheFacultyInfoBoxButton]').click()
|
||||
cy.get('[data-cy=CreditsProducedByTheFacultyInfoBoxContent]').contains('Sisältää opintopisteet')
|
||||
})
|
||||
})
|
||||
|
||||
it('All graphs and tables are shown', () => {
|
||||
cy.get('[data-cy="StudentsOfTheFacultyGraph"]').should('be.visible')
|
||||
cy.get('[data-cy="StudentsOfTheFacultyInteractiveDataTable"]').should('be.visible')
|
||||
cy.get('[data-cy="GraduatedOfTheFacultyGraph"]').should('be.visible')
|
||||
cy.get('[data-cy="GraduatedOfTheFacultyInteractiveDataTable"]').should('be.visible')
|
||||
cy.get('[data-cy="ThesisWritersOfTheFacultyGraph"]').should('be.visible')
|
||||
cy.get('[data-cy="ThesisWritersOfTheFacultyInteractiveDataTable"]').should('be.visible')
|
||||
cy.get('[data-cy="CreditsProducedByTheFacultyGraph"]').should('be.visible')
|
||||
cy.get('[data-cy="CreditsProducedByTheFacultyInteractiveDataTable"]').should('be.visible')
|
||||
})
|
||||
|
||||
it('Correct tabs are shown', () => {
|
||||
cy.get('[data-cy="FacultyTabs"]').should('contain', 'Basic information')
|
||||
cy.get('[data-cy="FacultyTabs"]').should('contain', 'Students by starting year')
|
||||
cy.get('[data-cy="FacultyTabs"]').should('contain', 'Graduation times')
|
||||
cy.get('[data-cy="FacultyTabs"]').should('not.contain', 'Update statistics')
|
||||
})
|
||||
|
||||
it('Toggle years works', () => {
|
||||
cy.get('[data-cy="CreditsProducedByTheFacultyInteractiveDataTable"]').should('contain', '2022')
|
||||
cy.get('[data-cy="ThesisWritersOfTheFacultyInteractiveDataTable"]').should('contain', '2022')
|
||||
cy.get('[data-cy="StudentsOfTheFacultyInteractiveDataTable"]').should('contain', '2022')
|
||||
cy.get('[data-cy="GraduatedOfTheFacultyInteractiveDataTable"]').should('contain', '2022')
|
||||
cy.get('[data-cy="YearToggle"]').click()
|
||||
cy.get('[data-cy="CreditsProducedByTheFacultyInteractiveDataTable"]').should('contain', '2022 - 2023')
|
||||
cy.get('[data-cy="ThesisWritersOfTheFacultyInteractiveDataTable"]').should('contain', '2022 - 2023')
|
||||
cy.get('[data-cy="StudentsOfTheFacultyInteractiveDataTable"]').should('contain', '2022 - 2023')
|
||||
cy.get('[data-cy="GraduatedOfTheFacultyInteractiveDataTable"]').should('contain', '2022 - 2023')
|
||||
})
|
||||
|
||||
it('Toggle programmes works', () => {
|
||||
cy.get('[data-cy="FacultyProgrammesShownInfo"]').should('not.exist')
|
||||
cy.get('[data-cy="ProgrammeToggle"]').click()
|
||||
cy.get('[data-cy="FacultyProgrammesShownInfo"]').should('be.visible')
|
||||
})
|
||||
|
||||
it('Toggle study rights works', () => {
|
||||
cy.get('[data-cy="FacultyExcludeSpecialsInfo"]').should('not.exist')
|
||||
cy.get('[data-cy="StudentToggle"]').click()
|
||||
cy.get('[data-cy="FacultyExcludeSpecialsInfo"]').should('be.visible')
|
||||
})
|
||||
|
||||
it.skip('Data can be exported to Excel files', { retries: 2 }, () => {
|
||||
// TODO: Fix this test
|
||||
const sections = [
|
||||
'StudentsOfTheFaculty',
|
||||
'GraduatedOfTheFaculty',
|
||||
'ThesisWritersOfTheFaculty',
|
||||
'CreditsProducedByTheFaculty',
|
||||
]
|
||||
sections.forEach(section => {
|
||||
cy.get(`[data-cy="${section}Graph"]`).within(() => {
|
||||
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))
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('Basic information: basic user', () => {
|
||||
describe('Students by starting year tab', () => {
|
||||
beforeEach(() => {
|
||||
cy.init('/faculties')
|
||||
cy.contains('a', 'Matemaattis-luonnontieteellinen tiedekunta').click()
|
||||
cy.contains('Students by starting year').click()
|
||||
})
|
||||
|
||||
it('Basic information tab show all graphs and tables', () => {
|
||||
cy.get('[data-cy="Table-CreditsProducedByTheFaculty"]').should('be.visible')
|
||||
cy.get('[data-cy="Graph-CreditsProducedByTheFaculty"]').should('be.visible')
|
||||
cy.get('[data-cy="Table-ThesisWritersOfTheFaculty"]').should('be.visible')
|
||||
cy.get('[data-cy="Graph-ThesisWritersOfTheFaculty"]').should('be.visible')
|
||||
cy.get('[data-cy="Table-StudentsOfTheFaculty"]').should('be.visible')
|
||||
cy.get('[data-cy="Graph-StudentsOfTheFaculty"]').should('be.visible')
|
||||
cy.get('[data-cy="Table-GraduatedOfTheFaculty"]').should('be.visible')
|
||||
cy.get('[data-cy="Graph-GraduatedOfTheFaculty"]').should('be.visible')
|
||||
})
|
||||
|
||||
it('Correct tabs are shown', () => {
|
||||
cy.get('[data-cy="FacultySegmentContainer"]').should('contain', 'Basic information')
|
||||
cy.get('[data-cy="FacultySegmentContainer"]').should('contain', 'Graduation times')
|
||||
cy.get('[data-cy="FacultySegmentContainer"]').should('contain', 'Programmes and student populations')
|
||||
cy.get('[data-cy="FacultySegmentContainer"]').should('not.contain', 'Update statistics')
|
||||
})
|
||||
|
||||
it('Toggle years works', () => {
|
||||
cy.get('[data-cy="Table-CreditsProducedByTheFaculty"]').should('contain', '2022')
|
||||
cy.get('[data-cy="Table-ThesisWritersOfTheFaculty"]').should('contain', '2022')
|
||||
cy.get('[data-cy="Table-StudentsOfTheFaculty"]').should('contain', '2022')
|
||||
cy.get('[data-cy="Table-GraduatedOfTheFaculty"]').should('contain', '2022')
|
||||
cy.get('[data-cy="YearToggle"]').click()
|
||||
cy.get('[data-cy="Table-CreditsProducedByTheFaculty"]').should('contain', '2022 - 2023')
|
||||
cy.get('[data-cy="Table-ThesisWritersOfTheFaculty"]').should('contain', '2022 - 2023')
|
||||
cy.get('[data-cy="Table-StudentsOfTheFaculty"]').should('contain', '2022 - 2023')
|
||||
cy.get('[data-cy="Table-GraduatedOfTheFaculty"]').should('contain', '2022 - 2023')
|
||||
})
|
||||
|
||||
it('Toggle programmes works', () => {
|
||||
cy.get('[data-cy="FacultyProgrammesShownInfo"]').should('not.exist')
|
||||
cy.get('[data-cy="ProgrammeToggle"]').click()
|
||||
cy.get('[data-cy="FacultyProgrammesShownInfo"]').should('be.visible')
|
||||
})
|
||||
|
||||
it('Students of the faculty infobox works', () => {
|
||||
cy.get('[data-cy="StudentsOfTheFaculty-info-content"]').should('not.exist')
|
||||
cy.get('[data-cy="StudentsOfTheFaculty-open-info"]').click()
|
||||
cy.get('[data-cy="StudentsOfTheFaculty-info-content"]').should('be.visible')
|
||||
cy.get('[data-cy="StudentsOfTheFaculty-close-info"]').click()
|
||||
cy.get('[data-cy="StudentsOfTheFaculty-info-content"]').should('not.exist')
|
||||
})
|
||||
|
||||
it('Data can be exported to Excel files', () => {
|
||||
const sections = [
|
||||
'StudentsOfTheFaculty',
|
||||
'GraduatedOfTheFaculty',
|
||||
'ThesisWritersOfTheFaculty',
|
||||
'CreditsProducedByTheFaculty',
|
||||
]
|
||||
sections.forEach(section => {
|
||||
cy.get(`[data-cy="Graph-${section}"]`).within(() => {
|
||||
cy.get('[aria-label="View chart menu, Chart"]').click()
|
||||
cy.contains('li.highcharts-menu-item', 'Download XLS').click()
|
||||
const downloadedFile = `oodikone_${section}_H50_${timestamp}.xls`
|
||||
cy.readFile(path.join(downloadsFolder, downloadedFile))
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('Study programme information: users with access', () => {
|
||||
beforeEach(() => {
|
||||
cy.init('/faculties')
|
||||
cy.contains('a', 'Maatalous-metsätieteellinen tiedekunta').click()
|
||||
})
|
||||
|
||||
it('Study programme credit information is not visible in the beginning', () => {
|
||||
cy.get('table[data-cy="Table-CreditsProducedByTheFaculty"]').should('be.visible')
|
||||
|
||||
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-0"]').should('not.be.visible')
|
||||
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-1"]').should('not.be.visible')
|
||||
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-2"]').should('not.be.visible')
|
||||
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-3"]').should('not.be.visible')
|
||||
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-4"]').should('not.be.visible')
|
||||
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-5"]').should('not.be.visible')
|
||||
})
|
||||
|
||||
it('Study programme credit information can be toggled', () => {
|
||||
cy.get('[data-cy="Table-CreditsProducedByTheFaculty"]').should('be.visible')
|
||||
|
||||
cy.get('[data-cy="Button-Show-CreditsProducedByTheFaculty-0"]').click()
|
||||
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-0"]').should('be.visible')
|
||||
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-1"]').should('not.be.visible')
|
||||
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-2"]').should('not.be.visible')
|
||||
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-3"]').should('not.be.visible')
|
||||
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-4"]').should('not.be.visible')
|
||||
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-5"]').should('not.be.visible')
|
||||
cy.get('[data-cy="Button-Show-CreditsProducedByTheFaculty-3"]').click()
|
||||
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-0"]').should('be.visible')
|
||||
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-1"]').should('not.be.visible')
|
||||
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-2"]').should('not.be.visible')
|
||||
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-3"]').should('be.visible')
|
||||
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-4"]').should('not.be.visible')
|
||||
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-5"]').should('not.be.visible')
|
||||
cy.get('[data-cy="Button-Hide-CreditsProducedByTheFaculty-0"]').click()
|
||||
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-0"]').should('not.be.visible')
|
||||
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-1"]').should('not.be.visible')
|
||||
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-2"]').should('not.be.visible')
|
||||
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-3"]').should('be.visible')
|
||||
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-4"]').should('not.be.visible')
|
||||
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-5"]').should('not.be.visible')
|
||||
})
|
||||
|
||||
it('Graph stays open when sorted', () => {
|
||||
cy.get('[data-cy="Table-CreditsProducedByTheFaculty"]').should('be.visible')
|
||||
cy.get('[data-cy="Button-Show-CreditsProducedByTheFaculty-0"]').click()
|
||||
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-0"]').should('be.visible')
|
||||
cy.get('[data-cy="Menu-CreditsProducedByTheFaculty-Total"]').click()
|
||||
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-0"]').should('be.visible')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Average graduation times', () => {
|
||||
beforeEach(() => {
|
||||
cy.init('/faculties')
|
||||
cy.contains('a', 'Matemaattis-luonnontieteellinen tiedekunta').click()
|
||||
cy.contains('Graduation times').click()
|
||||
})
|
||||
|
||||
it('User can view graduation graphs', () => {
|
||||
cy.get('[data-cy="Section-AverageGraduationTimes"]').should('be.visible')
|
||||
cy.get('[data-cy="Section-bachelor"]').should('be.visible')
|
||||
cy.get('[data-cy="Section-master"]').should('be.visible')
|
||||
cy.get('[data-cy="Section-master"]').within(() => {
|
||||
cy.contains('text.highcharts-axis-title', 'Graduation year')
|
||||
cy.contains('.message', "Click a bar to view that year's programme level breakdown").should('be.visible')
|
||||
})
|
||||
})
|
||||
|
||||
it('Graphs display data', () => {
|
||||
cy.get('[data-cy="Section-bachelor"]').within(() => {
|
||||
cy.get('div[class="faculty-breakdown-graph"]')
|
||||
})
|
||||
|
||||
cy.get('[data-cy="GraduationTimeToggle"]').click()
|
||||
|
||||
cy.get('[data-cy="Section-bachelor"]').within(() => {
|
||||
cy.get('div[class="faculty-graph"]')
|
||||
cy.contains('44 graduated').trigger('mouseover')
|
||||
cy.contains('44 students graduated in year 2023')
|
||||
cy.contains('median study time: 37.5 months')
|
||||
cy.contains('21 graduated on time')
|
||||
cy.contains('11 graduated max year overtime')
|
||||
cy.contains('12 graduated over year late')
|
||||
|
||||
cy.contains('44 graduated').click()
|
||||
cy.contains('Year 2023 by graduation year')
|
||||
cy.get('div[class="programmes-graph"]').should('be.visible')
|
||||
cy.get('div[class="programmes-graph"]').within(() => {
|
||||
cy.contains('MAT')
|
||||
cy.get('[aria-label="MAT, 36."]').trigger('mouseover')
|
||||
cy.contains('Matemaattisten tieteiden kandiohjelma')
|
||||
cy.contains('KH50_001')
|
||||
cy.contains('26 students graduated in year 2023')
|
||||
cy.contains('median study time: 36 months')
|
||||
cy.contains('14 graduated on time')
|
||||
cy.contains('6 graduated max year overtime')
|
||||
cy.contains('6 graduated over year late')
|
||||
})
|
||||
})
|
||||
|
||||
cy.get('[data-cy="Section-master"]').within(() => {
|
||||
cy.get('div[class="faculty-graph"]')
|
||||
cy.contains('4 graduated').trigger('mouseover')
|
||||
cy.contains('4 students graduated in year 2023')
|
||||
cy.contains('median study time: 23 months')
|
||||
cy.contains('3 graduated on time')
|
||||
cy.contains('0 graduated max year overtime')
|
||||
cy.contains('1 graduated over year late')
|
||||
|
||||
cy.contains('4 graduated').click()
|
||||
cy.contains('Year 2023 by graduation year')
|
||||
cy.get('div[class="programmes-graph"]').should('be.visible')
|
||||
cy.get('div[class="programmes-graph"]').within(() => {
|
||||
cy.contains('MAST')
|
||||
cy.get('[aria-label="MAST, 23."]').trigger('mouseover')
|
||||
cy.contains('Matematiikan ja tilastotieteen maisteriohjelma')
|
||||
cy.contains('MH50_001')
|
||||
cy.contains('3 students graduated in year 2023')
|
||||
cy.contains('median study time: 23 months')
|
||||
cy.contains('2 graduated on time')
|
||||
cy.contains('0 graduated max year overtime')
|
||||
cy.contains('1 graduated over year late')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('Graduation times grouping and time types can be toggled', () => {
|
||||
cy.get('[data-cy="GraduationTimeToggle"]').click()
|
||||
cy.get('[data-cy="GroupByToggle"]').click()
|
||||
cy.get('[data-cy="Section-master"]').should('be.visible')
|
||||
|
||||
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('From class of 2020 - 2021, 24/37 students have graduated')
|
||||
})
|
||||
|
||||
cy.get('[data-cy="GraduationTimeToggle"]').click()
|
||||
cy.get('[data-cy="Section-bachelor"]').within(() => {
|
||||
cy.get('[aria-label="2018 - 2019, 29. On time."]').click()
|
||||
cy.get('div[class="programmes-breakdown-graph"]').should('be.visible')
|
||||
cy.get('div[class="programmes-breakdown-graph"]').within(() => {
|
||||
cy.contains('Year 2018 - 2019 by start year')
|
||||
cy.contains('MAT')
|
||||
cy.get('[aria-label="MAT, 20. Max. year overtime."]').trigger('mouseover')
|
||||
cy.contains('Matemaattisten tieteiden kandiohjelma')
|
||||
cy.contains('KH50_001')
|
||||
cy.contains('Max. year overtime: 20')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('Data can be exported to an Excel file', () => {
|
||||
cy.get('[data-cy="DownloadButton-AverageGraduationTimes"]').click()
|
||||
const downloadedFile = `oodikone_H50_graduation_times_${timestamp}.xlsx`
|
||||
it('Export button', { retries: 2 }, () => {
|
||||
cy.get('[data-cy=FacultyStudentTableExportButton]').should('be.visible').and('not.be.disabled').click()
|
||||
const downloadedFile = `oodikone_H50_programme_stats_${timestamp}.xlsx`
|
||||
cy.readFile(path.join(downloadsFolder, downloadedFile))
|
||||
})
|
||||
|
||||
it('Info box', () => {
|
||||
cy.get('[data-cy=FacultyStudentTableInfoBoxButton]').click()
|
||||
cy.get('[data-cy=FacultyStudentTableInfoBoxContent]').contains('Opiskelijat, joiden')
|
||||
})
|
||||
|
||||
it.skip('Study right toggle', () => {
|
||||
// TODO: Implement
|
||||
})
|
||||
|
||||
it.skip('Graduated toggle', () => {
|
||||
// TODO: Implement
|
||||
})
|
||||
|
||||
it.skip('Hoverable country list', () => {
|
||||
// TODO: Implement
|
||||
})
|
||||
|
||||
it.skip('Expandable rows', () => {
|
||||
// TODO: Implement
|
||||
})
|
||||
|
||||
it.skip('Population links', () => {
|
||||
// TODO: Implement
|
||||
})
|
||||
|
||||
it('Percentage toggle works', () => {
|
||||
cy.get('[data-cy="FacultyStudentStatsTable"]').should('be.visible')
|
||||
cy.get('[data-cy="FacultyStudentStatsTable"]').should('not.contain', '92.3 %')
|
||||
cy.get('[data-cy="HidePercentagesToggle"]').click()
|
||||
cy.get('[data-cy="FacultyStudentStatsTable"]').should('contain', '92.3 %')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Faculty Progress and student population tab', () => {
|
||||
describe('Progress tab', () => {
|
||||
beforeEach(() => {
|
||||
cy.init('/faculties')
|
||||
cy.contains('a', 'Matemaattis-luonnontieteellinen tiedekunta').click()
|
||||
cy.contains('Programmes and student populations').click()
|
||||
cy.contains('Progress').click()
|
||||
})
|
||||
|
||||
it('User can view studentdata table without %', () => {
|
||||
cy.get('[data-cy="FacultyStudentStatsTable"]').should('be.visible')
|
||||
cy.get('[data-cy="FacultyStudentStatsTable"]').should('not.contain', '0 %')
|
||||
it.skip('Export button', { retries: 2 }, () => {
|
||||
// TODO: Fix this test
|
||||
cy.get('[data-cy=FacultyProgressExportButton]').should('be.visible').and('not.be.disabled').click()
|
||||
const downloadedFile = `oodikone_H50_progress_tab_${timestamp}.xlsx`
|
||||
cy.readFile(path.join(downloadsFolder, downloadedFile))
|
||||
})
|
||||
|
||||
it('User can view studentdata table with %', () => {
|
||||
cy.get('[data-cy="FacultyStudentStatsTable"]').should('be.visible')
|
||||
cy.get('[data-cy="HidePercentagesToggle"]').click()
|
||||
cy.get('[data-cy="FacultyStudentStatsTable"]').should('contain', '0 %')
|
||||
it.skip('Info boxes', () => {
|
||||
cy.get('[data-cy=FacultyProgressInfoBoxButton]').click()
|
||||
cy.get('[data-cy=FacultyProgressInfoBoxContent]').contains('Kuvaa tiedekuntaan')
|
||||
// TODO: Click open the accordion first
|
||||
cy.get('[data-cy=BachelorMastersProgressInfoBoxButton]').click()
|
||||
cy.get('[data-cy=BachelorMastersProgressInfoBoxContent]').contains('The starting year is the')
|
||||
})
|
||||
|
||||
it('Percentage Graphs exists', () => {
|
||||
cy.get('[data-cy="Graph-FacultyBachelorsProgress"]').should('be.visible')
|
||||
cy.get('[data-cy="Graph-FacultyBachelorMastersProgress"]').should('be.visible')
|
||||
cy.get('[data-cy="Graph-FacultyMastersProgress"]').should('be.visible')
|
||||
cy.get('[data-cy="Graph-FacultyBachelorsProgress"]').should('be.visible')
|
||||
it.skip('Progress bar charts exist', () => {
|
||||
// TODO: Click open the accordion first
|
||||
cy.get('[data-cy="FacultyBachelorsProgressBarChart"]').should('be.visible')
|
||||
cy.get('[data-cy="FacultyBachelorMastersProgressBarChart"]').should('be.visible')
|
||||
cy.get('[data-cy="FacultyMastersProgressBarChart"]').should('be.visible')
|
||||
cy.get('[data-cy="FacultyBachelorsProgressBarChart"]').should('be.visible')
|
||||
})
|
||||
|
||||
it('Graduations can be excluded', () => {
|
||||
cy.get('[data-cy="FacultyStudentStatsTable"]').should('be.visible')
|
||||
cy.get('[data-cy="StudentToggle"]').click()
|
||||
cy.get('[data-cy="FacultyStudentStatsTable"]').should('be.visible')
|
||||
})
|
||||
|
||||
it('Transfers can be excluded', () => {
|
||||
cy.get('[data-cy="FacultyStudentStatsTable"]').should('be.visible')
|
||||
cy.get('[data-cy="GraduatedToggle"]').click()
|
||||
cy.get('[data-cy="FacultyStudentStatsTable"]').should('be.visible')
|
||||
})
|
||||
|
||||
it('Progress tables exists', () => {
|
||||
it.skip('Progress tables exist', () => {
|
||||
// TODO: Click open the accordion first
|
||||
cy.get('[data-cy="FacultyBachelorsProgressTable"]').should('be.visible')
|
||||
cy.get('[data-cy="FacultyBachelorMasterProgressTable"]').should('be.visible')
|
||||
cy.get('[data-cy="FacultyMastersProgressTable"]').should('be.visible')
|
||||
cy.get('[data-cy="FacultyBachelorsProgressTable"]').should('be.visible')
|
||||
})
|
||||
|
||||
it('Progress tables can be toggled', () => {
|
||||
cy.get('[data-cy="Cell-FacultyBachelorsProgressTable-5"]').should('not.be.visible')
|
||||
cy.get('[data-cy="Button-Show-FacultyBachelorsProgressTable-5"]').click()
|
||||
cy.get('[data-cy="Cell-FacultyBachelorsProgressTable-0"]').should('not.be.visible')
|
||||
cy.get('[data-cy="Cell-FacultyBachelorsProgressTable-1"]').should('not.be.visible')
|
||||
cy.get('[data-cy="Cell-FacultyBachelorsProgressTable-2"]').should('not.be.visible')
|
||||
cy.get('[data-cy="Cell-FacultyBachelorsProgressTable-3"]').should('not.be.visible')
|
||||
cy.get('[data-cy="Cell-FacultyBachelorsProgressTable-5"]').should('be.visible')
|
||||
cy.get('[data-cy="Button-Hide-FacultyBachelorsProgressTable-5"]').click()
|
||||
cy.get('[data-cy="Cell-FacultyBachelorsProgressTable-5"]').should('not.be.visible')
|
||||
it.skip('Graduated toggle', () => {
|
||||
// TODO: Implement
|
||||
})
|
||||
|
||||
it('Studentstats table can be toggled', () => {
|
||||
cy.get('[data-cy="FacultyStudentStatsTable"]').should('not.contain', 'TKT')
|
||||
cy.get('[data-cy="Button-FacultyStudentStatsTable-6"]').click()
|
||||
cy.get('[data-cy="FacultyStudentStatsTable"]').should('contain', 'TKT')
|
||||
cy.get('[data-cy="Button-FacultyStudentStatsTable-6"]').click()
|
||||
cy.get('[data-cy="FacultyStudentStatsTable"]').should('not.contain', 'TKT')
|
||||
it.skip('Study right toggle', () => {
|
||||
// TODO: Implement
|
||||
})
|
||||
})
|
||||
|
||||
describe('Graduation times tab', () => {
|
||||
beforeEach(() => {
|
||||
cy.init('/faculties')
|
||||
cy.contains('a', 'Matemaattis-luonnontieteellinen tiedekunta').click()
|
||||
cy.contains('Graduation times').click()
|
||||
})
|
||||
|
||||
it('Progress infobox can be toggled', () => {
|
||||
cy.get('[data-cy="InfoFacultyProgress-info-content"]').should('not.exist')
|
||||
cy.get('[data-cy="InfoFacultyProgress-open-info"]').click()
|
||||
cy.get('[data-cy="InfoFacultyProgress-info-content"]').should('be.visible')
|
||||
cy.get('[data-cy="InfoFacultyProgress-close-info"]').click()
|
||||
cy.get('[data-cy="InfoFacultyProgress-info-content"]').should('not.exist')
|
||||
it.skip('Export button', { retries: 2 }, () => {
|
||||
// TODO: Fix this test
|
||||
cy.get('[data-cy=AverageGraduationTimesExportButton]').should('be.visible').and('not.be.disabled').click()
|
||||
const downloadedFile = `oodikone_H50_graduation_times_${timestamp}.xlsx`
|
||||
cy.readFile(path.join(downloadsFolder, downloadedFile))
|
||||
})
|
||||
|
||||
it('Info boxes', () => {
|
||||
cy.get('[data-cy=AverageGraduationTimesInfoBoxButton]').click()
|
||||
cy.get('[data-cy=AverageGraduationTimesInfoBoxContent]').contains('Opiskelijoiden keskimääräiset')
|
||||
})
|
||||
|
||||
it.skip('Study times toggle', () => {
|
||||
// TODO: Implement
|
||||
})
|
||||
|
||||
it.skip('Year toggle', () => {
|
||||
// TODO: Implement
|
||||
})
|
||||
|
||||
it.skip('Study programme toggle', () => {
|
||||
// TODO: Implement
|
||||
})
|
||||
|
||||
it('Graphs are visible', () => {
|
||||
cy.get('[data-cy="bachelorGraduationTimes"]').should('be.visible')
|
||||
// ! No test data for bachelor + master
|
||||
cy.get('[data-cy="masterGraduationTimes"]').should('be.visible')
|
||||
cy.get('[data-cy="doctorGraduationTimes"]').should('be.visible')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
+107
-51
@@ -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)
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -45,23 +48,23 @@ const testRangeFilter = (parentEl, min, max, expected) => {
|
||||
// functions don't take any parameters and using global object for matching test step name seemed overcomplicated.
|
||||
const createRunTestStepWithPreAndPostPartsFunction = amountWithoutFiltering => {
|
||||
return (filterName, testStepFunctionToRun) => {
|
||||
const card = cy.cs(`${filterName}-filter-card`)
|
||||
cy.cs(`${filterName}-filter-card`)
|
||||
.invoke('attr', 'data-open')
|
||||
.then(open => {
|
||||
const getHeader = () => cy.cs(`${filterName}-header`)
|
||||
|
||||
card.invoke('attr', 'data-open').then(open => {
|
||||
const getHeader = () => cy.cs(`${filterName}-header`)
|
||||
if (open === 'false') {
|
||||
getHeader().click({ force: true })
|
||||
}
|
||||
|
||||
if (open === 'false') {
|
||||
getHeader().click({ force: true })
|
||||
}
|
||||
checkFilteringResult(amountWithoutFiltering)
|
||||
testStepFunctionToRun()
|
||||
checkFilteringResult(amountWithoutFiltering)
|
||||
|
||||
checkFilteringResult(amountWithoutFiltering)
|
||||
testStepFunctionToRun()
|
||||
checkFilteringResult(amountWithoutFiltering)
|
||||
|
||||
if (open === 'false') {
|
||||
getHeader().click({ force: true })
|
||||
}
|
||||
})
|
||||
if (open === 'false') {
|
||||
getHeader().click({ force: true })
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,34 +79,32 @@ describe("Population statistics with a master's programme", () => {
|
||||
|
||||
it('Study track filter works', () => {
|
||||
runTestStepWithPreAndPostParts('StudyTrack', () => {
|
||||
const card = cy.cs('StudyTrack-filter-card')
|
||||
const programmeDropdown = card.cs('StudyTrack-filter-dropdown').selectFromDropdown(0)
|
||||
cy.cs('StudyTrack-filter-card').cs('StudyTrack-filter-dropdown').selectFromDropdown(0)
|
||||
checkFilteringResult(15)
|
||||
programmeDropdown.get('i.delete').click()
|
||||
cy.cs('StudyTrack-filter-card').cs('StudyTrack-filter-dropdown').get('i.delete').click()
|
||||
})
|
||||
})
|
||||
|
||||
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')
|
||||
card.get('[data-cy="all"] input').should('be.checked')
|
||||
runTestStepWithPreAndPostParts('studyRightTypeFilter', () => {
|
||||
cy.cs('studyRightTypeFilter-filter-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)
|
||||
})
|
||||
})
|
||||
@@ -124,8 +125,7 @@ describe('Population Statistics', () => {
|
||||
cy.contains('By default only students who have not transferred to this study programme are shown.')
|
||||
|
||||
runTestStepWithPreAndPostParts('TransferredToProgramme', () => {
|
||||
const card = cy.cs('TransferredToProgramme-filter-card')
|
||||
card.get('[data-cy="option-havenot"] input').should('be.checked')
|
||||
cy.cs('TransferredToProgramme-filter-card').get('[data-cy="option-havenot"] input').should('be.checked')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -169,6 +169,7 @@ describe('Population Statistics', () => {
|
||||
})
|
||||
|
||||
it('Age filter works', { retries: 3 }, () => {
|
||||
cy.clock(MOCKED_DATE, ['Date'])
|
||||
runTestStepWithPreAndPostParts('Age', () => {
|
||||
testRangeFilter('Age-filter-card', 23, 30, 21)
|
||||
})
|
||||
@@ -215,19 +216,24 @@ describe('Population Statistics', () => {
|
||||
{ code: 'MAT21012', name: 'Differentiaaliyhtälöt I' },
|
||||
{ code: 'MFK-M204', name: 'Matematiikkaa kaikkialla' },
|
||||
]
|
||||
cy.cs('courseFilter-course-dropdown').click().contains(`${courses[0].code} - ${courses[0].name}`).click()
|
||||
cy.cs('courseFilter-course-dropdown').click()
|
||||
cy.contains(`${courses[0].code} - ${courses[0].name}`).click()
|
||||
checkFilteringResult(10)
|
||||
cy.cs(`courseFilter-${courses[0].code}-dropdown`).selectFromDropdown(1) // Passed students
|
||||
checkFilteringResult(8)
|
||||
cy.cs('courseFilter-course-dropdown').click().contains(`${courses[1].code} - ${courses[1].name}`).click()
|
||||
cy.cs('courseFilter-course-dropdown').click()
|
||||
cy.contains(`${courses[1].code} - ${courses[1].name}`).click()
|
||||
checkFilteringResult(6)
|
||||
cy.cs(`courseFilter-${courses[1].code}-dropdown`).selectFromDropdown(1)
|
||||
checkFilteringResult(5)
|
||||
courses.forEach(({ code }) => cy.cs(`courseFilter-${code}-clear`).click())
|
||||
courses.forEach(({ code }) => {
|
||||
cy.cs(`courseFilter-${code}-clear`).click()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('Filter combinations work', () => {
|
||||
cy.clock(MOCKED_DATE, ['Date'])
|
||||
runTestStepWithPreAndPostParts('GraduatedFromProgramme', () => {
|
||||
runTestStepWithPreAndPostParts('Age', () => {
|
||||
const getCard = () => cy.cs('GraduatedFromProgramme-filter-card')
|
||||
@@ -249,8 +255,52 @@ 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')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Population Statistics with Bachelor + Master', () => {
|
||||
const pathToMathBSc2017 =
|
||||
'/populations?months=91&semesters=FALL&semesters=SPRING&showBachelorAndMaster=true&studyRights=%7B%22programme%22%3A%22KH50_001%22%2C%22combinedProgramme%22%3A%22%22%7D&year=2017'
|
||||
const defaultAmountOfStudents = 47
|
||||
const runTestStepWithPreAndPostParts = createRunTestStepWithPreAndPostPartsFunction(defaultAmountOfStudents)
|
||||
|
||||
beforeEach(() => {
|
||||
cy.init(pathToMathBSc2017)
|
||||
})
|
||||
|
||||
it('Graduation filter works', () => {
|
||||
runTestStepWithPreAndPostParts('GraduatedFromProgramme', () => {
|
||||
const getCard = () => cy.cs('GraduatedFromProgramme-filter-card')
|
||||
|
||||
getCard().cs('option-graduated-bachelor').click()
|
||||
checkFilteringResult(42)
|
||||
getCard().cs('option-graduated-master').click()
|
||||
checkFilteringResult(18)
|
||||
getCard().cs('option-not-graduated-bachelor').click()
|
||||
checkFilteringResult(5)
|
||||
getCard().cs('option-not-graduated-master').click()
|
||||
checkFilteringResult(29)
|
||||
getCard().cs('option-all').click()
|
||||
})
|
||||
})
|
||||
|
||||
it('Study right status filter works', () => {
|
||||
runTestStepWithPreAndPostParts('studyRightStatusFilter', () => {
|
||||
const getCard = () => cy.cs('studyRightStatusFilter-filter-card')
|
||||
|
||||
// First two are 0 because there are no semester enrollments for the current semester
|
||||
getCard().cs('option-active').click()
|
||||
checkFilteringResult(0)
|
||||
getCard().cs('option-active-combined').click()
|
||||
checkFilteringResult(0)
|
||||
getCard().cs('option-inactive').click()
|
||||
checkFilteringResult(5)
|
||||
getCard().cs('option-inactive-combined').click()
|
||||
checkFilteringResult(24)
|
||||
getCard().cs('option-activity-status-all').click()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -283,14 +333,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)
|
||||
programmeDropdown.get('i.delete').click()
|
||||
cy.cs('Programme-filter-card').cs('Programme-filter-dropdown').selectFromDropdown(1)
|
||||
checkFilteringResult(33)
|
||||
cy.cs('Programme-filter-card').cs('Programme-filter-dropdown').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 +372,7 @@ describe('Course Statistics', () => {
|
||||
})
|
||||
|
||||
it('Filter combinations work', () => {
|
||||
cy.clock(MOCKED_DATE, ['Date'])
|
||||
runTestStepWithPreAndPostParts('Grade', () => {
|
||||
runTestStepWithPreAndPostParts('Age', () => {
|
||||
cy.cs('gradeFilter-5').click()
|
||||
@@ -342,12 +393,14 @@ describe('Custom Population Statistics', () => {
|
||||
cy.fixture('customPopulations').then(({ studentNumbersForCSStudentsSet1, studentNumbersForCSStudentsSet2 }) => {
|
||||
const students = [...studentNumbersForCSStudentsSet1, ...studentNumbersForCSStudentsSet2]
|
||||
runTestStepWithPreAndPostParts = createRunTestStepWithPreAndPostPartsFunction(students.length)
|
||||
cy.cs('student-number-input').click().type(students.join('\n'))
|
||||
cy.cs('student-number-input').click()
|
||||
cy.cs('student-number-input').type(students.join('{enter}'))
|
||||
cy.cs('search-button').click()
|
||||
})
|
||||
cy.cs('search-button').click()
|
||||
})
|
||||
|
||||
it('Age filter works', () => {
|
||||
cy.clock(MOCKED_DATE, ['Date'])
|
||||
runTestStepWithPreAndPostParts('Age', () => {
|
||||
testRangeFilter('Age-filter-card', 24, 28, 5)
|
||||
})
|
||||
@@ -361,10 +414,9 @@ describe('Custom Population Statistics', () => {
|
||||
|
||||
it('Programme filter works', () => {
|
||||
runTestStepWithPreAndPostParts('Programme', () => {
|
||||
const card = cy.cs('Programme-filter-card')
|
||||
const programmeDropdown = card.cs('Programme-filter-dropdown').selectFromDropdown(1)
|
||||
cy.cs('Programme-filter-card').cs('Programme-filter-dropdown').selectFromDropdown(1)
|
||||
checkFilteringResult(3)
|
||||
programmeDropdown.get('i.delete').click()
|
||||
cy.cs('Programme-filter-card').cs('Programme-filter-dropdown').get('i.delete').click()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -394,15 +446,19 @@ describe('Custom Population Statistics', () => {
|
||||
{ code: 'MAT11005', name: 'Integraalilaskenta' },
|
||||
{ code: 'MAT21007', name: 'Mitta ja integraali' },
|
||||
]
|
||||
cy.cs('courseFilter-course-dropdown').click().contains(`${courses[0].code} - ${courses[0].name}`).click()
|
||||
cy.cs('courseFilter-course-dropdown').click()
|
||||
cy.contains(`${courses[0].code} - ${courses[0].name}`).click()
|
||||
checkFilteringResult(7)
|
||||
cy.cs(`courseFilter-${courses[0].code}-dropdown`).selectFromDropdown(1)
|
||||
checkFilteringResult(6)
|
||||
cy.cs('courseFilter-course-dropdown').click().contains(`${courses[1].code} - ${courses[1].name}`).click()
|
||||
cy.cs('courseFilter-course-dropdown').click()
|
||||
cy.contains(`${courses[1].code} - ${courses[1].name}`).click()
|
||||
checkFilteringResult(5)
|
||||
cy.cs(`courseFilter-${courses[1].code}-dropdown`).selectFromDropdown(3)
|
||||
checkFilteringResult(2)
|
||||
courses.forEach(({ code }) => cy.cs(`courseFilter-${code}-clear`).click())
|
||||
courses.forEach(({ code }) => {
|
||||
cy.cs(`courseFilter-${code}-clear`).click()
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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, 33, 756, 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, 221], 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, 27, 496, 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,65 @@ 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()
|
||||
cy.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()
|
||||
cy.contains('2020–2023').click({ force: true })
|
||||
|
||||
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()
|
||||
cy.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 +318,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 +347,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')
|
||||
|
||||
+108
-77
@@ -1,20 +1,24 @@
|
||||
/// <reference types="cypress" />
|
||||
|
||||
const student = {
|
||||
firstnames: 'Varpu Roope',
|
||||
lastname: 'Mårtensson',
|
||||
studentnumber: '550003',
|
||||
sis_person_id: 'hy-hlo-115926826',
|
||||
firstNames: 'Varpu Roope',
|
||||
lastName: 'Mårtensson',
|
||||
studentNumber: '550003',
|
||||
sisPersonId: 'hy-hlo-115926826',
|
||||
email: 'sisutestidata134902@testisisudata.fi',
|
||||
}
|
||||
|
||||
const typeToSearch = text => {
|
||||
cy.cs('student-search').type(text)
|
||||
}
|
||||
|
||||
const typeStudentNumberAndClick = studentNumber => {
|
||||
cy.get('.prompt').type(studentNumber)
|
||||
cy.contains('td a', studentNumber).click()
|
||||
typeToSearch(studentNumber)
|
||||
cy.contains('td', studentNumber).click()
|
||||
}
|
||||
|
||||
describe('Students tests', () => {
|
||||
describe('when using basic user', () => {
|
||||
describe('When using basic user', () => {
|
||||
beforeEach(() => {
|
||||
cy.init()
|
||||
cy.contains('Students').click()
|
||||
@@ -23,92 +27,112 @@ describe('Students tests', () => {
|
||||
})
|
||||
|
||||
it('Students search form is usable', () => {
|
||||
cy.get('.prompt').type(student.lastname)
|
||||
typeToSearch(student.lastName)
|
||||
cy.contains('Student number')
|
||||
cy.contains('Last name').should('not.exist')
|
||||
cy.contains('First names').should('not.exist')
|
||||
cy.contains('Started')
|
||||
cy.contains('Credits')
|
||||
cy.contains(student.firstnames).should('not.exist')
|
||||
cy.contains('Active study rights')
|
||||
cy.contains(student.firstNames).should('not.exist')
|
||||
cy.contains(student.lastName).should('not.exist')
|
||||
|
||||
cy.cs('toggleStudentNames').click()
|
||||
cy.contains(student.firstnames)
|
||||
cy.contains(student.firstNames)
|
||||
cy.contains('Student number')
|
||||
cy.contains('Last name')
|
||||
cy.contains('First names')
|
||||
cy.contains('Started')
|
||||
cy.contains('Credits')
|
||||
cy.contains('Active study rights')
|
||||
cy.contains(student.firstNames)
|
||||
cy.contains(student.lastName)
|
||||
|
||||
cy.cs('toggleStudentNames').click()
|
||||
cy.contains(student.firstnames).should('not.exist')
|
||||
cy.contains('Student number')
|
||||
cy.contains('Last name').should('not.exist')
|
||||
cy.contains('First names').should('not.exist')
|
||||
cy.contains('Started')
|
||||
cy.contains('Credits')
|
||||
cy.contains('Active study rights')
|
||||
cy.contains(student.firstNames).should('not.exist')
|
||||
cy.contains(student.lastName).should('not.exist')
|
||||
})
|
||||
|
||||
it('Search term must be at least 4 characters long', () => {
|
||||
cy.get('.prompt').type(student.lastname.slice(0, 3))
|
||||
cy.contains('No search results or search term is not accurate enough')
|
||||
cy.get('.prompt').type(student.lastname.slice(3))
|
||||
typeToSearch(student.lastName.slice(0, 3))
|
||||
cy.contains('Search term is not accurate enough')
|
||||
typeToSearch(student.lastName.slice(3))
|
||||
cy.get('table tbody tr').should('have.length', 1)
|
||||
})
|
||||
|
||||
it('can search with studentnumber too', () => {
|
||||
cy.get('.prompt').type(student.studentnumber)
|
||||
cy.contains(student.studentnumber)
|
||||
it('Can search with student number too', () => {
|
||||
typeToSearch(student.studentNumber)
|
||||
cy.contains(student.studentNumber)
|
||||
})
|
||||
|
||||
it('Can get student specific page by clicking student', () => {
|
||||
cy.get('.prompt').type(student.studentnumber)
|
||||
cy.contains('td a', student.studentnumber).click()
|
||||
typeStudentNumberAndClick(student.studentNumber)
|
||||
cy.contains('Matemaattisten tieteiden kandiohjelma (01.08.2020–31.07.2027)')
|
||||
cy.contains(student.lastname).should('not.exist')
|
||||
cy.contains(student.firstnames).should('not.exist')
|
||||
cy.contains(student.lastName).should('not.exist')
|
||||
cy.contains(student.firstNames).should('not.exist')
|
||||
cy.contains(student.email).should('not.exist')
|
||||
|
||||
cy.cs('toggleStudentNames').click()
|
||||
cy.contains(student.lastname)
|
||||
cy.contains(student.firstnames)
|
||||
cy.contains(student.lastName)
|
||||
cy.contains(student.firstNames)
|
||||
cy.contains(student.email)
|
||||
|
||||
cy.cs('toggleStudentNames').click()
|
||||
cy.contains(student.lastname).should('not.exist')
|
||||
cy.contains(student.firstnames).should('not.exist')
|
||||
cy.contains(student.lastName).should('not.exist')
|
||||
cy.contains(student.firstNames).should('not.exist')
|
||||
cy.contains(student.email).should('not.exist')
|
||||
})
|
||||
|
||||
it("'Update student' button is not shown", () => {
|
||||
typeStudentNumberAndClick(student.studentnumber)
|
||||
cy.get('div.ui.fluid.card').within(() => {
|
||||
typeStudentNumberAndClick(student.studentNumber)
|
||||
cy.cs('student-info-card').within(() => {
|
||||
cy.contains('button', 'Update student').should('not.exist')
|
||||
})
|
||||
})
|
||||
|
||||
it('Can get back to search menu', () => {
|
||||
typeStudentNumberAndClick(student.studentnumber)
|
||||
typeStudentNumberAndClick(student.studentNumber)
|
||||
cy.go('back')
|
||||
cy.contains('Student number').should('not.exist')
|
||||
cy.contains('Credits').should('not.exist')
|
||||
})
|
||||
|
||||
it('Can jump to course', () => {
|
||||
typeStudentNumberAndClick(student.studentnumber)
|
||||
typeStudentNumberAndClick(student.studentNumber)
|
||||
cy.contains('Tilastollinen päättely I (MAT12004)')
|
||||
.parent()
|
||||
.siblings()
|
||||
.last()
|
||||
.within(() => {
|
||||
cy.get('.level').click()
|
||||
cy.get('a').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', () => {
|
||||
typeStudentNumberAndClick(student.studentnumber)
|
||||
cy.get('[data-cy=sisulink] > a')
|
||||
typeStudentNumberAndClick(student.studentNumber)
|
||||
cy.cs('sisu-link')
|
||||
.should('have.attr', 'href')
|
||||
.and('include', `https://sisu.helsinki.fi/tutor/role/staff/student/${student.sis_person_id}/basic/basic-info`)
|
||||
.and('include', `https://sisu.helsinki.fi/tutor/role/staff/student/${student.sisPersonId}/basic/basic-info`)
|
||||
})
|
||||
|
||||
it('Semester enrollments can be toggled', () => {
|
||||
typeStudentNumberAndClick(student.studentnumber)
|
||||
typeStudentNumberAndClick(student.studentNumber)
|
||||
const programmes = [
|
||||
'Oikeustieteen tohtoriohjelma',
|
||||
'Matemaattisten tieteiden kandiohjelma',
|
||||
'Oikeustieteen maisterin koulutusohjelmaOikeusnotaarin koulutusohjelma',
|
||||
]
|
||||
cy.get('div.ui.fluid.card').within(() => {
|
||||
cy.cs('student-info-card').within(() => {
|
||||
cy.contains('Enrollments').click()
|
||||
cy.get('table tbody tr')
|
||||
.should('have.length', 3)
|
||||
@@ -120,52 +144,57 @@ describe('Students tests', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('Searching with bad inputs doesnt yield results', () => {
|
||||
cy.get('.prompt').type('SWAG LITTINEN')
|
||||
it("Searching with bad name doesn't yield results", () => {
|
||||
typeToSearch('SWAG LITTINEN')
|
||||
cy.contains('Student number').should('not.exist')
|
||||
})
|
||||
|
||||
cy.get('.prompt').clear().type('01114')
|
||||
it("Searching with bad student number doesn't yield results", () => {
|
||||
typeToSearch('01114')
|
||||
cy.contains('Student number').should('not.exist')
|
||||
})
|
||||
|
||||
it('Can jump to population page', () => {
|
||||
typeStudentNumberAndClick(student.studentnumber)
|
||||
cy.contains('.ui.table', 'Completed').within(() => {
|
||||
cy.get('i.level.up.alternate.icon').click()
|
||||
typeStudentNumberAndClick(student.studentNumber)
|
||||
cy.cs('study-rights-section').within(() => {
|
||||
cy.contains('Matemaattisten tieteiden kandiohjelma')
|
||||
.parent()
|
||||
.within(() => {
|
||||
cy.get('a').click()
|
||||
})
|
||||
})
|
||||
cy.contains('Matemaattisten tieteiden kandiohjelma 2020 - 2021')
|
||||
cy.contains('class size 30 students')
|
||||
})
|
||||
|
||||
it('Grade graph works in all three different modes', () => {
|
||||
typeStudentNumberAndClick(student.studentnumber)
|
||||
cy.contains('a.item', 'Grade graph').click()
|
||||
cy.contains('text.highcharts-title', 'Grade plot')
|
||||
cy.contains('a.item', 'Show group mean').click()
|
||||
cy.get('.labeled.input').within(() => {
|
||||
cy.contains('.label', 'Group size')
|
||||
typeStudentNumberAndClick(student.studentNumber)
|
||||
cy.contains('button', 'Grade graph').click()
|
||||
cy.contains('button', 'Show group mean').click()
|
||||
cy.cs('group-size-input').within(() => {
|
||||
cy.contains('label', 'Group size')
|
||||
cy.get('input').should('have.value', '5')
|
||||
})
|
||||
cy.contains('.highcharts-container text', "Nov '22")
|
||||
cy.contains('.highcharts-container text', "Jul '22").should('not.exist')
|
||||
cy.contains('text.highcharts-title', 'Grade plot')
|
||||
cy.get('.labeled.input').within(() => {
|
||||
cy.contains('.label', 'Group size')
|
||||
cy.get('input').clear().type('10')
|
||||
cy.cs('group-size-input').within(() => {
|
||||
cy.contains('label', 'Group size')
|
||||
cy.get('input').clear()
|
||||
cy.get('input').type('10')
|
||||
cy.get('input').should('have.value', '10')
|
||||
})
|
||||
cy.contains('.highcharts-container text', "Jul '22")
|
||||
cy.contains('.highcharts-container text', "Nov '22").should('not.exist')
|
||||
cy.contains('a.item', 'Show semester mean').click()
|
||||
cy.contains('button', 'Show semester mean').click()
|
||||
cy.contains('.highcharts-container text', "Jan '22")
|
||||
})
|
||||
|
||||
describe('Bachelor Honours section', () => {
|
||||
it("Shows 'Qualified for Honours' tag and main modules info when the student is qualified", () => {
|
||||
cy.visit('/students/495976')
|
||||
cy.contains('.divider h4', 'Bachelor Honours')
|
||||
cy.contains('.green.tag.label', 'Qualified for Honours')
|
||||
cy.contains('Main courses and other modules').click()
|
||||
cy.contains('h2', 'Bachelor Honours')
|
||||
cy.contains('[data-cy=honours-chip-qualified]', 'Qualified for Honours')
|
||||
cy.contains('Study modules').click()
|
||||
cy.get('[data-cy=main-modules] tbody tr')
|
||||
.should('have.length', 3)
|
||||
.each(($tr, index) => {
|
||||
@@ -184,54 +213,54 @@ describe('Students tests', () => {
|
||||
|
||||
it("Shows 'Did not graduate in time' when the student has graduated but not in time", () => {
|
||||
cy.visit('/students/540355')
|
||||
cy.contains('.divider h4', 'Bachelor Honours')
|
||||
cy.contains('.red.tag.label', 'Not qualified for Honours')
|
||||
cy.contains('.red.tag.label', 'Did not graduate in time')
|
||||
cy.contains('h2', 'Bachelor Honours')
|
||||
cy.contains('[data-cy=honours-chip-not-qualified]', 'Not qualified for Honours')
|
||||
cy.contains('[data-cy=honours-chip-error]', 'Did not graduate in time')
|
||||
})
|
||||
|
||||
it("Shows 'Module grades too low' when the student has graduated in time but has too low grades", () => {
|
||||
cy.visit('/students/547934')
|
||||
cy.contains('.divider h4', 'Bachelor Honours')
|
||||
cy.contains('.red.tag.label', 'Not qualified for Honours')
|
||||
cy.contains('.red.tag.label', 'Module grades too low')
|
||||
cy.contains('h2', 'Bachelor Honours')
|
||||
cy.contains('[data-cy=honours-chip-not-qualified]', 'Not qualified for Honours')
|
||||
cy.contains('[data-cy=honours-chip-error]', 'Module grades too low')
|
||||
})
|
||||
|
||||
it("Shows 'Might need further inspection' when the student has graduated in time but has more than four main modules", () => {
|
||||
cy.visit('/students/478837')
|
||||
cy.contains('.divider h4', 'Bachelor Honours')
|
||||
cy.contains('.blue.tag.label', 'Might need further inspection')
|
||||
cy.contains('h2', 'Bachelor Honours')
|
||||
cy.contains('[data-cy=honours-chip-inspection]', 'Might need further inspection')
|
||||
})
|
||||
|
||||
it("Shows 'Has not graduated' when the student has not graduated", () => {
|
||||
cy.visit(`students/${student.studentnumber}`)
|
||||
cy.contains('.divider h4', 'Bachelor Honours')
|
||||
cy.contains('.red.tag.label', 'Not qualified for Honours')
|
||||
cy.contains('.red.tag.label', 'Has not graduated')
|
||||
cy.visit(`students/${student.studentNumber}`)
|
||||
cy.contains('h2', 'Bachelor Honours')
|
||||
cy.contains('[data-cy=honours-chip-not-qualified]', 'Not qualified for Honours')
|
||||
cy.contains('[data-cy=honours-chip-error]', 'Has not graduated')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// Use admin to see all students
|
||||
describe('when using admin user', () => {
|
||||
describe('When using admin user', () => {
|
||||
beforeEach(() => {
|
||||
cy.init('/students', 'admin')
|
||||
})
|
||||
|
||||
it('Does not crash if student has no studyright or courses', () => {
|
||||
it('Does not crash if student has no study rights or courses', () => {
|
||||
typeStudentNumberAndClick('450730')
|
||||
cy.contains('Credits: 0')
|
||||
})
|
||||
|
||||
it("'Update student' button is shown", () => {
|
||||
typeStudentNumberAndClick(student.studentnumber)
|
||||
cy.get('div.ui.fluid.card').within(() => {
|
||||
typeStudentNumberAndClick(student.studentNumber)
|
||||
cy.cs('student-info-card').within(() => {
|
||||
cy.contains('button', 'Update student')
|
||||
})
|
||||
})
|
||||
|
||||
it('Bachelor Honours section is not shown for students outside of Faculty of Science', () => {
|
||||
cy.visit('/students/453146')
|
||||
cy.contains('.divider h4', 'Bachelor Honours').should('not.exist')
|
||||
cy.contains('h2', 'Bachelor Honours').should('not.exist')
|
||||
})
|
||||
|
||||
it('When a study plan is selected, courses included in the study plan are highlighted with a blue background', () => {
|
||||
@@ -239,15 +268,17 @@ describe('Students tests', () => {
|
||||
cy.contains('table tbody tr', 'Kulttuurien tutkimuksen kandiohjelma (01.08.2020–30.06.2023)').within(() => {
|
||||
cy.get('td').eq(0).click()
|
||||
})
|
||||
cy.contains('table tbody tr', 'Kandidaatintutkielma (KUKA-LIS222)')
|
||||
.should('have.attr', 'style')
|
||||
.and('equal', 'background-color: rgb(232, 244, 255);')
|
||||
cy.contains('table tbody tr', 'Kandidaatintutkielma (KUKA-LIS222)').should(
|
||||
'have.css',
|
||||
'background-color',
|
||||
'rgb(232, 244, 255)'
|
||||
)
|
||||
})
|
||||
|
||||
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',
|
||||
@@ -141,7 +141,7 @@ describe('Study guidance group tests', () => {
|
||||
'Transferred from',
|
||||
'Admission type',
|
||||
'Gender',
|
||||
'Citizenship',
|
||||
'Citizenships',
|
||||
'Curriculum period',
|
||||
'Latest attainment date',
|
||||
'Tags',
|
||||
|
||||
@@ -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', () => {
|
||||
@@ -81,11 +81,11 @@ describe('Studyprogramme overview', () => {
|
||||
const tableContents = [
|
||||
// [Year, Graduated, Wrote thesis]
|
||||
...years.map(year => [year, 0, 0]),
|
||||
[2023, 26, 0],
|
||||
[2022, 48, 0],
|
||||
[2021, 47, 66],
|
||||
[2020, 12, 18],
|
||||
[2019, 1, 15],
|
||||
[2023, 26, 5],
|
||||
[2022, 48, 13],
|
||||
[2021, 47, 76],
|
||||
[2020, 12, 23],
|
||||
[2019, 1, 19],
|
||||
[2018, 0, 0],
|
||||
[2017, 0, 1],
|
||||
]
|
||||
@@ -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,16 +108,16 @@ describe('Studyprogramme overview', () => {
|
||||
[2017, 41, 47, 0],
|
||||
]
|
||||
|
||||
cy.checkTableStats(studentTableContents, 'StudentsOfTheStudyprogramme')
|
||||
cy.checkTableStats(studentTableContents, 'StudentsOfTheStudyProgramme')
|
||||
|
||||
const graduatedTableContents = [
|
||||
// [Year, Graduated, Wrote thesis]
|
||||
...years.map(year => [year, 0, 0]),
|
||||
[2023, 24, 0],
|
||||
[2022, 44, 0],
|
||||
[2021, 46, 60],
|
||||
[2020, 11, 17],
|
||||
[2019, 1, 15],
|
||||
[2023, 24, 5],
|
||||
[2022, 44, 12],
|
||||
[2021, 46, 69],
|
||||
[2020, 11, 22],
|
||||
[2019, 1, 18],
|
||||
[2018, 0, 0],
|
||||
[2017, 0, 1],
|
||||
]
|
||||
@@ -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)
|
||||
@@ -175,7 +175,7 @@ describe('Studyprogramme overview', () => {
|
||||
.should('contain', 'Graduated students')
|
||||
.should('contain', 'Wrote thesis')
|
||||
.should('contain', 47)
|
||||
.should('contain', 66)
|
||||
.should('contain', 76)
|
||||
|
||||
cy.get('[data-cy=graduation-times-graph-breakdownBachelor]')
|
||||
cy.get('[data-cy=GraduationTimeToggle]').click()
|
||||
@@ -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,39 +235,39 @@ 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],
|
||||
['2023 - 2024', 8, 8, 0, 0, 8, 0, 5, 3, 0, 8, 1],
|
||||
['2022 - 2023', 26, 25, 0, 0, 24, 2, 19, 7, 0, 25, 1],
|
||||
['2021 - 2022', 38, 29, 0, 0, 33, 5, 29, 9, 0, 35, 3],
|
||||
['2020 - 2021', 30, 26, 0, 0, 11, 19, 15, 15, 0, 29, 1],
|
||||
['2019 - 2020', 35, 28, 0, 0, 8, 27, 22, 13, 0, 34, 1],
|
||||
['2021 - 2022', 38, 29, 0, 0, 33, 5, 29, 9, 0, 36, 4],
|
||||
['2020 - 2021', 30, 26, 0, 0, 11, 19, 15, 15, 0, 29, 3],
|
||||
['2019 - 2020', 35, 28, 0, 0, 8, 27, 22, 13, 0, 35, 2],
|
||||
['2018 - 2019', 45, 40, 0, 0, 6, 39, 26, 19, 0, 44, 1],
|
||||
['2017 - 2018', 47, 41, 0, 0, 5, 42, 31, 16, 0, 47, 0],
|
||||
['Total', 229, 197, 0, 0, 95, 134, 147, 82, 0, 222, 7],
|
||||
['Total', 229, 197, 0, 0, 95, 134, 147, 82, 0, 224, 12],
|
||||
]
|
||||
|
||||
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(() => {
|
||||
@@ -276,7 +276,7 @@ describe('Studyprogramme overview', () => {
|
||||
|
||||
const dataForStudyTracks = [
|
||||
['Ekonometria, MAT-EKO', 2, 2, 0, 0, 0, 2, 1, 1, 0, 2, 0],
|
||||
['Matematiikka, MAT-MAT', 13, 10, 0, 0, 1, 12, 7, 6, 0, 12, 1],
|
||||
['Matematiikka, MAT-MAT', 13, 10, 0, 0, 1, 12, 7, 6, 0, 12, 3],
|
||||
['Tietojenkäsittelyteoria, MAT-TIE', 2, 1, 0, 0, 1, 1, 2, 0, 0, 2, 0],
|
||||
['Tilastotiede, MAT-TIL', 4, 4, 0, 0, 0, 4, 1, 3, 0, 4, 0],
|
||||
]
|
||||
@@ -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,29 +399,31 @@ 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],
|
||||
['2021 - 2022', 5, 1, 0, 0, 1, 4, 3, 2, 0, 5, 0],
|
||||
['2020 - 2021', 13, 10, 0, 0, 1, 12, 7, 6, 0, 12, 1],
|
||||
['2019 - 2020', 17, 14, 0, 0, 0, 17, 10, 7, 0, 16, 1],
|
||||
['2020 - 2021', 13, 10, 0, 0, 1, 12, 7, 6, 0, 12, 3],
|
||||
['2019 - 2020', 17, 14, 0, 0, 0, 17, 10, 7, 0, 17, 1],
|
||||
['2018 - 2019', 23, 21, 0, 0, 2, 21, 11, 12, 0, 23, 0],
|
||||
['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],
|
||||
['Total', 89, 73, 0, 0, 6, 83, 48, 41, 0, 88, 4],
|
||||
]
|
||||
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(() => {
|
||||
@@ -732,16 +742,23 @@ describe('Studyprogramme overview', () => {
|
||||
cy.contains(studentNumber)
|
||||
}
|
||||
|
||||
cy.go('back')
|
||||
for (const studentNumber of studentNumbers) {
|
||||
cy.visit(`/students/${studentNumber}`)
|
||||
cy.contains('Tags')
|
||||
cy.contains(name)
|
||||
}
|
||||
|
||||
cy.get('a').contains('Matemaattisten tieteiden kandiohjelma').invoke('removeAttr', 'target').click()
|
||||
cy.url().should('include', '/study-programme/KH50_001?p_tab=4')
|
||||
|
||||
deleteTag(name)
|
||||
})
|
||||
|
||||
it('deleting a tag from tag view also removes it from students', () => {
|
||||
cy.contains(name).should('not.exist')
|
||||
for (const studentNumber of studentNumbers) {
|
||||
cy.contains('Students').click()
|
||||
cy.get('.prompt').type(studentNumber)
|
||||
cy.contains('a', studentNumber).click()
|
||||
cy.visit(`/students/${studentNumber}`)
|
||||
cy.contains('Tags').should('not.exist')
|
||||
cy.contains(name).should('not.exist')
|
||||
}
|
||||
})
|
||||
@@ -756,7 +773,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 +782,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", () => {
|
||||
|
||||
+50
-114
@@ -1,159 +1,95 @@
|
||||
/// <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')
|
||||
})
|
||||
|
||||
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')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
+50
-41
@@ -1,47 +1,53 @@
|
||||
/// <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 containsLinks = links => {
|
||||
cy.cs('nav-bar').within(() => {
|
||||
for (const link of links) {
|
||||
cy.contains(link)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const userButtonWorks = (username, mocking = false) => {
|
||||
cy.cs('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)
|
||||
}
|
||||
})
|
||||
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)
|
||||
}
|
||||
})
|
||||
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)
|
||||
}
|
||||
})
|
||||
containsLinks(visibleLinks.basic)
|
||||
userButtonWorks('basic')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -51,27 +57,30 @@ 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)
|
||||
}
|
||||
})
|
||||
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', { retries: 3 }, () => {
|
||||
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.visit('/populations')
|
||||
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,8 +15,21 @@
|
||||
/**
|
||||
* @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
|
||||
on('before:browser:launch', (browser = {}, launchOptions) => {
|
||||
// By manually setting the browser width and height in headless
|
||||
// mode, screenshots and videos will have a better quality
|
||||
// https://www.cypress.io/blog/generate-high-resolution-videos-and-screenshots
|
||||
const width = 2560
|
||||
const height = 1440
|
||||
|
||||
if (browser.name === 'chrome' && browser.isHeadless) {
|
||||
launchOptions.args.push(`--window-size=${width},${height}`)
|
||||
launchOptions.args.push('--force-device-scale-factor=1')
|
||||
}
|
||||
return launchOptions
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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:7.2.6
|
||||
ports:
|
||||
- 6379:6379
|
||||
|
||||
@@ -83,7 +83,7 @@ services:
|
||||
|
||||
jami-db:
|
||||
<<: *defaults
|
||||
image: postgres:15.8
|
||||
image: postgres:15.10
|
||||
environment:
|
||||
- PGDATA=/data
|
||||
- POSTGRES_PASSWORD=postgres
|
||||
|
||||
@@ -21,6 +21,7 @@ services:
|
||||
SENTRY_DSN: ''
|
||||
SERVICE_PROVIDER: fd
|
||||
LANGUAGE_CENTER_VIEW_ENABLED: 'false' # currently no intention to be enabled
|
||||
SIS_URL: 'https://sis-qa.funidata.fi'
|
||||
NODE_ENV: production
|
||||
image: ${DOCKER_OODIKONE_NAMESPACE:-637423527834.dkr.ecr.eu-north-1.amazonaws.com/oodikone}/oodikone-frontend:${DOCKER_IMAGE_TAG:-dev}
|
||||
|
||||
|
||||
@@ -64,6 +64,7 @@ services:
|
||||
environment:
|
||||
REACT_APP_SERVICE_PROVIDER: fd
|
||||
REACT_APP_LANGUAGE_CENTER_VIEW_ENABLED: 'false'
|
||||
REACT_APP_SIS_URL: 'https://sis-qa.funidata.fi'
|
||||
volumes:
|
||||
- ./services/frontend/src/:/opt/app-root/src/src
|
||||
- ./services/shared/:/opt/app-root/src/src/shared
|
||||
@@ -75,7 +76,7 @@ services:
|
||||
PGDATA: /data
|
||||
POSTGRES_DB: kone-db
|
||||
POSTGRES_HOST_AUTH_METHOD: trust
|
||||
image: postgres:15.8
|
||||
image: postgres:15.10
|
||||
ports:
|
||||
- 12349:5432
|
||||
volumes:
|
||||
@@ -85,7 +86,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:
|
||||
@@ -98,7 +99,7 @@ services:
|
||||
PGDATA: /data
|
||||
POSTGRES_DB: sis-db
|
||||
POSTGRES_HOST_AUTH_METHOD: trust
|
||||
image: postgres:15.8
|
||||
image: postgres:15.10
|
||||
volumes:
|
||||
- sis-db-data:/data
|
||||
shm_size: 2gb
|
||||
@@ -175,7 +176,7 @@ services:
|
||||
PGDATA: /data
|
||||
POSTGRES_DB: user-db
|
||||
POSTGRES_HOST_AUTH_METHOD: trust
|
||||
image: postgres:15.8
|
||||
image: postgres:15.10
|
||||
volumes:
|
||||
- user-db-data:/data
|
||||
ports:
|
||||
|
||||
+2
-2
@@ -76,7 +76,7 @@ services:
|
||||
<<: *defaults
|
||||
command: 'redis-server --appendonly yes'
|
||||
container_name: redis
|
||||
image: redis:6.2.14
|
||||
image: redis:7.2.6
|
||||
ports:
|
||||
- '6379:6379'
|
||||
volumes:
|
||||
@@ -189,7 +189,7 @@ services:
|
||||
|
||||
jami-db:
|
||||
<<: *defaults
|
||||
image: postgres:15.8
|
||||
image: postgres:15.10
|
||||
environment:
|
||||
- PGDATA=/data
|
||||
- POSTGRES_PASSWORD=postgres
|
||||
|
||||
@@ -8,24 +8,22 @@ Roles are defined [here](../services/backend/src/config/roles.ts). Criteria for
|
||||
- courseStatistics
|
||||
- facultyStatistics
|
||||
- fullSisuAccess
|
||||
- katselmusViewer
|
||||
- openUniSearch
|
||||
- studyGuidanceGroups
|
||||
- teachers
|
||||
|
||||
The roles `openUniSearch`, `teachers` and `studyGuidanceGroups` simply enable certain views (_Open uni student population_, _Teachers_ and _Guidance groups_ respectively).
|
||||
The roles `facultyStatistics`, `openUniSearch`, and `studyGuidanceGroups` simply enable certain views (_Faculties_, _Open uni student population_, and _Guidance groups_ respectively).
|
||||
|
||||
### courseStatistics
|
||||
|
||||
- Can see statistics of every course but year statistics of ≤ 5 students are hidden
|
||||
- If the user has [full rights](#programmes) to **any** programme, they can also see exact statistics even if there are ≤ 5 students (the same behavior as with `admin` or `fullSisuAccess` roles)
|
||||
|
||||
### facultyStatistics
|
||||
### teachers
|
||||
|
||||
- Can see statistics of every course and year
|
||||
|
||||
### katselemusViewer
|
||||
|
||||
- Enables the _Evaluation overview_ view
|
||||
- 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
|
||||
|
||||
@@ -46,7 +44,6 @@ The roles `openUniSearch`, `teachers` and `studyGuidanceGroups` simply enable ce
|
||||
| Completed courses | ✅ | ✅ | ✅ |
|
||||
| Close to graduation | | ✅ | ✅ |
|
||||
| Open uni population | | ✅ | |
|
||||
| Evaluation overview | | ✅ | |
|
||||
| Courses | | ✅ | ✅ |
|
||||
|
||||
\* = anyone who can log in to Oodikone, check the [IAM groups](#iam-groups) section for more information
|
||||
@@ -66,8 +63,6 @@ A user must be a member of either the `grp-oodikone-basic-users` or `grp-oodikon
|
||||
| grp-oodikone-basic-users | courseStatistics |
|
||||
| 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 |
@@ -0,0 +1,127 @@
|
||||
# How to update the database schema
|
||||
|
||||
These instructions explain how to update the schemas for the following databases used by Oodikone:
|
||||
|
||||
- `sis-db`
|
||||
- `user-db`
|
||||
- `kone-db`
|
||||
|
||||
The steps to **add, remove, or modify columns** are mostly the same for each database, but the file paths and container names differ. Always **double-check** that you have the correct function name, column name, data type, and other relevant details in your migration file.
|
||||
|
||||
> [!NOTE]
|
||||
> If you need to change the `sis-importer-db` schema, see the instructions in the [sis-importer repo](https://github.com/UniversityOfHelsinkiCS/sis-importer/blob/master/README.md#tricks--tips).
|
||||
|
||||
## General guidelines
|
||||
|
||||
### File names
|
||||
|
||||
- Name new migration files using the format `YYYYMMDD_XX_description.js`.
|
||||
- The `XX` part is a migration number (starting from `00` each day).
|
||||
- Example: `20250122_00_add_column_to_table.js`.
|
||||
|
||||
### Model vs. column names
|
||||
|
||||
- Use **camelCase** in your model definitions.
|
||||
- Use **snake_case** in the actual database columns.
|
||||
- If a model has `underscored: true`, Sequelize automatically converts camelCase to snake_case in all queries. Otherwise, specify the column name explicitly.
|
||||
|
||||
> [!IMPORTANT]
|
||||
> The column name **in the database** (snake_case) must be used in the migration file, not the camelCase name from your model.
|
||||
|
||||
### Verifying the migration locally
|
||||
|
||||
- Migrations are run automatically whenever the server restarts.
|
||||
- After creating a migration, ensure the relevant service is running:
|
||||
- For `sis-db`: `npm run both:real` or `npm run updater:real`
|
||||
- For `user-db`/`kone-db`: `npm run both:real` or `npm run oodikone:real`
|
||||
- **Connect to the database container** and check:
|
||||
|
||||
```bash
|
||||
docker exec -it <container-name> psql -U postgres <database-name>-real
|
||||
```
|
||||
|
||||
Replace `<container-name>` and `<database-name>` with the actual values (e.g., `sis-db`, `user-db`, or `kone-db`).
|
||||
|
||||
- In the psql prompt, verify your migration:
|
||||
|
||||
```sql
|
||||
SELECT * FROM migrations;
|
||||
```
|
||||
|
||||
Also confirm that the new (or updated) column exists in the appropriate table and has the correct type.
|
||||
|
||||
### Reverting a migration locally
|
||||
|
||||
If you need to **revert a migration locally**, follow these steps:
|
||||
|
||||
1. Locate the `migrator.up()` function in the code:
|
||||
- For `user-db` and `kone-db`: [`services/backend/src/database/connection.ts`](../services/backend/src/database/connection.ts)
|
||||
- For `sis-db`: [`updater/sis-updater-worker/src/db/connection.js`](../updater/sis-updater-worker/src/db/connection.js)
|
||||
1. Change `migrator.up()` to `migrator.down()` and save the file. The latest migration will be reverted automatically when the server reloads.
|
||||
1. **Important:** After the migration is reverted, change `migrator.down()` back to `migrator.up()` to ensure future migrations run correctly.
|
||||
1. For more instructions, see the [umzug documentation](https://github.com/sequelize/umzug?tab=readme-ov-file#usage).
|
||||
|
||||
### Deploying changes to production
|
||||
|
||||
- **Push changes** to the `master` branch and wait for the workflow to finish.
|
||||
- For **`sis-db`** migrations (in the updater):
|
||||
- You **do not** need to create a new release. Once tests pass, changes to the updater go directly to production.
|
||||
- For **`user-db`** and **`kone-db`** migrations (in the backend):
|
||||
- A new release must be made before the changes go to production.
|
||||
- After deployment, check logs (for example, in OpenShift) to confirm the migration ran successfully.
|
||||
- Optionally, use the `db-cli` tool to inspect the production database. See `how_to_db_cli.md` (in GitLab) for instructions.
|
||||
|
||||
## `sis-db`
|
||||
|
||||
> [!NOTE]
|
||||
> These instructions assume that needed data is already in the `sis-importer-db`. If not, you must add the data there first. See the [sis-importer repo](https://github.com/UniversityOfHelsinkiCS/sis-importer/blob/master/README.md#tricks--tips) for more details.
|
||||
|
||||
1. **Update the updater model:**
|
||||
|
||||
- Go to `updater/sis-updater-worker/src/db/models/` and choose the model you want to change.
|
||||
- Add or modify the column (with its **type** and any other necessary fields).
|
||||
- Check the correct data type in the importer.
|
||||
- Use camelCase in the model, snake_case in the database.
|
||||
|
||||
1. **Update the backend model:**
|
||||
|
||||
- Go to `services/backend/src/models/`.
|
||||
- The models are in different formats (JavaScript in the updater, TypeScript in the backend), but follow existing examples.
|
||||
|
||||
1. **Create a migration file:**
|
||||
|
||||
- Go to `updater/sis-updater-worker/src/db/migrations/`.
|
||||
- Name your file using the format described above (e.g. `20250122_00_add_column_to_table.js`).
|
||||
- **Remember**: The column name **in the database** (snake_case) must be used in the migration file, not the camelCase name from your model.
|
||||
|
||||
1. **Run and verify the migration:**
|
||||
|
||||
- Ensure that the updater is running (`npm run both:real` or `npm run updater:real`).
|
||||
- Use the steps in [General guidelines](#general-guidelines) to verify.
|
||||
|
||||
1. **Push to master and deploy:**
|
||||
- Your migration will go to production automatically after tests pass.
|
||||
- Check logs in production (OpenShift) or use `db-cli` to confirm success.
|
||||
|
||||
## `user-db` and `kone-db`
|
||||
|
||||
1. **Update the model:**
|
||||
|
||||
- Go to `services/backend/src/models/kone` (for `kone-db`) or `services/backend/src/models/user.ts` (the only model for user-db)
|
||||
- Add or modify the column, type, and any other needed fields. Follow existing models for structure.
|
||||
|
||||
1. **Create a migration file:**
|
||||
|
||||
- Go to `services/backend/src/database/`.
|
||||
- Use `migrations_kone` (for `kone-db`) or `migrations_user` (for `user-db`).
|
||||
- Name the file using the same `YYYYMMDD_XX_description.js` format.
|
||||
- **Remember**: The column name **in the database** (snake_case) must be used in the migration file, not the camelCase name from your model.
|
||||
|
||||
1. **Run and verify the migration:**
|
||||
|
||||
- Make sure Oodikone is running (`npm run both:real` or `npm run oodikone:real`).
|
||||
- Follow the same verification steps as in [General Guidelines](#general-guidelines).
|
||||
|
||||
1. **Push to master and release:**
|
||||
- Wait for the CI workflow to pass.
|
||||
- **Create a new release**; then check the production logs (in OpenShift) to confirm the migrationor use `db-cli`.
|
||||
@@ -42,6 +42,9 @@ 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/)
|
||||
- [Material React Table documentation](https://www.material-react-table.com/docs)
|
||||
- [Semantic UI React documentation](https://react.semantic-ui.com)
|
||||
|
||||
<!--
|
||||
|
||||
@@ -0,0 +1,184 @@
|
||||
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-boolean-value': ['error', 'never'],
|
||||
'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': 'warn', // 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: ['services/**/shared/**/*'],
|
||||
rules: {
|
||||
'import/no-unused-modules': '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
+1656
-1332
File diff suppressed because it is too large
Load Diff
+20
-22
@@ -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.11",
|
||||
"@eslint/js": "^9.19.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",
|
||||
"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-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",
|
||||
"cypress": "^14.0.1",
|
||||
"eslint": "^9.19.0",
|
||||
"eslint-config-prettier": "^10.0.1",
|
||||
"eslint-import-resolver-typescript": "^3.7.0",
|
||||
"eslint-plugin-cypress": "^4.1.0",
|
||||
"eslint-plugin-import": "^2.31.0",
|
||||
"eslint-plugin-prettier": "^5.2.3",
|
||||
"eslint-plugin-react": "^7.37.4",
|
||||
"eslint-plugin-react-hooks": "^5.1.0",
|
||||
"globals": "^15.14.0",
|
||||
"husky": "^9.1.7",
|
||||
"lint-staged": "^15.4.3",
|
||||
"node-actionlint": "^1.2.2",
|
||||
"nyc": "^17.0.0",
|
||||
"prettier": "3.3.3",
|
||||
"stylelint": "^16.2.1",
|
||||
"stylelint-config-standard": "^36.0.0"
|
||||
"nyc": "^17.1.0",
|
||||
"prettier": "3.4.2",
|
||||
"stylelint": "^16.14.1",
|
||||
"stylelint-config-standard": "^37.0.0",
|
||||
"typescript-eslint": "^8.22.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"lodash": "^4.17.21",
|
||||
|
||||
+7
-6
@@ -5,18 +5,19 @@
|
||||
{
|
||||
"groupName": "all non-major dependencies",
|
||||
"groupSlug": "all-minor-patch",
|
||||
"matchPackagePatterns": ["*"],
|
||||
"matchUpdateTypes": ["minor", "patch"]
|
||||
"matchUpdateTypes": ["minor", "patch"],
|
||||
"matchCurrentVersion": "!/^0/",
|
||||
"matchPackageNames": ["*"]
|
||||
},
|
||||
{
|
||||
"matchDatasources": ["docker"],
|
||||
"matchPackageNames": ["node"],
|
||||
"versionCompatibility": "^(?<version>[^-]+)(?<compatibility>-.*)?$",
|
||||
"versioning": "node"
|
||||
"enabled": false
|
||||
}
|
||||
],
|
||||
"rebaseWhen": "never",
|
||||
"vulnerabilityAlerts": {
|
||||
"labels": ["security"]
|
||||
}
|
||||
},
|
||||
"addLabels": ["dependencies"],
|
||||
"rangeStrategy": "bump"
|
||||
}
|
||||
|
||||
@@ -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
+1098
-400
File diff suppressed because it is too large
Load Diff
@@ -5,47 +5,44 @@
|
||||
"build": "tsc"
|
||||
},
|
||||
"dependencies": {
|
||||
"@sentry/node": "^5.30.0",
|
||||
"@sentry/tracing": "^6.18.2",
|
||||
"axios": "^0.28.1",
|
||||
"bluebird": "^3.7.2",
|
||||
"bullmq": "^5.0.0",
|
||||
"compression": "^1.7.4",
|
||||
"@sentry/node": "^8.53.0",
|
||||
"@sentry/profiling-node": "^8.53.0",
|
||||
"axios": "^1.7.9",
|
||||
"bullmq": "^5.39.1",
|
||||
"compression": "^1.7.5",
|
||||
"cors": "^2.8.5",
|
||||
"cron": "^3.0.0",
|
||||
"express": "^4.19.2",
|
||||
"cron": "^3.5.0",
|
||||
"express": "^4.21.2",
|
||||
"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",
|
||||
"morgan": "^1.10.0",
|
||||
"oodikone-shared": "file:../oodikone-shared",
|
||||
"papaparse": "^5.4.1",
|
||||
"pg": "^8.11.5",
|
||||
"redis": "^3.1.2",
|
||||
"papaparse": "^5.5.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.15",
|
||||
"@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.12.0",
|
||||
"@types/validator": "^13.12.2",
|
||||
"tsx": "^4.19.2",
|
||||
"typescript": "^5.7.3"
|
||||
},
|
||||
"overrides": {
|
||||
"winston-gelf-transporter": {
|
||||
"gelf-pro": "^1.3.4"
|
||||
"gelf-pro": "^1.4.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -3,7 +3,6 @@ export const roles = [
|
||||
'courseStatistics',
|
||||
'facultyStatistics',
|
||||
'fullSisuAccess',
|
||||
'katselmusViewer',
|
||||
'openUniSearch',
|
||||
'studyGuidanceGroups',
|
||||
'teachers',
|
||||
|
||||
@@ -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,7 +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 '../shared/types'
|
||||
import { GenderCode } from '../types'
|
||||
import { Credit, Enrollment, SISStudyRight, Studyplan } from '.'
|
||||
|
||||
@@ -27,7 +27,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' })
|
||||
@@ -57,23 +57,8 @@ export class Student extends Model<InferAttributes<Student>> {
|
||||
@Column(DataType.STRING)
|
||||
phone_number!: string
|
||||
|
||||
@Column(DataType.STRING)
|
||||
country_fi!: string
|
||||
|
||||
@Column(DataType.STRING)
|
||||
country_sv!: string
|
||||
|
||||
@Column(DataType.STRING)
|
||||
country_en!: string
|
||||
|
||||
@Column(DataType.STRING)
|
||||
home_country_fi!: string
|
||||
|
||||
@Column(DataType.STRING)
|
||||
home_country_sv!: string
|
||||
|
||||
@Column(DataType.STRING)
|
||||
home_country_en!: string
|
||||
@Column(DataType.JSONB)
|
||||
citizenships!: Name[]
|
||||
|
||||
@Column(DataType.STRING)
|
||||
gender_code!: GenderCode
|
||||
@@ -81,6 +66,12 @@ export class Student extends Model<InferAttributes<Student>> {
|
||||
@Column(DataType.STRING)
|
||||
sis_person_id!: string
|
||||
|
||||
@Column(DataType.BOOLEAN)
|
||||
hasPersonalIdentityCode!: boolean
|
||||
|
||||
@Column(DataType.STRING)
|
||||
preferredLanguage!: string
|
||||
|
||||
@CreatedAt
|
||||
@Column(DataType.DATE)
|
||||
createdAt!: Date
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
/* eslint-disable import/no-cycle */
|
||||
import { InferAttributes } from 'sequelize'
|
||||
import {
|
||||
BelongsTo,
|
||||
@@ -44,6 +43,9 @@ export class Studyplan extends Model<InferAttributes<Studyplan>> {
|
||||
@Column(DataType.ARRAY(DataType.STRING))
|
||||
included_courses!: string[]
|
||||
|
||||
@Column({ type: DataType.ARRAY(DataType.TEXT), field: 'included_modules' })
|
||||
includedModules!: string[]
|
||||
|
||||
@Column(DataType.STRING)
|
||||
sisu_id!: string
|
||||
|
||||
|
||||
@@ -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,8 @@
|
||||
/* 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 } from '../shared/language'
|
||||
import { 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,48 @@ 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 3',
|
||||
time: new Date().toISOString(),
|
||||
version: '0.0.3',
|
||||
},
|
||||
{
|
||||
description: "Let's not spam the GitHub API in development!",
|
||||
title: 'Release 2',
|
||||
time: new Date().toISOString(),
|
||||
version: '0.0.2',
|
||||
},
|
||||
{
|
||||
description: 'This release should not be visible on the frontpage',
|
||||
title: 'Release 1',
|
||||
time: new Date().toISOString(),
|
||||
version: '0.0.1',
|
||||
},
|
||||
]
|
||||
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,
|
||||
version: release.tag_name,
|
||||
}))
|
||||
changelog.data = newChangelogData
|
||||
res.status(200).send(newChangelogData)
|
||||
changelog.data = releasesFromAPI
|
||||
res.status(200).json(releasesFromAPI)
|
||||
})
|
||||
|
||||
export default router
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import crypto from 'crypto'
|
||||
import { Request, Response, Router } from 'express'
|
||||
import moment from 'moment'
|
||||
|
||||
import { getCourseYearlyStats } from '../services/courses'
|
||||
import { getCoursesByNameAndOrCode, getCoursesByCodes } from '../services/courses/courseFinders'
|
||||
import { CourseWithSubsId } from '../types'
|
||||
import { getFullStudyProgrammeRights, hasFullAccessToStudentData, validateParamLength } from '../util'
|
||||
|
||||
const router = Router()
|
||||
@@ -22,24 +22,45 @@ router.get('/v2/coursesmulti', async (req: GetCoursesRequest, res: Response) =>
|
||||
return res.status(400).json({ error: 'Query parameter name or code is invalid' })
|
||||
}
|
||||
|
||||
let results: { courses: CourseWithSubsId[] } = { courses: [] }
|
||||
results = await getCoursesByNameAndOrCode(name, code)
|
||||
const courses = await getCoursesByNameAndOrCode(name, code)
|
||||
|
||||
if (combineSubstitutions === 'false') {
|
||||
const courseCodes = results.courses.map(course => course.code)
|
||||
const courseCodes = courses.map(course => course.code)
|
||||
const substitutions = [
|
||||
...new Set(
|
||||
results.courses
|
||||
.flatMap(course => course.substitutions)
|
||||
.filter(code => code !== null && !courseCodes.includes(code))
|
||||
courses.flatMap(course => course.substitutions).filter(code => code !== null && !courseCodes.includes(code))
|
||||
),
|
||||
]
|
||||
const substitutionDetails = await getCoursesByCodes(substitutions)
|
||||
for (const substitution of substitutionDetails) {
|
||||
results.courses.push(substitution.dataValues)
|
||||
courses.push(substitution.toJSON())
|
||||
}
|
||||
}
|
||||
res.json(results)
|
||||
|
||||
const mergedCourses = {}
|
||||
|
||||
for (const course of courses) {
|
||||
const groupId = combineSubstitutions === 'true' ? course.subsId! : course.code
|
||||
if (!(course.max_attainment_date && course.min_attainment_date)) {
|
||||
continue
|
||||
}
|
||||
if (!mergedCourses[groupId]) {
|
||||
mergedCourses[groupId] = {
|
||||
...course,
|
||||
substitutions: combineSubstitutions === 'true' ? course.substitutions : [],
|
||||
}
|
||||
} else {
|
||||
const mergedCourse = mergedCourses[groupId]
|
||||
if (moment(mergedCourse.max_attainment_date).isBefore(course.max_attainment_date)) {
|
||||
mergedCourse.max_attainment_date = course.max_attainment_date
|
||||
}
|
||||
if (moment(mergedCourse.min_attainment_date).isAfter(course.min_attainment_date)) {
|
||||
mergedCourse.min_attainment_date = course.min_attainment_date
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
res.json({ courses: Object.values(mergedCourses) })
|
||||
})
|
||||
|
||||
interface GetCourseYearlyStatsRequest extends Request {
|
||||
@@ -54,7 +75,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
|
||||
|
||||
@@ -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,11 +1,11 @@
|
||||
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'
|
||||
import { getSortedFaculties } from '../services/faculty/facultyHelpers'
|
||||
import { getFacultiesForFacultyList } from '../services/faculty/facultyHelpers'
|
||||
import {
|
||||
getBasicStats,
|
||||
setBasicStats,
|
||||
@@ -22,7 +22,7 @@ import { combineFacultyStudentProgress } from '../services/faculty/facultyStuden
|
||||
import { combineFacultyStudents } from '../services/faculty/facultyStudents'
|
||||
import { combineFacultyThesisWriters } from '../services/faculty/facultyThesisWriters'
|
||||
import { updateFacultyOverview, updateFacultyProgressOverview } from '../services/faculty/facultyUpdates'
|
||||
import { Graduated, ProgrammeFilter, SpecialGroups, StatsType, YearType } from '../types'
|
||||
import { Graduated, ProgrammeFilter, SpecialGroups, StatsType, YearType } from '../shared/types'
|
||||
import logger from '../util/logger'
|
||||
|
||||
// Faculty uses a lot of tools designed for Study programme.
|
||||
@@ -31,7 +31,7 @@ import logger from '../util/logger'
|
||||
const router = Router()
|
||||
|
||||
router.get('/', async (_req: Request, res: Response) => {
|
||||
const faculties = await getSortedFaculties()
|
||||
const faculties = await getFacultiesForFacultyList()
|
||||
res.json(faculties)
|
||||
})
|
||||
|
||||
@@ -43,34 +43,31 @@ interface GetStatsRequest extends Request {
|
||||
}
|
||||
}
|
||||
|
||||
router.get(
|
||||
'/:id/basicstats',
|
||||
auth.roles(['facultyStatistics', 'katselmusViewer']),
|
||||
async (req: GetStatsRequest, res: Response) => {
|
||||
const code = req.params.id
|
||||
if (!code) {
|
||||
return res.status(422).end()
|
||||
}
|
||||
|
||||
const { year_type: yearType, programme_filter: programmeFilter, special_groups: specialGroups } = req.query
|
||||
const data = await getBasicStats(code, yearType, programmeFilter, specialGroups)
|
||||
if (data) {
|
||||
return res.json(data)
|
||||
}
|
||||
|
||||
const programmes = await getDegreeProgrammesOfFaculty(code, programmeFilter === 'NEW_STUDY_PROGRAMMES')
|
||||
if (!programmes.length) {
|
||||
return res.status(422).end()
|
||||
}
|
||||
|
||||
// TODO: The types could be handled better. Possible null value is causing issues with TS.
|
||||
let updatedStats: any = await combineFacultyBasics(code, programmes, yearType, specialGroups)
|
||||
if (updatedStats) {
|
||||
updatedStats = await setBasicStats(updatedStats, yearType, programmeFilter, specialGroups)
|
||||
}
|
||||
return res.json(updatedStats)
|
||||
router.get('/:id/basicstats', auth.roles(['facultyStatistics']), async (req: GetStatsRequest, res: Response) => {
|
||||
const facultyId = req.params.id
|
||||
const code = await getFacultyCodeById(facultyId)
|
||||
if (!code) {
|
||||
return res.status(422).end()
|
||||
}
|
||||
)
|
||||
|
||||
const { year_type: yearType, programme_filter: programmeFilter, special_groups: specialGroups } = req.query
|
||||
const data = await getBasicStats(code, yearType, programmeFilter, specialGroups)
|
||||
if (data) {
|
||||
return res.json(data)
|
||||
}
|
||||
|
||||
const programmes = await getDegreeProgrammesOfFaculty(code, programmeFilter === 'NEW_STUDY_PROGRAMMES')
|
||||
if (!programmes.length) {
|
||||
return res.status(422).end()
|
||||
}
|
||||
|
||||
// TODO: The types could be handled better. Possible null value is causing issues with TS.
|
||||
let updatedStats: any = await combineFacultyBasics(code, programmes, yearType, specialGroups)
|
||||
if (updatedStats) {
|
||||
updatedStats = await setBasicStats(updatedStats, yearType, programmeFilter, specialGroups)
|
||||
}
|
||||
return res.json(updatedStats)
|
||||
})
|
||||
|
||||
interface GetCreditStatsRequest extends Request {
|
||||
query: {
|
||||
@@ -78,44 +75,38 @@ interface GetCreditStatsRequest extends Request {
|
||||
}
|
||||
}
|
||||
|
||||
router.get(
|
||||
'/:id/creditstats',
|
||||
auth.roles(['facultyStatistics', 'katselmusViewer']),
|
||||
async (req: GetCreditStatsRequest, res: Response) => {
|
||||
const code = req.params.id
|
||||
const { year_type: yearType } = req.query
|
||||
const stats = await getFacultyCredits(code, yearType === 'ACADEMIC_YEAR')
|
||||
return res.json(stats)
|
||||
router.get('/:id/creditstats', auth.roles(['facultyStatistics']), async (req: GetCreditStatsRequest, res: Response) => {
|
||||
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)
|
||||
})
|
||||
|
||||
router.get('/:id/thesisstats', auth.roles(['facultyStatistics']), async (req: GetStatsRequest, res: Response) => {
|
||||
const facultyId = req.params.id
|
||||
const code = await getFacultyCodeById(facultyId)
|
||||
if (!code) {
|
||||
return res.status(422).end()
|
||||
}
|
||||
)
|
||||
|
||||
router.get(
|
||||
'/:id/thesisstats',
|
||||
auth.roles(['facultyStatistics', 'katselmusViewer']),
|
||||
async (req: GetStatsRequest, res: Response) => {
|
||||
const code = req.params.id
|
||||
if (!code) {
|
||||
return res.status(422).end()
|
||||
}
|
||||
|
||||
const { year_type: yearType, programme_filter: programmeFilter, special_groups: specialGroups } = req.query
|
||||
const data = await getThesisWritersStats(code, yearType, programmeFilter, specialGroups)
|
||||
if (data) {
|
||||
return res.json(data)
|
||||
}
|
||||
|
||||
const programmes = await getDegreeProgrammesOfFaculty(code, programmeFilter === 'NEW_STUDY_PROGRAMMES')
|
||||
if (!programmes.length) {
|
||||
return res.status(422).end()
|
||||
}
|
||||
|
||||
let updatedStats: any = await combineFacultyThesisWriters(code, programmes, yearType, specialGroups)
|
||||
if (updatedStats) {
|
||||
updatedStats = await setThesisWritersStats(updatedStats, yearType, programmeFilter, specialGroups)
|
||||
}
|
||||
return res.json(updatedStats)
|
||||
const { year_type: yearType, programme_filter: programmeFilter, special_groups: specialGroups } = req.query
|
||||
const data = await getThesisWritersStats(code, yearType, programmeFilter, specialGroups)
|
||||
if (data) {
|
||||
return res.json(data)
|
||||
}
|
||||
)
|
||||
|
||||
const programmes = await getDegreeProgrammesOfFaculty(code, programmeFilter === 'NEW_STUDY_PROGRAMMES')
|
||||
if (!programmes.length) {
|
||||
return res.status(422).end()
|
||||
}
|
||||
|
||||
let updatedStats: any = await combineFacultyThesisWriters(code, programmes, yearType, specialGroups)
|
||||
if (updatedStats) {
|
||||
updatedStats = await setThesisWritersStats(updatedStats, yearType, programmeFilter, specialGroups)
|
||||
}
|
||||
return res.json(updatedStats)
|
||||
})
|
||||
|
||||
interface GetGraduationStatsRequest extends Request {
|
||||
query: {
|
||||
@@ -125,9 +116,10 @@ interface GetGraduationStatsRequest extends Request {
|
||||
|
||||
router.get(
|
||||
'/:id/graduationtimes',
|
||||
auth.roles(['facultyStatistics', 'katselmusViewer']),
|
||||
auth.roles(['facultyStatistics']),
|
||||
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()
|
||||
}
|
||||
@@ -160,9 +152,10 @@ interface GetProgressStatsRequest extends Request {
|
||||
|
||||
router.get(
|
||||
'/:id/progressstats',
|
||||
auth.roles(['facultyStatistics', 'katselmusViewer']),
|
||||
auth.roles(['facultyStatistics']),
|
||||
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()
|
||||
}
|
||||
@@ -190,9 +183,10 @@ interface GetStudentStatsRequest extends GetProgressStatsRequest {}
|
||||
|
||||
router.get(
|
||||
'/:id/studentstats',
|
||||
auth.roles(['facultyStatistics', 'katselmusViewer']),
|
||||
auth.roles(['facultyStatistics']),
|
||||
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()
|
||||
}
|
||||
@@ -225,9 +219,10 @@ interface GetUpdateBasicViewRequest extends Request {
|
||||
|
||||
router.get(
|
||||
'/:id/update_basicview',
|
||||
auth.roles(['facultyStatistics', 'katselmusViewer']),
|
||||
auth.roles(['facultyStatistics']),
|
||||
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()
|
||||
}
|
||||
@@ -243,23 +238,20 @@ router.get(
|
||||
}
|
||||
)
|
||||
|
||||
router.get(
|
||||
'/:id/update_progressview',
|
||||
auth.roles(['facultyStatistics', 'katselmusViewer']),
|
||||
async (req: Request, res: Response) => {
|
||||
const code = req.params.id
|
||||
if (!code) {
|
||||
return res.status(422).end()
|
||||
}
|
||||
try {
|
||||
const result = await updateFacultyProgressOverview(code)
|
||||
return res.json(result)
|
||||
} catch (error) {
|
||||
const message = `Failed to update progress tab stats for faculty ${code}`
|
||||
logger.error(message, { error })
|
||||
return res.status(500).json({ error: message })
|
||||
}
|
||||
router.get('/:id/update_progressview', auth.roles(['facultyStatistics']), async (req: Request, res: Response) => {
|
||||
const facultyId = req.params.id
|
||||
const code = await getFacultyCodeById(facultyId)
|
||||
if (!code) {
|
||||
return res.status(422).end()
|
||||
}
|
||||
)
|
||||
try {
|
||||
const result = await updateFacultyProgressOverview(code)
|
||||
return res.json(result)
|
||||
} catch (error) {
|
||||
const message = `Failed to update progress tab stats for faculty ${code}`
|
||||
logger.error(message, { error })
|
||||
return res.status(500).json({ error: message })
|
||||
}
|
||||
})
|
||||
|
||||
export default router
|
||||
|
||||
@@ -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 '../shared/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,7 +15,7 @@ import {
|
||||
} from '../services/faculty/facultyService'
|
||||
import { combineFacultyStudentProgress, FacultyProgressData } from '../services/faculty/facultyStudentProgress'
|
||||
import { getMedian } from '../services/studyProgramme/studyProgrammeHelpers'
|
||||
import { Graduated } from '../types'
|
||||
import { Graduated, NameWithCode } from '../shared/types'
|
||||
import logger from '../util/logger'
|
||||
|
||||
const router = Router()
|
||||
@@ -24,7 +23,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
|
||||
@@ -33,7 +32,7 @@ const getProgrammeNames = (faculties: Organization[]) => {
|
||||
|
||||
interface GetProgressStatsRequest extends Request {
|
||||
query: {
|
||||
specialsIncluded: 'true' | undefined
|
||||
specialsIncluded: 'true' | 'false'
|
||||
graduated: Graduated
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -4,8 +4,7 @@ import { roles } from '../config/roles'
|
||||
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_CODES, Language } from '../shared/language'
|
||||
import logger from '../util/logger'
|
||||
|
||||
const router = Router()
|
||||
@@ -15,7 +14,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 '../shared/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
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { col, fn, Op, where } from 'sequelize'
|
||||
|
||||
import { Course, Credit, Enrollment, Student } from '../models/index'
|
||||
import { Course, Credit, Enrollment, Student, Studyplan } from '../models/index'
|
||||
import { CreditTypeCode, EnrollmentState } from '../types'
|
||||
|
||||
type Courses = Array<Pick<Course, 'code' | 'name' | 'substitutions'>>
|
||||
@@ -15,7 +15,6 @@ interface StudentCredit {
|
||||
interface StudentEnrollment {
|
||||
date: Date
|
||||
courseCode: string
|
||||
state: EnrollmentState
|
||||
substitution: string | null
|
||||
}
|
||||
|
||||
@@ -24,9 +23,10 @@ interface StudentInfo {
|
||||
lastname: string
|
||||
email: string
|
||||
sis_person_id: string
|
||||
secondary_email: string | null
|
||||
coursesInStudyPlan: string[]
|
||||
secondaryEmail: string | null
|
||||
credits: StudentCredit[]
|
||||
enrollments: StudentEnrollment[]
|
||||
enrollments: Record<string, StudentEnrollment>
|
||||
}
|
||||
|
||||
type StudentCredits = Record<
|
||||
@@ -37,16 +37,18 @@ type StudentCredits = Record<
|
||||
}
|
||||
>
|
||||
|
||||
interface FormattedStudent {
|
||||
interface FormattedStudent extends StudentInfo {
|
||||
studentNumber: string
|
||||
sis_person_id: string
|
||||
credits: StudentCredit[]
|
||||
enrollments: Record<string, any>
|
||||
allEnrollments: StudentEnrollment[]
|
||||
firstnames: string
|
||||
lastname: string
|
||||
email: string
|
||||
secondaryEmail: string | null
|
||||
}
|
||||
|
||||
interface StudentWithStudyplanNested
|
||||
extends Pick<Student, 'studentnumber' | 'firstnames' | 'lastname' | 'email' | 'sis_person_id' | 'secondary_email'> {
|
||||
studyplans: { included_courses: string[] }[]
|
||||
}
|
||||
|
||||
interface StudentWithCourses extends Omit<StudentWithStudyplanNested, 'studyplans'> {
|
||||
coursesInStudyPlan: string[]
|
||||
}
|
||||
|
||||
const getCourses = async (courseCodes: string[]) => {
|
||||
@@ -84,7 +86,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 +97,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,30 +115,37 @@ 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
|
||||
}
|
||||
|
||||
const getStudents = async (studentNumbers: string[]) => {
|
||||
const students: Array<
|
||||
Pick<Student, 'studentnumber' | 'firstnames' | 'lastname' | 'email' | 'sis_person_id' | 'secondary_email'>
|
||||
> = (
|
||||
await Student.findAll({
|
||||
attributes: ['studentnumber', 'firstnames', 'lastname', 'email', 'sis_person_id', 'secondary_email'],
|
||||
where: {
|
||||
studentnumber: {
|
||||
[Op.in]: studentNumbers,
|
||||
},
|
||||
const students = await Student.findAll({
|
||||
attributes: ['studentnumber', 'firstnames', 'lastname', 'email', 'sis_person_id', 'secondary_email'],
|
||||
where: {
|
||||
studentnumber: {
|
||||
[Op.in]: studentNumbers,
|
||||
},
|
||||
})
|
||||
).map(student => student.toJSON())
|
||||
return students
|
||||
},
|
||||
include: [
|
||||
{
|
||||
model: Studyplan,
|
||||
as: 'studyplans',
|
||||
attributes: ['included_courses'],
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
return students.map(student => {
|
||||
const { studyplans, ...rest } = student.toJSON() as StudentWithStudyplanNested
|
||||
const coursesInStudyPlan = studyplans.flatMap(studyplan => studyplan.included_courses)
|
||||
return { ...rest, coursesInStudyPlan } as StudentWithCourses
|
||||
})
|
||||
}
|
||||
|
||||
export const getCompletedCourses = async (studentNumbers: string[], courseCodes: string[]) => {
|
||||
@@ -161,7 +170,8 @@ export const getCompletedCourses = async (studentNumbers: string[], courseCodes:
|
||||
lastname: student.lastname,
|
||||
email: student.email,
|
||||
sis_person_id: student.sis_person_id,
|
||||
secondary_email: student.secondary_email,
|
||||
secondaryEmail: student.secondary_email,
|
||||
coursesInStudyPlan: student.coursesInStudyPlan,
|
||||
credits: [],
|
||||
enrollments: [],
|
||||
}
|
||||
@@ -201,7 +211,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,
|
||||
})
|
||||
})
|
||||
@@ -217,7 +226,8 @@ export const getCompletedCourses = async (studentNumbers: string[], courseCodes:
|
||||
firstnames: student.firstnames,
|
||||
lastname: student.lastname,
|
||||
email: student.email,
|
||||
secondaryEmail: student.secondary_email,
|
||||
secondaryEmail: student.secondaryEmail,
|
||||
coursesInStudyPlan: student.coursesInStudyPlan,
|
||||
})
|
||||
return acc
|
||||
}, [] as FormattedStudent[])
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { orderBy } from 'lodash'
|
||||
import { Op } from 'sequelize'
|
||||
|
||||
import { Course } from '../../models'
|
||||
@@ -52,7 +53,17 @@ const getRawCourses = async (name: string, code: string) => {
|
||||
const getCourses = async (name: string, code: string) => {
|
||||
const rawCourses = await getRawCourses(name, code)
|
||||
const courses: CourseWithSubsId[] = rawCourses
|
||||
.map(course => ({ ...course.dataValues }))
|
||||
.map(course => ({
|
||||
...course.toJSON(),
|
||||
substitutions: orderBy(course.substitutions, [
|
||||
substitution => {
|
||||
if (/^A/.exec(substitution)) return 4 // open university codes come last
|
||||
if (/^\d/.exec(substitution)) return 2 // old numeric codes come second
|
||||
if (/^[A-Za-z]/.exec(substitution)) return 1 // new letter based codes come first
|
||||
return 3 // unknown, comes before open uni?
|
||||
},
|
||||
]),
|
||||
}))
|
||||
.sort((a, b) => getSortRank(b.code) - getSortRank(a.code))
|
||||
return courses
|
||||
}
|
||||
@@ -87,7 +98,7 @@ export const getCoursesByNameAndOrCode = async (name: string, code: string) => {
|
||||
}
|
||||
})
|
||||
|
||||
return { courses }
|
||||
return courses
|
||||
}
|
||||
|
||||
export const getCoursesByCodes = (codes: string[]) => {
|
||||
|
||||
@@ -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,7 @@
|
||||
import { EnrollmentState, Name, PassedCategory } from '../../types'
|
||||
import { max, min } from 'lodash'
|
||||
|
||||
import { Name } from '../../shared/types'
|
||||
import { getSemesterNameByCode } from '../semesters'
|
||||
import { FormattedProgramme, OrganizationDetails } from './helpers'
|
||||
|
||||
type Programme = {
|
||||
@@ -34,13 +37,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 +47,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 +114,12 @@ export class CourseYearlyStatsCounter {
|
||||
},
|
||||
},
|
||||
students: {
|
||||
categories: {
|
||||
passedFirst: [],
|
||||
passedEventually: [],
|
||||
neverPassed: [],
|
||||
},
|
||||
grades: {},
|
||||
studentnumbers: [],
|
||||
studentNumbers: [],
|
||||
},
|
||||
enrollments: [],
|
||||
allEnrollments: [],
|
||||
yearcode: yearCode,
|
||||
yearCode,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -147,7 +139,6 @@ export class CourseYearlyStatsCounter {
|
||||
if (!this.facultyStats[yearCode]) {
|
||||
this.initFacultyYear(yearCode)
|
||||
}
|
||||
|
||||
if (
|
||||
facultyCode &&
|
||||
!this.facultyStats[yearCode].faculties[facultyCode] &&
|
||||
@@ -163,7 +154,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 +162,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 +200,6 @@ export class CourseYearlyStatsCounter {
|
||||
|
||||
public markEnrollmentToGroup(
|
||||
studentNumber: string,
|
||||
state: string,
|
||||
enrollmentDateTime: Date,
|
||||
groupCode: number,
|
||||
groupName: string | Name,
|
||||
@@ -221,22 +209,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 +251,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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -339,15 +287,42 @@ export class CourseYearlyStatsCounter {
|
||||
return this.programmes
|
||||
}
|
||||
|
||||
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)
|
||||
private insertEmptyRows = async () => {
|
||||
const first = min(Object.keys(this.groups).map(Number))
|
||||
const last = max(Object.keys(this.groups).map(Number))
|
||||
if (!(first && last)) {
|
||||
return
|
||||
}
|
||||
|
||||
const getNextYear = (name: string) => {
|
||||
const years = name.split('-')
|
||||
const startYear = parseInt(years[0], 10) + 1
|
||||
const endYear = parseInt(years[1], 10) + 1
|
||||
return `${startYear}-${endYear}`
|
||||
}
|
||||
|
||||
for (let i = first + 1; i <= last; i++) {
|
||||
if (this.groups[i]) {
|
||||
continue
|
||||
}
|
||||
|
||||
const previous = this.groups[i - 1]
|
||||
const semester = await getSemesterNameByCode(i)
|
||||
const name = typeof previous.name === 'string' ? getNextYear(previous.name) : semester.name
|
||||
this.initGroup(i, name, this.groups[i - 1].coursecode, i)
|
||||
}
|
||||
}
|
||||
|
||||
private async parseGroupStatistics(anonymizationSalt: string | null) {
|
||||
for (const [studentNumber, data] of this.students) {
|
||||
this.groups[data.code].students.studentNumbers.push(studentNumber)
|
||||
}
|
||||
|
||||
await this.insertEmptyRows()
|
||||
|
||||
const groupStatistics = Object.values(this.groups).map(({ ...rest }) => {
|
||||
const { students } = rest
|
||||
const grades = {}
|
||||
const grades = {} as Grades
|
||||
Object.keys(students.grades).forEach(student => {
|
||||
const { grade, passed } = students.grades[student]
|
||||
const parsedGrade = passed ? grade : '0'
|
||||
@@ -360,7 +335,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 +353,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
|
||||
@@ -411,10 +381,10 @@ export class CourseYearlyStatsCounter {
|
||||
return this.facultyStats
|
||||
}
|
||||
|
||||
public getFinalStatistics(anonymizationSalt: string | null) {
|
||||
public async getFinalStatistics(anonymizationSalt: string | null) {
|
||||
return {
|
||||
programmes: this.parseProgrammeStatistics(anonymizationSalt),
|
||||
statistics: this.parseGroupStatistics(anonymizationSalt),
|
||||
statistics: await this.parseGroupStatistics(anonymizationSalt),
|
||||
facultyStats: this.parseFacultyStatistics(anonymizationSalt),
|
||||
obfuscated: this.obfuscated,
|
||||
}
|
||||
|
||||
@@ -53,7 +53,7 @@ export const getStudentNumberToSrElementsMap = async (studentNumbers: string[])
|
||||
const studyRightIds = Object.keys(studyRightMap)
|
||||
|
||||
const studyRightElements = await SISStudyRightElement.findAll({
|
||||
attributes: ['code', 'name', 'startDate', 'studyRightId'],
|
||||
attributes: ['code', 'name', 'startDate', 'endDate', 'studyRightId'],
|
||||
where: {
|
||||
studyRightId: {
|
||||
[Op.in]: studyRightIds,
|
||||
@@ -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,9 +1,10 @@
|
||||
/* eslint-disable @typescript-eslint/naming-convention */
|
||||
import crypto from 'crypto'
|
||||
import moment from 'moment'
|
||||
import { Op } from 'sequelize'
|
||||
|
||||
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 { getSortRank } from '../../util/sortRank'
|
||||
import { CourseYearlyStatsCounter } from './courseYearlyStatsCounter'
|
||||
@@ -65,11 +66,22 @@ const parseCredit = (
|
||||
credits,
|
||||
attainment_date: attainmentDate,
|
||||
student_studentnumber: studentNumber,
|
||||
studyright_id: studyRightId,
|
||||
} = credit
|
||||
const { yearcode: yearCode, yearname: yearName, semestercode: semesterCode, name: semesterName } = semester
|
||||
|
||||
const studyRightElements = studentNumberToSrElementsMap[studentNumber] || []
|
||||
const programmes = studyRightElements.map(formatStudyRightElement)
|
||||
let programmeOfCredit: SISStudyRightElement | undefined
|
||||
programmeOfCredit = studyRightElements.find(studyRightElement => studyRightElement.studyRightId === studyRightId)
|
||||
if (!programmeOfCredit) {
|
||||
programmeOfCredit = studyRightElements
|
||||
.filter(studyRightElement =>
|
||||
moment(attainmentDate).isBetween(moment(studyRightElement.startDate), moment(studyRightElement.endDate))
|
||||
)
|
||||
.sort((a, b) => b.startDate.getTime() - a.startDate.getTime())[0] // The newest studyRightElement
|
||||
}
|
||||
|
||||
const programmes = programmeOfCredit ? [programmeOfCredit].map(formatStudyRightElement) : []
|
||||
|
||||
const formattedCredit: FormattedCredit = {
|
||||
yearCode,
|
||||
@@ -99,7 +111,6 @@ type FormattedEnrollment = {
|
||||
semesterCode: number
|
||||
semesterName: Name
|
||||
courseCode: string
|
||||
state: string
|
||||
enrollmentDateTime: Date
|
||||
studentNumber: string
|
||||
programmes: Array<Record<string, any>>
|
||||
@@ -114,7 +125,6 @@ const parseEnrollment = (
|
||||
const {
|
||||
studentnumber: studentNumber,
|
||||
semester,
|
||||
state,
|
||||
enrollment_date_time: enrollmentDateTime,
|
||||
course_code: courseCode,
|
||||
} = enrollment
|
||||
@@ -128,7 +138,6 @@ const parseEnrollment = (
|
||||
semesterCode,
|
||||
semesterName,
|
||||
courseCode,
|
||||
state,
|
||||
enrollmentDateTime,
|
||||
studentNumber,
|
||||
programmes: studyRightElements.map(formatStudyRightElement),
|
||||
@@ -218,32 +227,42 @@ 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)
|
||||
})
|
||||
|
||||
const statistics = counter.getFinalStatistics(anonymizationSalt)
|
||||
const statistics = await counter.getFinalStatistics(anonymizationSalt)
|
||||
|
||||
let substitutionCourses: Course[] | undefined
|
||||
if (combineSubstitutions && courseForSubs?.substitutions && courseForSubs.substitutions.length > 0) {
|
||||
substitutionCourses = await Course.findAll({
|
||||
where: {
|
||||
code: {
|
||||
[Op.in]: codes,
|
||||
},
|
||||
},
|
||||
attributes: ['code', 'name'],
|
||||
})
|
||||
}
|
||||
|
||||
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 +289,7 @@ export const maxYearsToCreatePopulationFrom = async (courseCodes: string[], unif
|
||||
attainment_date: {
|
||||
[Op.gt]: attainmentThreshold,
|
||||
},
|
||||
is_open,
|
||||
is_open: getIsOpen(unification),
|
||||
},
|
||||
order: [['attainment_date', 'ASC']],
|
||||
})
|
||||
@@ -311,7 +330,12 @@ 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,
|
||||
},
|
||||
})
|
||||
|
||||
const studentNumbers = {}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import { cloneDeep } from 'lodash'
|
||||
|
||||
import { NameWithCode } from '../../shared/types'
|
||||
import { DegreeProgrammeType } from '../../types'
|
||||
import { getBasicStats, setBasicStats } from '../analyticsService'
|
||||
import { getBasicStatsForStudytrack } from '../studyProgramme/studyProgrammeBasics'
|
||||
import type { ProgrammesOfOrganization } from './faculty'
|
||||
|
||||
type ProgrammeName = { code: string; en?: string; fi?: string; sv?: string }
|
||||
|
||||
type StudyTrackBasicStats = Awaited<ReturnType<typeof getBasicStatsForStudytrack>>
|
||||
|
||||
const calculateCombinedStats = async (
|
||||
@@ -19,12 +18,12 @@ const calculateCombinedStats = async (
|
||||
graphStats: [] as StudyTrackBasicStats['graphStats'],
|
||||
programmeTableStats: {} as Record<string, StudyTrackBasicStats['tableStats']>,
|
||||
tableStats: [] as StudyTrackBasicStats['tableStats'],
|
||||
titles: ['', 'Started\nstudying', 'Accepted', 'Graduated'],
|
||||
titles: ['Code', 'Started\nstudying', 'Accepted', 'Graduated'],
|
||||
}
|
||||
if (includeAllSpecials) {
|
||||
studentInfo.titles.push('Transferred out\nof programme', 'Transferred into\nprogramme')
|
||||
}
|
||||
const programmeNames: Record<string, ProgrammeName> = {}
|
||||
const programmeNames: Record<string, NameWithCode> = {}
|
||||
let years: Array<string | number> = []
|
||||
|
||||
for (const programme of facultyProgrammes) {
|
||||
@@ -106,7 +105,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]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -143,7 +142,7 @@ const calculateCombinedStats = async (
|
||||
graphStats: combinedGraphStats,
|
||||
programmeTableStats: combinedProgrammeTableStats,
|
||||
tableStats: combinedTableStats,
|
||||
titles: ['', 'All', 'Bachelors', 'Masters', 'Doctors', 'Others'],
|
||||
titles: ['Code', 'All', 'Bachelors', 'Masters', 'Doctors', 'Others'],
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user