mirror of
https://github.com/UniversityOfHelsinkiCS/oodikone.git
synced 2026-10-13 03:13:40 +03:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
26e153ed05 | ||
|
|
cce80ca01e | ||
|
|
17266587b8 | ||
|
|
b6e477dfe9 | ||
|
|
e25bf0968a | ||
|
|
fcb5da2d84 | ||
|
|
cb0456c57c | ||
|
|
95e2bc1dfd | ||
|
|
4e2cd8198c | ||
|
|
b8915e8179 | ||
|
|
23e9a6d2cb | ||
|
|
107d83e254 | ||
|
|
08248da57d | ||
|
|
f7ae0e7f73 | ||
|
|
34047cbefe | ||
|
|
66e96f47ca | ||
|
|
c7b7a7b75d | ||
|
|
70cf120d09 | ||
|
|
1c1305c839 | ||
|
|
b5e3ba4212 | ||
|
|
7f262875fc | ||
|
|
9837231125 | ||
|
|
7339456f99 | ||
|
|
e65b860b1e | ||
|
|
2555991b3b | ||
|
|
18016688fb | ||
|
|
76edc86d01 | ||
|
|
9795c1b172 | ||
|
|
ff2f5d5770 | ||
|
|
990b778338 | ||
|
|
2dcf460a60 | ||
|
|
010a3435a8 | ||
|
|
136ca9f2d5 | ||
|
|
f243e996d5 | ||
|
|
336337728a | ||
|
|
ba2191790f | ||
|
|
690ab3c996 | ||
|
|
7b7c17e5b2 | ||
|
|
7bcbbf0bba | ||
|
|
e432c8ea07 | ||
|
|
3773c9f766 | ||
|
|
80ee1f0eb4 | ||
|
|
3475ca7691 | ||
|
|
b56383b4ef | ||
|
|
3fa91c715b | ||
|
|
539c8401fd | ||
|
|
48afd61942 | ||
|
|
671e0c62a3 | ||
|
|
f1c03b4425 | ||
|
|
c3da2389aa | ||
|
|
870664a99c | ||
|
|
4c85b5a240 | ||
|
|
7796c515a8 | ||
|
|
530c066993 | ||
|
|
8247ce80c3 | ||
|
|
7b8088e25c | ||
|
|
d2aebfb529 | ||
|
|
0d39a500c2 | ||
|
|
3ec2bf67c2 | ||
|
|
67ffcb0936 | ||
|
|
30861dbd2b | ||
|
|
522497a458 | ||
|
|
f8cb49da85 | ||
|
|
2689390ff5 | ||
|
|
da89addb64 | ||
|
|
e808233592 | ||
|
|
d3eb17b3c8 | ||
|
|
be23bb702d | ||
|
|
beb78f260c | ||
|
|
0dfc356f26 | ||
|
|
f195cdc923 | ||
|
|
234bb52a98 | ||
|
|
23972100b3 | ||
|
|
9ffee4e1b7 | ||
|
|
e7d01bb7e5 | ||
|
|
515ea95b5a | ||
|
|
ba69a8ceaa | ||
|
|
efa03ad2f7 | ||
|
|
25967f3904 | ||
|
|
5cabec2a65 | ||
|
|
bff11a5e10 | ||
|
|
5230df7fa4 | ||
|
|
47b15d7bd7 | ||
|
|
e35a44687f | ||
|
|
322e7d9fb0 | ||
|
|
4c293b2668 | ||
|
|
d1c95d96a6 | ||
|
|
7722db6944 | ||
|
|
0de825f726 | ||
|
|
a4d6d3e17e | ||
|
|
f33d9a5f70 | ||
|
|
639ee6b875 | ||
|
|
a5957ca119 | ||
|
|
6783919799 | ||
|
|
f6951b6749 | ||
|
|
05055ec7ba | ||
|
|
2d1659325c | ||
|
|
2ce4130988 | ||
|
|
011d70df19 | ||
|
|
46f8c36ec0 | ||
|
|
1edc18e7c2 | ||
|
|
553fcaa062 | ||
|
|
98a359f5e3 | ||
|
|
c929b51fbe | ||
|
|
9f0a0b6263 | ||
|
|
8d9868cbea | ||
|
|
96f9d710af | ||
|
|
0e7a8be6f0 | ||
|
|
edab4c7be9 | ||
|
|
4dc592beb3 | ||
|
|
7ec551d04c | ||
|
|
7335072042 | ||
|
|
6b64de8639 | ||
|
|
09ad10d3f6 | ||
|
|
ea52669d60 | ||
|
|
ec78960166 | ||
|
|
34da9e4b51 | ||
|
|
4d56d3033b | ||
|
|
45f4bf2f98 | ||
|
|
6cad2430f6 | ||
|
|
7e68fdc537 | ||
|
|
a2e3560768 | ||
|
|
ad96c6d94b | ||
|
|
ce79f98db9 | ||
|
|
bb8816657e | ||
|
|
9aac1a398d | ||
|
|
1503b73ba3 | ||
|
|
62272d335a | ||
|
|
131ef56038 | ||
|
|
e7ac072a6b | ||
|
|
2049ec9a74 | ||
|
|
879387f7c6 | ||
|
|
b0bccbbb82 | ||
|
|
e48e4b445a | ||
|
|
2b1e75ff79 | ||
|
|
771ab99c75 | ||
|
|
0e4fbcd2eb | ||
|
|
08454bf727 | ||
|
|
4205614f2b | ||
|
|
c2634ba4d8 | ||
|
|
62e5f80901 | ||
|
|
878441dd71 | ||
|
|
2729fb2b60 | ||
|
|
8785591229 | ||
|
|
ec2dcd952a | ||
|
|
c622ed3fed | ||
|
|
fbc91af5dd | ||
|
|
c1bb118b77 | ||
|
|
b1802b7f39 | ||
|
|
a862b466ae | ||
|
|
0d7c9ea4c5 | ||
|
|
2512025efe | ||
|
|
aec91f47f6 | ||
|
|
e9c97ae1ae | ||
|
|
8b0b762493 | ||
|
|
e94d26fb93 | ||
|
|
c272c80d2f | ||
|
|
bc1dd24485 | ||
|
|
0ad9baf316 | ||
|
|
a269bf1e06 | ||
|
|
5aae988c9b | ||
|
|
c021a7d5f3 | ||
|
|
54ff132b4d | ||
|
|
7a896add54 | ||
|
|
a34280a7f7 | ||
|
|
d41e48c2b1 | ||
|
|
2eb3b692fe | ||
|
|
4686f08133 | ||
|
|
58a62e6099 | ||
|
|
46c7e32d7d | ||
|
|
7447cc3c24 | ||
|
|
854bee4f70 | ||
|
|
0074eceae1 | ||
|
|
5e3df648b7 | ||
|
|
521359ca54 | ||
|
|
1d4ef346be | ||
|
|
42777e7ca0 | ||
|
|
13d68152c6 | ||
|
|
695bf9ea9e | ||
|
|
e90df09334 | ||
|
|
f84b80efa6 | ||
|
|
181d934562 | ||
|
|
12da4909a7 | ||
|
|
80ea0ebe55 | ||
|
|
22763aa5c0 | ||
|
|
7ef058effd | ||
|
|
5290ebbd3a | ||
|
|
9f270db4a1 | ||
|
|
b79a60c18b | ||
|
|
aba33dc64e | ||
|
|
ea4081722a | ||
|
|
3d37ef2f8f | ||
|
|
1641d0b345 | ||
|
|
1027fcb386 | ||
|
|
6fce473592 | ||
|
|
3578c2d3aa | ||
|
|
f0455fd54d | ||
|
|
94938d03b5 | ||
|
|
744359ed5b | ||
|
|
1d75db9991 | ||
|
|
66c0b1b7ce | ||
|
|
8f43628f16 | ||
|
|
552099840a | ||
|
|
02912e8ad6 | ||
|
|
4e55304a45 | ||
|
|
a6d4b8962d | ||
|
|
f98133b158 | ||
|
|
93a338a23b | ||
|
|
170c0478a4 | ||
|
|
af8d71de9f | ||
|
|
e82dd33be1 | ||
|
|
0ce0baf60d | ||
|
|
e28768e729 | ||
|
|
f195a3ca2a | ||
|
|
d7cac8ff7f | ||
|
|
43519fac11 | ||
|
|
2ec286e958 | ||
|
|
17c82bf346 | ||
|
|
1234a5c028 | ||
|
|
30331d9548 | ||
|
|
4e09c252c5 | ||
|
|
5e996b39db | ||
|
|
e256e74c79 | ||
|
|
c7e068be32 | ||
|
|
fbc04d2d20 | ||
|
|
986a6299be | ||
|
|
ea0c635069 | ||
|
|
abe2ed9253 | ||
|
|
4f5398c5e5 | ||
|
|
f69c724a4d | ||
|
|
9fbc072df6 | ||
|
|
50b1c5e758 | ||
|
|
9f4dc4ad8c | ||
|
|
56567486df | ||
|
|
4ca3712ca7 | ||
|
|
9979d4f114 | ||
|
|
d2fef216e4 | ||
|
|
d752850af5 | ||
|
|
664f56a92d | ||
|
|
ee9bd8e3b5 | ||
|
|
db071a3b0d | ||
|
|
e62e5f5b46 | ||
|
|
9014529780 | ||
|
|
744c89a5fe | ||
|
|
ac842447bf | ||
|
|
b00a753c8b | ||
|
|
c1e9acf004 | ||
|
|
449f401e0e | ||
|
|
8ec4fcb768 | ||
|
|
30019549e6 | ||
|
|
2c2aa2586a | ||
|
|
0d17357f84 | ||
|
|
1b5b566bb8 | ||
|
|
c098ef6c50 | ||
|
|
4feb48aacd | ||
|
|
102e854a9a | ||
|
|
f5267147c1 | ||
|
|
afa380fa0b | ||
|
|
b141664c4c | ||
|
|
9514690dad | ||
|
|
18036ee725 | ||
|
|
6c21710d85 | ||
|
|
4846ba89a3 | ||
|
|
1822b6143b | ||
|
|
cbe78827e6 | ||
|
|
5d73f3a196 | ||
|
|
01aa928a75 | ||
|
|
1100824402 | ||
|
|
67ae9471dd | ||
|
|
6e8c3d04c0 | ||
|
|
3447bb6458 | ||
|
|
0afb553402 | ||
|
|
739be5ff04 | ||
|
|
362ce2a11e | ||
|
|
d05cf6e0de | ||
|
|
14707c37f0 | ||
|
|
a2b9552360 | ||
|
|
26f8322aa0 | ||
|
|
6fdae28caf | ||
|
|
e83a5145fb | ||
|
|
1a4ff32fd7 | ||
|
|
ff32321aa6 | ||
|
|
1ce7c0d44e | ||
|
|
9442f36619 | ||
|
|
0a6fb30315 | ||
|
|
712bbc320b | ||
|
|
83c9036463 | ||
|
|
be6e533658 | ||
|
|
a25d9f48fd | ||
|
|
827978948f | ||
|
|
bde98ec459 | ||
|
|
35fb9924da | ||
|
|
fee7e5977c | ||
|
|
f6be1eccd2 | ||
|
|
839f09baf5 | ||
|
|
3b9ecc9f63 | ||
|
|
3f0e5d26f2 | ||
|
|
27e7fc2ef3 | ||
|
|
eddf75de8d | ||
|
|
a1b1c83571 | ||
|
|
b99cadd306 | ||
|
|
4ef2c42681 | ||
|
|
a3bb1b55f7 | ||
|
|
d4be4a17cd | ||
|
|
35eb08c6fb | ||
|
|
8e90562c6d | ||
|
|
01cdbf5b16 | ||
|
|
1ffc0864bd | ||
|
|
f7ff684cc9 | ||
|
|
7ea5d1b150 | ||
|
|
7c1ed89de0 | ||
|
|
3d2195b690 | ||
|
|
e2d169dea0 | ||
|
|
c9d06c284c | ||
|
|
edda7d49f5 | ||
|
|
20f4942e40 | ||
|
|
912a32a83e | ||
|
|
89d685bee1 | ||
|
|
8535938ca5 | ||
|
|
1213fdaf5a | ||
|
|
7894621cd8 | ||
|
|
1d50dc6a28 | ||
|
|
7f62197c89 | ||
|
|
40b1586f84 | ||
|
|
d0ee0e5d5c | ||
|
|
8acfe6b1b1 | ||
|
|
2baf4f8a5d | ||
|
|
50779b8371 | ||
|
|
dc5a4f79c6 | ||
|
|
9c49678285 | ||
|
|
a253d2af0f | ||
|
|
acc8eb52b2 | ||
|
|
8006d2ccde | ||
|
|
92f13dbc7f | ||
|
|
c0d57c764a | ||
|
|
99fd5578a3 | ||
|
|
732a226412 | ||
|
|
f37aa1c93f | ||
|
|
d68136a450 | ||
|
|
d0dbb41eb2 | ||
|
|
4b050c7422 | ||
|
|
d4a5eb1ba0 | ||
|
|
59bf9f52b8 | ||
|
|
03d0851eac | ||
|
|
9063b45c0a | ||
|
|
de7ed6fb27 | ||
|
|
f132ebf8af | ||
|
|
7bef495e49 | ||
|
|
e4d18f7609 | ||
|
|
c8ba472c01 | ||
|
|
3ea8ab07a1 | ||
|
|
4e324dc5d3 | ||
|
|
47b25a76ce | ||
|
|
8eba3bcb7b | ||
|
|
7a6f3b348a | ||
|
|
3d4f1a6c74 | ||
|
|
84dd0cd9ff | ||
|
|
ded35789ff | ||
|
|
0fbdf676b2 | ||
|
|
7162a6e432 | ||
|
|
8b2dabd497 | ||
|
|
d444fc411e | ||
|
|
6fb329684b | ||
|
|
e2547abf97 | ||
|
|
457f9162d9 | ||
|
|
324d453df6 | ||
|
|
2b7cc537a9 | ||
|
|
97d9d06252 | ||
|
|
4f8284d235 | ||
|
|
c586c4a3d7 | ||
|
|
5e9fc54c9c | ||
|
|
4a448d6661 | ||
|
|
120024aaf2 | ||
|
|
9f59017b6b | ||
|
|
846311d6b6 | ||
|
|
16a3b58e7f | ||
|
|
7ffb30821e | ||
|
|
19106b8208 | ||
|
|
56e604f586 | ||
|
|
0f2d85d948 | ||
|
|
7ce6326339 | ||
|
|
e88cca1e7a | ||
|
|
ebff7ff114 | ||
|
|
25307cc69d | ||
|
|
9864d85ec9 | ||
|
|
1011141394 | ||
|
|
2a0500acc9 | ||
|
|
3129130120 | ||
|
|
dfe22dc2dc | ||
|
|
4f00b09052 | ||
|
|
4f1e6bc7f1 | ||
|
|
b38c1ca65d | ||
|
|
5c0b2454a1 | ||
|
|
487d45ee38 | ||
|
|
804eb072b9 | ||
|
|
3222e71444 | ||
|
|
fcbf2ed7ae | ||
|
|
fda3081df8 | ||
|
|
f0821ed78d | ||
|
|
05cd3221e4 | ||
|
|
1165875599 | ||
|
|
14bc6dc4e8 | ||
|
|
886adcd5fb | ||
|
|
f2c01e6c6a | ||
|
|
f7aff532a8 | ||
|
|
0d13d75a82 | ||
|
|
758884f3ee | ||
|
|
7bceadcd2a | ||
|
|
e83ae489ad | ||
|
|
160dff6e89 | ||
|
|
f6efe102c7 | ||
|
|
ac06cb95b6 | ||
|
|
33d7972835 | ||
|
|
197dbb90b6 | ||
|
|
8c6ad1da30 | ||
|
|
149d4485d2 | ||
|
|
e7fbf9c44b | ||
|
|
b5dc1ea51c | ||
|
|
4447ea66e9 | ||
|
|
be8f5de412 | ||
|
|
bbda9e3914 | ||
|
|
c04ad4f02d | ||
|
|
b70dc1abd9 | ||
|
|
a7038e3ada | ||
|
|
519ad889c7 | ||
|
|
e45620b541 | ||
|
|
a814909a27 | ||
|
|
7723cb03a3 | ||
|
|
d23923cc66 | ||
|
|
674349bce5 | ||
|
|
b517457c16 | ||
|
|
adc975e46e | ||
|
|
5c4a7b85a6 | ||
|
|
94c63f9429 | ||
|
|
4bd37de38e | ||
|
|
4b13dfeae5 | ||
|
|
1a7a625e81 | ||
|
|
2cd9077088 | ||
|
|
d45b57bc67 | ||
|
|
bc1eb1fe48 | ||
|
|
a8c9c0dd15 | ||
|
|
5b14afc587 | ||
|
|
56eef7742c | ||
|
|
2ebc864a24 | ||
|
|
0c0dcb697c | ||
|
|
decf88bd52 | ||
|
|
872e9b1849 | ||
|
|
3f03cbe63d | ||
|
|
4f630ed168 | ||
|
|
bb83a0f09a | ||
|
|
8d1f41ecf7 | ||
|
|
5769a7041c | ||
|
|
18088ab0e9 | ||
|
|
db133cc6ec | ||
|
|
b741404678 | ||
|
|
1e68713472 | ||
|
|
e04a8b5fe2 | ||
|
|
997dddc298 | ||
|
|
783d720a0a | ||
|
|
4f36bb6a55 | ||
|
|
a577caaf46 | ||
|
|
5196f3158e | ||
|
|
646617be61 | ||
|
|
b1030a4076 | ||
|
|
5120517754 | ||
|
|
fd5d755a29 | ||
|
|
88ba467ef7 | ||
|
|
c951e49aa3 | ||
|
|
caafdb3139 | ||
|
|
aad66cf9e4 | ||
|
|
32d91b916e | ||
|
|
d6f35c38f8 | ||
|
|
87823b5bde | ||
|
|
efe8f802a6 | ||
|
|
8463cdc327 | ||
|
|
11cdaf997d | ||
|
|
db025d68af | ||
|
|
fb65fe89c3 | ||
|
|
c01b5415a0 | ||
|
|
8c9c091e87 | ||
|
|
deb08e886f | ||
|
|
7715e5964a | ||
|
|
7a226c2895 | ||
|
|
861badff3d | ||
|
|
c7639859b0 | ||
|
|
c2c3d4495c | ||
|
|
92c05650fd | ||
|
|
0fc4e57798 | ||
|
|
2be9712b19 | ||
|
|
dab251084e | ||
|
|
e4337d0cf2 | ||
|
|
1272a342e6 | ||
|
|
3ee3baf548 | ||
|
|
761612878a | ||
|
|
9453877c00 | ||
|
|
4c5b571df7 | ||
|
|
39a646c12d | ||
|
|
66039b184e | ||
|
|
4eab18ea31 | ||
|
|
0b9fe19272 | ||
|
|
9a1668e132 | ||
|
|
720fda4725 | ||
|
|
b3904a56c7 | ||
|
|
c035404b89 | ||
|
|
b36b98e6b2 | ||
|
|
f70d237163 | ||
|
|
2a6bedccfb | ||
|
|
25ea34fbd4 | ||
|
|
2f2e86d975 | ||
|
|
7da0a4de0d | ||
|
|
3bffb3d934 | ||
|
|
123a86077b | ||
|
|
6c2c216797 | ||
|
|
bfaad2af95 | ||
|
|
9faa9d0ef4 | ||
|
|
4e917800c9 | ||
|
|
146166e5ed | ||
|
|
9e49edb33a | ||
|
|
a681cf3b36 | ||
|
|
7af8e848f9 | ||
|
|
ca79c82ddd | ||
|
|
46b7659e73 | ||
|
|
26f44079e2 | ||
|
|
03da43bfe1 | ||
|
|
251f5526c0 | ||
|
|
764e09b34a | ||
|
|
933f332ea3 | ||
|
|
6facbbdb36 | ||
|
|
23e2622c3a | ||
|
|
1fab787ed9 | ||
|
|
db86775288 | ||
|
|
288403c90c | ||
|
|
93b8bef0a5 | ||
|
|
e5880373b1 | ||
|
|
d47985349a | ||
|
|
7e177d6252 | ||
|
|
f224903e7c | ||
|
|
feff8bed08 | ||
|
|
59287b9622 | ||
|
|
f62635076b | ||
|
|
eab18870ea | ||
|
|
cf5078b7a3 | ||
|
|
3e14a7a2e4 | ||
|
|
81a22c571a | ||
|
|
297d697e75 | ||
|
|
7b180d63a1 | ||
|
|
12520a9665 | ||
|
|
f403be88b3 | ||
|
|
0c82f32d20 | ||
|
|
61fb4da81c | ||
|
|
0d5daf423f | ||
|
|
42b41ecece | ||
|
|
b21b098ff4 | ||
|
|
9be3c4e852 | ||
|
|
ca67096bc5 | ||
|
|
5d5ed09b98 | ||
|
|
5e2ed40ac5 | ||
|
|
2e2c7c4ad4 | ||
|
|
db64cb8ae2 | ||
|
|
7422a55799 | ||
|
|
59f29de345 | ||
|
|
b9a10d8d51 | ||
|
|
391ffeefe0 | ||
|
|
6a2f6fc50c | ||
|
|
29c38ee38e | ||
|
|
8d4ec802ec | ||
|
|
99dfb4d26f | ||
|
|
1eb4e9a46f | ||
|
|
e89d4709c1 | ||
|
|
e226bdcdca | ||
|
|
575cddfd10 | ||
|
|
4e37801352 | ||
|
|
2fb270a070 | ||
|
|
c894f858ca | ||
|
|
f5b04beda0 | ||
|
|
c20a4e4727 | ||
|
|
3081752d8f | ||
|
|
19fe7fd817 | ||
|
|
99b3f7756c | ||
|
|
51475be0db | ||
|
|
a8eb68e317 | ||
|
|
8281564fef | ||
|
|
4373065fdd | ||
|
|
8dbbf00b51 | ||
|
|
584a1b474a | ||
|
|
8151c4f463 | ||
|
|
bb818ee9ac | ||
|
|
cd3b9bb843 | ||
|
|
aaa012894c | ||
|
|
4d06eb7558 | ||
|
|
b563b29a36 | ||
|
|
617e234798 | ||
|
|
754d525f87 | ||
|
|
a0e6d342b9 | ||
|
|
60f500c9e7 | ||
|
|
dc9ce89aa7 | ||
|
|
8a1fb2725b | ||
|
|
0ca6bca243 | ||
|
|
7457bc412b | ||
|
|
3d2e8fb1ca | ||
|
|
1d980795c6 | ||
|
|
6bc6a28127 | ||
|
|
a2f84931cd | ||
|
|
1f1bae8557 | ||
|
|
c714c61269 | ||
|
|
e85deff947 | ||
|
|
8c87135f58 | ||
|
|
7eac6c686e | ||
|
|
11172f7d77 | ||
|
|
be29486cdd | ||
|
|
04cbec19a5 | ||
|
|
7c3cfb56b5 | ||
|
|
090305be3b | ||
|
|
c1e0d34670 | ||
|
|
9cfc51382d | ||
|
|
3f179d3460 | ||
|
|
0ad0d73d18 | ||
|
|
7952b3703f | ||
|
|
e9902a0bbe | ||
|
|
b225e03bd5 | ||
|
|
374cef4a1a | ||
|
|
6dff16f683 | ||
|
|
73a1d50ec1 | ||
|
|
b473b393f0 | ||
|
|
218f09ea65 | ||
|
|
5948a34c6e | ||
|
|
6db0e982d9 | ||
|
|
205637c4db | ||
|
|
7ac397969a | ||
|
|
9fc927daff | ||
|
|
72635b53ac | ||
|
|
980a7a6703 | ||
|
|
80709a129c | ||
|
|
26c7f3e00b | ||
|
|
cc60c9f415 | ||
|
|
7f8f40a193 | ||
|
|
1749503aa4 | ||
|
|
da55bfefac | ||
|
|
0c2f197460 | ||
|
|
6503971915 | ||
|
|
c30624d319 | ||
|
|
a4c798ea25 | ||
|
|
bf175e9140 | ||
|
|
70f4440974 | ||
|
|
cde5dd5ced | ||
|
|
35777287b2 | ||
|
|
3ec010f347 | ||
|
|
36792ac667 | ||
|
|
d4f187ce92 | ||
|
|
0af1e0d82e | ||
|
|
74803f7af4 | ||
|
|
4817393665 | ||
|
|
80de8768e3 | ||
|
|
3f696acf99 | ||
|
|
831cab9e2b | ||
|
|
94b0c0f241 | ||
|
|
5d51096557 | ||
|
|
be90065bd4 | ||
|
|
9501055406 | ||
|
|
66685ee0ff | ||
|
|
84cb0ca7a1 | ||
|
|
c16e85d688 | ||
|
|
0781f2a9b0 | ||
|
|
0689e83cbd | ||
|
|
6e407fc196 | ||
|
|
4d6abef0b9 | ||
|
|
1f1e2d45fa | ||
|
|
3028cdb6ec | ||
|
|
ed71a0ce3b | ||
|
|
f3c290a5ee | ||
|
|
7ab8236e14 | ||
|
|
a884069e77 | ||
|
|
1d8a06c477 | ||
|
|
88ba4af98c | ||
|
|
4ed75331ff | ||
|
|
63c2d9756f | ||
|
|
2592adfd18 | ||
|
|
d8976adb46 | ||
|
|
f47f4bca29 | ||
|
|
a5d824b927 | ||
|
|
e20e1137ae | ||
|
|
11792b0397 | ||
|
|
3cb2e370aa | ||
|
|
db5b0a29cd | ||
|
|
79f9a59116 | ||
|
|
fdba5dc6a6 | ||
|
|
d0e8a9632d | ||
|
|
16b7debbb5 | ||
|
|
6081bb3e78 | ||
|
|
de2823f3b5 | ||
|
|
07e2dabe3f | ||
|
|
1cccecc7f5 | ||
|
|
37a50731aa | ||
|
|
eba296f5fd | ||
|
|
fbe0b0af3a | ||
|
|
ea4d299f16 | ||
|
|
f2f9bdd9e8 | ||
|
|
4b86ed252f | ||
|
|
f535a5dbdb | ||
|
|
ffe6425999 | ||
|
|
256997633a | ||
|
|
1d3fef6c8d | ||
|
|
428e46084c | ||
|
|
09f4814f80 | ||
|
|
a220050a53 | ||
|
|
115674636f | ||
|
|
f901fec6a3 | ||
|
|
a77ba0e1e1 | ||
|
|
434183bac0 | ||
|
|
7993c2b45a | ||
|
|
8551a76fc9 | ||
|
|
b25f9ba55f | ||
|
|
f28276e086 | ||
|
|
6327ca7e2c | ||
|
|
625f2962eb | ||
|
|
f85e9f0a5d | ||
|
|
d4d67b239e | ||
|
|
83d17a3026 | ||
|
|
e3f8fdb4ae | ||
|
|
b7051ccd41 | ||
|
|
b0a0be7969 | ||
|
|
896cb46ad1 | ||
|
|
17f7289b1c | ||
|
|
a019dd9672 | ||
|
|
881aaf93b4 | ||
|
|
5c973d4f8c | ||
|
|
f9e8bf6d31 | ||
|
|
a2726d4894 | ||
|
|
11377da7bd | ||
|
|
8c4515b158 | ||
|
|
db3cd52d69 | ||
|
|
b4c8bf0f4a | ||
|
|
384afb1e5f | ||
|
|
090d327405 | ||
|
|
30117e60b3 | ||
|
|
9a4fec0974 | ||
|
|
37114c5570 | ||
|
|
63b7c5876d | ||
|
|
9790c829c6 | ||
|
|
082aaa4022 | ||
|
|
3906efbcca | ||
|
|
9d575d6c88 | ||
|
|
4d4a288719 | ||
|
|
a39f522501 | ||
|
|
32293eb050 | ||
|
|
ed56d32cce | ||
|
|
f4b04d3283 | ||
|
|
3a6e5132f3 | ||
|
|
694e537baa | ||
|
|
372ac53a46 | ||
|
|
5d9bbe368c | ||
|
|
3d552a5811 | ||
|
|
624a103f08 | ||
|
|
9f56554ed9 | ||
|
|
0d1bc3c519 | ||
|
|
aa1d21bc09 | ||
|
|
eaf5d7574e | ||
|
|
9db14a839f | ||
|
|
b55d649622 | ||
|
|
47c792e1f1 | ||
|
|
fffa3d3ee1 | ||
|
|
c9407ff4b0 | ||
|
|
1b43d4fc39 | ||
|
|
c0ce396d4d | ||
|
|
f15ffa95f5 | ||
|
|
765d2f771a | ||
|
|
e1271d7d1f | ||
|
|
7f632bde34 | ||
|
|
9dd47d002d | ||
|
|
16a07ace39 | ||
|
|
ac756cb35b | ||
|
|
b9059eb058 | ||
|
|
1f967a1f2c | ||
|
|
e8ef944a9d | ||
|
|
0e9ea20235 | ||
|
|
8d4f1c7794 | ||
|
|
6f3e4b8f64 | ||
|
|
316ed2af2f | ||
|
|
2f753d976b | ||
|
|
9b6aa96531 | ||
|
|
229a64aa45 | ||
|
|
6456b84777 | ||
|
|
9d34c34de7 | ||
|
|
2573976ea4 | ||
|
|
389b71cfbf | ||
|
|
c0ee74b7f8 | ||
|
|
e3fbf91b99 | ||
|
|
3d1f7cc655 | ||
|
|
069bd97729 | ||
|
|
96fded285b | ||
|
|
9c172c2f97 | ||
|
|
d37b037cc6 | ||
|
|
a55eef0f34 | ||
|
|
01ec83d909 | ||
|
|
81e8a0c77e | ||
|
|
7782f4f863 | ||
|
|
80f52355bb | ||
|
|
28cf6c855d | ||
|
|
6d446a1c05 | ||
|
|
d8214e7432 | ||
|
|
a8b04d56d6 | ||
|
|
0fc7bfda51 | ||
|
|
5614a6eb3f | ||
|
|
dfde4adf87 | ||
|
|
32a8c7c261 | ||
|
|
b7390912f4 | ||
|
|
6af7a1a5d0 | ||
|
|
e24b63da74 | ||
|
|
bb5bb8d04e | ||
|
|
a91fb5d7b7 | ||
|
|
5369e75b61 | ||
|
|
7a43433947 | ||
|
|
656870028e | ||
|
|
1315d7ca81 | ||
|
|
3b1f002ac8 | ||
|
|
00116544bf | ||
|
|
7bbccc9a81 | ||
|
|
c18187d094 | ||
|
|
7688a22c53 | ||
|
|
49df0a0d21 | ||
|
|
449e8976d9 | ||
|
|
6871502cb9 | ||
|
|
b72bedc49f | ||
|
|
9f300ccab0 | ||
|
|
cb8aa00f6f | ||
|
|
4a05f136cc | ||
|
|
50840a3fe2 | ||
|
|
ac440ff843 | ||
|
|
b6d28c6cff | ||
|
|
f79539c2a6 | ||
|
|
c46173cc98 | ||
|
|
a13c0e4628 | ||
|
|
a4887a30bc | ||
|
|
bf3adf446f | ||
|
|
a03d1c9707 | ||
|
|
2d9fe72481 | ||
|
|
15ef2f156e | ||
|
|
5470d53a19 | ||
|
|
65eedf1de8 | ||
|
|
1a9e51534b | ||
|
|
0d7d059541 | ||
|
|
2c76703085 | ||
|
|
81c45c383f | ||
|
|
9bb456e258 | ||
|
|
c9641bb118 | ||
|
|
06d129b685 | ||
|
|
b78446a11a | ||
|
|
3511b947c4 | ||
|
|
a338b2f4d5 | ||
|
|
3de3bbab43 | ||
|
|
b24b30b179 | ||
|
|
8af06138a9 | ||
|
|
8b30e2b416 | ||
|
|
562ee801d7 | ||
|
|
eb9b3ccaae | ||
|
|
3bf1c0e47d | ||
|
|
2f028ff9e0 | ||
|
|
b529c219ea | ||
|
|
6369bdb62c | ||
|
|
69803e528f | ||
|
|
e71f75bd72 | ||
|
|
964500fb52 | ||
|
|
9241efb16a | ||
|
|
e585e59907 | ||
|
|
5a4e724021 | ||
|
|
7adc101024 | ||
|
|
57bfa879ae | ||
|
|
38174d36d9 | ||
|
|
062bdfbe0b | ||
|
|
d0540610c1 | ||
|
|
db43907ce3 | ||
|
|
be906d262e | ||
|
|
a28b7d8bc5 | ||
|
|
ba9c3ac3e3 | ||
|
|
644ef5a031 | ||
|
|
80e6e7f5dc | ||
|
|
962381e8b7 | ||
|
|
26f0eb80c1 | ||
|
|
ff901f63b8 | ||
|
|
0e601fa712 | ||
|
|
dde20472e4 | ||
|
|
9097a3e32d | ||
|
|
533dd9aeba | ||
|
|
c2b6006580 | ||
|
|
b051783ecf | ||
|
|
a3730bb88f | ||
|
|
3deedff357 | ||
|
|
906d66e5cd | ||
|
|
467009c598 | ||
|
|
ba233aba5e | ||
|
|
93656322b2 | ||
|
|
819c8cda22 | ||
|
|
ab76ee4349 | ||
|
|
0ccb1de58b | ||
|
|
128a41c0ab | ||
|
|
277f88a215 | ||
|
|
354e97e100 | ||
|
|
e65a96ced5 | ||
|
|
e19a2b574d | ||
|
|
26dd92288e | ||
|
|
9c83bae7af | ||
|
|
57c6c500a2 | ||
|
|
1ecd01d55f | ||
|
|
80248639df | ||
|
|
ad0bbf996c | ||
|
|
f904f63b9a | ||
|
|
3b82be53d0 | ||
|
|
6d1120f3a4 | ||
|
|
7180dd5a34 | ||
|
|
5806cd6baf | ||
|
|
e98df56fab | ||
|
|
1b7f60c4a2 | ||
|
|
6a85dd10a1 | ||
|
|
679175385f | ||
|
|
84cfde0c39 | ||
|
|
878b391aac | ||
|
|
53129e7683 | ||
|
|
27eb8984b5 | ||
|
|
e1f5bc5ec2 | ||
|
|
3698b8d38b | ||
|
|
850a429165 | ||
|
|
9e1c0751d3 | ||
|
|
414495590f | ||
|
|
915128dce6 | ||
|
|
8cb8e3c595 | ||
|
|
432fd7dbc0 | ||
|
|
b07070dc7f | ||
|
|
2ef6c2a47f | ||
|
|
67de743838 | ||
|
|
37f7e06ba5 | ||
|
|
7e798821c8 | ||
|
|
04692463f0 | ||
|
|
c3fecddc61 | ||
|
|
bed3e64e1f | ||
|
|
98cb0f5e3e | ||
|
|
d062788864 | ||
|
|
f2b19d5f74 | ||
|
|
e624152e3e | ||
|
|
d296508eb5 | ||
|
|
88b00beb5d | ||
|
|
a9057b949a | ||
|
|
d1744d9fb8 | ||
|
|
3e83b800d7 | ||
|
|
49891f3779 | ||
|
|
0aaa745763 | ||
|
|
e4edf31057 | ||
|
|
8c8b8ccb1d | ||
|
|
dc339896a1 | ||
|
|
da0f76e2a8 | ||
|
|
0fa9d8a1ca | ||
|
|
95066dde6e | ||
|
|
e17ff572bf | ||
|
|
4eb748f76d | ||
|
|
e6147effdf | ||
|
|
7e333668c5 | ||
|
|
45fb6659a9 | ||
|
|
0f73e5025b | ||
|
|
69f0b4cb75 |
@@ -15,24 +15,17 @@ inputs:
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- name: 'Download images'
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
path: /tmp/
|
||||
- name: 'Load built images'
|
||||
shell: bash
|
||||
run: |
|
||||
docker load --input /tmp/artifact/backend.tar
|
||||
docker load --input /tmp/artifact/frontend.tar
|
||||
- name: 'Login to toska docker registry'
|
||||
- name: 'Login to toska Docker registry'
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
registry: registry.toska.cs.helsinki.fi
|
||||
registry: registry-toska.ext.ocp-prod-0.k8s.it.helsinki.fi
|
||||
username: ${{ inputs.username }}
|
||||
password: ${{ inputs.password }}
|
||||
|
||||
- name: 'Run services, pull rest of the needed images'
|
||||
shell: bash
|
||||
run: docker-compose --file docker-compose.ci.yml up --detach --no-recreate
|
||||
run: docker-compose --file docker-compose.ci.yml up --detach
|
||||
|
||||
- name: 'Run cypress'
|
||||
uses: cypress-io/github-action@v4
|
||||
with:
|
||||
@@ -40,12 +33,14 @@ runs:
|
||||
spec: ${{ inputs.spec }}
|
||||
env:
|
||||
CYPRESS_baseUrl: http://localhost:1337
|
||||
|
||||
- name: 'Upload video if tests fail'
|
||||
uses: actions/upload-artifact@v3
|
||||
if: failure()
|
||||
with:
|
||||
name: Cypress videos
|
||||
path: ./cypress/videos/*
|
||||
|
||||
- name: 'Print logs on failure'
|
||||
shell: bash
|
||||
if: failure()
|
||||
@@ -0,0 +1,149 @@
|
||||
name: 'Release to production'
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
jobs:
|
||||
release_frontend:
|
||||
name: 'Build and release frontend'
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: 'Get current time'
|
||||
id: get_current_time
|
||||
run: echo "NOW=$(date --iso-8601=seconds)" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Extract release version
|
||||
id: extract_version
|
||||
run: echo "VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Build frontend image
|
||||
id: build-image
|
||||
uses: redhat-actions/buildah-build@v2
|
||||
with:
|
||||
image: oodikone-frontend
|
||||
tags: production ${{ github.sha }}
|
||||
context: services/frontend
|
||||
containerfiles: |
|
||||
services/frontend/Dockerfile
|
||||
build-args: |
|
||||
BUILT_AT=${{ steps.get_current_time.outputs.NOW }}
|
||||
SENTRY_RELEASE=${{ steps.extract_version.outputs.VERSION }}
|
||||
SENTRY_ENVIRONMENT=production
|
||||
|
||||
- name: Push to quay.io
|
||||
uses: redhat-actions/push-to-registry@v2
|
||||
with:
|
||||
image: ${{ steps.build-image.outputs.image }}
|
||||
tags: ${{ steps.build-image.outputs.tags }}
|
||||
registry: quay.io/toska
|
||||
username: toska+github
|
||||
password: ${{ secrets.QUAY_IO_TOKEN }}
|
||||
|
||||
release_backend:
|
||||
name: 'Build and release backend'
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Extract release version
|
||||
id: extract_version
|
||||
run: echo "VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Build backend image
|
||||
id: build-image
|
||||
uses: redhat-actions/buildah-build@v2
|
||||
with:
|
||||
image: oodikone-backend
|
||||
tags: production ${{ github.sha }}
|
||||
context: services/backend
|
||||
containerfiles: |
|
||||
services/backend/Dockerfile
|
||||
build-args: |
|
||||
SENTRY_RELEASE=${{ steps.extract_version.outputs.VERSION }}
|
||||
SENTRY_ENVIRONMENT=production
|
||||
|
||||
- name: Push to quay.io
|
||||
uses: redhat-actions/push-to-registry@v2
|
||||
with:
|
||||
image: ${{ steps.build-image.outputs.image }}
|
||||
tags: ${{ steps.build-image.outputs.tags }}
|
||||
registry: quay.io/toska
|
||||
username: toska+github
|
||||
password: ${{ secrets.QUAY_IO_TOKEN }}
|
||||
|
||||
sentry_release:
|
||||
name: 'Create Sentry release'
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- release_frontend
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Extract release version
|
||||
id: extract_version
|
||||
run: echo "VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Pull image and create a container
|
||||
run: docker create --name frontend quay.io/toska/oodikone-frontend:production
|
||||
|
||||
- name: Copy assets from the container
|
||||
run: docker cp frontend:/opt/app-root/src/build ./build
|
||||
|
||||
- name: Create Sentry release
|
||||
uses: getsentry/action-release@v1
|
||||
env:
|
||||
SENTRY_URL: https://toska.cs.helsinki.fi/
|
||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_TOKEN }}
|
||||
SENTRY_ORG: toska
|
||||
SENTRY_PROJECT: oodikone
|
||||
with:
|
||||
environment: production
|
||||
set_commits: 'skip'
|
||||
sourcemaps: './build/static/js'
|
||||
url_prefix: '~/static/js'
|
||||
version: ${{ steps.extract_version.outputs.VERSION }}
|
||||
|
||||
report_success:
|
||||
name: 'Report deployment success to oodikone notifications'
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- release_frontend
|
||||
- release_backend
|
||||
- sentry_release
|
||||
|
||||
if: success()
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: 'Send notification to slack'
|
||||
uses: UniversityOfHelsinkiCS/slackbot-action@v1.4.2
|
||||
with:
|
||||
webhook-url: ${{ secrets.WEBHOOK_URL }}
|
||||
message-type: deployment
|
||||
softa-url: https://oodikone.cs.helsinki.fi/
|
||||
deployment-target: production
|
||||
|
||||
report_failure:
|
||||
name: 'Report deployment failure to oodikone notifications'
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- release_frontend
|
||||
- release_backend
|
||||
- sentry_release
|
||||
|
||||
if: failure()
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: 'Send notification to slack'
|
||||
uses: UniversityOfHelsinkiCS/slackbot-action@v1.4.2
|
||||
with:
|
||||
webhook-url: ${{ secrets.WEBHOOK_URL }}
|
||||
message-type: deployment-failure
|
||||
deployment-target: production
|
||||
@@ -1,122 +0,0 @@
|
||||
name: 'Release oodikone to production'
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
jobs:
|
||||
build_and_release_backend:
|
||||
name: 'Build and release backend'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: docker/setup-buildx-action@v2
|
||||
- name: 'Login to DockerHub'
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
- name: 'Extract release version'
|
||||
# Github context (e.g. github.ref) only has ref formatted as /refs/tags/vX.X.X
|
||||
# This steps extracts release version from GITHUB_REF env var
|
||||
# See: https://github.community/t/how-to-get-just-the-tag-name/16241/10
|
||||
id: extract_version
|
||||
run: echo "VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT
|
||||
- name: 'Build and push'
|
||||
uses: docker/build-push-action@v3
|
||||
with:
|
||||
build-args: |
|
||||
SENTRY_RELEASE=${{ steps.extract_version.outputs.VERSION }}
|
||||
SENTRY_ENVIRONMENT=production
|
||||
cache-from: |
|
||||
type=registry,ref=toska/oodikone2-backend:staging
|
||||
type=registry,ref=toska/oodikone2-backend:latest
|
||||
cache-to: type=inline
|
||||
context: services/backend
|
||||
push: true
|
||||
tags: toska/oodikone2-backend:latest
|
||||
target: production
|
||||
|
||||
build_and_release_frontend:
|
||||
name: 'Build and release frontend'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: docker/setup-buildx-action@v2
|
||||
- name: 'Login to DockerHub'
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
- name: 'Get current time'
|
||||
id: get_current_time
|
||||
run: echo "NOW=$(date --iso-8601=seconds)" >> $GITHUB_OUTPUT
|
||||
- name: 'Extract release version'
|
||||
# Github context (e.g. github.ref) only has ref formatted as /refs/tags/vX.X.X
|
||||
# This steps extracts release version from GITHUB_REF env var
|
||||
# See: https://github.community/t/how-to-get-just-the-tag-name/16241/10
|
||||
id: extract_version
|
||||
run: echo "VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT
|
||||
- name: 'Build and push'
|
||||
uses: docker/build-push-action@v3
|
||||
with:
|
||||
build-args: |
|
||||
SENTRY_RELEASE=${{ steps.extract_version.outputs.VERSION }}
|
||||
SENTRY_ENVIRONMENT=production
|
||||
BUILT_AT=${{ steps.get_current_time.outputs.NOW }}
|
||||
cache-from: |
|
||||
type=registry,ref=toska/oodikone2-frontend:staging
|
||||
type=registry,ref=toska/oodikone2-frontend:latest
|
||||
cache-to: type=inline
|
||||
context: services/frontend
|
||||
push: true
|
||||
tags: toska/oodikone2-frontend:latest
|
||||
target: production
|
||||
- name: 'Create a container from built image '
|
||||
run: docker create --name frontend toska/oodikone2-frontend:latest
|
||||
- name: 'Copy assets from the container'
|
||||
run: docker cp frontend:/usr/src/app/build ./build
|
||||
- name: 'Create Sentry release'
|
||||
uses: getsentry/action-release@v1
|
||||
env:
|
||||
SENTRY_URL: ${{ secrets.SENTRY_URL }}
|
||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||
SENTRY_ORG: toska
|
||||
SENTRY_PROJECT: oodikone
|
||||
with:
|
||||
environment: production
|
||||
set_commits: 'skip'
|
||||
sourcemaps: './build/static/js'
|
||||
url_prefix: '~/static/js'
|
||||
version: ${{ steps.extract_version.outputs.VERSION }}
|
||||
|
||||
report_success:
|
||||
runs-on: ubuntu-latest
|
||||
name: 'Report deployment success to oodikone notifications'
|
||||
needs:
|
||||
- build_and_release_backend
|
||||
- build_and_release_frontend
|
||||
if: success()
|
||||
steps:
|
||||
- name: 'Send notification to slack'
|
||||
uses: UniversityOfHelsinkiCS/slackbot-action@v1.4.2
|
||||
with:
|
||||
webhook-url: ${{ secrets.WEBHOOK_URL }}
|
||||
message-type: deployment
|
||||
softa-url: https://oodikone.cs.helsinki.fi/
|
||||
deployment-target: production
|
||||
|
||||
report_failure:
|
||||
runs-on: ubuntu-latest
|
||||
name: 'Report deployment failure to oodikone notifications'
|
||||
needs:
|
||||
- build_and_release_backend
|
||||
- build_and_release_frontend
|
||||
if: failure()
|
||||
steps:
|
||||
- name: 'Send notification to slack'
|
||||
uses: UniversityOfHelsinkiCS/slackbot-action@v1.4.2
|
||||
with:
|
||||
webhook-url: ${{ secrets.WEBHOOK_URL }}
|
||||
message-type: deployment-failure
|
||||
deployment-target: production
|
||||
@@ -0,0 +1,128 @@
|
||||
name: 'Test and release to staging'
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
paths:
|
||||
- '.github/workflows/test.yaml'
|
||||
- '.github/workflows/staging.yaml'
|
||||
- '.github/actions/**'
|
||||
- 'cypress/**'
|
||||
- 'cypress.config.json'
|
||||
- 'services/**'
|
||||
- '!services/backend/src/rapodiff.js'
|
||||
- 'docker-compose.ci.yml'
|
||||
- 'package*.json'
|
||||
- 'updater/sis-updater-worker/src/db/migrations/**'
|
||||
|
||||
jobs:
|
||||
test:
|
||||
uses: ./.github/workflows/test.yaml
|
||||
secrets:
|
||||
username: ${{ secrets.TOSKAREGISTRY_USERNAME }}
|
||||
password: ${{ secrets.TOSKAREGISTRY_PASSWORD }}
|
||||
|
||||
release_frontend:
|
||||
name: 'Build and release frontend'
|
||||
runs-on: ubuntu-latest
|
||||
needs: test
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Get current time
|
||||
id: get_current_time
|
||||
run: echo "NOW=$(date --iso-8601=seconds)" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Build frontend image
|
||||
id: build-image
|
||||
uses: redhat-actions/buildah-build@v2
|
||||
with:
|
||||
image: oodikone-frontend
|
||||
tags: staging ${{ github.sha }}
|
||||
context: services/frontend
|
||||
containerfiles: |
|
||||
services/frontend/Dockerfile
|
||||
build-args: |
|
||||
BUILT_AT=${{ steps.get_current_time.outputs.NOW }}
|
||||
BASE_PATH=/oodikone
|
||||
GIT_SHA=${{ github.sha }}
|
||||
STAGING=true
|
||||
|
||||
- name: Push to quay.io
|
||||
uses: redhat-actions/push-to-registry@v2
|
||||
with:
|
||||
image: ${{ steps.build-image.outputs.image }}
|
||||
tags: ${{ steps.build-image.outputs.tags }}
|
||||
registry: quay.io/toska
|
||||
username: toska+github
|
||||
password: ${{ secrets.QUAY_IO_TOKEN }}
|
||||
|
||||
release_backend:
|
||||
name: 'Build and release backend'
|
||||
runs-on: ubuntu-latest
|
||||
needs: test
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Build backend image
|
||||
id: build-image
|
||||
uses: redhat-actions/buildah-build@v2
|
||||
with:
|
||||
image: oodikone-backend
|
||||
tags: staging ${{ github.sha }}
|
||||
context: services/backend
|
||||
containerfiles: |
|
||||
services/backend/Dockerfile
|
||||
build-args: |
|
||||
BASE_PATH=/oodikone
|
||||
GIT_SHA=${{ github.sha }}
|
||||
STAGING=true
|
||||
|
||||
- name: Push to quay.io
|
||||
uses: redhat-actions/push-to-registry@v2
|
||||
with:
|
||||
image: ${{ steps.build-image.outputs.image }}
|
||||
tags: ${{ steps.build-image.outputs.tags }}
|
||||
registry: quay.io/toska
|
||||
username: toska+github
|
||||
password: ${{ secrets.QUAY_IO_TOKEN }}
|
||||
|
||||
report_success:
|
||||
name: 'Report deployment success to oodikone notifications'
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- release_frontend
|
||||
- release_backend
|
||||
|
||||
if: success()
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: 'Send notification to slack'
|
||||
uses: UniversityOfHelsinkiCS/slackbot-action@v1.4.2
|
||||
with:
|
||||
webhook-url: ${{ secrets.WEBHOOK_URL }}
|
||||
message-type: deployment
|
||||
softa-url: https://toska-staging.cs.helsinki.fi/oodikone
|
||||
deployment-target: staging
|
||||
|
||||
report_failure:
|
||||
name: 'Report deployment failure to oodikone notifications'
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- release_frontend
|
||||
- release_backend
|
||||
|
||||
if: failure()
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: 'Send notification to slack'
|
||||
uses: UniversityOfHelsinkiCS/slackbot-action@v1.4.2
|
||||
with:
|
||||
webhook-url: ${{ secrets.WEBHOOK_URL }}
|
||||
message-type: deployment-failure
|
||||
deployment-target: staging
|
||||
@@ -1,162 +0,0 @@
|
||||
name: 'Test and release updater'
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
paths:
|
||||
- '.github/workflows/test-and-release-updater.yaml'
|
||||
- 'updater/sis-updater-worker/**'
|
||||
- 'updater/sis-updater-scheduler/**'
|
||||
|
||||
jobs:
|
||||
build_worker:
|
||||
name: 'Build sis-updater-worker'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: docker/setup-buildx-action@v2
|
||||
- name: 'Build the image'
|
||||
uses: docker/build-push-action@v3
|
||||
with:
|
||||
build-args: |
|
||||
SENTRY_RELEASE=${{ github.sha }}
|
||||
SENTRY_ENVIRONMENT=production
|
||||
context: updater/sis-updater-worker/
|
||||
target: production
|
||||
push: false
|
||||
outputs: type=docker,dest=/tmp/worker.tar
|
||||
tags: toska/sis-updater-worker:latest
|
||||
cache-from: type=registry,ref=toska/sis-updater-worker:latest
|
||||
cache-to: type=inline
|
||||
- name: 'Store built image'
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
path: /tmp/worker.tar
|
||||
|
||||
build_scheduler:
|
||||
name: 'Build sis-updater-scheduler'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: docker/setup-buildx-action@v2
|
||||
- name: 'Build the image'
|
||||
uses: docker/build-push-action@v3
|
||||
with:
|
||||
build-args: |
|
||||
SENTRY_RELEASE=${{ github.sha }}
|
||||
SENTRY_ENVIRONMENT=production
|
||||
context: updater/sis-updater-scheduler/
|
||||
target: production
|
||||
push: false
|
||||
outputs: type=docker,dest=/tmp/scheduler.tar
|
||||
tags: toska/sis-updater-scheduler:latest
|
||||
cache-from: type=registry,ref=toska/sis-updater-scheduler:latest
|
||||
cache-to: type=inline
|
||||
- name: 'Store built image'
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
path: /tmp/scheduler.tar
|
||||
|
||||
test_updater:
|
||||
name: 'Test updater'
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- build_worker
|
||||
- build_scheduler
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: 'Login to toska docker registry'
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
registry: registry.toska.cs.helsinki.fi
|
||||
username: ${{ secrets.TOSKAREGISTRY_USERNAME }}
|
||||
password: ${{ secrets.TOSKAREGISTRY_PASSWORD }}
|
||||
- name: 'Download built images'
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
path: /tmp/
|
||||
- name: 'Load worker image'
|
||||
run: docker load --input /tmp/artifact/worker.tar
|
||||
- name: 'Load scheduler image'
|
||||
run: docker load --input /tmp/artifact/scheduler.tar
|
||||
- name: 'Start worker'
|
||||
run: docker-compose --file docker-compose.ci.yml up --detach --no-recreate sis-updater-worker
|
||||
- name: 'Run hourly jobs'
|
||||
run: docker-compose --file docker-compose.ci.yml run sis-updater-scheduler npm start
|
||||
env:
|
||||
SCHEDULE_IMMEDIATE: hourly
|
||||
EXIT_AFTER_IMMEDIATES: yes
|
||||
- name: 'Run daily jobs'
|
||||
run: docker-compose --file docker-compose.ci.yml run sis-updater-scheduler npm start
|
||||
env:
|
||||
SCHEDULE_IMMEDIATE: daily
|
||||
EXIT_AFTER_IMMEDIATES: yes
|
||||
- name: 'Run weekly jobs'
|
||||
run: docker-compose --file docker-compose.ci.yml run sis-updater-scheduler npm start
|
||||
env:
|
||||
SCHEDULE_IMMEDIATE: weekly
|
||||
EXIT_AFTER_IMMEDIATES: yes
|
||||
- name: 'Run prepurge'
|
||||
run: docker-compose --file docker-compose.ci.yml run sis-updater-scheduler npm start
|
||||
env:
|
||||
SCHEDULE_IMMEDIATE: prepurge
|
||||
EXIT_AFTER_IMMEDIATES: yes
|
||||
- name: 'Run purge'
|
||||
run: docker-compose --file docker-compose.ci.yml run sis-updater-scheduler npm start
|
||||
env:
|
||||
SCHEDULE_IMMEDIATE: purge
|
||||
EXIT_AFTER_IMMEDIATES: yes
|
||||
|
||||
release_updater:
|
||||
name: 'Release updater'
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- test_updater
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: docker/setup-buildx-action@v2
|
||||
- name: 'Login to DockerHub'
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
- name: 'Build and push scheduler'
|
||||
uses: docker/build-push-action@v3
|
||||
with:
|
||||
cache-from: type=registry,ref=toska/sis-updater-scheduler:latest
|
||||
cache-to: type=inline
|
||||
context: updater/sis-updater-scheduler/
|
||||
push: true
|
||||
tags: toska/sis-updater-scheduler:latest
|
||||
target: production
|
||||
- name: 'Build and push worker'
|
||||
uses: docker/build-push-action@v3
|
||||
with:
|
||||
cache-from: type=registry,ref=toska/sis-updater-worker:latest
|
||||
cache-to: type=inline
|
||||
context: updater/sis-updater-worker/
|
||||
push: true
|
||||
tags: toska/sis-updater-worker:latest
|
||||
target: production
|
||||
- name: 'Create Sentry release'
|
||||
uses: getsentry/action-release@v1
|
||||
env:
|
||||
SENTRY_URL: ${{ secrets.SENTRY_URL }}
|
||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||
SENTRY_ORG: toska
|
||||
SENTRY_PROJECT: updater
|
||||
with:
|
||||
environment: production
|
||||
set_commits: 'skip'
|
||||
sourcemaps: './build/static/js'
|
||||
url_prefix: '~/static/js'
|
||||
version: ${{ github.sha }}
|
||||
- name: 'Send notification to slack'
|
||||
uses: UniversityOfHelsinkiCS/slackbot-action@v1.4.2
|
||||
with:
|
||||
webhook-url: ${{ secrets.WEBHOOK_URL }}
|
||||
message-type: deployment
|
||||
softa-url: https://oodikone.cs.helsinki.fi
|
||||
deployment-target: production
|
||||
softa-to-deploy: sis-updater
|
||||
@@ -1,25 +1,17 @@
|
||||
name: 'Test oodikone and release to staging'
|
||||
name: 'Lint and test'
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
paths:
|
||||
- '.github/workflows/test-oodikone.yaml'
|
||||
- '.github/actions/**'
|
||||
- 'cypress/**'
|
||||
- 'cypress.config.json'
|
||||
- 'services/**'
|
||||
- '!services/backend/src/rapodiff.js'
|
||||
- 'docker-compose.ci.yml'
|
||||
- 'docker-compose.test.yml'
|
||||
- 'nginx.conf'
|
||||
- 'package*.json'
|
||||
- 'updater/sis-updater-worker/src/db/migrations/**'
|
||||
workflow_call:
|
||||
secrets:
|
||||
username:
|
||||
required: true
|
||||
description: 'Toska registry username'
|
||||
password:
|
||||
required: true
|
||||
description: 'Toska registry password'
|
||||
|
||||
jobs:
|
||||
# === Static tests / linting ===
|
||||
|
||||
lint_backend:
|
||||
name: 'Lint backend'
|
||||
runs-on: ubuntu-latest
|
||||
@@ -48,7 +40,7 @@ jobs:
|
||||
'eslint --ext .jsx,.js services/frontend' \
|
||||
'stylelint services/frontend/src/**/*.css'
|
||||
|
||||
# === Build images ===
|
||||
# === Build database images ===
|
||||
build_backend:
|
||||
name: 'Build backend'
|
||||
runs-on: ubuntu-latest
|
||||
@@ -61,50 +53,15 @@ jobs:
|
||||
build-args: |
|
||||
SENTRY_RELEASE=${{ github.sha }}
|
||||
SENTRY_ENVIRONMENT=staging
|
||||
cache-from: |
|
||||
type=registry,ref=toska/oodikone2-backend:staging
|
||||
type=registry,ref=toska/oodikone2-backend:latest
|
||||
cache-to: type=inline
|
||||
context: services/backend
|
||||
push: false
|
||||
tags: toska/oodikone2-backend:staging
|
||||
target: production
|
||||
tags: oodikone-backend:staging
|
||||
outputs: type=docker,dest=/tmp/backend.tar
|
||||
- name: 'Upload image for other jobs'
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
path: /tmp/backend.tar
|
||||
|
||||
build_frontend:
|
||||
name: 'Build frontend'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: docker/setup-buildx-action@v2
|
||||
- name: 'Get current time'
|
||||
id: get_current_time
|
||||
run: echo "NOW=$(date --iso-8601=seconds)" >> $GITHUB_OUTPUT
|
||||
- name: 'Build image and export it'
|
||||
uses: docker/build-push-action@v3
|
||||
with:
|
||||
build-args: |
|
||||
SENTRY_RELEASE=${{ github.sha }}
|
||||
SENTRY_ENVIRONMENT=staging
|
||||
BUILT_AT=${{ steps.get_current_time.outputs.NOW }}
|
||||
cache-from: |
|
||||
type=registry,ref=toska/oodikone2-frontend:staging
|
||||
type=registry,ref=toska/oodikone2-frontend:latest
|
||||
cache-to: type=inline
|
||||
context: services/frontend
|
||||
push: false
|
||||
tags: toska/oodikone2-frontend:staging
|
||||
target: production
|
||||
outputs: type=docker,dest=/tmp/frontend.tar
|
||||
- name: 'Upload image for other jobs'
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
path: /tmp/frontend.tar
|
||||
|
||||
build_and_push_anon_sis_db:
|
||||
name: 'Build anon sis-db and push to toska hub if new migrations have been added'
|
||||
runs-on: ubuntu-latest
|
||||
@@ -116,13 +73,13 @@ jobs:
|
||||
filters: |
|
||||
migrations:
|
||||
- 'updater/sis-updater-worker/src/db/migrations/**'
|
||||
- '.github/workflows/test-oodikone.yaml'
|
||||
- '.github/workflows/test.yaml'
|
||||
- name: 'Login to toska docker registry'
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
registry: registry.toska.cs.helsinki.fi
|
||||
username: ${{ secrets.TOSKAREGISTRY_USERNAME }}
|
||||
password: ${{ secrets.TOSKAREGISTRY_PASSWORD }}
|
||||
registry: registry-toska.ext.ocp-prod-0.k8s.it.helsinki.fi
|
||||
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: |
|
||||
@@ -143,7 +100,7 @@ jobs:
|
||||
filters: |
|
||||
migrations:
|
||||
- 'services/backend/src/database/migrations_kone/**'
|
||||
- '.github/workflows/test-oodikone.yaml'
|
||||
- '.github/workflows/test.yaml'
|
||||
- name: 'Download images'
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
@@ -153,9 +110,9 @@ jobs:
|
||||
- name: 'Login to toska docker registry'
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
registry: registry.toska.cs.helsinki.fi
|
||||
username: ${{ secrets.TOSKAREGISTRY_USERNAME }}
|
||||
password: ${{ secrets.TOSKAREGISTRY_PASSWORD }}
|
||||
registry: registry-toska.ext.ocp-prod-0.k8s.it.helsinki.fi
|
||||
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: |
|
||||
@@ -176,7 +133,7 @@ jobs:
|
||||
filters: |
|
||||
migrations:
|
||||
- 'services/backend/src/database/migrations_user/**'
|
||||
- '.github/workflows/test-oodikone.yaml'
|
||||
- '.github/workflows/test.yaml'
|
||||
- name: 'Download images'
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
@@ -186,9 +143,9 @@ jobs:
|
||||
- name: 'Login to toska docker registry'
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
registry: registry.toska.cs.helsinki.fi
|
||||
username: ${{ secrets.TOSKAREGISTRY_USERNAME }}
|
||||
password: ${{ secrets.TOSKAREGISTRY_PASSWORD }}
|
||||
registry: registry-toska.ext.ocp-prod-0.k8s.it.helsinki.fi
|
||||
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: |
|
||||
@@ -200,8 +157,6 @@ jobs:
|
||||
test_course_statistics:
|
||||
name: 'Test course statistics'
|
||||
needs:
|
||||
- build_backend
|
||||
- build_frontend
|
||||
- build_and_push_anon_sis_db
|
||||
- build_and_push_anon_kone_db
|
||||
- build_and_push_anon_user_db
|
||||
@@ -211,15 +166,13 @@ jobs:
|
||||
- name: 'Run tests'
|
||||
uses: ./.github/actions/run-tests
|
||||
with:
|
||||
username: ${{ secrets.TOSKAREGISTRY_USERNAME }}
|
||||
password: ${{ secrets.TOSKAREGISTRY_PASSWORD }}
|
||||
username: ${{ secrets.username }}
|
||||
password: ${{ secrets.password }}
|
||||
spec: cypress/e2e/Course_statistics.js
|
||||
|
||||
test_custom_population:
|
||||
name: 'Test custom population'
|
||||
needs:
|
||||
- build_backend
|
||||
- build_frontend
|
||||
- build_and_push_anon_sis_db
|
||||
- build_and_push_anon_kone_db
|
||||
- build_and_push_anon_user_db
|
||||
@@ -229,51 +182,45 @@ jobs:
|
||||
- name: 'Run tests'
|
||||
uses: ./.github/actions/run-tests
|
||||
with:
|
||||
username: ${{ secrets.TOSKAREGISTRY_USERNAME }}
|
||||
password: ${{ secrets.TOSKAREGISTRY_PASSWORD }}
|
||||
username: ${{ secrets.username }}
|
||||
password: ${{ secrets.password }}
|
||||
spec: cypress/e2e/Custom_population.js
|
||||
|
||||
test_population_statistics:
|
||||
name: 'Test population statistics'
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- build_backend
|
||||
- build_frontend
|
||||
- build_and_push_anon_sis_db
|
||||
- build_and_push_anon_kone_db
|
||||
- build_and_push_anon_user_db
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: 'Run tests'
|
||||
uses: ./.github/actions/run-tests
|
||||
with:
|
||||
username: ${{ secrets.TOSKAREGISTRY_USERNAME }}
|
||||
password: ${{ secrets.TOSKAREGISTRY_PASSWORD }}
|
||||
username: ${{ secrets.username }}
|
||||
password: ${{ secrets.password }}
|
||||
spec: cypress/e2e/Population_statistics.js
|
||||
|
||||
test_filters:
|
||||
name: 'Test filters'
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- build_backend
|
||||
- build_frontend
|
||||
- build_and_push_anon_sis_db
|
||||
- build_and_push_anon_kone_db
|
||||
- build_and_push_anon_user_db
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: 'Run tests'
|
||||
uses: ./.github/actions/run-tests
|
||||
with:
|
||||
username: ${{ secrets.TOSKAREGISTRY_USERNAME }}
|
||||
password: ${{ secrets.TOSKAREGISTRY_PASSWORD }}
|
||||
username: ${{ secrets.username }}
|
||||
password: ${{ secrets.password }}
|
||||
spec: cypress/e2e/Filters.js
|
||||
|
||||
test_student_statistics:
|
||||
name: 'Test student statistics'
|
||||
needs:
|
||||
- build_backend
|
||||
- build_frontend
|
||||
- build_and_push_anon_sis_db
|
||||
- build_and_push_anon_kone_db
|
||||
- build_and_push_anon_user_db
|
||||
@@ -283,15 +230,29 @@ jobs:
|
||||
- name: 'Run tests'
|
||||
uses: ./.github/actions/run-tests
|
||||
with:
|
||||
username: ${{ secrets.TOSKAREGISTRY_USERNAME }}
|
||||
password: ${{ secrets.TOSKAREGISTRY_PASSWORD }}
|
||||
username: ${{ secrets.username }}
|
||||
password: ${{ secrets.password }}
|
||||
spec: cypress/e2e/Student_statistics.js
|
||||
|
||||
test_completed_courses:
|
||||
name: 'Test completed courses'
|
||||
needs:
|
||||
- build_and_push_anon_sis_db
|
||||
- build_and_push_anon_kone_db
|
||||
- build_and_push_anon_user_db
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: 'Run tests'
|
||||
uses: ./.github/actions/run-tests
|
||||
with:
|
||||
username: ${{ secrets.username }}
|
||||
password: ${{ secrets.password }}
|
||||
spec: cypress/e2e/Completed_courses.js
|
||||
|
||||
test_studyprogramme_overview:
|
||||
name: 'Test studyprogramme overview'
|
||||
needs:
|
||||
- build_backend
|
||||
- build_frontend
|
||||
- build_and_push_anon_sis_db
|
||||
- build_and_push_anon_kone_db
|
||||
- build_and_push_anon_user_db
|
||||
@@ -301,15 +262,13 @@ jobs:
|
||||
- name: 'Run tests'
|
||||
uses: ./.github/actions/run-tests
|
||||
with:
|
||||
username: ${{ secrets.TOSKAREGISTRY_USERNAME }}
|
||||
password: ${{ secrets.TOSKAREGISTRY_PASSWORD }}
|
||||
username: ${{ secrets.username }}
|
||||
password: ${{ secrets.password }}
|
||||
spec: cypress/e2e/Studyprogramme_overview.js
|
||||
|
||||
test_teachers_and_users:
|
||||
name: 'Test teachers and users'
|
||||
needs:
|
||||
- build_backend
|
||||
- build_frontend
|
||||
- build_and_push_anon_sis_db
|
||||
- build_and_push_anon_kone_db
|
||||
- build_and_push_anon_user_db
|
||||
@@ -319,8 +278,8 @@ jobs:
|
||||
- name: 'Run tests'
|
||||
uses: ./.github/actions/run-tests
|
||||
with:
|
||||
username: ${{ secrets.TOSKAREGISTRY_USERNAME }}
|
||||
password: ${{ secrets.TOSKAREGISTRY_PASSWORD }}
|
||||
username: ${{ secrets.username }}
|
||||
password: ${{ secrets.password }}
|
||||
spec: |
|
||||
cypress/e2e/Teachers.js
|
||||
cypress/e2e/Users.js
|
||||
@@ -328,8 +287,6 @@ jobs:
|
||||
test_faculty_statistics:
|
||||
name: 'Test faculty statistics'
|
||||
needs:
|
||||
- build_backend
|
||||
- build_frontend
|
||||
- build_and_push_anon_sis_db
|
||||
- build_and_push_anon_kone_db
|
||||
- build_and_push_anon_user_db
|
||||
@@ -339,39 +296,46 @@ jobs:
|
||||
- name: 'Run tests'
|
||||
uses: ./.github/actions/run-tests
|
||||
with:
|
||||
username: ${{ secrets.TOSKAREGISTRY_USERNAME }}
|
||||
password: ${{ secrets.TOSKAREGISTRY_PASSWORD }}
|
||||
username: ${{ secrets.username }}
|
||||
password: ${{ secrets.password }}
|
||||
spec: cypress/e2e/Faculty_statistics.js
|
||||
|
||||
# === Other tests ===
|
||||
|
||||
test_service_exit_statuses:
|
||||
name: 'Check services run correctly'
|
||||
test_open_uni_search:
|
||||
name: 'Test open uni search'
|
||||
needs:
|
||||
- build_backend
|
||||
- build_frontend
|
||||
- build_and_push_anon_sis_db
|
||||
- build_and_push_anon_kone_db
|
||||
- build_and_push_anon_user_db
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: 'Download images'
|
||||
uses: actions/download-artifact@v3
|
||||
- name: 'Run tests'
|
||||
uses: ./.github/actions/run-tests
|
||||
with:
|
||||
path: /tmp/
|
||||
- name: 'Load built images'
|
||||
run: |
|
||||
docker load --input /tmp/artifact/backend.tar
|
||||
docker load --input /tmp/artifact/frontend.tar
|
||||
username: ${{ secrets.username }}
|
||||
password: ${{ secrets.password }}
|
||||
spec: cypress/e2e/Openuni.js
|
||||
|
||||
# === Other tests ===
|
||||
test_service_exit_statuses:
|
||||
name: 'Check services run correctly'
|
||||
needs:
|
||||
- build_and_push_anon_sis_db
|
||||
- build_and_push_anon_kone_db
|
||||
- build_and_push_anon_user_db
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: 'Login to toska docker registry'
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
registry: registry.toska.cs.helsinki.fi
|
||||
username: ${{ secrets.TOSKAREGISTRY_USERNAME }}
|
||||
password: ${{ secrets.TOSKAREGISTRY_PASSWORD }}
|
||||
registry: registry-toska.ext.ocp-prod-0.k8s.it.helsinki.fi
|
||||
username: ${{ secrets.username }}
|
||||
password: ${{ secrets.password }}
|
||||
|
||||
- name: 'Run services, pull rest of the needed images'
|
||||
run: docker-compose --file docker-compose.ci.yml up --detach --no-recreate
|
||||
run: docker-compose --file docker-compose.ci.yml up --detach
|
||||
|
||||
- name: 'Check that exit statuses are not errors'
|
||||
run: |
|
||||
docker ps -a
|
||||
@@ -379,99 +343,7 @@ jobs:
|
||||
echo 'Some process had nonzero exit code';
|
||||
exit 1;
|
||||
fi
|
||||
|
||||
- name: 'Print logs on failure'
|
||||
if: failure()
|
||||
run: docker-compose --file docker-compose.ci.yml logs
|
||||
|
||||
# === Release ===
|
||||
|
||||
release_images:
|
||||
name: 'Release built images'
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- lint_backend
|
||||
- lint_frontend
|
||||
- test_course_statistics
|
||||
- test_custom_population
|
||||
- test_filters
|
||||
- test_population_statistics
|
||||
- test_student_statistics
|
||||
- test_studyprogramme_overview
|
||||
- test_teachers_and_users
|
||||
- test_faculty_statistics
|
||||
- test_service_exit_statuses
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: 'Download images'
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
path: /tmp/
|
||||
- name: 'Load built images'
|
||||
run: |
|
||||
docker load --input /tmp/artifact/backend.tar
|
||||
docker load --input /tmp/artifact/frontend.tar
|
||||
- name: 'Login to DockerHub'
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
- name: 'Create a container from frontend'
|
||||
run: docker create --name frontend toska/oodikone2-frontend:staging
|
||||
- name: 'Copy assets from the container'
|
||||
run: docker cp frontend:/usr/src/app/build ./build
|
||||
- name: 'Create Sentry release'
|
||||
uses: getsentry/action-release@v1
|
||||
env:
|
||||
SENTRY_URL: ${{ secrets.SENTRY_URL }}
|
||||
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
|
||||
SENTRY_ORG: toska
|
||||
SENTRY_PROJECT: oodikone
|
||||
with:
|
||||
environment: staging
|
||||
set_commits: 'skip'
|
||||
sourcemaps: './build/static/js'
|
||||
url_prefix: '~/static/js'
|
||||
version: ${{ github.sha }}
|
||||
- name: 'Push to docker hub'
|
||||
run: |
|
||||
docker push toska/oodikone2-backend:staging
|
||||
docker push toska/oodikone2-frontend:staging
|
||||
- name: 'Send deployment success notification to slack'
|
||||
if: success()
|
||||
uses: UniversityOfHelsinkiCS/slackbot-action@v1.4.2
|
||||
with:
|
||||
webhook-url: ${{ secrets.WEBHOOK_URL }}
|
||||
message-type: deployment
|
||||
softa-url: https://oodikone-staging.cs.helsinki.fi/
|
||||
deployment-target: staging
|
||||
- name: 'Send deployment failure notification to slack'
|
||||
if: failure()
|
||||
uses: UniversityOfHelsinkiCS/slackbot-action@v1.4.2
|
||||
with:
|
||||
webhook-url: ${{ secrets.WEBHOOK_URL }}
|
||||
message-type: deployment-failure
|
||||
deployment-target: staging
|
||||
|
||||
report_test_failure:
|
||||
runs-on: ubuntu-latest
|
||||
name: 'Report test failure to oodikone notifications'
|
||||
needs:
|
||||
- lint_backend
|
||||
- lint_frontend
|
||||
- test_course_statistics
|
||||
- test_custom_population
|
||||
- test_filters
|
||||
- test_population_statistics
|
||||
- test_student_statistics
|
||||
- test_studyprogramme_overview
|
||||
- test_teachers_and_users
|
||||
- test_faculty_statistics
|
||||
- test_service_exit_statuses
|
||||
if: failure()
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: 'Send notification to slack'
|
||||
uses: UniversityOfHelsinkiCS/slackbot-action@v1.4.2
|
||||
with:
|
||||
webhook-url: ${{ secrets.WEBHOOK_URL }}
|
||||
message-type: test-failure
|
||||
@@ -0,0 +1,150 @@
|
||||
name: 'Test and release Updater'
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
paths:
|
||||
- '.github/workflows/updater.yaml'
|
||||
- 'updater/sis-updater-worker/**'
|
||||
- 'updater/sis-updater-scheduler/**'
|
||||
|
||||
jobs:
|
||||
build_worker:
|
||||
name: 'Build Updater worker'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: docker/setup-buildx-action@v2
|
||||
- name: 'Build the image'
|
||||
uses: docker/build-push-action@v3
|
||||
with:
|
||||
context: updater/sis-updater-worker/
|
||||
push: false
|
||||
outputs: type=docker,dest=/tmp/worker.tar
|
||||
tags: toska/sis-updater-worker:latest
|
||||
- name: 'Store built image'
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
path: /tmp/worker.tar
|
||||
|
||||
build_scheduler:
|
||||
name: 'Build Updater scheduler'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: docker/setup-buildx-action@v2
|
||||
- name: 'Build the image'
|
||||
uses: docker/build-push-action@v3
|
||||
with:
|
||||
context: updater/sis-updater-scheduler/
|
||||
push: false
|
||||
outputs: type=docker,dest=/tmp/scheduler.tar
|
||||
tags: toska/sis-updater-scheduler:latest
|
||||
- name: 'Store built image'
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
path: /tmp/scheduler.tar
|
||||
|
||||
# test_updater:
|
||||
# name: 'Test Updater'
|
||||
# runs-on: ubuntu-latest
|
||||
# needs:
|
||||
# - build_worker
|
||||
# - build_scheduler
|
||||
# steps:
|
||||
# - uses: actions/checkout@v3
|
||||
# - name: 'Login to toska docker registry'
|
||||
# uses: docker/login-action@v2
|
||||
# with:
|
||||
# registry: registry-toska.ext.ocp-prod-0.k8s.it.helsinki.fi
|
||||
# username: ${{ secrets.TOSKAREGISTRY_USERNAME }}
|
||||
# password: ${{ secrets.TOSKAREGISTRY_PASSWORD }}
|
||||
# - name: 'Download built images'
|
||||
# uses: actions/download-artifact@v3
|
||||
# with:
|
||||
# path: /tmp/
|
||||
# - name: 'Load worker image'
|
||||
# run: docker load --input /tmp/artifact/worker.tar
|
||||
# - name: 'Load scheduler image'
|
||||
# run: docker load --input /tmp/artifact/scheduler.tar
|
||||
# - name: 'Start worker'
|
||||
# run: docker-compose --file docker-compose.ci.yml up --detach --no-recreate sis-updater-worker
|
||||
# - name: 'Run hourly jobs'
|
||||
# run: docker-compose --file docker-compose.ci.yml run sis-updater-scheduler npm start
|
||||
# env:
|
||||
# SCHEDULE_IMMEDIATE: hourly
|
||||
# EXIT_AFTER_IMMEDIATES: yes
|
||||
# - name: 'Run daily jobs'
|
||||
# run: docker-compose --file docker-compose.ci.yml run sis-updater-scheduler npm start
|
||||
# env:
|
||||
# SCHEDULE_IMMEDIATE: daily
|
||||
# EXIT_AFTER_IMMEDIATES: yes
|
||||
# - name: 'Run weekly jobs'
|
||||
# run: docker-compose --file docker-compose.ci.yml run sis-updater-scheduler npm start
|
||||
# env:
|
||||
# SCHEDULE_IMMEDIATE: weekly
|
||||
# EXIT_AFTER_IMMEDIATES: yes
|
||||
# - name: 'Run prepurge'
|
||||
# run: docker-compose --file docker-compose.ci.yml run sis-updater-scheduler npm start
|
||||
# env:
|
||||
# SCHEDULE_IMMEDIATE: prepurge
|
||||
# EXIT_AFTER_IMMEDIATES: yes
|
||||
# - name: 'Run purge'
|
||||
# run: docker-compose --file docker-compose.ci.yml run sis-updater-scheduler npm start
|
||||
# env:
|
||||
# SCHEDULE_IMMEDIATE: purge
|
||||
# EXIT_AFTER_IMMEDIATES: yes
|
||||
|
||||
release_worker:
|
||||
name: 'Build and release Updater worker'
|
||||
runs-on: ubuntu-latest
|
||||
# needs: test_updater
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Build worker image
|
||||
id: build-image
|
||||
uses: redhat-actions/buildah-build@v2
|
||||
with:
|
||||
image: updater-worker
|
||||
tags: staging production ${{ github.sha }}
|
||||
context: updater/sis-updater-worker
|
||||
containerfiles: |
|
||||
updater/sis-updater-worker/Dockerfile
|
||||
|
||||
- name: Push to quay.io
|
||||
uses: redhat-actions/push-to-registry@v2
|
||||
with:
|
||||
image: ${{ steps.build-image.outputs.image }}
|
||||
tags: ${{ steps.build-image.outputs.tags }}
|
||||
registry: quay.io/toska
|
||||
username: toska+github
|
||||
password: ${{ secrets.QUAY_IO_TOKEN }}
|
||||
|
||||
release_scheduler:
|
||||
name: 'Build and release Updater scheduler'
|
||||
runs-on: ubuntu-latest
|
||||
# needs: test_updater
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Build scheduler image
|
||||
id: build-image
|
||||
uses: redhat-actions/buildah-build@v2
|
||||
with:
|
||||
image: updater-scheduler
|
||||
tags: staging production ${{ github.sha }}
|
||||
context: updater/sis-updater-scheduler
|
||||
containerfiles: |
|
||||
updater/sis-updater-scheduler/Dockerfile
|
||||
|
||||
- name: Push to quay.io
|
||||
uses: redhat-actions/push-to-registry@v2
|
||||
with:
|
||||
image: ${{ steps.build-image.outputs.image }}
|
||||
tags: ${{ steps.build-image.outputs.tags }}
|
||||
registry: quay.io/toska
|
||||
username: toska+github
|
||||
password: ${{ secrets.QUAY_IO_TOKEN }}
|
||||
@@ -2,3 +2,11 @@
|
||||
. "$(dirname "$0")/_/husky.sh"
|
||||
|
||||
npx lint-staged
|
||||
|
||||
if git grep -qE --cached "(it|describe)\.only" $(git diff --cached --name-only) ./cypress; then
|
||||
echo "\e[0;31mDo not commit .only-tests."
|
||||
exit 1
|
||||
else
|
||||
echo "No .only-tests found"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
+1
-1
@@ -7,7 +7,7 @@ module.exports = {
|
||||
'{services,updater}/**/*.{js,jsx}': files => `eslint --fix ${files.join(' ')}`,
|
||||
'*.{js,json,md,yml,yaml,html}': files => `prettier --write ${files.join(' ')}`,
|
||||
'*.css': files => `stylelint --fix ${files.join(' ')}`,
|
||||
Dockerfile: files => `${dockerCmdBase} hadolint/hadolint:v2.7.0 hadolint ${relativeFilePaths(files)}`,
|
||||
Dockerfile: files => `${dockerCmdBase} hadolint/hadolint:v2.12.0 hadolint ${relativeFilePaths(files)}`,
|
||||
'*.sh': files => `${dockerCmdBase} koalaman/shellcheck:v0.7.2 ${relativeFilePaths(files)} -x`,
|
||||
'.github/{workflows,actions}/*': files => `npm run actionlint ${relativeFilePaths(files)}`,
|
||||
'docker-compose*': files => {
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 106 KiB |
@@ -1,6 +1,6 @@
|
||||
# Oodikone
|
||||
|
||||
An application for analyzing university data, running at [https://oodikone.cs.helsinki.fi/](https://oodikone.cs.helsinki.fi/).
|
||||
An application for analyzing university data, running at [https://oodikone.helsinki.fi/](https://oodikone.helsinki.fi/).
|
||||
|
||||
## ✔️ Requirements
|
||||
|
||||
@@ -8,7 +8,7 @@ To run Oodikone locally, you will need the following:
|
||||
|
||||
1. Applications:
|
||||
- [Docker](https://docs.docker.com/install/) (version 20.10+),
|
||||
- [Docker Compose](https://docs.docker.com/compose/install/)(version 1.29+) and
|
||||
- [Docker Compose](https://docs.docker.com/compose/install/) (version 1.29+) and
|
||||
- [npm](https://docs.npmjs.com/cli/v7) (version 7+)
|
||||
2. Access to Toska Docker Hub,
|
||||
3. For real data setup, access to Toskas production servers.
|
||||
@@ -45,12 +45,61 @@ What different CLI options do:
|
||||
- Creates real data databases and populates them with downloaded dumps.
|
||||
3. Reset single database
|
||||
- Like option 2, but only for single database. Leaves other databases untouched.
|
||||
4. Restore data from dumps
|
||||
- Like option 2, but skips downloading, and simply restores databases from the dumps downloaded earlier.
|
||||
5. Docker system prune
|
||||
- Runs `docker system prune -a && docker volume prune -a`. This will remove all your docker data, also related to any other applications than oodikone! It is sometimes necessary after strange errors due to caching.
|
||||
|
||||
## ⌨️ Development
|
||||
|
||||
### Architecture
|
||||
|
||||

|
||||
```mermaid
|
||||
graph TB
|
||||
subgraph Oodikone & Updater
|
||||
subgraph Oodikone
|
||||
client[React app] --> server[NodeJS server]
|
||||
server --> ok_redis(Redis)
|
||||
konedb[(kone-db)]
|
||||
end
|
||||
server -->|Read| sisdb[(sis-db)]
|
||||
subgraph Updater
|
||||
updaterscheduler[Scheduler] --> |Push jobs| nats
|
||||
nats(NATS server)
|
||||
updaterworker --> |Fetch jobs| nats
|
||||
updaterworker[Worker] -->|Write| sisdb
|
||||
updaterscheduler --> redis(Redis)
|
||||
updaterworker --> redis
|
||||
end
|
||||
end
|
||||
|
||||
subgraph Common Toska services
|
||||
jami[JAMI]
|
||||
pate[Pate]
|
||||
importer[Importer]
|
||||
end
|
||||
|
||||
updaterworker -->|Read db| importer
|
||||
updaterscheduler -->|Read db| importer
|
||||
importer -->|Fetch data| sisu[Sisu export APIs]
|
||||
server -->|Get IAM access| jami
|
||||
server -->|Send mail| pate
|
||||
|
||||
subgraph Analytics
|
||||
direction TB
|
||||
sentry[Sentry]
|
||||
graylog[Graylog]
|
||||
grafana[Grafana]
|
||||
end
|
||||
```
|
||||
|
||||
In development, backend and updater use the same redis, but in production they are separate.
|
||||
|
||||
See also [older graph](documentation/Oodikone.png), which presents more information.
|
||||
|
||||
### Documentation
|
||||
|
||||
Folder `documentation` currently contains some notes on sis-db schema and is meant as a place for storing useful information on Oodikone to make life easier for future developers. Feel free to add anything relevant!
|
||||
|
||||
### Basics
|
||||
|
||||
@@ -67,6 +116,8 @@ Some useful commands are defined in `package.json` and can be run with `npm run
|
||||
- `npm run both`: starts oodikone and updater with anonymized data
|
||||
- `npm run both:real`: starts oodikone and updater with real data
|
||||
- `npm run docker:down`: stops the whole environment
|
||||
- `npm run testupdater`: runs tests for updater
|
||||
- `npm run flushredis`: Clears redis, which forces big calculations to be redone in e.g. studyprogramme overview and faculty views.
|
||||
|
||||
Once you have ran setup for oodikone , you can just execute the first one (`npm run oodikone`). After starting and waiting for a while for containers to compile, oodikone can be accessed at [http://localhost:3000/](http://localhost:3000/) and Adminer (database investigation tool) at [http://localhost:5050/](http://localhost:5050/). Adminer requires you to login with username `postgres` and with any password you choose (for example `p`).
|
||||
|
||||
@@ -76,13 +127,13 @@ As said, the development environment runs entirely inside docker containers. To
|
||||
|
||||
`./run.sh` is simply a wrapper script to run oodikone, updater or both services in either anon or real mode. If you take a look at `package.json`, you can see that most of the predefined scripts above use `./run.sh` under the hood.
|
||||
|
||||
It is recommended to spend some time to become familiar with `docker` and `docker-compose` cli commands. You can then use them directly or with `./run.sh` wrapper. Here is some examples for day-to-day development situations:
|
||||
It is recommended to spend some time to become familiar with `docker` and `docker compose` cli commands. You can then use them directly or with `./run.sh` wrapper. Here is some examples for day-to-day development situations:
|
||||
|
||||
- `./run.sh oodikone anon pull`: Pull all images related to oodikone development
|
||||
- `./run.sh updater real up --build --force-recreate --detach`: Start updater detached (=in the background) in real data mode, but build new images before starting
|
||||
- `docker-compose ps`: view the containers in the running environment
|
||||
- `docker-compose logs frontend`: print logs for just frontend
|
||||
- `docker-compose logs --follow --tail 100 backend`: print last hundred rows of backend logs and begin to follow them in your terminal window
|
||||
- `docker compose ps`: view the containers in the running environment
|
||||
- `docker compose logs frontend`: print logs for just frontend
|
||||
- `docker compose logs --follow --tail 100 backend`: print last hundred rows of backend logs and begin to follow them in your terminal window
|
||||
- `docker exec -it backend sh`: open bash terminal inside backend container
|
||||
- `docker exec -it sis-db psql -U postgres sis-db-real`: open psql client to investigate sis real data database.
|
||||
|
||||
@@ -98,31 +149,27 @@ Cli script sets up pre-commit hooks that are used to lint and fix files before c
|
||||
|
||||
For more information on how files are tested, take a look at "lint-staged" in `package.json`. Some files (e.g. github action files, dockerfiles, shell scripts) are checked with external tools and may require you to install those tools in case you're modifying files in question.
|
||||
|
||||
There's no separate "lintfix" / "format" / "lint" command available, since pre-commit hooks will fix formatting and basic linting errors for you. In case pre-commit fails, you should fix the erroring code manually.
|
||||
|
||||
However, if you want to run linting / formatting manually from command line, `package.json` defines `npm run eslint`, `npm run prettier` and `npm run stylelint` which you can use as an entrypoint.
|
||||
Pre-commit hooks will fix auto-fixable problems. To set up quick formatting: In VSCode you can go to command palette, select "ESLint: Fix all auto-fixable problems" and assign a hotkey for it, like `SHIFT + CTRL + '.'`.
|
||||
|
||||
## 🔨 Testing & CI
|
||||
|
||||
There are three types of tests in this project: static tests with eslint/prettier/other tools, jest integration tests for single service and cypress end-to-end tests. Linting is described above and other do work as follows:
|
||||
We use cypress for end-to-end testing. No unit tests are currently used.
|
||||
|
||||
- Cypress
|
||||
- can be launched in interactive mode with `npm run cypress open`. `package.json` defines entrypoint `npm run cypress` so you can basically run cypress with any arguments you want
|
||||
- are defined in cypress -folder and cypress.config.json
|
||||
- whole cypress test stack takes about 15 to 20 mins to run. Since tests are ran in our Github actions CI pipe, you're encouraged to take advantage of this instead of running all tests locally.
|
||||
- There are some different user types and cypress commands defined for testing. Take a look at these when debugging tests.
|
||||
- Jest
|
||||
- are ran with `docker-compose.test.yml`
|
||||
- test command and environment themselves are in Dockerfile for service in question
|
||||
- can be debugged by overriding default command in `docker-compose.test.yml` and running docker container in interactive mode
|
||||
Cypress
|
||||
|
||||
- Can be launched in interactive mode with `npm run cypress open`. `package.json` defines entrypoint `npm run cypress` so you can basically run cypress with any arguments you want
|
||||
- Are defined in cypress -folder and cypress.config.json
|
||||
- The entire cypress test stack takes about 15 to 20 mins to run. Since tests are ran in our Github actions CI pipe, you're encouraged to take advantage of this instead of running all tests locally.
|
||||
- Running tests locally sometimes causes problems after running the whole tests suite due to some login-related issue. Also for this reason, it is usually best practice to only run single views locally, and let the CI run the entire test suite.
|
||||
- There are some different user types and cypress commands defined for testing. Take a look at these when debugging tests.
|
||||
|
||||
Continuous integration (CI) works with Github actions and is defined in workflow files in `.github/workflows` folder:
|
||||
|
||||
- oodikone setup for cypress and other tests in CI is defined in `docker-compose.ci.yml`. Take a look at this too if debugging github action workflows.
|
||||
- tests are run on every push.
|
||||
- after successful test run, oodikone is deployed to staging
|
||||
- after creating a release, oodikone is deployed to production
|
||||
- updater is deployed to production on every push, no tests included
|
||||
- Oodikone setup for cypress and other tests in CI is defined in `docker-compose.ci.yml`. Take a look at this too if debugging github action workflows.
|
||||
- Tests are run on every push
|
||||
- After a successful test run, oodikone is deployed to staging
|
||||
- After creating a release, oodikone is deployed to production
|
||||
- Updater is deployed to production when pushed if tests pass
|
||||
|
||||
## ❓FAQ
|
||||
|
||||
@@ -130,21 +177,19 @@ Continuous integration (CI) works with Github actions and is defined in workflow
|
||||
|
||||
You should always install the dependencies **inside** the container to have the application **inside** the container access them. Module might be missing for example when someone else installs a new library and you only pull the changes in package.json. Use `docker exec <service> npm ci` to install modules inside the container.
|
||||
|
||||
### I get the "You are not authorized to use Oodikone" -message when I try to work with anon-data
|
||||
### Studyguidance groups don't work on my machine
|
||||
|
||||
Make sure you have your VPN on
|
||||
|
||||
### I made a change to sis-db models aand created a migration, but tests are failing in CI!
|
||||
|
||||
Migrations are handled by sis-importer-worker, since oodikone is using only replica. Since CI is testing only services included in oodikone, your new changes aren't migrated and thus are failing. Open anonyymioodi-folder in this repo to find more about how to to update the anon sis-db.
|
||||
Make sure you have your VPN on.
|
||||
|
||||
### Everything is broken, can't get oodikone running, data is not there etc.
|
||||
|
||||
Just do clean install by launching cli with `npm cli` and running option 1: _Set up oodikone from scratch_.
|
||||
First: Try `npm run cli` option 1: _Set up oodikone from scratch_ and then option 2 to download and restore databases (or option 4 to skip downloading, if you've done it recently).
|
||||
|
||||
If that does not help, try option 5: _Docker system prune_. Notice that this clears up **all docker data**, including data related to other projects. If this is not ok, you'll need to identify the oodikone-specific docker volumes yourself and caches and clear those invididually.
|
||||
|
||||
## ✌🏼 Maintainers and contribution
|
||||
|
||||
Toska of course.
|
||||
[Toska](https://toska.dev) of course.
|
||||
|
||||
University of Helsinki
|
||||
|
||||
|
||||
@@ -43,6 +43,6 @@ fi
|
||||
|
||||
infomsg "Committing and pushing to registry"
|
||||
database=${args[0]}
|
||||
docker commit "$database" "registry.toska.cs.helsinki.fi/$database"
|
||||
docker push "registry.toska.cs.helsinki.fi/$database"
|
||||
docker commit "$database" "registry-toska.ext.ocp-prod-0.k8s.it.helsinki.fi/$database"
|
||||
docker push "registry-toska.ext.ocp-prod-0.k8s.it.helsinki.fi/$database"
|
||||
successmsg "Succesfully pushed"
|
||||
|
||||
@@ -9,7 +9,7 @@ services:
|
||||
PGDATA: /data
|
||||
POSTGRES_DB: kone-db
|
||||
POSTGRES_HOST_AUTH_METHOD: trust
|
||||
image: registry.toska.cs.helsinki.fi/kone-db
|
||||
image: registry-toska.ext.ocp-prod-0.k8s.it.helsinki.fi/kone-db
|
||||
# image: postgres:13.4-alpine
|
||||
profiles:
|
||||
- oodikone
|
||||
@@ -21,7 +21,7 @@ services:
|
||||
PGDATA: /data
|
||||
POSTGRES_DB: sis-db
|
||||
POSTGRES_HOST_AUTH_METHOD: trust
|
||||
# image: registry.toska.cs.helsinki.fi/sis-db
|
||||
# image: registry-toska.ext.ocp-prod-0.k8s.it.helsinki.fi/sis-db
|
||||
image: postgres:13.2
|
||||
shm_size: 2gb
|
||||
|
||||
@@ -32,7 +32,7 @@ services:
|
||||
PGDATA: /data
|
||||
POSTGRES_DB: sis-importer-db
|
||||
POSTGRES_HOST_AUTH_METHOD: trust
|
||||
# image: registry.toska.cs.helsinki.fi/sis-importer-db
|
||||
# image: registry-toska.ext.ocp-prod-0.k8s.it.helsinki.fi/sis-importer-db
|
||||
image: postgres:13.1
|
||||
profiles:
|
||||
- updater
|
||||
@@ -44,7 +44,7 @@ services:
|
||||
PGDATA: /data
|
||||
POSTGRES_DB: user-db
|
||||
POSTGRES_HOST_AUTH_METHOD: trust
|
||||
image: registry.toska.cs.helsinki.fi/user-db
|
||||
image: registry-toska.ext.ocp-prod-0.k8s.it.helsinki.fi/user-db
|
||||
# image: postgres:13.4-alpine
|
||||
profiles:
|
||||
- oodikone
|
||||
|
||||
@@ -51,6 +51,8 @@ set_custom_select_prompt() {
|
||||
"Set up oodikone from scratch."
|
||||
"Reset all real data."
|
||||
"Reset single database."
|
||||
"Restore data from dumps"
|
||||
"Docker system prune"
|
||||
"Quit."
|
||||
)
|
||||
}
|
||||
@@ -72,6 +74,10 @@ while true; do
|
||||
reset_single_database
|
||||
set_custom_select_prompt # Set prompt back to initial values
|
||||
;;
|
||||
"Restore data from dumps")
|
||||
restore_data_from_dumps;;
|
||||
"Docker system prune")
|
||||
docker_prune;;
|
||||
"Quit.")
|
||||
break 2;;
|
||||
*) msg "${RED}Invalid option:${NOFORMAT} $REPLY
|
||||
|
||||
@@ -15,5 +15,6 @@ module.exports = defineConfig({
|
||||
},
|
||||
baseUrl: 'http://localhost:3000',
|
||||
specPattern: 'cypress/e2e/**/*.{js,jsx,ts,tsx}',
|
||||
testIsolation: false,
|
||||
},
|
||||
})
|
||||
|
||||
@@ -0,0 +1,159 @@
|
||||
/// <reference types="Cypress" />
|
||||
|
||||
const openCompletedCoursesModal = () => {
|
||||
cy.get('button').contains('Search completed courses of students').click()
|
||||
}
|
||||
|
||||
const openSearch = () => {
|
||||
cy.init('/completedcoursessearch')
|
||||
hasLanded()
|
||||
}
|
||||
const selectSavedCourselist = name => {
|
||||
cy.get('[data-cy="history-search"]').children().eq(0).type(name).type('{enter}')
|
||||
}
|
||||
|
||||
const hasLanded = () => {
|
||||
cy.contains('Search completed courses')
|
||||
cy.contains(
|
||||
'Here you can search by a list of student and course numbers to see whether students have completed certain courses yet'
|
||||
)
|
||||
}
|
||||
|
||||
const deleteAllSearches = () => {
|
||||
cy.contains('Saved courselists')
|
||||
.get('.dropdown')
|
||||
.then(d => {
|
||||
const searchItems = d.find('div[role=option] > span[class=text]')
|
||||
for (let i = 0; i < searchItems.length; i++) {
|
||||
if (searchItems[i].textContent.includes('TEST-')) {
|
||||
cy.get('[data-cy="history-search"]').children().eq(0).type(searchItems[i].textContent).type('{enter}')
|
||||
cy.get('button').contains('Delete').click()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
describe('When search modal is opened', () => {
|
||||
after(() => {
|
||||
cy.init('/completedcoursessearch')
|
||||
hasLanded()
|
||||
cy.url().should('include', '/completedcoursessearch')
|
||||
cy.contains('Search completed courses of students')
|
||||
cy.get('[data-cy="open-completed-courses-modal-button"]').click()
|
||||
deleteAllSearches()
|
||||
})
|
||||
|
||||
it('Modal opens correctly', () => {
|
||||
cy.init('/completedcoursessearch')
|
||||
hasLanded()
|
||||
openCompletedCoursesModal()
|
||||
cy.contains('Search completed courses of students')
|
||||
cy.contains('Insert one or more student numbers, separated by a space, a newline, or a comma.')
|
||||
cy.contains('Insert one or more courses, separated by a space, a newline, or a comma.')
|
||||
cy.contains('Insert name for this course list if you wish to save it')
|
||||
})
|
||||
|
||||
describe('When a search is executed with invalid input', () => {
|
||||
it('Returns empty table and notification', () => {
|
||||
cy.init('/completedcoursessearch')
|
||||
hasLanded()
|
||||
openCompletedCoursesModal()
|
||||
cy.get('[data-cy="student-no-input"]').type('1')
|
||||
cy.get('[data-cy="course-list-input"]').type('1')
|
||||
cy.get('[data-cy="completed-courses-search-button"]').click()
|
||||
cy.get('[data-cy="rights-notification"]').should(
|
||||
'contain.text',
|
||||
'The following students information could not be displayed'
|
||||
)
|
||||
cy.get('[data-cy="rights-notification"]').should('contain.text', '1')
|
||||
cy.get('[data-cy="completed-courses-table-div"]').should('contain.text', 'Completed courses search')
|
||||
cy.get('[data-cy="completed-courses-table-div"]').should('contain.text', 'Student Number')
|
||||
cy.get('[data-cy="completed-courses-table-div"]').should('not.contain', '1')
|
||||
})
|
||||
})
|
||||
|
||||
describe('When a search with correct data is executed', () => {
|
||||
before(() => {
|
||||
cy.init('/completedcoursessearch')
|
||||
hasLanded()
|
||||
openCompletedCoursesModal()
|
||||
cy.fixture('completedCoursesData').then(({ studentSet1, coursesSet1 }) => {
|
||||
cy.get('[data-cy="student-no-input"]').type(studentSet1.join('\n'))
|
||||
cy.get('[data-cy="course-list-input"]').type(coursesSet1.join('\n'))
|
||||
cy.get('[data-cy="completed-courses-search-button"]').click()
|
||||
})
|
||||
})
|
||||
|
||||
it('Pushes the query to url', () => {
|
||||
cy.url().should(
|
||||
'include',
|
||||
'/completedcoursessearch?courseList=TKT10001&courseList=TKT10002&courseList=TKT10003&courseList=TKT10004&studentList=010450938&studentList=010589388&studentList=010614509'
|
||||
)
|
||||
})
|
||||
/*
|
||||
TODO: FIX
|
||||
This is skipped because it's flaky in github actions for unknown reasons.
|
||||
Instead of the results, a text appears saying "Something went wrong, please try refreshing the page".
|
||||
*/
|
||||
it.skip('Finds correct students and courses', () => {
|
||||
// eslint-disable-next-line cypress/no-unnecessary-waiting
|
||||
cy.wait(5000)
|
||||
cy.fixture('completedCoursesData').then(({ studentSet1, coursesSet1 }) => {
|
||||
cy.get('[data-cy="completed-courses-table-div"]').should('contain.text', studentSet1[0])
|
||||
cy.get('[data-cy="completed-courses-table-div"]').should('contain.text', studentSet1[1])
|
||||
cy.get('[data-cy="completed-courses-table-div"]').should('contain.text', studentSet1[2])
|
||||
cy.get('[data-cy="completed-courses-table-div"]').should('contain.text', coursesSet1[0])
|
||||
cy.get('[data-cy="completed-courses-table-div"]').should('contain.text', coursesSet1[1])
|
||||
cy.get('[data-cy="completed-courses-table-div"]').should('contain.text', coursesSet1[2])
|
||||
cy.get('[data-cy="completed-courses-table-div"]').should('contain.text', coursesSet1[3])
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
const courseListNames = ['TEST-courselist1', 'TEST-courselist2']
|
||||
|
||||
describe('Courselist saving-related functions work', () => {
|
||||
beforeEach(() => {
|
||||
openSearch()
|
||||
openCompletedCoursesModal()
|
||||
cy.get('[data-cy="course-list-input"]').type('CSM14204\nTKT10004')
|
||||
cy.get('[data-cy="search-name"]').type(courseListNames[0])
|
||||
cy.get('button').contains('Save').click()
|
||||
})
|
||||
|
||||
it('Course list can be saved', () => {
|
||||
selectSavedCourselist(courseListNames[0])
|
||||
openSearch()
|
||||
openCompletedCoursesModal()
|
||||
selectSavedCourselist(courseListNames[0])
|
||||
cy.get('[data-cy="course-list-input"]').contains('TKT10004').contains('CSM14204')
|
||||
})
|
||||
|
||||
it('Course list can be deleted', () => {
|
||||
openSearch()
|
||||
openCompletedCoursesModal()
|
||||
cy.get('[data-cy="search-name"]').type(courseListNames[1])
|
||||
cy.get('button').contains('Save').click()
|
||||
openSearch()
|
||||
openCompletedCoursesModal()
|
||||
selectSavedCourselist(courseListNames[1])
|
||||
cy.get('button').contains('Delete').click()
|
||||
cy.get('[data-cy="history-search"]')
|
||||
.type(courseListNames[1])
|
||||
.type('{enter}')
|
||||
.should('not.contain', courseListNames[1])
|
||||
})
|
||||
|
||||
it('Course list can be updated', () => {
|
||||
openSearch()
|
||||
openCompletedCoursesModal()
|
||||
selectSavedCourselist(courseListNames[0])
|
||||
cy.get('[data-cy="course-list-input"]').type(`,TKT10001`)
|
||||
cy.get('button').contains('Save').click()
|
||||
openSearch()
|
||||
openCompletedCoursesModal()
|
||||
selectSavedCourselist(courseListNames[0])
|
||||
cy.get('[data-cy="course-list-input"]').contains(`TKT10001`)
|
||||
})
|
||||
})
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable cypress/no-unnecessary-waiting */
|
||||
/// <reference types="Cypress" />
|
||||
|
||||
describe('Course Statistics tests', () => {
|
||||
@@ -154,7 +155,7 @@ describe('Course Statistics tests', () => {
|
||||
cy.contains('Fetch statistics').should('be.enabled').click()
|
||||
cy.contains('Käyttöjärjestelmät')
|
||||
cy.contains('TKT20003')
|
||||
|
||||
cy.wait(5000)
|
||||
cy.get('tbody > :nth-child(3) > :nth-child(2) .level').click()
|
||||
cy.contains('Population of course Käyttöjärjestelmät 2019-2020 (open and normal')
|
||||
cy.contains('TKT20003')
|
||||
@@ -166,12 +167,17 @@ describe('Course Statistics tests', () => {
|
||||
|
||||
it('Population of course shows grades for each student', () => {
|
||||
cy.get("input[placeholder='Search by a course code']").type('TKT20001')
|
||||
cy.contains(/^TKT20001, AYTKT20001$/).click()
|
||||
cy.contains('Fetch statistics').should('be.enabled').click()
|
||||
cy.wait(2000)
|
||||
cy.contains(/^TKT20001, AYTKT20001$/).click({ force: true })
|
||||
cy.wait(2000)
|
||||
cy.contains('Fetch statistics').should('be.enabled').click({ force: true })
|
||||
cy.wait(2000)
|
||||
cy.contains('TKT20001, 58131, AYTKT20001 Tietorakenteet ja algoritmit')
|
||||
cy.get('tbody >:nth-child(3) > :nth-child(2) .level').click()
|
||||
cy.get('tbody >:nth-child(3) > :nth-child(2) .level').click({ force: true })
|
||||
cy.wait(2000)
|
||||
cy.contains('Population of course Tietorakenteet ja algoritmit 2018-2019 (open and normal)')
|
||||
cy.contains('Students (204)').click()
|
||||
cy.contains('Students (204)').click({ force: true })
|
||||
cy.wait(2000)
|
||||
cy.contains('td', '010262566').siblings().contains('4')
|
||||
cy.contains('td', '010674989').siblings().contains('1')
|
||||
})
|
||||
@@ -182,6 +188,7 @@ describe('Course Statistics tests', () => {
|
||||
cy.contains('Fetch statistics').should('be.enabled').click()
|
||||
cy.contains('TKT20003, 582219 Käyttöjärjestelmät')
|
||||
cy.get('[data-cy=unify_radio_reqular]').click()
|
||||
cy.wait(5000)
|
||||
cy.get('tbody > :nth-child(3) > :nth-child(2) .level').click()
|
||||
cy.contains('Population of course Käyttöjärjestelmät 2019-2020')
|
||||
cy.contains('Language distribution').click()
|
||||
@@ -360,7 +367,7 @@ describe('Course Statistics tests', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it.skip('Some features of Course Statistics are hidden for courseStatistics-users without other rights', () => {
|
||||
it('Some features of Course Statistics are hidden for courseStatistics-users without other rights', () => {
|
||||
cy.init('/coursestatistics', 'onlycoursestatistics')
|
||||
cy.get('[data-cy=navbar-courseStatistics]').click()
|
||||
cy.get('[data-cy=course-code-input]').type('TKT20003')
|
||||
@@ -373,11 +380,9 @@ describe('Course Statistics tests', () => {
|
||||
cy.cs('viewMode-Attempts').click()
|
||||
|
||||
const attemptsTableContents = [
|
||||
// [time, passed, failed, passrate]
|
||||
['Total', 288, 213, 75, '73.96 %'], //Somehow cypress in CI can't find the number even if it is present in the artifact video
|
||||
['2020-2021', '5 or less students', 'NA', 'NA', 'NA'],
|
||||
// ['Total', null, 295, 216, 79, '73.22 %'],
|
||||
// ['2020-2021', null, 5, 2, 3, '40.00 %'],
|
||||
// [time, passed, failed, passrate, total enrollments, enrolled, rejected, aborted]
|
||||
['Total', null, 288, 213, 75, '73.96 %', 0, 0, 0, 0],
|
||||
['2020-2021', null, '5 or less students', 'NA', 'NA', 'NA', 'NA', 'NA', 'NA', 'NA'],
|
||||
['2019-2020', null, 164, 121, 43, '73.78 %'],
|
||||
['2018-2019', null, 85, 60, 25, '70.59 %'],
|
||||
['2017-2018', null, 39, 32, 7, '82.05 %'],
|
||||
|
||||
@@ -41,7 +41,7 @@ const deleteAllSearches = () => {
|
||||
}
|
||||
|
||||
const fillForm = content => {
|
||||
cy.contains('Insert studentnumbers you wish to use for population here')
|
||||
cy.contains('Insert student numbers you wish to use for population here')
|
||||
.siblings()
|
||||
.get('textarea')
|
||||
.type(content.join('\n'))
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
/* eslint-disable cypress/no-unnecessary-waiting */
|
||||
/// <reference types="Cypress" />
|
||||
|
||||
// Change admin to regular user when feature is ready for general use
|
||||
|
||||
describe('Faculty overview', () => {
|
||||
describe('Faculty can be selected', () => {
|
||||
it('Faculties are listed and one can be chosen', () => {
|
||||
cy.init('/faculties', 'admin')
|
||||
cy.init('/faculties')
|
||||
cy.get('[data-cy=select-faculty]').contains('td', 'H10')
|
||||
cy.contains('td', 'H99').should('not.exist')
|
||||
cy.contains('td', 'H90').click()
|
||||
@@ -31,6 +30,7 @@ describe('Faculty overview', () => {
|
||||
describe('Faculty basic information: basic user', () => {
|
||||
beforeEach(() => {
|
||||
cy.init('/faculties')
|
||||
cy.wait(3000)
|
||||
cy.contains('td', 'H80').click()
|
||||
})
|
||||
it('Basic information tab show all graphs and tables', () => {
|
||||
@@ -100,7 +100,6 @@ describe('Faculty overview', () => {
|
||||
|
||||
// I tried to fix this without cy.wait() method without effort. Sorting the programmes
|
||||
// or something else causes rerendering of the view and yield to failing tests.
|
||||
// eslint-disable-next-line cypress/no-unnecessary-waiting
|
||||
cy.wait(10000)
|
||||
cy.get('[data-cy="Button-Show-CreditsProducedByTheFaculty-0"]').click()
|
||||
cy.get('[data-cy="Cell-CreditsProducedByTheFaculty-0"]').should('be.visible')
|
||||
@@ -139,8 +138,11 @@ describe('Faculty overview', () => {
|
||||
describe('Average graduation times', () => {
|
||||
beforeEach(() => {
|
||||
cy.init('/faculties')
|
||||
cy.wait(3000)
|
||||
cy.contains('td', 'H60').click()
|
||||
cy.wait(3000)
|
||||
cy.contains('Graduation times').click()
|
||||
cy.wait(3000)
|
||||
})
|
||||
|
||||
it('User can view graduation graphs', () => {
|
||||
@@ -154,6 +156,12 @@ describe('Faculty overview', () => {
|
||||
})
|
||||
|
||||
it('Graphs display data', () => {
|
||||
cy.get('[data-cy="Section-bachelor"]').within(() => {
|
||||
cy.get('div[class="faculty-breakdown-graph"]')
|
||||
})
|
||||
|
||||
cy.get('[data-cy="GraduationTimeToggle"]').click()
|
||||
|
||||
cy.get('[data-cy="Section-bachelor"]').within(() => {
|
||||
cy.get('div[class="faculty-graph"]')
|
||||
cy.contains('1 graduated').should('have.length', 1)
|
||||
@@ -196,6 +204,7 @@ describe('Faculty overview', () => {
|
||||
})
|
||||
|
||||
it('Graduation times grouping and time types can be toggled', () => {
|
||||
cy.get('[data-cy="GraduationTimeToggle"]').click()
|
||||
cy.get('[data-cy="GroupByToggle"]').click()
|
||||
cy.get('[data-cy="Section-bachelor"]').should('not.exist')
|
||||
cy.get('[data-cy="Section-master"]').should('be.visible')
|
||||
@@ -204,20 +213,93 @@ describe('Faculty overview', () => {
|
||||
cy.get('div[class="faculty-graph"]')
|
||||
cy.contains('1 graduated (100 % of class)').should('have.length', 1)
|
||||
cy.contains('1 graduated').trigger('mouseover')
|
||||
cy.contains('From class of 2018, 1/undefined students have graduated')
|
||||
cy.contains('From class of 2018, 1/1 students have graduated')
|
||||
})
|
||||
|
||||
cy.get('[data-cy="GraduationTimeToggle"]').click()
|
||||
cy.get('[data-cy="Section-master"]').within(() => {
|
||||
cy.contains('1 graduated (100 % of class)').click()
|
||||
cy.get('div[class="programmes-graph"]').should('be.visible')
|
||||
cy.get('div[class="programmes-graph"]').within(() => {
|
||||
cy.contains('1').click()
|
||||
cy.get('div[class="programmes-breakdown-graph"]').should('be.visible')
|
||||
cy.get('div[class="programmes-breakdown-graph"]').within(() => {
|
||||
cy.contains('Year 2018 by start year')
|
||||
cy.contains('1 graduated').trigger('mouseover')
|
||||
cy.contains('From class of 2018, 1/1 students have graduated')
|
||||
cy.contains('mean study time: 25 months')
|
||||
cy.contains('EDUM')
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('Faculty Progress and student population -tab', () => {
|
||||
beforeEach(() => {
|
||||
cy.init('/faculties')
|
||||
cy.contains('td', 'H50').click()
|
||||
cy.wait(1000)
|
||||
cy.contains('Programmes and student populations').click()
|
||||
cy.wait(1000)
|
||||
})
|
||||
|
||||
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('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('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')
|
||||
})
|
||||
// Following two tests just test that nothing goes broken after toggling
|
||||
// No data for testing that data actual change as supposed
|
||||
it('Graduations can be excluded', () => {
|
||||
cy.get('[data-cy="FacultyStudentStatsTable"]').should('be.visible')
|
||||
cy.get('[data-cy="StudentToggle"]').click()
|
||||
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', () => {
|
||||
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('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('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')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
+49
-38
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable cypress/no-unnecessary-waiting */
|
||||
/// <reference types="Cypress" />
|
||||
|
||||
// const baseUrl = Cypress.config().baseUrl
|
||||
@@ -20,6 +21,7 @@ const clearSingleDropdownSelection = dataCyAttribute => {
|
||||
}
|
||||
|
||||
const testRangeFilter = (parentEl, min, max, expected) => {
|
||||
cy.clock(MOCKED_DATE, ['Date'])
|
||||
cy.cs(parentEl)
|
||||
.cs('range-selector-min')
|
||||
.find('input')
|
||||
@@ -49,8 +51,6 @@ const createRunTestStepWithPreAndPostPartsFunction = amountWithoutFiltering => {
|
||||
const card = cy.cs(`${filterName}-filter-card`)
|
||||
|
||||
card.invoke('attr', 'data-open').then(open => {
|
||||
console.log('open', open)
|
||||
|
||||
const getHeader = () => cy.cs(`${filterName}-header`)
|
||||
|
||||
if (open === 'false') {
|
||||
@@ -68,6 +68,25 @@ const createRunTestStepWithPreAndPostPartsFunction = amountWithoutFiltering => {
|
||||
}
|
||||
}
|
||||
|
||||
describe('Population Statistics with CS master', () => {
|
||||
const pathToCSMast2019 =
|
||||
'/populations?months=36&semesters=FALL&semesters=SPRING&studyRights={%22programme%22%3A%22MH50_009%22}&tag&year=2019'
|
||||
const defaultAmountOfStudents = 27
|
||||
const runTestStepWithPreAndPostParts = createRunTestStepWithPreAndPostPartsFunction(defaultAmountOfStudents)
|
||||
before(() => {
|
||||
cy.init(pathToCSMast2019)
|
||||
})
|
||||
|
||||
it('Study track filter works', () => {
|
||||
runTestStepWithPreAndPostParts('StudyTrack', () => {
|
||||
const card = cy.cs('StudyTrack-filter-card')
|
||||
const programmeDropdown = card.cs('StudyTrack-filter-dropdown').selectFromDropdown(0)
|
||||
checkFilteringResult(1)
|
||||
programmeDropdown.get('i.delete').click()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('Population Statistics', () => {
|
||||
const pathToCSBach2018 =
|
||||
'/populations?months=36&semesters=FALL&semesters=SPRING&studyRights={%22programme%22%3A%22KH50_005%22}&tag&year=2018'
|
||||
@@ -87,18 +106,6 @@ describe('Population Statistics', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// The test does not work since it tries to filter by "Ravitsemustiede"-studytrack when checking
|
||||
// CS Bachelor students. Now that the irrelevant studytracks have been removed, the test fails.
|
||||
// So fix this
|
||||
it.skip('Study track filter works', () => {
|
||||
runTestStepWithPreAndPostParts('StudyTrack', () => {
|
||||
const card = cy.cs('StudyTrack-filter-card')
|
||||
const programmeDropdown = card.cs('StudyTrack-filter-dropdown').selectFromDropdown(0)
|
||||
checkFilteringResult(1)
|
||||
programmeDropdown.get('i.delete').click()
|
||||
})
|
||||
})
|
||||
|
||||
it('Graduation filter works', () => {
|
||||
runTestStepWithPreAndPostParts('GraduatedFromProgramme', () => {
|
||||
const getCard = () => cy.cs('GraduatedFromProgramme-filter-card')
|
||||
@@ -110,6 +117,7 @@ describe('Population Statistics', () => {
|
||||
checkFilteringResult(defaultAmountOfStudents - graduated)
|
||||
getCard().cs('option-all').click()
|
||||
})
|
||||
cy.cs('reset-all-filters').click()
|
||||
})
|
||||
|
||||
it('Transfer filter works', () => {
|
||||
@@ -121,12 +129,13 @@ describe('Population Statistics', () => {
|
||||
checkFilteringResult(defaultAmountOfStudents + transferred)
|
||||
cy.cs('TransferredToProgramme-filter-card').cs('option-havenot').click()
|
||||
})
|
||||
cy.cs('reset-all-filters').click()
|
||||
})
|
||||
|
||||
it('Enrollment filter works', () => {
|
||||
runTestStepWithPreAndPostParts('EnrollmentStatus', () => {
|
||||
cy.cs('enrollmentStatusFilter-status').selectFromDropdown(0)
|
||||
cy.cs('enrollmentStatusFilter-semesters').selectFromDropdown(0)
|
||||
cy.cs('enrollmentStatusFilter-semesters').selectFromDropdown(5)
|
||||
checkFilteringResult(148)
|
||||
clearSemanticUIMultipleDropDownSelection('enrollmentStatusFilter-semesters')
|
||||
})
|
||||
@@ -136,6 +145,7 @@ describe('Population Statistics', () => {
|
||||
runTestStepWithPreAndPostParts('CreditsEarned', () => {
|
||||
testRangeFilter('CreditsEarned-filter-card', 50, 150, 118)
|
||||
})
|
||||
cy.cs('reset-all-filters').click()
|
||||
})
|
||||
|
||||
it('Age filter works', () => {
|
||||
@@ -146,6 +156,7 @@ describe('Population Statistics', () => {
|
||||
})
|
||||
|
||||
it('Gender filter works', () => {
|
||||
cy.cs('reset-all-filters').click()
|
||||
runTestStepWithPreAndPostParts('Gender', () => {
|
||||
cy.cs('genderFilter-dropdown').selectFromDropdown(0)
|
||||
checkFilteringResult(42)
|
||||
@@ -181,21 +192,24 @@ describe('Population Statistics', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// plz fix
|
||||
it.skip('Courses filter works', () => {
|
||||
// courses takes some time to load, wait for it to complete
|
||||
cy.wait(10000)
|
||||
// courses takes long time to load, wait for it to complete
|
||||
cy.wait(25000)
|
||||
cy.contains('Courses of class').click()
|
||||
runTestStepWithPreAndPostParts('Courses', () => {
|
||||
cy.cs('courseFilter-course-dropdown').click()
|
||||
const courses = ['TKT20001', 'MAT11002']
|
||||
cy.cs('courseFilter-course-dropdown').click().contains(courses[0]).click()
|
||||
checkFilteringResult(140)
|
||||
const courses = ['TKT20002', 'MAT11002']
|
||||
cy.cs('courseFilter-course-dropdown').click().contains(`${courses[0]} - Ohjelmistotekniikka`).click()
|
||||
checkFilteringResult(118)
|
||||
cy.cs(`courseFilter-${courses[0]}-dropdown`).selectFromDropdown(1)
|
||||
checkFilteringResult(131)
|
||||
cy.cs('courseFilter-course-dropdown').click().contains(courses[1]).click()
|
||||
checkFilteringResult(56)
|
||||
checkFilteringResult(117)
|
||||
cy.cs('courseFilter-course-dropdown')
|
||||
.click()
|
||||
.contains(`${courses[1]} - Lineaarialgebra ja matriisilaskenta I`)
|
||||
.click()
|
||||
checkFilteringResult(50)
|
||||
cy.cs(`courseFilter-${courses[1]}-dropdown`).selectFromDropdown(2)
|
||||
checkFilteringResult(2)
|
||||
checkFilteringResult(1)
|
||||
cy.contains('Courses of class').click()
|
||||
courses.forEach(course => cy.cs(`courseFilter-${course}-clear`).click())
|
||||
})
|
||||
})
|
||||
@@ -263,12 +277,14 @@ describe('Course Statistics', () => {
|
||||
|
||||
it('Age filter works', () => {
|
||||
cy.clock(MOCKED_DATE, ['Date'])
|
||||
cy.cs('reset-all-filters').click()
|
||||
runTestStepWithPreAndPostParts('Age', () => {
|
||||
testRangeFilter('Age-filter-card', 20, 40, 33)
|
||||
})
|
||||
})
|
||||
|
||||
it('Gender filter works', () => {
|
||||
cy.cs('reset-all-filters').click()
|
||||
runTestStepWithPreAndPostParts('Gender', () => {
|
||||
cy.cs('genderFilter-dropdown').selectFromDropdown(0)
|
||||
checkFilteringResult(27)
|
||||
@@ -325,6 +341,7 @@ describe('Custom Population Statistics', () => {
|
||||
runTestStepWithPreAndPostParts('Age', () => {
|
||||
testRangeFilter('Age-filter-card', 20, 30, 1)
|
||||
})
|
||||
cy.cs('reset-all-filters').click()
|
||||
})
|
||||
|
||||
it('Programme defaults to "Active Study Right" mode', () => {
|
||||
@@ -364,22 +381,16 @@ describe('Custom Population Statistics', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it.skip('Courses filter works', () => {
|
||||
it('Courses filter works', () => {
|
||||
runTestStepWithPreAndPostParts('Courses', () => {
|
||||
const courses = ['MAT11001', 'TKT20004']
|
||||
cy.cs('courseFilter-course-dropdown')
|
||||
.click()
|
||||
.contains(courses[0] + ' ')
|
||||
.click()
|
||||
checkFilteringResult(5)
|
||||
cy.cs('courseFilter-course-dropdown').click().contains(`${courses[0]} - Johdatus yliopistomatematiikkaan`).click()
|
||||
checkFilteringResult(4)
|
||||
cy.cs(`courseFilter-${courses[0]}-dropdown`).selectFromDropdown(1)
|
||||
checkFilteringResult(5)
|
||||
cy.cs('courseFilter-course-dropdown')
|
||||
.click()
|
||||
.contains(courses[1] + ' ')
|
||||
.click()
|
||||
checkFilteringResult(4)
|
||||
cy.cs('courseFilter-course-dropdown').click().contains(`${courses[1]} - Tietoliikenteen perusteet`).click()
|
||||
checkFilteringResult(3)
|
||||
cy.cs(`courseFilter-${courses[1]}-dropdown`).selectFromDropdown(3)
|
||||
cy.cs(`courseFilter-${courses[1]}-dropdown`).selectFromDropdown(2)
|
||||
checkFilteringResult(1)
|
||||
courses.forEach(course => cy.cs(`courseFilter-${course}-clear`).click())
|
||||
})
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
/// <reference types="Cypress" />
|
||||
|
||||
const deleteAllSearches = () => {
|
||||
cy.contains('Saved populations')
|
||||
.parent()
|
||||
.parent()
|
||||
.parent()
|
||||
.parent()
|
||||
.get('.dropdown')
|
||||
.then(d => {
|
||||
const searchItems = d.find('div[role=option] > span[class=text]')
|
||||
for (let i = 0; i < searchItems.length; i++) {
|
||||
if (searchItems[i].textContent.includes('TEST-')) {
|
||||
cy.get('[data-cy="history-search"]').children().eq(0).type(searchItems[i].textContent).type('{enter}')
|
||||
cy.get('button').contains('Delete').click()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
describe('Open uni population tests', () => {
|
||||
describe('Open uni form can be used', () => {
|
||||
// When openUniUser is created, change it here
|
||||
beforeEach(() => {
|
||||
cy.init('/openunipopulation', 'admin')
|
||||
})
|
||||
after(() => {
|
||||
cy.init('/openunipopulation', 'admin')
|
||||
cy.url().should('include', '/openunipopulation')
|
||||
cy.contains('Open uni student population')
|
||||
cy.get('[data-cy="open-uni-search-button"]').click()
|
||||
deleteAllSearches()
|
||||
})
|
||||
|
||||
it('Finds a proper population', () => {
|
||||
cy.fixture('openUniPopulation').then(({ courseCodesSet1 }) => {
|
||||
cy.get('[data-cy="open-uni-search-button"]').click()
|
||||
cy.contains('Insert course code(s)').siblings().get('textarea').type(courseCodesSet1.join('\n'))
|
||||
cy.get('[data-cy="begin-of-search"]').clear().type('01.01.2015')
|
||||
cy.get('[data-cy="end-of-search"]').clear().type('01.01.2020')
|
||||
cy.get('button').contains('Search population').click()
|
||||
cy.contains('Beginning of the search for all fields:')
|
||||
cy.contains('01.01.2015')
|
||||
cy.contains('End of the search for enrollments:')
|
||||
cy.contains('01.01.2020')
|
||||
cy.url().should(
|
||||
'include',
|
||||
'/openunipopulation?courseCode=TKT10001&courseCode=TKT10002&enddate=01-01-2020&startdate=01-01-2015'
|
||||
)
|
||||
cy.get('[data-cy="open-uni-table-div"]').should('contain.text', courseCodesSet1[0])
|
||||
cy.get('[data-cy="open-uni-table-div"]').should('contain.text', courseCodesSet1[1])
|
||||
})
|
||||
})
|
||||
|
||||
it('Textarea can handle different kinds of input', () => {
|
||||
cy.get('[data-cy="open-uni-search-button"]').click()
|
||||
cy.contains('Insert course code(s)')
|
||||
.siblings()
|
||||
.get('textarea')
|
||||
.type('tkt10001 tkt10002,tkt10003\n TKT10004,,TKt10005\nTKT-1234')
|
||||
cy.get('button').contains('Search population').click()
|
||||
cy.get('[data-cy="open-uni-table-div"]').should('contain.text', 'TKT10001')
|
||||
cy.get('[data-cy="open-uni-table-div"]').should('contain.text', 'TKT10002')
|
||||
cy.get('[data-cy="open-uni-table-div"]').should('contain.text', 'TKT10003')
|
||||
cy.get('[data-cy="open-uni-table-div"]').should('contain.text', 'TKT10004')
|
||||
cy.get('[data-cy="open-uni-table-div"]').should('contain.text', 'TKT10005')
|
||||
cy.get('[data-cy="open-uni-table-div"]').should('not.contain', 'TKT-1234')
|
||||
})
|
||||
|
||||
it('Saves a custom population search', () => {
|
||||
cy.fixture('openUniPopulation').then(({ courseCodesSet1 }) => {
|
||||
cy.get('[data-cy="open-uni-search-button"]').click()
|
||||
cy.get('[data-cy="search-name"]').type('TEST-Avoimen-populaatio')
|
||||
cy.contains('Insert course code(s)').siblings().get('textarea').type(courseCodesSet1.join('\n'))
|
||||
cy.get('[data-cy="save-search"]').click()
|
||||
cy.get('[data-cy="history-search"]').children().eq(0).type('TEST-Avoimen-populaatio').type('{enter}')
|
||||
cy.contains('TEST-Avoimen-populaatio')
|
||||
cy.get('button').contains('Search population').click()
|
||||
cy.get('[data-cy="open-uni-table-div"]').should('contain.text', courseCodesSet1[0])
|
||||
cy.get('[data-cy="open-uni-table-div"]').should('contain.text', courseCodesSet1[1])
|
||||
})
|
||||
})
|
||||
|
||||
it('Updates a custom population search', () => {
|
||||
cy.fixture('openUniPopulation').then(({ courseCodesSet1, courseCodesSet2 }) => {
|
||||
cy.get('[data-cy="open-uni-search-button"]').click()
|
||||
cy.get('[data-cy="search-name"]').type('TEST-Avoimen-populaatio')
|
||||
cy.contains('Insert course code(s)').siblings().get('textarea').type(courseCodesSet1.join('\n'))
|
||||
cy.get('[data-cy="save-search"]').click()
|
||||
cy.get('button').contains('Search population').click()
|
||||
cy.get('[data-cy="open-uni-table-div"]').should('contain.text', courseCodesSet1[0])
|
||||
cy.get('[data-cy="open-uni-table-div"]').should('contain.text', courseCodesSet1[1])
|
||||
|
||||
cy.get('[data-cy="open-uni-search-button"]').click()
|
||||
cy.get('[data-cy="history-search"]').children().eq(0).type('TEST-Avoimen-populaatio').type('{enter}')
|
||||
cy.contains('Insert course code(s)').siblings().get('textarea').should('contain', courseCodesSet1.join(', '))
|
||||
cy.contains('Insert course code(s)').siblings().get('textarea').type('\n')
|
||||
cy.contains('Insert course code(s)').siblings().get('textarea').type(courseCodesSet2.join('\n'))
|
||||
cy.get('[data-cy="save-search"]').click()
|
||||
cy.get('button').contains('Search population').click()
|
||||
cy.get('[data-cy="open-uni-table-div"]').should('contain.text', courseCodesSet1[0])
|
||||
cy.get('[data-cy="open-uni-table-div"]').should('contain.text', courseCodesSet1[1])
|
||||
cy.get('[data-cy="open-uni-table-div"]').should('contain.text', courseCodesSet2[0])
|
||||
cy.get('[data-cy="open-uni-table-div"]').should('contain.text', courseCodesSet2[1])
|
||||
cy.get('[data-cy="open-uni-table-div"]').should('contain.text', courseCodesSet2[2])
|
||||
})
|
||||
})
|
||||
|
||||
it('View does not crash with non-existing courses', () => {
|
||||
cy.get('[data-cy="open-uni-search-button"]').click()
|
||||
cy.contains('Insert course code(s)').siblings().get('textarea').type('cypress-fun')
|
||||
cy.get('button').contains('Search population').click()
|
||||
cy.get('[data-cy="open-uni-table-div"]').should('not.contain', 'cypress-fun')
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,19 +1,8 @@
|
||||
/// <reference types="Cypress" />
|
||||
// Now "Class statistics" in UI
|
||||
|
||||
const moment = require('moment')
|
||||
const _ = require('lodash')
|
||||
|
||||
// const setPopStatsUntil = (until, includeSettings = []) => {
|
||||
// cy.contains('Advanced settings').siblings().get('[data-cy=advanced-toggle]').click()
|
||||
// includeSettings.forEach(setting => {
|
||||
// cy.contains('Advanced settings').parent().siblings().contains(setting).click()
|
||||
// })
|
||||
// cy.get('.adv-stats-until > .form-control').click().clear().type(until)
|
||||
// cy.contains('Fetch population with new settings').click()
|
||||
// cy.contains('Advanced settings')
|
||||
// }
|
||||
|
||||
describe('Population Statistics tests', () => {
|
||||
const pathToCSBach2017 =
|
||||
'/populations?months=36&semesters=FALL&semesters=SPRING&studyRights={%22programme%22%3A%22KH50_005%22}&tag&year=2017'
|
||||
@@ -24,6 +13,18 @@ describe('Population Statistics tests', () => {
|
||||
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')
|
||||
})
|
||||
|
||||
it('Population statistics search form is usable', () => {
|
||||
cy.contains('See class').should('be.disabled')
|
||||
cy.contains('Search for class')
|
||||
@@ -66,34 +67,35 @@ describe('Population Statistics tests', () => {
|
||||
it('Population statistics is usable on general level', () => {
|
||||
cy.visit(pathToCSBach2017)
|
||||
cy.cs('filtered-students')
|
||||
// This advanced settings does not work correctly
|
||||
// see https://github.com/UniversityOfHelsinkiCS/oodikone/issues/3552
|
||||
// setPopStatsUntil('toukokuu 2020')
|
||||
cy.get('.header').within(() => {
|
||||
cy.contains('Tietojenkäsittelytieteen kandiohjelma')
|
||||
cy.contains('class size 170 students')
|
||||
})
|
||||
cy.get('.card').within(() => {
|
||||
cy.contains('Excludes exchange students')
|
||||
cy.contains('Excludes students with non-degree study right')
|
||||
cy.contains('Excludes students who have transferred out of this programme')
|
||||
})
|
||||
cy.contains('Tietojenkäsittelytieteen kandiohjelma')
|
||||
cy.contains('class size 170 students')
|
||||
cy.contains('Excludes exchange students')
|
||||
cy.contains('Excludes students with non-degree study right')
|
||||
cy.contains('Excludes students who have transferred out of this programme')
|
||||
cy.cs('filtered-students')
|
||||
cy.contains('Courses of class').click()
|
||||
|
||||
cy.intercept('/api/v3/courseyearlystats**').as('coursePage')
|
||||
// eslint-disable-next-line cypress/no-unnecessary-waiting
|
||||
cy.wait(25000) // a bit hacky way, wait until ui is ready
|
||||
cy.get('[data-cy=toggle-group-module-TKT1]').click()
|
||||
cy.contains('td', 'TKT10002').siblings().find('i.level.up').click()
|
||||
cy.wait('@coursePage')
|
||||
cy.url().should('include', '/coursestatistics')
|
||||
cy.contains('TKT10002, 581325, AYTKT10002, A581325 Ohjelmoinnin perusteet')
|
||||
// Tests that use curriculum structure are commented out until
|
||||
// the test database is fixed to the same structure as real database
|
||||
// after the modules rework
|
||||
// cy.contains('Courses of class').click()
|
||||
|
||||
// cy.intercept('/api/v3/courseyearlystats**').as('coursePage')
|
||||
// // eslint-disable-next-line cypress/no-unnecessary-waiting
|
||||
// cy.wait(25000) // a bit hacky way, wait until ui is ready
|
||||
// cy.get('[data-cy=toggle-group-module-TKT1]').click()
|
||||
// cy.contains('td', 'TKT10002').siblings().find('i.level.up').click()
|
||||
// cy.wait('@coursePage')
|
||||
// cy.url().should('include', '/coursestatistics')
|
||||
// cy.contains('TKT10002, 581325, AYTKT10002, A581325 Ohjelmoinnin perusteet')
|
||||
})
|
||||
|
||||
it("Empty 'tags' tab has a link to the page where tags can be created", () => {
|
||||
cy.visit(pathToCSBach2017)
|
||||
cy.contains('Students (170)').click()
|
||||
cy.contains('Students (170)')
|
||||
.parent()
|
||||
.then($parentDiv => {
|
||||
if (!$parentDiv.hasClass('active')) cy.contains('Students (170)').click()
|
||||
})
|
||||
cy.get('[data-cy=student-table-tabs]').contains('Tags').click()
|
||||
cy.contains('No tags defined. You can define them here.').find('a').click()
|
||||
cy.contains('Tags').click()
|
||||
@@ -101,49 +103,45 @@ describe('Population Statistics tests', () => {
|
||||
cy.contains('Create new tag')
|
||||
})
|
||||
|
||||
// TODO: this fails in CI, but not locally. Investigate and fix
|
||||
it.skip('Advanced settings work', () => {
|
||||
it('Advanced settings work', () => {
|
||||
cy.visit(pathToCSBach2017)
|
||||
cy.get('[data-cy=advanced-toggle]').click()
|
||||
cy.contains('Statistics until')
|
||||
// only spring
|
||||
cy.cs('toggle-fall').click()
|
||||
cy.contains('Fetch class').click()
|
||||
|
||||
// moar waiting hack
|
||||
// eslint-disable-next-line cypress/no-unnecessary-waiting
|
||||
cy.wait(10000)
|
||||
|
||||
cy.contains('No statistics found for the given query.')
|
||||
|
||||
cy.contains('Students (0)')
|
||||
// only fall
|
||||
cy.get('[data-cy=advanced-toggle]').click()
|
||||
cy.cs('toggle-fall').click()
|
||||
cy.cs('toggle-spring').click()
|
||||
|
||||
cy.contains('Fetch class').click()
|
||||
|
||||
cy.contains('Credit accumulation (for 170 students)')
|
||||
|
||||
// spring + fall
|
||||
cy.get('[data-cy=advanced-toggle]').click()
|
||||
cy.cs('toggle-spring').click()
|
||||
cy.contains('Fetch class').click()
|
||||
|
||||
cy.contains('Credit accumulation (for 170 students)')
|
||||
})
|
||||
|
||||
it('Credit Statistics, Credits Gained tab works', () => {
|
||||
cy.selectStudyProgramme('Tietojenkäsittelytieteen kandiohjelma')
|
||||
cy.contains('Credit statistics').click()
|
||||
cy.contains('Credit statistics')
|
||||
.parent()
|
||||
.then($parentDiv => {
|
||||
if (!$parentDiv.hasClass('active')) cy.contains('Credit statistics').click()
|
||||
})
|
||||
cy.contains('Credits Gained').click()
|
||||
cy.get("[data-cy='credits-gained-main-table']").should('contain', 'All students of the class')
|
||||
const today = moment().endOf('month')
|
||||
const months = Math.round(moment.duration(moment(today).diff(moment('2017-08-01'))).asMonths())
|
||||
const limits = [1, ..._.range(1, 4).map(p => Math.ceil(months * ((p * 15) / 12))), null]
|
||||
// Months are capped to goal time of the degree, 36 months is the goal time for bachelor's degree
|
||||
const monthsForLimits = Math.min(36, months)
|
||||
const limits = [1, ..._.range(1, 5).map(p => Math.ceil(monthsForLimits * ((p * 15) / 12))), null]
|
||||
const ranges = _.range(1, limits.length).map(i => _.slice(limits, i - 1, i + 1))
|
||||
|
||||
cy.get('.credits-gained-table').should('contain', '(n=170)')
|
||||
cy.get('.credits-gained-table').should('contain', '(n = 170)')
|
||||
|
||||
for (const [start, end] of ranges) {
|
||||
let value = 'credits'
|
||||
@@ -161,7 +159,6 @@ describe('Population Statistics tests', () => {
|
||||
|
||||
cy.get("[data-cy='credits-gained-table-body'] td:nth-child(3)").then($els => {
|
||||
const sum = [...$els].map($el => parseInt($el.innerText)).reduce((a, b) => a + b, 0)
|
||||
|
||||
expect(sum).to.equal(170)
|
||||
})
|
||||
|
||||
@@ -172,7 +169,11 @@ describe('Population Statistics tests', () => {
|
||||
cy.visit(pathToCSMaster2019)
|
||||
cy.cs('filtered-students')
|
||||
cy.contains('Credit statistics')
|
||||
cy.get("[data-cy='credit-statistics']").click()
|
||||
cy.get("[data-cy='Credit statistics']")
|
||||
.parent()
|
||||
.then($parentDiv => {
|
||||
if (!$parentDiv.hasClass('active')) cy.get("[data-cy='Credit statistics']").click()
|
||||
})
|
||||
cy.contains('Credits Gained').click()
|
||||
cy.get('.credits-gained-divider').click()
|
||||
cy.get("[data-cy='credits-gained-table-Avoin väylä']").should('exist')
|
||||
@@ -183,7 +184,11 @@ describe('Population Statistics tests', () => {
|
||||
|
||||
it('Credit Statistics, Statistics pane works', () => {
|
||||
cy.selectStudyProgramme('Tietojenkäsittelytieteen kandiohjelma')
|
||||
cy.contains('Credit statistics').click({ force: true })
|
||||
cy.contains('Credit statistics')
|
||||
.parent()
|
||||
.then($parentDiv => {
|
||||
if (!$parentDiv.hasClass('active')) cy.contains('Credit statistics').click()
|
||||
})
|
||||
cy.get("[data-cy='credit-stats-tab'] > .menu > :nth-child(2)").click()
|
||||
|
||||
cy.get("[data-cy='credit-stats-table-name-header']").should('contain', 'Statistic for n = 170 Students')
|
||||
@@ -202,7 +207,11 @@ describe('Population Statistics tests', () => {
|
||||
const existing = '010113437'
|
||||
const nonExisting = '66666666'
|
||||
cy.visit(pathToCSBach2017)
|
||||
cy.contains('Students (170)').click()
|
||||
cy.contains('Students (170)')
|
||||
.parent()
|
||||
.then($parentDiv => {
|
||||
if (!$parentDiv.hasClass('active')) cy.contains('Students (170)').click()
|
||||
})
|
||||
cy.contains(existing)
|
||||
cy.contains(nonExisting).should('not.exist')
|
||||
cy.contains('button', 'Check studentnumbers').click()
|
||||
@@ -220,11 +229,11 @@ describe('Population Statistics tests', () => {
|
||||
})
|
||||
})
|
||||
|
||||
/*
|
||||
describe('when using IAM user', () => {
|
||||
beforeEach(() => {
|
||||
cy.init(pathToCSBach2017, 'onlyiamrights')
|
||||
})
|
||||
|
||||
it('Population statistics is visible', () => {
|
||||
cy.reload()
|
||||
|
||||
@@ -241,9 +250,10 @@ describe('Population Statistics tests', () => {
|
||||
cy.contains('Credit statistics')
|
||||
})
|
||||
|
||||
it('Students tab is not available', () => {
|
||||
it.skip('Students tab is not available', () => {
|
||||
cy.contains('Students (170)').should('not.exist')
|
||||
// TODO: somehow check that obfuscation works
|
||||
})
|
||||
})
|
||||
*/
|
||||
})
|
||||
|
||||
@@ -66,6 +66,7 @@ describe('Student Statistics tests', () => {
|
||||
cy.contains('td a', student.studentnumber).click()
|
||||
cy.contains('Introduction to Machine Learning (DATA11002)')
|
||||
.siblings()
|
||||
.last()
|
||||
.within(() => {
|
||||
cy.get('.level').click()
|
||||
})
|
||||
@@ -78,7 +79,7 @@ describe('Student Statistics tests', () => {
|
||||
cy.contains('td a', student.studentnumber).click()
|
||||
cy.get('[data-cy=sisulink] > a')
|
||||
.should('have.attr', 'href')
|
||||
.and('include', 'https://sis-helsinki.funidata.fi/tutor/role/staff/student/hy-hlo-84830887/basic/basic-info')
|
||||
.and('include', 'https://sisu.helsinki.fi/tutor/role/staff/student/hy-hlo-84830887/basic/basic-info')
|
||||
})
|
||||
|
||||
it('Searching with bad inputs doesnt yield results', () => {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable cypress/no-unnecessary-waiting */
|
||||
/// <reference types="Cypress" />
|
||||
|
||||
const deleteTag = name => {
|
||||
@@ -8,9 +9,11 @@ const deleteTag = name => {
|
||||
}
|
||||
|
||||
const getEmptyYears = isAcademicYear => {
|
||||
const thisYear = new Date().getFullYear()
|
||||
const today = new Date()
|
||||
const latestYear = isAcademicYear && today.getMonth() < 7 ? today.getFullYear() - 1 : today.getFullYear()
|
||||
|
||||
const years = []
|
||||
for (let year = thisYear; year > 2021; year--) {
|
||||
for (let year = latestYear; year > 2021; year--) {
|
||||
if (isAcademicYear) {
|
||||
years.push(`${year} - ${year + 1}`)
|
||||
} else {
|
||||
@@ -21,6 +24,20 @@ const getEmptyYears = isAcademicYear => {
|
||||
}
|
||||
|
||||
describe('Studyprogramme overview', () => {
|
||||
before(() => {
|
||||
cy.init('/study-programme', 'admin')
|
||||
cy.contains('a', 'Tietojenkäsittelytieteen kandiohjelma').click({ force: true })
|
||||
cy.get('.attached').contains('Update statistics').click()
|
||||
// eslint-disable-next-line cypress/no-unnecessary-waiting
|
||||
cy.wait(5000)
|
||||
cy.get('button').eq(0).click({ force: true })
|
||||
// eslint-disable-next-line cypress/no-unnecessary-waiting
|
||||
cy.wait(10000)
|
||||
cy.get('button').eq(1).click({ force: true })
|
||||
// eslint-disable-next-line cypress/no-unnecessary-waiting
|
||||
cy.wait(10000)
|
||||
})
|
||||
|
||||
/* Basic information overview -tests*/
|
||||
describe('Basic information -view works for basic user', () => {
|
||||
beforeEach(() => {
|
||||
@@ -46,8 +63,8 @@ describe('Studyprogramme overview', () => {
|
||||
// [Year, Started, Graduated, Transferred Away, Transferred to]
|
||||
...years.map(year => [year, 0, 0, 0, 0]),
|
||||
[2021, 0, 0, 1, 0],
|
||||
[2020, 12, 35, 3, 2],
|
||||
[2019, 87, 17, 0, 1],
|
||||
[2020, 10, 35, 3, 2],
|
||||
[2019, 86, 17, 0, 1],
|
||||
[2018, 161, 1, 0, 0],
|
||||
[2017, 171, 0, 0, 0],
|
||||
]
|
||||
@@ -62,8 +79,8 @@ describe('Studyprogramme overview', () => {
|
||||
...years.map(year => [year, 0, 0, 0, 0, 0]),
|
||||
[2021, 0, 0, 0, 0, 0],
|
||||
[2020, 5860, 5591, 102, 0, 167],
|
||||
[2019, 8696, 7737, 577, 33, 349],
|
||||
[2018, 7818, 6584, 852, 110, 272],
|
||||
[2019, 8696, 7737, 587, 23, 349],
|
||||
[2018, 7818, 6584, 837, 125, 272],
|
||||
[2017, 3085, 2492, 153, 52, 388],
|
||||
]
|
||||
|
||||
@@ -85,17 +102,19 @@ describe('Studyprogramme overview', () => {
|
||||
cy.checkTableStats(tableContents, 'GraduatedAndThesisWritersOfTheProgramme')
|
||||
})
|
||||
|
||||
it.skip('Special studyrights can be excluded and basic data changes accordingly', () => {
|
||||
it('Special studyrights can be excluded and basic data changes accordingly', () => {
|
||||
cy.get('[data-cy=StudentToggle]').click()
|
||||
// eslint-disable-next-line cypress/no-unnecessary-waiting
|
||||
cy.wait(15000)
|
||||
const years = getEmptyYears()
|
||||
const studentTableContents = [
|
||||
// [Year, Started, Graduated]
|
||||
...years.map(year => [year, 0, 0]),
|
||||
[2021, 0, 0],
|
||||
[2020, 22, 0],
|
||||
[2019, 12, 0],
|
||||
[2018, 0, 0],
|
||||
[2017, 0, 0],
|
||||
[2020, 10, 35],
|
||||
[2019, 86, 17],
|
||||
[2018, 161, 1],
|
||||
[2017, 171, 0],
|
||||
]
|
||||
|
||||
cy.checkTableStats(studentTableContents, 'StudentsOfTheStudyprogramme')
|
||||
@@ -104,10 +123,10 @@ describe('Studyprogramme overview', () => {
|
||||
// [Year, Total, Major students credits, Transferred credits]
|
||||
...years.map(year => [year, 0, 0, 0]),
|
||||
[2021, 0, 0, 0],
|
||||
[2020, 117.5, 0, 7.5],
|
||||
[2019, 67, 0, 0],
|
||||
[2018, 0, 0, 0],
|
||||
[2017, 0, 0, 0],
|
||||
[2020, 5018, 4694, 167],
|
||||
[2019, 6185, 5702, 349],
|
||||
[2018, 4885, 4538, 272],
|
||||
[2017, 1988, 1600, 388],
|
||||
]
|
||||
|
||||
cy.checkTableStats(creditTableContents, 'CreditsProducedByTheStudyprogramme')
|
||||
@@ -116,9 +135,9 @@ describe('Studyprogramme overview', () => {
|
||||
// [Year, Graduated, Wrote thesis]
|
||||
...years.map(year => [year, 0, 0]),
|
||||
[2021, 0, 0],
|
||||
[2020, 0, 0],
|
||||
[2019, 0, 0],
|
||||
[2018, 0, 0],
|
||||
[2020, 35, 35],
|
||||
[2019, 17, 25],
|
||||
[2018, 1, 5],
|
||||
[2017, 0, 0],
|
||||
]
|
||||
|
||||
@@ -127,17 +146,18 @@ describe('Studyprogramme overview', () => {
|
||||
})
|
||||
|
||||
it('Year can be changed to academic year, and data changes accordingly', () => {
|
||||
// eslint-disable-next-line cypress/no-unnecessary-waiting
|
||||
cy.get('[data-cy=YearToggle]').click({ force: true })
|
||||
// eslint-disable-next-line cypress/no-unnecessary-waiting
|
||||
cy.wait(15000)
|
||||
const isAcademicYear = true
|
||||
const years = getEmptyYears(isAcademicYear)
|
||||
const studentTableContents = [
|
||||
// [Year, Started, Graduated, Transferred away, Transferred to]
|
||||
...years.map(year => [year, 0, 0, 0, 0]),
|
||||
['2021 - 2022', 0, 0, 0, 0],
|
||||
['2020 - 2021', 12, 6, 3, 2],
|
||||
['2019 - 2020', 86, 41, 1, 0],
|
||||
['2018 - 2019', 162, 5, 0, 1],
|
||||
['2020 - 2021', 10, 6, 3, 2],
|
||||
['2019 - 2020', 86, 38, 1, 0],
|
||||
['2018 - 2019', 161, 8, 0, 1],
|
||||
['2017 - 2018', 171, 1, 0, 0],
|
||||
]
|
||||
// eslint-disable-next-line cypress/no-unnecessary-waiting
|
||||
@@ -148,10 +168,10 @@ describe('Studyprogramme overview', () => {
|
||||
// [Year, Total, Major students credits, Non-major students credits, Non-degree credits, Transferred credits]
|
||||
...years.map(year => [year, 0, 0, 0, 0, 0]),
|
||||
['2021 - 2022', 0, 0, 0, 0, 0],
|
||||
['2020 - 2021', 566, 486, 20, 0, 60],
|
||||
['2019 - 2020', 8595, 8271, 106, 0, 218],
|
||||
['2018 - 2019', 9992, 8855, 781, 53, 303],
|
||||
['2017 - 2018', 6055, 4792, 777, 142, 344],
|
||||
['2020 - 2021', 541, 471, 10, 0, 60],
|
||||
['2019 - 2020', 8575, 8266, 116, 0, 193],
|
||||
['2018 - 2019', 10013, 8851, 791, 43, 328],
|
||||
['2017 - 2018', 6079, 4816, 762, 157, 344],
|
||||
]
|
||||
|
||||
cy.checkTableStats(creditTableContents, 'CreditsProducedByTheStudyprogramme')
|
||||
@@ -169,8 +189,8 @@ describe('Studyprogramme overview', () => {
|
||||
.should('contain', 'Major students credits')
|
||||
.should('contain', 'Non-major students credits')
|
||||
.should('contain', 'Transferred credits')
|
||||
.should('contain', 852)
|
||||
.should('contain', 577)
|
||||
.should('contain', 7737)
|
||||
.should('contain', 6584)
|
||||
|
||||
cy.get('[data-cy=Graph-GraduatedAndThesisWritersOfTheProgramme')
|
||||
.should('contain', 'Graduated students')
|
||||
@@ -181,10 +201,16 @@ describe('Studyprogramme overview', () => {
|
||||
.should('contain', 25)
|
||||
.should('contain', 35)
|
||||
|
||||
cy.get('[data-cy=Graph-2019-AverageGraduationTimes]')
|
||||
.should('contain', '2019')
|
||||
.should('contain', 'n = 17')
|
||||
.should('contain', '23 kk')
|
||||
cy.get('[data-cy=graduation-times-graph-breakdownBachelor]')
|
||||
cy.get('[data-cy=GraduationTimeToggle]').click()
|
||||
cy.get('[data-cy=graduation-times-graphBachelor]').within(() => {
|
||||
cy.contains('Graduation year')
|
||||
cy.contains('2019')
|
||||
cy.contains('17 graduated').trigger('mouseover')
|
||||
cy.contains('study time: 23 months')
|
||||
cy.contains('17 graduated on time')
|
||||
cy.contains('0 graduated max year overtime')
|
||||
})
|
||||
|
||||
cy.get('[data-cy=Graph-ProgrammesBeforeOrAfter')
|
||||
.should('contain', 'Tietojenkäsittelytieteen maisteriohjelma')
|
||||
@@ -196,29 +222,61 @@ describe('Studyprogramme overview', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('Graduation times of master programmes', () => {
|
||||
it('are split into two graphs', () => {
|
||||
cy.init('/study-programme', 'admin')
|
||||
// eslint-disable-next-line cypress/no-unnecessary-waiting
|
||||
cy.wait(5000)
|
||||
cy.contains('a', 'Kasvatustieteiden maisteriohjelma').click({ force: true })
|
||||
|
||||
cy.get('[data-cy=graduation-times-graph-breakdownMaster]')
|
||||
cy.get('[data-cy=graduation-times-graph-breakdownBachelor]')
|
||||
|
||||
cy.get('[data-cy=GraduationTimeToggle]').click()
|
||||
cy.get('[data-cy=graduation-times-graphMaster]').within(() => {
|
||||
cy.contains('Graduation year')
|
||||
cy.contains('2020')
|
||||
cy.contains('24 graduated').trigger('mouseover')
|
||||
cy.contains('study time: 22 months')
|
||||
cy.contains('20 graduated on time')
|
||||
cy.contains('4 graduated max year overtime')
|
||||
})
|
||||
|
||||
cy.get('[data-cy=graduation-times-graphBachelor]').within(() => {
|
||||
cy.contains('Graduation year')
|
||||
cy.contains('2020')
|
||||
cy.contains('0 graduated').trigger('mouseover')
|
||||
cy.contains('study time: 0 months')
|
||||
cy.contains('0 graduated on time')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
/* Studytrack overview -tests*/
|
||||
describe('Studytrack overview works for basic user', () => {
|
||||
beforeEach(() => {
|
||||
cy.init('/study-programme')
|
||||
cy.wait(10000)
|
||||
cy.contains('a', 'Tietojenkäsittelytieteen kandiohjelma').click()
|
||||
cy.get('.attached').contains('Studytracks and student populations').click()
|
||||
cy.wait(5000)
|
||||
cy.get('.attached').contains('Studytracks 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 student populations -tab loads', () => {
|
||||
it('Studytracks and class statistics -tab loads', () => {
|
||||
cy.get('[data-cy=Section-StudytrackOverview]')
|
||||
cy.get('[data-cy=Section-StudytrackProgress]')
|
||||
cy.get('[data-cy=Section-AverageGraduationTimes]')
|
||||
cy.get('[data-cy=Section-AverageGraduationTimesStudytracks]')
|
||||
})
|
||||
|
||||
it('Students of the studyprogramme are shown correctly', () => {
|
||||
const tableContents = [
|
||||
// [Year, All, Started studying, Currently enrolled, Absent, Cancelled, Graduated, Men, Women, Finnish]
|
||||
['2020 - 2021', 12, 10, 0, 0, 0, 0, 10, 2, 12],
|
||||
['2019 - 2020', 86, 86, 0, 0, 1, 1, 65, 21, 86],
|
||||
['2018 - 2019', 162, 161, 0, 0, 0, 11, 118, 44, 161],
|
||||
['2017 - 2018', 171, 171, 0, 0, 4, 42, 135, 36, 166],
|
||||
['Total', 431, 431, 0, 0, 5, 54, 328, 103, 425],
|
||||
// [Year, All, Started studying, Currently enrolled, Absent, Inactive, Graduated, Men, Women, other/unknown Finnish, other]
|
||||
['2020 - 2021', 12, 10, 0, 0, 0, 0, 10, 2, 0, 12, 0],
|
||||
['2019 - 2020', 86, 86, 0, 0, 0, 1, 65, 21, 0, 86, 0],
|
||||
['2018 - 2019', 162, 161, 0, 0, 0, 11, 118, 44, 0, 161, 1],
|
||||
['2017 - 2018', 171, 171, 0, 0, 0, 42, 135, 36, 0, 166, 5],
|
||||
['Total', 431, 428, 0, 0, 0, 54, 328, 103, 0, 425, 6],
|
||||
]
|
||||
|
||||
cy.checkTableStats(tableContents, 'StudytrackOverview')
|
||||
@@ -226,11 +284,14 @@ describe('Studyprogramme overview', () => {
|
||||
|
||||
it('Student progress data is shown correctly', () => {
|
||||
const tableContents = [
|
||||
['2020 - 2021', 12, 12, 0, 0, 0, 0, 0, 0, 0],
|
||||
['2019 - 2020', 86, 11, 15, 35, 20, 5, 0, 0, 0],
|
||||
['2018 - 2019', 162, 3, 9, 28, 50, 33, 25, 11, 3],
|
||||
['2017 - 2018', 171, 7, 13, 22, 32, 18, 23, 32, 24],
|
||||
['Total', 431, 16, 35, 65, 103, 75, 56, 50, 31],
|
||||
['2023 - 2024', 0, 0, 0, 0, 0, 0, 0, 0],
|
||||
['2022 - 2023', 0, 0, 0, 0, 0, 0, 0, 0],
|
||||
['2021 - 2022', 0, 0, 0, 0, 0, 0, 0, 0],
|
||||
['2020 - 2021', 12, 12, 0, 0, 0, 0, 0, 0],
|
||||
['2019 - 2020', 86, 86, 0, 0, 0, 0, 0, 0],
|
||||
['2018 - 2019', 162, 162, 0, 0, 0, 0, 0, 0],
|
||||
['2017 - 2018', 171, 171, 0, 0, 0, 0, 0, 0],
|
||||
['Total', 431, 431, 0, 0, 0, 0, 0, 0],
|
||||
]
|
||||
|
||||
cy.checkTableStats(tableContents, 'StudytrackProgress')
|
||||
@@ -238,17 +299,21 @@ describe('Studyprogramme overview', () => {
|
||||
|
||||
it('Studytrack overview graphs render', () => {
|
||||
cy.get('[data-cy=Graph-StudytrackProgress]')
|
||||
.should('contain', 'Less than 15 credits')
|
||||
.should('contain', '30-59 credits')
|
||||
.should('contain', '180 or more credits')
|
||||
.should('contain', '4.1%') // The percentage for less than 15 credits in 2017-2018, to check that the graph renders
|
||||
.should('contain', '5.6%') // The percentage for 15-29 credits in 2018-2019
|
||||
.should('contain', '12.8%') // The percentage for less than 15 credits in 2019-2020
|
||||
.should('contain', 'Less than 30 credits')
|
||||
.should('contain', '30–60 credits')
|
||||
.should('contain', 'At least 180 credits')
|
||||
.should('contain', '100.0%') // The percentage for less than 15 credits in 2017-2018, to check that the graph renders
|
||||
|
||||
cy.get('[data-cy=Graph-2018-AverageGraduationTimes]')
|
||||
.should('contain', '2018 - 2019')
|
||||
.should('contain', 'n = 11 / 162')
|
||||
.should('contain', '22 kk')
|
||||
cy.get('[data-cy=graduation-times-graph-breakdownBachelor]')
|
||||
cy.get('[data-cy=GraduationTimeToggle]').click()
|
||||
|
||||
cy.get('[data-cy=graduation-times-graphBachelor]').within(() => {
|
||||
cy.contains('Start year')
|
||||
cy.contains('2019 - 2020')
|
||||
cy.contains('42 graduated').trigger('mouseover')
|
||||
cy.contains('study time: 31.5 months')
|
||||
cy.contains('39 graduated on time')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -256,6 +321,8 @@ describe('Studyprogramme overview', () => {
|
||||
describe('Programme courses works for basic user', () => {
|
||||
beforeEach(() => {
|
||||
cy.init('/study-programme')
|
||||
// eslint-disable-next-line cypress/no-unnecessary-waiting
|
||||
cy.wait(10000)
|
||||
cy.contains('a', 'Tietojenkäsittelytieteen kandiohjelma').click()
|
||||
cy.get('.attached').contains('Programme courses').click()
|
||||
})
|
||||
@@ -307,7 +374,7 @@ describe('Studyprogramme overview', () => {
|
||||
cy.get('[data-cy=CoursesSortableTable]').within(() => {
|
||||
// Test sorting by different columns
|
||||
cy.get('th').eq(0).click()
|
||||
cy.get('tr').eq(2).contains('Tietojenkäsittelytieteen kisälliopetus: Java-ohjelmointi')
|
||||
cy.get('tr').eq(2).contains('Tietojenkäsittelytieteen kisälliopetus')
|
||||
cy.get('th').eq(1).click()
|
||||
cy.get('tr').eq(1).contains('Äidinkielinen viestintä')
|
||||
cy.get('th').eq(2).click()
|
||||
@@ -323,7 +390,7 @@ describe('Studyprogramme overview', () => {
|
||||
cy.get('[data-cy=creditsStudentsToggle]').first().click()
|
||||
|
||||
cy.get('[data-cy=CoursesSortableTable]').within(() => {
|
||||
cy.get('tr').eq(1).contains('267')
|
||||
cy.get('tr').eq(2).contains('267')
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -332,6 +399,8 @@ describe('Studyprogramme overview', () => {
|
||||
describe('Basic information -view works for basic user', () => {
|
||||
beforeEach(() => {
|
||||
cy.init('/study-programme')
|
||||
// eslint-disable-next-line cypress/no-unnecessary-waiting
|
||||
cy.wait(10000)
|
||||
cy.contains('a', 'Tietojenkäsittelytieteen kandiohjelma').click()
|
||||
cy.get('.attached').contains('Tags').click()
|
||||
})
|
||||
@@ -392,25 +461,25 @@ describe('Studyprogramme overview', () => {
|
||||
})
|
||||
})
|
||||
|
||||
/*
|
||||
describe('IAM user', () => {
|
||||
beforeEach(() => {
|
||||
cy.init('/study-programme', 'onlyiamrights')
|
||||
|
||||
cy.contains('a', 'Tietojenkäsittelytieteen kandiohjelma').dblclick()
|
||||
})
|
||||
|
||||
it('can access programme and correct tabs are visible', () => {
|
||||
cy.reload()
|
||||
|
||||
cy.contains('a', 'Tietojenkäsittelytieteen kandiohjelma').click()
|
||||
|
||||
cy.contains('Basic information')
|
||||
cy.contains('Studytracks and student populations')
|
||||
cy.contains('Studytracks and class statistics')
|
||||
|
||||
cy.contains('Update statistics').should('not.exist')
|
||||
cy.contains('Degree Courses').should('not.exist')
|
||||
cy.contains('Degree courses').should('not.exist')
|
||||
})
|
||||
|
||||
it('can access basic information', () => {
|
||||
cy.contains('a', 'Tietojenkäsittelytieteen kandiohjelma').click()
|
||||
cy.contains('Basic information').click()
|
||||
|
||||
cy.get('[data-cy=Section-StudentsOfTheStudyprogramme]')
|
||||
@@ -421,12 +490,12 @@ describe('Studyprogramme overview', () => {
|
||||
})
|
||||
|
||||
it('can access studytracks', () => {
|
||||
cy.contains('a', 'Tietojenkäsittelytieteen kandiohjelma').click()
|
||||
cy.get('.attached').contains('Studytracks and student populations').click()
|
||||
cy.get('.attached').contains('Studytracks and class statistics').click()
|
||||
|
||||
cy.get('[data-cy=Section-StudytrackOverview]')
|
||||
cy.get('[data-cy=Section-StudytrackProgress]')
|
||||
cy.get('[data-cy=Section-AverageGraduationTimes]')
|
||||
cy.get('[data-cy=Section-AverageGraduationTimesStudytracks]')
|
||||
})
|
||||
})
|
||||
*/
|
||||
})
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"studentSet1": ["010450938", "010589388", "010614509"],
|
||||
"coursesSet1": ["TKT10001", "TKT10002", "TKT10003", "TKT10004"]
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"courseCodesSet1": ["TKT10001", "TKT10002"],
|
||||
"courseCodesSet2": ["TKT10003", "TKT10004", "TKT10005"]
|
||||
}
|
||||
@@ -53,6 +53,18 @@ const onlyIamRightsUserHeaders = {
|
||||
hypersonsisuid: 'hy-hlo-1111111',
|
||||
}
|
||||
|
||||
// TODO add user into anon-DB (check the need for changing hypersonid)
|
||||
// REMEMBER TO ADD ROLE openUniSearch
|
||||
// const openUniUser = {
|
||||
// uid: 'openuniuser',
|
||||
// displayname: 'Open Uni User',
|
||||
// 'shib-session-id': 'mock-cypress-session',
|
||||
// hygroupcn: 'grp-oodikone-basics-users;hy-ypa-opa-dojo',
|
||||
// edupersonaffiliation: 'member',
|
||||
// mail: 'grp-toska+mockopenuniuser@helsinki.fi',
|
||||
// hypersonsisuid: 'hy-hlo-9999999',
|
||||
// }
|
||||
|
||||
const userHeaders = [
|
||||
adminUserHeaders,
|
||||
basicUserHeaders,
|
||||
|
||||
+37
-11
@@ -19,12 +19,16 @@ services:
|
||||
SECRET_TOKEN: dev
|
||||
SIS_DB_URL: postgres://postgres@sis-db:5432/sis-db
|
||||
SIS_UPDATER_URL: http://sis-updater-scheduler:8082
|
||||
image: toska/oodikone2-backend:staging
|
||||
IMPORTER_DB_API_URL: 'https://api-toska.apps.ocp-prod-0.k8s.it.helsinki.fi/importer'
|
||||
JAMI_URL: 'http://jami:3003/'
|
||||
build:
|
||||
context: services/backend
|
||||
|
||||
frontend:
|
||||
<<: *defaults
|
||||
container_name: frontend
|
||||
image: toska/oodikone2-frontend:staging
|
||||
build:
|
||||
context: services/frontend
|
||||
|
||||
kone-db:
|
||||
<<: *defaults
|
||||
@@ -33,20 +37,20 @@ services:
|
||||
PGDATA: /data
|
||||
POSTGRES_DB: kone-db
|
||||
POSTGRES_HOST_AUTH_METHOD: trust
|
||||
image: registry.toska.cs.helsinki.fi/kone-db
|
||||
image: registry-toska.ext.ocp-prod-0.k8s.it.helsinki.fi/kone-db
|
||||
|
||||
redis:
|
||||
<<: *defaults
|
||||
command: 'redis-server --appendonly yes'
|
||||
container_name: redis
|
||||
image: redis
|
||||
command: >
|
||||
sh -c 'redis-server --appendonly yes && redis-cli flushall'
|
||||
image: redis:6.2.13
|
||||
ports:
|
||||
- 6379:6379
|
||||
|
||||
sis-db:
|
||||
<<: *defaults
|
||||
container_name: sis-db
|
||||
image: registry.toska.cs.helsinki.fi/sis-db
|
||||
image: registry-toska.ext.ocp-prod-0.k8s.it.helsinki.fi/sis-db
|
||||
environment:
|
||||
PGDATA: /data
|
||||
POSTGRES_DB: sis-db
|
||||
@@ -60,7 +64,28 @@ services:
|
||||
PGDATA: /data
|
||||
POSTGRES_DB: user-db
|
||||
POSTGRES_HOST_AUTH_METHOD: trust
|
||||
image: registry.toska.cs.helsinki.fi/user-db
|
||||
image: registry-toska.ext.ocp-prod-0.k8s.it.helsinki.fi/user-db
|
||||
|
||||
jami:
|
||||
<<: *defaults
|
||||
image: quay.io/toska/jami:production
|
||||
environment:
|
||||
- PORT=3003
|
||||
- NODE_ENV=production
|
||||
- DATABASE_URL=postgres://postgres:postgres@jami-db:5432/postgres
|
||||
ports:
|
||||
- 3003:3003
|
||||
container_name: jami
|
||||
|
||||
jami-db:
|
||||
<<: *defaults
|
||||
image: postgres:13.2
|
||||
environment:
|
||||
- PGDATA=/data
|
||||
- POSTGRES_PASSWORD=postgres
|
||||
container_name: jami-db
|
||||
ports:
|
||||
- 5433:5432
|
||||
|
||||
nginx:
|
||||
image: nginx:latest
|
||||
@@ -77,14 +102,13 @@ services:
|
||||
sis-importer-db:
|
||||
<<: *defaults
|
||||
container_name: sis-importer-db
|
||||
image: registry.toska.cs.helsinki.fi/sis-importer-db
|
||||
image: registry-toska.ext.ocp-prod-0.k8s.it.helsinki.fi/sis-importer-db
|
||||
profiles:
|
||||
- updater
|
||||
|
||||
sis-updater-worker:
|
||||
<<: *defaults
|
||||
build:
|
||||
target: development
|
||||
context: updater/sis-updater-worker
|
||||
depends_on:
|
||||
- redis
|
||||
@@ -92,12 +116,14 @@ services:
|
||||
- sis-importer-db
|
||||
- sis-updater-nats
|
||||
environment:
|
||||
CI: 'true'
|
||||
DB_URL: postgres://postgres@sis-db:5432/sis-db
|
||||
REDIS_URI: //redis:6379
|
||||
SIS_IMPORTER_DATABASE: sis-importer-db
|
||||
SIS_IMPORTER_HOST: sis-importer-db
|
||||
SIS_IMPORTER_PASSWORD: postgres
|
||||
SIS_IMPORTER_USER: postgres
|
||||
SIS_PASSWORD: postgres
|
||||
SIS_NATS_TOKEN: dev
|
||||
SIS_NATS_URI: sis-updater-nats://sis-updater-nats:4222
|
||||
profiles:
|
||||
@@ -106,7 +132,6 @@ services:
|
||||
sis-updater-scheduler:
|
||||
<<: *defaults
|
||||
build:
|
||||
target: development
|
||||
context: updater/sis-updater-scheduler
|
||||
container_name: sis-updater-scheduler
|
||||
depends_on:
|
||||
@@ -114,6 +139,7 @@ services:
|
||||
- sis-importer-db
|
||||
- sis-updater-nats
|
||||
environment:
|
||||
CI: 'true'
|
||||
REDIS_URI: //redis:6379
|
||||
SECRET_TOKEN: dev
|
||||
SIS_IMPORTER_DATABASE: sis-importer-db
|
||||
|
||||
+45
-14
@@ -4,7 +4,7 @@ x-defaults: &defaults
|
||||
restart: unless-stopped
|
||||
|
||||
x-build_defaults: &build_defaults
|
||||
target: development
|
||||
dockerfile: dev.Dockerfile
|
||||
|
||||
services:
|
||||
adminer:
|
||||
@@ -39,13 +39,15 @@ services:
|
||||
SECRET_TOKEN: dev
|
||||
SIS_DB_URL: postgres://postgres@sis-db:5432/sis-db
|
||||
SIS_UPDATER_URL: http://sis-updater-scheduler:8082
|
||||
IMPORTER_DB_API_URL: 'https://api-toska.apps.ocp-prod-0.k8s.it.helsinki.fi/importer'
|
||||
JAMI_URL: 'http://jami:3003/'
|
||||
ports:
|
||||
- 8080:8080
|
||||
profiles:
|
||||
- oodikone
|
||||
volumes:
|
||||
- backend-nodemod:/usr/src/app/node_modules
|
||||
- ./services/backend/:/usr/src/app
|
||||
- backend-nodemod:/opt/app-root/src/node_modules
|
||||
- ./services/backend/:/opt/app-root/src
|
||||
|
||||
frontend:
|
||||
<<: *defaults
|
||||
@@ -58,13 +60,13 @@ services:
|
||||
profiles:
|
||||
- oodikone
|
||||
volumes:
|
||||
- frontend-nodemod:/usr/src/app/node_modules
|
||||
- ./services/frontend/:/usr/src/app
|
||||
- frontend-nodemod:/opt/app-root/src/node_modules
|
||||
- ./services/frontend/:/opt/app-root/src
|
||||
|
||||
kone-db:
|
||||
<<: *defaults
|
||||
container_name: kone-db
|
||||
image: registry.toska.cs.helsinki.fi/kone-db
|
||||
image: registry-toska.ext.ocp-prod-0.k8s.it.helsinki.fi/kone-db
|
||||
profiles:
|
||||
- oodikone
|
||||
volumes:
|
||||
@@ -74,7 +76,7 @@ services:
|
||||
<<: *defaults
|
||||
command: 'redis-server --appendonly yes'
|
||||
container_name: redis
|
||||
image: redis
|
||||
image: redis:6.2.13
|
||||
ports:
|
||||
- '6379:6379'
|
||||
volumes:
|
||||
@@ -83,19 +85,23 @@ services:
|
||||
sis-db:
|
||||
<<: *defaults
|
||||
container_name: sis-db
|
||||
image: registry.toska.cs.helsinki.fi/sis-db
|
||||
image: registry-toska.ext.ocp-prod-0.k8s.it.helsinki.fi/sis-db
|
||||
volumes:
|
||||
- sis-db-data:/data
|
||||
shm_size: 2gb
|
||||
ports:
|
||||
- 12345:5432
|
||||
|
||||
sis-importer-db:
|
||||
<<: *defaults
|
||||
container_name: sis-importer-db
|
||||
image: registry.toska.cs.helsinki.fi/sis-importer-db
|
||||
image: registry-toska.ext.ocp-prod-0.k8s.it.helsinki.fi/sis-importer-db
|
||||
profiles:
|
||||
- updater
|
||||
volumes:
|
||||
- sis-importer-db-data:/data
|
||||
ports:
|
||||
- 12346:5432
|
||||
|
||||
sis-updater-nats:
|
||||
<<: *defaults
|
||||
@@ -131,8 +137,8 @@ services:
|
||||
profiles:
|
||||
- updater
|
||||
volumes:
|
||||
- sis-updater-scheduler-nodemod:/usr/src/app/node_modules
|
||||
- ./updater/sis-updater-scheduler/:/usr/src/app
|
||||
- sis-updater-scheduler-nodemod:/opt/app-root/src/node_modules
|
||||
- ./updater/sis-updater-scheduler/:/opt/app-root/src
|
||||
|
||||
sis-updater-worker:
|
||||
<<: *defaults
|
||||
@@ -151,23 +157,47 @@ services:
|
||||
SIS_IMPORTER_HOST: sis-importer-db
|
||||
SIS_IMPORTER_PASSWORD: postgres
|
||||
SIS_IMPORTER_USER: postgres
|
||||
SIS_PASSWORD: postgres
|
||||
SIS_NATS_TOKEN: dev
|
||||
SIS_NATS_URI: sis-updater-nats://sis-updater-nats:4222
|
||||
profiles:
|
||||
- updater
|
||||
volumes:
|
||||
- sis-updater-worker-nodemod:/usr/src/app/node_modules
|
||||
- ./updater/sis-updater-worker/:/usr/src/app
|
||||
- sis-updater-worker-nodemod:/opt/app-root/src/node_modules
|
||||
- ./updater/sis-updater-worker/:/opt/app-root/src
|
||||
|
||||
user-db:
|
||||
<<: *defaults
|
||||
container_name: user-db
|
||||
image: registry.toska.cs.helsinki.fi/user-db
|
||||
image: registry-toska.ext.ocp-prod-0.k8s.it.helsinki.fi/user-db
|
||||
profiles:
|
||||
- oodikone
|
||||
volumes:
|
||||
- user-db-data:/data
|
||||
|
||||
jami:
|
||||
<<: *defaults
|
||||
image: quay.io/toska/jami:production
|
||||
environment:
|
||||
- PORT=3003
|
||||
- NODE_ENV=dev
|
||||
- DATABASE_URL=postgres://postgres:postgres@jami-db:5432/postgres
|
||||
ports:
|
||||
- 3003:3003
|
||||
container_name: jami
|
||||
|
||||
jami-db:
|
||||
<<: *defaults
|
||||
image: postgres:13.2
|
||||
environment:
|
||||
- PGDATA=/data
|
||||
- POSTGRES_PASSWORD=postgres
|
||||
container_name: jami-db
|
||||
ports:
|
||||
- 5433:5432
|
||||
volumes:
|
||||
- jami-data:/data
|
||||
|
||||
volumes:
|
||||
backend-nodemod:
|
||||
frontend-nodemod:
|
||||
@@ -178,3 +208,4 @@ volumes:
|
||||
sis-updater-scheduler-nodemod:
|
||||
sis-updater-worker-nodemod:
|
||||
user-db-data:
|
||||
jami-data:
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 106 KiB |
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 72 KiB |
@@ -0,0 +1,96 @@
|
||||
# Combined programmes
|
||||
|
||||
At the moment only programmes KH90_001 and MH90_001 have a combined view.
|
||||
|
||||
The made changes do not have any effects on faculty or trends views. **Needs manual work** is added into those parts that needs visit, if this functionality is added to other programmes.
|
||||
|
||||
Basic rule of thumb is that for the master/licentiate programme I used names `combinedProgramme` and in some rare cases `secondProgramme` as whole or part of the variable names.
|
||||
|
||||
## Study programme
|
||||
|
||||
**Needs manual work** in the function `getCombinedProgrammeId` in `services/frontend/src/common/index.js` if new programmes is added. For showing the both programme data correctly, the filtering mechanism is changed to add both programmes and it is not caring for which programme user has acual rights or IAMrights. However, the links for the class stats level are hidden in studytrack tables, if the user has no rights for the programme.
|
||||
|
||||
- Search by class
|
||||
- **Needs manual work** Combine programmes here
|
||||
- Overview
|
||||
- **Needs manual work**
|
||||
|
||||
**Redis nightly study programme update**
|
||||
|
||||
- Updates automatically combined programme stats. **Needs manual work** here `services/backend/src/services/studyprogrammeHelpers.js` in the constant `combinedStudyprogrammes`. This might needs refactoring if bachelor programme can have many separate master programmes. Then remember to change function `refreshNewOverview` in `services/backend/src/events.js` accordingly to handle lists.
|
||||
|
||||
**NOTE:** `combinedStudyprogrammes` is also used to fetch course modules (mandatory courses). If the logic of this constant is changed in `studyProgrammeHelpers.js` -> the needed changes must be done also here `services/backend/src/services/programmeModules.js`. While writing this, the cleverer idea of doing this might have been to send `code` in form `KHxx_xxx+MHxx_xxx` and do the splitting into two codes in the backend as done in Study guidance groups logic. Changes were done here 'services/backend/src/routes/population.js', acual place is near `getFilteredAssociations` function.
|
||||
|
||||
**Basic information**
|
||||
|
||||
All things listed below should work automatically, if new combinations are done.
|
||||
|
||||
- Credits produced byt the studyprogramme
|
||||
- credits are computed for both programmes separately
|
||||
- Average graduation times
|
||||
- For bachelor + master/licentiate studyright the time is the whole study time starting from bachelor
|
||||
- _Students of the programme_ and _Graduated and thesis writers of the programme_
|
||||
- separate fields for started, graduated and thesis writers
|
||||
|
||||
**Studytracks and class statistics**
|
||||
|
||||
- _Students of the studyprogramme by starting year_
|
||||
- Graduated field separated for both programmes
|
||||
- All = currently enrolled + absent + inactive + graduated licentiate
|
||||
- _Progress of students of the studyprogramme by starting year_
|
||||
- computed for whole bachelor + master/licentiate studyright period
|
||||
- _Average graduation times_
|
||||
- bachelor + master/licentiate graph: start is bachelor studyright start
|
||||
|
||||
**Programme courses**
|
||||
|
||||
- Fetch all courses for both programmes and show them in one table.
|
||||
|
||||
**Degree courses**
|
||||
|
||||
- Possibility to add criteria for whole bachelor-master study time
|
||||
- For eläinlääkis this criteria is saved by bachelor programme code. Other cases may need other approaches.
|
||||
In eläinklääkis this done by adding the criteria for 6 years -> the whole study time criteria is saved with the bachelor programme code. That is, same criteria is found, if bachelor or combined programme view is shown. This approach might need extra thinking for different programmes.
|
||||
- Show own tables for both programmes
|
||||
- To fetch data **needs manual work** here `services/backend/src/services/studyprogrammeHelpers.js` in the constant `combinedStudyprogrammes`.
|
||||
|
||||
**Tags**
|
||||
|
||||
- Possibility to add tags for combined programme.
|
||||
|
||||
## Class statistics view
|
||||
|
||||
**Credit accumulation**
|
||||
|
||||
- Show graduations for both programmes
|
||||
|
||||
**Credit statistics**
|
||||
|
||||
- Bachelor and bachelor+master combo have own graphs.
|
||||
|
||||
**Courses of class**
|
||||
|
||||
- Show courses for both programmes
|
||||
|
||||
**Students**
|
||||
|
||||
- **General tab** - Additional fields for graduation and hops
|
||||
- **Courses tab** - show all courses for both programmes. The courses are fetched with same logic as for Degree programmes.
|
||||
- **Tags tab** - show tabs related to this combination.
|
||||
- **Progress tab** - possibility to have progress for whole bachelor-master study time. **See Degree courses approach** to add criteria. **May need future manual work**, depending on the programme id with which the data is saved on the database.
|
||||
|
||||
## Filters
|
||||
|
||||
- **Hops (study plan)** - possibility filter out credits separate for both programmes or for both at the same time
|
||||
|
||||
- **Graduated from programme** - possibility to filter out students based on graduation either in bachelor's or master's.
|
||||
|
||||
- **Studyright status** - active or inactive based on the chosen degree
|
||||
|
||||
- **Studytrack filter** (needs programme code) is not touched at the moment, thus **may need future manual work**.
|
||||
|
||||
## Study guidance groups
|
||||
|
||||
- Backend: student data is fetched by bachelor study programme code in `/v3/populationstatisticsbystudentnumbers`. Separation from code 'KHxx_xxx+MHxx_xxx' is done in backend.
|
||||
- Frontend: things are done in the same way as with class stats.
|
||||
- **Need manual work**: `./oodikone/services/frontend/src/redux/elementdetails.js` add combined programme codes (both bachelor and master) to `combinedProgrammeCodes` and modify `combinedOptions` list accordinly in `useFilterAndFormattedElementDetails()` function.
|
||||
@@ -0,0 +1,48 @@
|
||||
# Mystery numbers
|
||||
|
||||
Sis data uses some numeric classifiers that are not self-explanatory and (most) previously needed to be deciphered from existing code.
|
||||
Some mystery numbers are gathered here for convenience. Feel free to add anything that could be useful. Remember to use the spelling used in code to enable project wide search.
|
||||
|
||||
## extentcode
|
||||
|
||||
(from **studyright**)
|
||||
|
||||
These are actually defined in db table studyright_extents. Tells studyright level or type, eg. bachelor's degree or non-degree studies.
|
||||
|
||||
## prioritycode
|
||||
|
||||
(from **studyright**)
|
||||
|
||||
1: Primary
|
||||
|
||||
2: Secondary
|
||||
|
||||
5: Rescinded (Seemingly not in use anymore)
|
||||
|
||||
6: Option (Bachelor+master studyright students can add a master programme to their study plan before graduation in bachelor programme: marked as option in Oodikone)
|
||||
|
||||
30: Graduated
|
||||
|
||||
## type
|
||||
|
||||
(from **element_details**)
|
||||
|
||||
20: A study programme
|
||||
|
||||
30: Module/Studytrack/kokonaisuus? Eg. Neurology
|
||||
|
||||
## enrollmenttype
|
||||
|
||||
(from **semester_enrollments**)
|
||||
|
||||
1: Present
|
||||
|
||||
2: Absent
|
||||
|
||||
3: Inactive (neither 1 or 2 has been declared)
|
||||
|
||||
## credittypecode
|
||||
|
||||
(from **credit**)
|
||||
|
||||
Defined in credit_types db table. Completed/Improved/Transferred/Failed
|
||||
Generated
+172
-78
@@ -15,7 +15,7 @@
|
||||
"@babel/eslint-parser": "^7.19.1",
|
||||
"@babel/plugin-syntax-jsx": "^7.16.7",
|
||||
"concurrently": "^7.4.0",
|
||||
"cypress": "^10.9.0",
|
||||
"cypress": "^12.17.4",
|
||||
"eslint": "^8.25.0",
|
||||
"eslint-config-airbnb": "^19.0.4",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
@@ -447,9 +447,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@cypress/request": {
|
||||
"version": "2.88.10",
|
||||
"resolved": "https://registry.npmjs.org/@cypress/request/-/request-2.88.10.tgz",
|
||||
"integrity": "sha512-Zp7F+R93N0yZyG34GutyTNr+okam7s/Fzc1+i3kcqOP8vk6OuajuE9qZJ6Rs+10/1JFtXFYMdyarnU1rZuJesg==",
|
||||
"version": "2.88.12",
|
||||
"resolved": "https://registry.npmjs.org/@cypress/request/-/request-2.88.12.tgz",
|
||||
"integrity": "sha512-tOn+0mDZxASFM+cuAP9szGUGPI1HwWVSvdzm7V4cCsPdFTx6qMj29CwaQmRAMIEhORIUBFBsYROYJcveK4uOjA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"aws-sign2": "~0.7.0",
|
||||
@@ -465,9 +465,9 @@
|
||||
"json-stringify-safe": "~5.0.1",
|
||||
"mime-types": "~2.1.19",
|
||||
"performance-now": "^2.1.0",
|
||||
"qs": "~6.5.2",
|
||||
"qs": "~6.10.3",
|
||||
"safe-buffer": "^5.1.2",
|
||||
"tough-cookie": "~2.5.0",
|
||||
"tough-cookie": "^4.1.3",
|
||||
"tunnel-agent": "^0.6.0",
|
||||
"uuid": "^8.3.2"
|
||||
},
|
||||
@@ -681,9 +681,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "14.18.22",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.22.tgz",
|
||||
"integrity": "sha512-qzaYbXVzin6EPjghf/hTdIbnVW1ErMx8rPzwRNJhlbyJhu2SyqlvjGOY/tbUt6VFyzg56lROcOeSQRInpt63Yw==",
|
||||
"version": "16.18.54",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.54.tgz",
|
||||
"integrity": "sha512-oTmGy68gxZZ21FhTJVVvZBYpQHEBZxHKTsGshobMqm9qWpbqdZsA5jvsuPZcHu0KwpmLrOHWPdEfg7XDpNT9UA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/normalize-package-data": {
|
||||
@@ -991,9 +991,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/aws4": {
|
||||
"version": "1.11.0",
|
||||
"resolved": "https://registry.npmjs.org/aws4/-/aws4-1.11.0.tgz",
|
||||
"integrity": "sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==",
|
||||
"version": "1.12.0",
|
||||
"resolved": "https://registry.npmjs.org/aws4/-/aws4-1.12.0.tgz",
|
||||
"integrity": "sha512-NmWvPnx0F1SfrQbYwOi7OeaNGokp9XhzNioJ/CSBs8Qa4vxug81mhJEAVZwxXuBmYB5KDRfMq/F3RR0BIU7sWg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/axe-core": {
|
||||
@@ -1352,9 +1352,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/commander": {
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz",
|
||||
"integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==",
|
||||
"version": "6.2.1",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz",
|
||||
"integrity": "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
@@ -1571,15 +1571,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/cypress": {
|
||||
"version": "10.9.0",
|
||||
"resolved": "https://registry.npmjs.org/cypress/-/cypress-10.9.0.tgz",
|
||||
"integrity": "sha512-MjIWrRpc+bQM9U4kSSdATZWZ2hUqHGFEQTF7dfeZRa4MnalMtc88FIE49USWP2ZVtfy5WPBcgfBX+YorFqGElA==",
|
||||
"version": "12.17.4",
|
||||
"resolved": "https://registry.npmjs.org/cypress/-/cypress-12.17.4.tgz",
|
||||
"integrity": "sha512-gAN8Pmns9MA5eCDFSDJXWKUpaL3IDd89N9TtIupjYnzLSmlpVr+ZR+vb4U/qaMp+lB6tBvAmt7504c3Z4RU5KQ==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"@cypress/request": "^2.88.10",
|
||||
"@cypress/request": "2.88.12",
|
||||
"@cypress/xvfb": "^1.2.4",
|
||||
"@types/node": "^14.14.31",
|
||||
"@types/node": "^16.18.39",
|
||||
"@types/sinonjs__fake-timers": "8.1.1",
|
||||
"@types/sizzle": "^2.3.2",
|
||||
"arch": "^2.2.0",
|
||||
@@ -1591,10 +1591,10 @@
|
||||
"check-more-types": "^2.24.0",
|
||||
"cli-cursor": "^3.1.0",
|
||||
"cli-table3": "~0.6.1",
|
||||
"commander": "^5.1.0",
|
||||
"commander": "^6.2.1",
|
||||
"common-tags": "^1.8.0",
|
||||
"dayjs": "^1.10.4",
|
||||
"debug": "^4.3.2",
|
||||
"debug": "^4.3.4",
|
||||
"enquirer": "^2.3.6",
|
||||
"eventemitter2": "6.4.7",
|
||||
"execa": "4.1.0",
|
||||
@@ -1609,12 +1609,13 @@
|
||||
"listr2": "^3.8.3",
|
||||
"lodash": "^4.17.21",
|
||||
"log-symbols": "^4.0.0",
|
||||
"minimist": "^1.2.6",
|
||||
"minimist": "^1.2.8",
|
||||
"ospath": "^1.2.2",
|
||||
"pretty-bytes": "^5.6.0",
|
||||
"process": "^0.11.10",
|
||||
"proxy-from-env": "1.0.0",
|
||||
"request-progress": "^3.0.0",
|
||||
"semver": "^7.3.2",
|
||||
"semver": "^7.5.3",
|
||||
"supports-color": "^8.1.1",
|
||||
"tmp": "~0.2.1",
|
||||
"untildify": "^4.0.0",
|
||||
@@ -1624,7 +1625,7 @@
|
||||
"cypress": "bin/cypress"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
"node": "^14.0.0 || ^16.0.0 || >=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/cypress/node_modules/ansi-styles": {
|
||||
@@ -1698,9 +1699,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/cypress/node_modules/semver": {
|
||||
"version": "7.3.7",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz",
|
||||
"integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==",
|
||||
"version": "7.5.4",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
|
||||
"integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"lru-cache": "^6.0.0"
|
||||
@@ -4765,10 +4766,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/minimist": {
|
||||
"version": "1.2.6",
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz",
|
||||
"integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==",
|
||||
"dev": true
|
||||
"version": "1.2.8",
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
|
||||
"integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
|
||||
"dev": true,
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/minimist-options": {
|
||||
"version": "4.1.0",
|
||||
@@ -5400,6 +5404,15 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/process": {
|
||||
"version": "0.11.10",
|
||||
"resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz",
|
||||
"integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">= 0.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/prop-types": {
|
||||
"version": "15.8.1",
|
||||
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
|
||||
@@ -5443,14 +5456,26 @@
|
||||
}
|
||||
},
|
||||
"node_modules/qs": {
|
||||
"version": "6.5.3",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.5.3.tgz",
|
||||
"integrity": "sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==",
|
||||
"version": "6.10.4",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.10.4.tgz",
|
||||
"integrity": "sha512-OQiU+C+Ds5qiH91qh/mg0w+8nwQuLjM4F4M/PbmhDOoYehPh+Fb0bDjtR1sOvy7YKxvj28Y/M0PhP5uVX0kB+g==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"side-channel": "^1.0.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/querystringify": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz",
|
||||
"integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/queue-microtask": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
|
||||
@@ -5708,6 +5733,12 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/requires-port": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
|
||||
"integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/resolve": {
|
||||
"version": "1.22.1",
|
||||
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz",
|
||||
@@ -6470,16 +6501,27 @@
|
||||
}
|
||||
},
|
||||
"node_modules/tough-cookie": {
|
||||
"version": "2.5.0",
|
||||
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz",
|
||||
"integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==",
|
||||
"version": "4.1.3",
|
||||
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.3.tgz",
|
||||
"integrity": "sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"psl": "^1.1.28",
|
||||
"punycode": "^2.1.1"
|
||||
"psl": "^1.1.33",
|
||||
"punycode": "^2.1.1",
|
||||
"universalify": "^0.2.0",
|
||||
"url-parse": "^1.5.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.8"
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/tough-cookie/node_modules/universalify": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz",
|
||||
"integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">= 4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/tree-kill": {
|
||||
@@ -6640,6 +6682,16 @@
|
||||
"punycode": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/url-parse": {
|
||||
"version": "1.5.10",
|
||||
"resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz",
|
||||
"integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"querystringify": "^2.1.1",
|
||||
"requires-port": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/util-deprecate": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||
@@ -7176,9 +7228,9 @@
|
||||
"requires": {}
|
||||
},
|
||||
"@cypress/request": {
|
||||
"version": "2.88.10",
|
||||
"resolved": "https://registry.npmjs.org/@cypress/request/-/request-2.88.10.tgz",
|
||||
"integrity": "sha512-Zp7F+R93N0yZyG34GutyTNr+okam7s/Fzc1+i3kcqOP8vk6OuajuE9qZJ6Rs+10/1JFtXFYMdyarnU1rZuJesg==",
|
||||
"version": "2.88.12",
|
||||
"resolved": "https://registry.npmjs.org/@cypress/request/-/request-2.88.12.tgz",
|
||||
"integrity": "sha512-tOn+0mDZxASFM+cuAP9szGUGPI1HwWVSvdzm7V4cCsPdFTx6qMj29CwaQmRAMIEhORIUBFBsYROYJcveK4uOjA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"aws-sign2": "~0.7.0",
|
||||
@@ -7194,9 +7246,9 @@
|
||||
"json-stringify-safe": "~5.0.1",
|
||||
"mime-types": "~2.1.19",
|
||||
"performance-now": "^2.1.0",
|
||||
"qs": "~6.5.2",
|
||||
"qs": "~6.10.3",
|
||||
"safe-buffer": "^5.1.2",
|
||||
"tough-cookie": "~2.5.0",
|
||||
"tough-cookie": "^4.1.3",
|
||||
"tunnel-agent": "^0.6.0",
|
||||
"uuid": "^8.3.2"
|
||||
}
|
||||
@@ -7365,9 +7417,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"@types/node": {
|
||||
"version": "14.18.22",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.22.tgz",
|
||||
"integrity": "sha512-qzaYbXVzin6EPjghf/hTdIbnVW1ErMx8rPzwRNJhlbyJhu2SyqlvjGOY/tbUt6VFyzg56lROcOeSQRInpt63Yw==",
|
||||
"version": "16.18.54",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.54.tgz",
|
||||
"integrity": "sha512-oTmGy68gxZZ21FhTJVVvZBYpQHEBZxHKTsGshobMqm9qWpbqdZsA5jvsuPZcHu0KwpmLrOHWPdEfg7XDpNT9UA==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/normalize-package-data": {
|
||||
@@ -7592,9 +7644,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"aws4": {
|
||||
"version": "1.11.0",
|
||||
"resolved": "https://registry.npmjs.org/aws4/-/aws4-1.11.0.tgz",
|
||||
"integrity": "sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==",
|
||||
"version": "1.12.0",
|
||||
"resolved": "https://registry.npmjs.org/aws4/-/aws4-1.12.0.tgz",
|
||||
"integrity": "sha512-NmWvPnx0F1SfrQbYwOi7OeaNGokp9XhzNioJ/CSBs8Qa4vxug81mhJEAVZwxXuBmYB5KDRfMq/F3RR0BIU7sWg==",
|
||||
"dev": true
|
||||
},
|
||||
"axe-core": {
|
||||
@@ -7846,9 +7898,9 @@
|
||||
}
|
||||
},
|
||||
"commander": {
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz",
|
||||
"integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==",
|
||||
"version": "6.2.1",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz",
|
||||
"integrity": "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==",
|
||||
"dev": true
|
||||
},
|
||||
"common-tags": {
|
||||
@@ -8006,14 +8058,14 @@
|
||||
"dev": true
|
||||
},
|
||||
"cypress": {
|
||||
"version": "10.9.0",
|
||||
"resolved": "https://registry.npmjs.org/cypress/-/cypress-10.9.0.tgz",
|
||||
"integrity": "sha512-MjIWrRpc+bQM9U4kSSdATZWZ2hUqHGFEQTF7dfeZRa4MnalMtc88FIE49USWP2ZVtfy5WPBcgfBX+YorFqGElA==",
|
||||
"version": "12.17.4",
|
||||
"resolved": "https://registry.npmjs.org/cypress/-/cypress-12.17.4.tgz",
|
||||
"integrity": "sha512-gAN8Pmns9MA5eCDFSDJXWKUpaL3IDd89N9TtIupjYnzLSmlpVr+ZR+vb4U/qaMp+lB6tBvAmt7504c3Z4RU5KQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@cypress/request": "^2.88.10",
|
||||
"@cypress/request": "2.88.12",
|
||||
"@cypress/xvfb": "^1.2.4",
|
||||
"@types/node": "^14.14.31",
|
||||
"@types/node": "^16.18.39",
|
||||
"@types/sinonjs__fake-timers": "8.1.1",
|
||||
"@types/sizzle": "^2.3.2",
|
||||
"arch": "^2.2.0",
|
||||
@@ -8025,10 +8077,10 @@
|
||||
"check-more-types": "^2.24.0",
|
||||
"cli-cursor": "^3.1.0",
|
||||
"cli-table3": "~0.6.1",
|
||||
"commander": "^5.1.0",
|
||||
"commander": "^6.2.1",
|
||||
"common-tags": "^1.8.0",
|
||||
"dayjs": "^1.10.4",
|
||||
"debug": "^4.3.2",
|
||||
"debug": "^4.3.4",
|
||||
"enquirer": "^2.3.6",
|
||||
"eventemitter2": "6.4.7",
|
||||
"execa": "4.1.0",
|
||||
@@ -8043,12 +8095,13 @@
|
||||
"listr2": "^3.8.3",
|
||||
"lodash": "^4.17.21",
|
||||
"log-symbols": "^4.0.0",
|
||||
"minimist": "^1.2.6",
|
||||
"minimist": "^1.2.8",
|
||||
"ospath": "^1.2.2",
|
||||
"pretty-bytes": "^5.6.0",
|
||||
"process": "^0.11.10",
|
||||
"proxy-from-env": "1.0.0",
|
||||
"request-progress": "^3.0.0",
|
||||
"semver": "^7.3.2",
|
||||
"semver": "^7.5.3",
|
||||
"supports-color": "^8.1.1",
|
||||
"tmp": "~0.2.1",
|
||||
"untildify": "^4.0.0",
|
||||
@@ -8107,9 +8160,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"semver": {
|
||||
"version": "7.3.7",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz",
|
||||
"integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==",
|
||||
"version": "7.5.4",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
|
||||
"integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"lru-cache": "^6.0.0"
|
||||
@@ -10312,9 +10365,9 @@
|
||||
}
|
||||
},
|
||||
"minimist": {
|
||||
"version": "1.2.6",
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz",
|
||||
"integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==",
|
||||
"version": "1.2.8",
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
|
||||
"integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
|
||||
"dev": true
|
||||
},
|
||||
"minimist-options": {
|
||||
@@ -10761,6 +10814,12 @@
|
||||
"integrity": "sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==",
|
||||
"dev": true
|
||||
},
|
||||
"process": {
|
||||
"version": "0.11.10",
|
||||
"resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz",
|
||||
"integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==",
|
||||
"dev": true
|
||||
},
|
||||
"prop-types": {
|
||||
"version": "15.8.1",
|
||||
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
|
||||
@@ -10801,9 +10860,18 @@
|
||||
"dev": true
|
||||
},
|
||||
"qs": {
|
||||
"version": "6.5.3",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.5.3.tgz",
|
||||
"integrity": "sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==",
|
||||
"version": "6.10.4",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.10.4.tgz",
|
||||
"integrity": "sha512-OQiU+C+Ds5qiH91qh/mg0w+8nwQuLjM4F4M/PbmhDOoYehPh+Fb0bDjtR1sOvy7YKxvj28Y/M0PhP5uVX0kB+g==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"side-channel": "^1.0.4"
|
||||
}
|
||||
},
|
||||
"querystringify": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz",
|
||||
"integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==",
|
||||
"dev": true
|
||||
},
|
||||
"queue-microtask": {
|
||||
@@ -10990,6 +11058,12 @@
|
||||
"integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
|
||||
"dev": true
|
||||
},
|
||||
"requires-port": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
|
||||
"integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==",
|
||||
"dev": true
|
||||
},
|
||||
"resolve": {
|
||||
"version": "1.22.1",
|
||||
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz",
|
||||
@@ -11572,13 +11646,23 @@
|
||||
}
|
||||
},
|
||||
"tough-cookie": {
|
||||
"version": "2.5.0",
|
||||
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz",
|
||||
"integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==",
|
||||
"version": "4.1.3",
|
||||
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.3.tgz",
|
||||
"integrity": "sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"psl": "^1.1.28",
|
||||
"punycode": "^2.1.1"
|
||||
"psl": "^1.1.33",
|
||||
"punycode": "^2.1.1",
|
||||
"universalify": "^0.2.0",
|
||||
"url-parse": "^1.5.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"universalify": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz",
|
||||
"integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"tree-kill": {
|
||||
@@ -11695,6 +11779,16 @@
|
||||
"punycode": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"url-parse": {
|
||||
"version": "1.5.10",
|
||||
"resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz",
|
||||
"integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"querystringify": "^2.1.1",
|
||||
"requires-port": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"util-deprecate": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||
|
||||
+4
-2
@@ -16,8 +16,10 @@
|
||||
"both": "./run.sh both anon up --detach",
|
||||
"both:real": "./run.sh both real up --detach",
|
||||
"docker:down": "./run.sh both anon down --remove-orphans; ./run.sh both real down --remove-orphans",
|
||||
"testupdater": "docker-compose --file docker-compose.ci.yml up --detach --no-recreate sis-updater-worker && SCHEDULE_IMMEDIATE=weekly EXIT_AFTER_IMMEDIATES=yes docker-compose --file docker-compose.ci.yml run sis-updater-scheduler npm start",
|
||||
"flushredis": "docker exec -it redis redis-cli flushall",
|
||||
"prepare": "husky install",
|
||||
"rapodiff": "docker exec backend node /usr/src/app/src/rapodiff.js"
|
||||
"rapodiff": "docker exec backend node /opt/app-root/src/src/rapodiff.js"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -33,7 +35,7 @@
|
||||
"@babel/eslint-parser": "^7.19.1",
|
||||
"@babel/plugin-syntax-jsx": "^7.16.7",
|
||||
"concurrently": "^7.4.0",
|
||||
"cypress": "^10.9.0",
|
||||
"cypress": "^12.17.4",
|
||||
"eslint": "^8.25.0",
|
||||
"eslint-config-airbnb": "^19.0.4",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
PROJECT_ROOT="$(git rev-parse --show-toplevel)"
|
||||
DUMP_DIR="$PROJECT_ROOT/.databasedumps"
|
||||
USER_DATA_FILE="$DUMP_DIR/hyuserdata"
|
||||
DOCKER_COMPOSE=$PROJECT_ROOT/docker-compose.yml
|
||||
|
||||
## Following the naming convention in docker-compose, these are names for database
|
||||
## services. The real data databases inside service will have names suffixed by "-real".
|
||||
@@ -20,10 +21,10 @@ USER_DB_NAME="user-db"
|
||||
DATABASES=("$KONE_DB_NAME" "$SIS_DB_NAME" "$SIS_IMPORTER_DB_NAME" "$USER_DB_NAME")
|
||||
|
||||
## Urls should be in same order as databases as both are iterated through by indexes.
|
||||
KONE_DB_REAL_DUMP_URL="svm-101.cs.helsinki.fi:/home/oodikone_user/backups/latest-kone-db.sqz"
|
||||
SIS_DB_REAL_DUMP_URL="svm-96.cs.helsinki.fi:/home/updater_user/backups/latest-sis.sqz"
|
||||
SIS_IMPORTER_DB_REAL_DUMP_URL="svm-92.cs.helsinki.fi:/home/importer_user/importer-db/backup/importer-db.sqz"
|
||||
USER_DB_REAL_DUMP_URL="svm-101.cs.helsinki.fi:/home/oodikone_user/backups/latest-user-db.sqz"
|
||||
KONE_DB_REAL_DUMP_URL="svm-116.cs.helsinki.fi:/home/toska_user/most_recent_backup_store/oodikone-kone-db.sql.gz"
|
||||
SIS_DB_REAL_DUMP_URL="svm-116.cs.helsinki.fi:/home/toska_user/most_recent_backup_store/oodikone-sis-db.sql.gz"
|
||||
SIS_IMPORTER_DB_REAL_DUMP_URL="svm-116.cs.helsinki.fi:/home/toska_user/most_recent_backup_store/importer.sql.gz"
|
||||
USER_DB_REAL_DUMP_URL="svm-116.cs.helsinki.fi:/home/toska_user/most_recent_backup_store/oodikone-user-db.sql.gz"
|
||||
REAL_DUMP_URLS=("$KONE_DB_REAL_DUMP_URL" "$SIS_DB_REAL_DUMP_URL" "$SIS_IMPORTER_DB_REAL_DUMP_URL" "$USER_DB_REAL_DUMP_URL")
|
||||
|
||||
# Source utility functions
|
||||
@@ -55,7 +56,7 @@ retry() {
|
||||
download_real_dump() {
|
||||
local database=$1
|
||||
local pannu_url=$2
|
||||
local dump_destination="$DUMP_DIR/$database.sqz"
|
||||
local dump_destination="$DUMP_DIR/$database.sql.gz"
|
||||
scp -r -o ProxyCommand="ssh -l $username -W %h:%p melkki.cs.helsinki.fi" "$username@$pannu_url" "$dump_destination"
|
||||
}
|
||||
|
||||
@@ -75,7 +76,7 @@ reset_databases() {
|
||||
docker-compose up -d "${databases[@]}"
|
||||
|
||||
for database in "${databases[@]}"; do
|
||||
local database_dump="$database_dump_dir/$database.sqz"
|
||||
local database_dump="$database_dump_dir/$database.sql.gz"
|
||||
local database_container="$database"
|
||||
local database_name="$database-real"
|
||||
|
||||
@@ -90,16 +91,36 @@ reset_databases() {
|
||||
docker exec -u postgres "$database_container" createdb "$database_name" || warningmsg "This is okay, continuing"
|
||||
|
||||
infomsg "Restoring database from dump database"
|
||||
msg "1. Copying dump..."
|
||||
docker cp "$database_dump" "$database_container:/asd.sqz"
|
||||
msg "2. Writing database..."
|
||||
docker exec "$database_container" pg_restore -U postgres --no-owner -F c --dbname="$database_name" -j4 /asd.sqz
|
||||
msg "Writing database..."
|
||||
docker exec -i "$database_container" /bin/bash -c "gunzip | psql -U postgres -d $database_name" < "$database_dump" 2> /dev/null
|
||||
msg ""
|
||||
done
|
||||
|
||||
successmsg "Database setup finished"
|
||||
}
|
||||
|
||||
reset_jami_data() {
|
||||
local database="jami-db"
|
||||
local pannu_url="svm-116.cs.helsinki.fi:/home/toska_user/most_recent_backup_store/jami.sql.gz"
|
||||
local dump_destination="$DUMP_DIR/$database.sql.gz"
|
||||
|
||||
infomsg "Downloading Jami data"
|
||||
scp -r -o ProxyCommand="ssh -l $username -W %h:%p melkki.cs.helsinki.fi" "$username@$pannu_url" "$dump_destination"
|
||||
|
||||
infomsg "Removing database and related volume"
|
||||
docker volume rm oodikone_jami-data || warningmsg "This is okay, continuing"
|
||||
docker-compose -f "$DOCKER_COMPOSE" down "$database" || warningmsg "This is okay, continuing"
|
||||
|
||||
infomsg "Starting postgres in the background"
|
||||
docker-compose -f "$DOCKER_COMPOSE" up -d "$database"
|
||||
retry docker-compose -f "$DOCKER_COMPOSE" exec "$database" pg_isready --dbname="postgres"
|
||||
|
||||
infomsg "Populating Jami"
|
||||
docker exec -i "$database" /bin/bash -c "gunzip | psql -U postgres" < "$dump_destination" 2> /dev/null
|
||||
docker-compose stop "$database"
|
||||
msg ""
|
||||
}
|
||||
|
||||
reset_all_real_data() {
|
||||
infomsg "Downloading real data dumps, asking for pannu password when needed"
|
||||
for i in ${!DATABASES[*]}; do
|
||||
@@ -107,6 +128,12 @@ reset_all_real_data() {
|
||||
local url="${REAL_DUMP_URLS[$i]}"
|
||||
download_real_dump "$database" "$url"
|
||||
done
|
||||
reset_jami_data
|
||||
reset_databases "${DATABASES[@]}"
|
||||
}
|
||||
|
||||
restore_data_from_dumps() {
|
||||
infomsg "Restoring earlier-downloaded dumps to databases"
|
||||
reset_databases "${DATABASES[@]}"
|
||||
}
|
||||
|
||||
@@ -119,6 +146,7 @@ reset_single_database() {
|
||||
"sis-db"
|
||||
"sis-importer-db"
|
||||
"user-db"
|
||||
"jami-db"
|
||||
"Return to main menu."
|
||||
)
|
||||
|
||||
@@ -144,6 +172,9 @@ reset_single_database() {
|
||||
local database=$USER_DB_NAME
|
||||
local url=$USER_DB_REAL_DUMP_URL
|
||||
break 2;;
|
||||
"jami-db")
|
||||
reset_jami_data
|
||||
break 2;;
|
||||
"Return to main menu.")
|
||||
break 2;;
|
||||
*) msg "${RED}Invalid option:${NOFORMAT} $REPLY
|
||||
@@ -201,6 +232,11 @@ linting and formatting"
|
||||
successmsg "Setup ready, oodikone can be started! See README for more info."
|
||||
}
|
||||
|
||||
docker_prune() {
|
||||
infomsg "Running: docker system prune -a && docker volume prune -a"
|
||||
docker system prune -a && docker volume prune -a
|
||||
}
|
||||
|
||||
init_dirs() {
|
||||
if [[ ! -d "$DUMP_DIR" ]]; then
|
||||
infomsg "Creating directory for dumps"
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
.dockerignore
|
||||
Dockerfile
|
||||
*Dockerfile
|
||||
node_modules/
|
||||
|
||||
+18
-14
@@ -1,23 +1,27 @@
|
||||
FROM node:14.17-alpine3.13 as base
|
||||
FROM registry.access.redhat.com/ubi8/nodejs-14-minimal
|
||||
|
||||
ENV TZ="Europe/Helsinki"
|
||||
|
||||
WORKDIR /opt/app-root/src
|
||||
|
||||
WORKDIR /usr/src/app
|
||||
EXPOSE 8080
|
||||
|
||||
FROM base as development
|
||||
ENV NODE_ENV=development
|
||||
COPY ./package* /usr/src/app/
|
||||
RUN npm ci
|
||||
COPY . /usr/src/app/
|
||||
CMD ["npm", "run", "dev"]
|
||||
|
||||
FROM base as production
|
||||
ENV NODE_ENV=production
|
||||
|
||||
ARG BASE_PATH
|
||||
ENV PUBLIC_URL=$BASE_PATH
|
||||
|
||||
ARG STAGING
|
||||
ENV REACT_APP_STAGING=$STAGING
|
||||
|
||||
ARG SENTRY_ENVIRONMENT
|
||||
ENV SENTRY_ENVIRONMENT=$SENTRY_ENVIRONMENT
|
||||
|
||||
ARG SENTRY_RELEASE
|
||||
ENV SENTRY_RELEASE=$SENTRY_RELEASE
|
||||
COPY --chown=node:node ./package* /usr/src/app/
|
||||
RUN npm ci --production && apk add --no-cache vim curl nano
|
||||
COPY --chown=node:node . /usr/src/app/
|
||||
USER node
|
||||
|
||||
COPY ./package* ./
|
||||
RUN npm ci --omit-dev
|
||||
COPY . .
|
||||
|
||||
CMD ["npm", "start"]
|
||||
|
||||
@@ -1,438 +0,0 @@
|
||||
const { facultyMap } = require('./data')
|
||||
|
||||
const joryMap = {
|
||||
'hy-ttdk-tuk-jory': '100-K001',
|
||||
'hy-ttdk-tum-jory': '100-M001',
|
||||
'hy-oiktdk-on-jory': '200-K001',
|
||||
'hy-oiktdk-otm-jory': '200-M001',
|
||||
'hy-oiktdk-ibl-jory': '200-M002',
|
||||
'hy-oiktdk-ggl-jory': '200-M003',
|
||||
'hy-ltdk-psyk-jory': ['300-K001', '300-M004'],
|
||||
'hy-ltdk-logo-jory': ['300-K002', '300-M005'],
|
||||
'hy-ltdk-ll-jory': '300-M001',
|
||||
'hy-ltdk-tmed-jory': '300-M002',
|
||||
'hy-ltdk-hll-jory': '300-M003',
|
||||
'hy-humtdk-filk-jory': '400-K001',
|
||||
'hy-humtdk-ttk-jory': '400-K002',
|
||||
'hy-humtdk-kik-jory': '400-K003',
|
||||
'hy-humtdk-kok-jory': '400-K004',
|
||||
'hy-humtdk-kuka-jory': '400-K005',
|
||||
'hy-humtdk-hisk-jory': '400-K006',
|
||||
'hy-humtdk-ttm-jory': '400-M001',
|
||||
'hy-humtdk-kim-jory': '400-M002',
|
||||
'hy-humtdk-eng-jory': '400-M003',
|
||||
'hy-humtdk-rus-jory': '400-M004',
|
||||
'hy-humtdk-lingdig-jory': '400-M005',
|
||||
'hy-humtdk-tra-jory': '400-M006',
|
||||
'hy-humtdk-suku-jory': '400-M007',
|
||||
'hy-humtdk-nor-jory': '400-M008',
|
||||
'hy-humtdk-kir-jory': '400-M009',
|
||||
'hy-humtdk-kuma-jory': '400-M010',
|
||||
'hy-humtdk-ice-jory': '400-M011',
|
||||
'hy-humtdk-alku-jory': '400-M012',
|
||||
'hy-humtdk-spt-jory': '400-M014',
|
||||
'hy-humtdk-hism-jory': '400-M015',
|
||||
'hy-mltdk-mat-jory': '500-K001',
|
||||
'hy-mltdk-fys-jory': '500-K002',
|
||||
'hy-mltdk-kemk-jory': '500-K003',
|
||||
'hy-mltdk-mfkk-jory': '500-K004',
|
||||
'hy-mltdk-tkt-jory': '500-K005',
|
||||
'hy-mltdk-geok-jory': '500-K006',
|
||||
'hy-mltdk-maa-jory': '500-K007',
|
||||
'hy-mltdk-bsc-jory': '500-K008',
|
||||
'hy-mltdk-mast-jory': '500-M001',
|
||||
'hy-mltdk-lsi-jory': '500-M002',
|
||||
'hy-mltdk-tcm-jory': '500-M003',
|
||||
'hy-mltdk-paras-jory': '500-M004',
|
||||
'hy-mltdk-matres-jory': '500-M005',
|
||||
'hy-mltdk-atm-jory': '500-M006',
|
||||
'hy-mltdk-kem-jory': '500-M007',
|
||||
'hy-mltdk-mfkm-jory': '500-M008',
|
||||
'hy-mltdk-csm-jory': '500-M009',
|
||||
'hy-mltdk-data-jory': '500-M010',
|
||||
'hy-mltdk-geom-jory': '500-M011',
|
||||
'hy-mltdk-geog-jory': '500-M012',
|
||||
'hy-mltdk-usp-jory': '500-M013',
|
||||
'hy-ftdk-farm-jory': '550-K001',
|
||||
'hy-ftdk-prov-jory': '550-M001',
|
||||
'hy-ftdk-mpharm-jory': '550-M002',
|
||||
'hy-bytdk-bio-jory': '570-K001',
|
||||
'hy-bytdk-mole-jory': '570-K002',
|
||||
'hy-bytdk-env-jory': '570-K003',
|
||||
'hy-bytdk-eeb-jory': '570-M001',
|
||||
'hy-bytdk-ips-jory': '570-M002',
|
||||
'hy-bytdk-gmb-jory': '570-M003',
|
||||
'hy-bytdk-neuro-jory': '570-M004',
|
||||
'hy-bytdk-ecgs-jory': '570-M005',
|
||||
'hy-ktdk-eduk-jory': '600-K001',
|
||||
'hy-ktdk-edum-jory': '600-M001',
|
||||
'hy-ktdk-ce-jory': '600-M002',
|
||||
'hy-valttdk-pvk-jory': '700-K001',
|
||||
'hy-valttdk-yk-jory': '700-K002',
|
||||
'hy-valttdk-sosk-jory': '700-K003',
|
||||
'hy-valttdk-ecok-jory': '700-K004',
|
||||
'hy-valttdk-film-jory': '700-M001',
|
||||
'hy-valttdk-pvm-jory': '700-M002',
|
||||
'hy-valttdk-gpc-jory': '700-M003',
|
||||
'hy-valttdk-ym-jory': '700-M004',
|
||||
'hy-valttdk-cos-jory': '700-M005',
|
||||
'hy-valttdk-ens-jory': '700-M006',
|
||||
'hy-valttdk-msv-jory': '700-M007',
|
||||
'hy-valttdk-sosm-jory': '700-M008',
|
||||
'hy-valttdk-econ-jory': '700-M009',
|
||||
'hy-valttdk-sote-jory': '700-M011',
|
||||
'hy-sskh-ksv-jory': '740-K001',
|
||||
'hy-mmtdk-maatk-jory': '800-K001',
|
||||
'hy-mmtdk-metsak-jory': '800-K002',
|
||||
'hy-mmtdk-etk-jory': '800-K003',
|
||||
'hy-mmtdk-yet-jory': '800-K004',
|
||||
'hy-mmtdk-agri-jory': '800-M001',
|
||||
'hy-mmtdk-agere-jory': '800-M002',
|
||||
'hy-mmtdk-for-jory': '800-M003',
|
||||
'hy-mmtdk-food-jory': '800-M004',
|
||||
'hy-mmtdk-hnfb-jory': '800-M005',
|
||||
'hy-mmtdk-ekm-jory': '800-M006',
|
||||
'hy-mmtdk-mmb-jory': '800-M007',
|
||||
'hy-eltdk-elk-jory': '900-K001',
|
||||
'hy-eltdk-ell-jory': '900-M001',
|
||||
'hy-dp-dptheol-jory': 'T920101',
|
||||
'hy-dp-dplaw-jory': 'T920102',
|
||||
'hy-dp-philartsoc-jory': 'T920103',
|
||||
'hy-dp-dphistcult-jory': 'T920104',
|
||||
'hy-dp-dplang-jory': 'T920105',
|
||||
'hy-dp-sky-jory': 'T920106',
|
||||
'hy-dp-dpsocs-jory': 'T920107',
|
||||
'hy-dp-pyam-jory': 'T920108',
|
||||
'hy-dp-dpe-jory': 'T920109',
|
||||
'hy-dp-seduce-jory': 'T920110',
|
||||
'hy-dp-clic-jory': 'T920111',
|
||||
'hy-dp-dpbm-jory': 'T921101',
|
||||
'hy-dp-klto-jory': 'T921102',
|
||||
'hy-dp-docpop-jory': 'T921103',
|
||||
'hy-dp-findos-jory': 'T921104',
|
||||
'hy-dp-dpdr-jory': 'T921105',
|
||||
'hy-dp-ils-jory': 'T921106',
|
||||
'hy-dp-bandm-jory': 'T921107',
|
||||
'hy-dp-cvm-jory': 'T921108',
|
||||
'hy-dp-dphub-jory': 'T921109',
|
||||
'hy-dp-luova-jory': 'T922101',
|
||||
'hy-dp-dpps-jory': 'T922102',
|
||||
'hy-dp-denvi-jory': 'T922103',
|
||||
'hy-dp-agforee-jory': 'T922104',
|
||||
'hy-dp-mbdp-jory': 'T922105',
|
||||
'hy-dp-foodhealth-jory': 'T922106',
|
||||
'hy-dp-papu-jory': 'T923101',
|
||||
'hy-dp-geodoc-jory': 'T923102',
|
||||
'hy-dp-atm-dp-jory': 'T923103',
|
||||
'hy-dp-chems-jory': 'T923104',
|
||||
'hy-dp-domast-jory': 'T923105',
|
||||
'hy-dp-matrena-jory': 'T923106',
|
||||
'hy-dp-docs-jory': 'T923107',
|
||||
}
|
||||
|
||||
const kojoMap = {
|
||||
'hy-ttdk-tuk-jory': 'hy-ttdk-kandi-kojot',
|
||||
'hy-ttdk-tum-jory': 'hy-ttdk-maisteri-kojot',
|
||||
'hy-oiktdk-on-jory': 'hy-oiktdk-kandi-kojot',
|
||||
'hy-oiktdk-otm-jory': 'hy-oiktdk-maisteri-kojot',
|
||||
'hy-oiktdk-ibl-jory': 'hy-oiktdk-maisteri-kojot',
|
||||
'hy-oiktdk-ggl-jory': 'hy-oiktdk-maisteri-kojot',
|
||||
'hy-ltdk-psyk-jory': 'hy-ltdk-maisteri-kojot',
|
||||
'hy-ltdk-logo-jory': 'hy-ltdk-maisteri-kojot',
|
||||
'hy-ltdk-ll-jory': 'hy-ltdk-maisteri-kojot',
|
||||
'hy-ltdk-tmed-jory': 'hy-ltdk-maisteri-kojot',
|
||||
'hy-ltdk-hll-jory': 'hy-ltdk-maisteri-kojot',
|
||||
'hy-humtdk-filk-jory': 'hy-humtdk-kandi-kojot',
|
||||
'hy-humtdk-ttk-jory': 'hy-humtdk-kandi-kojot',
|
||||
'hy-humtdk-kik-jory': 'hy-humtdk-kandi-kojot',
|
||||
'hy-humtdk-kok-jory': 'hy-humtdk-kandi-kojot',
|
||||
'hy-humtdk-kuka-jory': 'hy-humtdk-kandi-kojot',
|
||||
'hy-humtdk-hisk-jory': 'hy-humtdk-kandi-kojot',
|
||||
'hy-humtdk-ttm-jory': 'hy-humtdk-maisteri-kojot',
|
||||
'hy-humtdk-kim-jory': 'hy-humtdk-maisteri-kojot',
|
||||
'hy-humtdk-eng-jory': 'hy-humtdk-maisteri-kojot',
|
||||
'hy-humtdk-rus-jory': 'hy-humtdk-maisteri-kojot',
|
||||
'hy-humtdk-lingdig-jory': 'hy-humtdk-maisteri-kojot',
|
||||
'hy-humtdk-tra-jory': 'hy-humtdk-maisteri-kojot',
|
||||
'hy-humtdk-suku-jory': 'hy-humtdk-maisteri-kojot',
|
||||
'hy-humtdk-nor-jory': 'hy-humtdk-maisteri-kojot',
|
||||
'hy-humtdk-kir-jory': 'hy-humtdk-maisteri-kojot',
|
||||
'hy-humtdk-kuma-jory': 'hy-humtdk-maisteri-kojot',
|
||||
'hy-humtdk-ice-jory': 'hy-humtdk-maisteri-kojot',
|
||||
'hy-humtdk-alku-jory': 'hy-humtdk-maisteri-kojot',
|
||||
'hy-humtdk-spt-jory': 'hy-humtdk-maisteri-kojot',
|
||||
'hy-humtdk-hism-jory': 'hy-humtdk-maisteri-kojot',
|
||||
'hy-mltdk-mat-jory': 'hy-mltdk-kandi-kojot',
|
||||
'hy-mltdk-fys-jory': 'hy-mltdk-kandi-kojot',
|
||||
'hy-mltdk-kemk-jory': 'hy-mltdk-kandi-kojot',
|
||||
'hy-mltdk-mfkk-jory': 'hy-mltdk-kandi-kojot',
|
||||
'hy-mltdk-tkt-jory': 'hy-mltdk-kandi-kojot',
|
||||
'hy-mltdk-geok-jory': 'hy-mltdk-kandi-kojot',
|
||||
'hy-mltdk-maa-jory': 'hy-mltdk-kandi-kojot',
|
||||
'hy-mltdk-bsc-jory': 'hy-mltdk-kandi-kojot',
|
||||
'hy-mltdk-mast-jory': 'hy-mltdk-maisteri-kojot',
|
||||
'hy-mltdk-lsi-jory': 'hy-mltdk-maisteri-kojot',
|
||||
'hy-mltdk-tcm-jory': 'hy-mltdk-maisteri-kojot',
|
||||
'hy-mltdk-paras-jory': 'hy-mltdk-maisteri-kojot',
|
||||
'hy-mltdk-matres-jory': 'hy-mltdk-maisteri-kojot',
|
||||
'hy-mltdk-atm-jory': 'hy-mltdk-maisteri-kojot',
|
||||
'hy-mltdk-kem-jory': 'hy-mltdk-maisteri-kojot',
|
||||
'hy-mltdk-mfkm-jory': 'hy-mltdk-maisteri-kojot',
|
||||
'hy-mltdk-csm-jory': 'hy-mltdk-maisteri-kojot',
|
||||
'hy-mltdk-data-jory': 'hy-mltdk-maisteri-kojot',
|
||||
'hy-mltdk-geom-jory': 'hy-mltdk-maisteri-kojot',
|
||||
'hy-mltdk-geog-jory': 'hy-mltdk-maisteri-kojot',
|
||||
'hy-mltdk-usp-jory': 'hy-mltdk-maisteri-kojot',
|
||||
'hy-ftdk-farm-jory': 'hy-ftdk-kandi-kojot',
|
||||
'hy-ftdk-prov-jory': 'hy-ftdk-maisteri-kojot',
|
||||
'hy-ftdk-mpharm-jory': 'hy-ftdk-maisteri-kojot',
|
||||
'hy-bytdk-bio-jory': 'hy-bytdk-kandi-kojot',
|
||||
'hy-bytdk-mole-jory': 'hy-bytdk-kandi-kojot',
|
||||
'hy-bytdk-env-jory': 'hy-bytdk-kandi-kojot',
|
||||
'hy-bytdk-eeb-jory': 'hy-bytdk-maisteri-kojot',
|
||||
'hy-bytdk-ips-jory': 'hy-bytdk-maisteri-kojot',
|
||||
'hy-bytdk-gmb-jory': 'hy-bytdk-maisteri-kojot',
|
||||
'hy-bytdk-neuro-jory': 'hy-bytdk-maisteri-kojot',
|
||||
'hy-bytdk-ecgs-jory': 'hy-bytdk-maisteri-kojot',
|
||||
'hy-ktdk-eduk-jory': 'hy-ktdk-kandi-kojot',
|
||||
'hy-ktdk-edum-jory': 'hy-ktdk-maisteri-kojot',
|
||||
'hy-ktdk-ce-jory': 'hy-ktdk-maisteri-kojot',
|
||||
'hy-valttdk-pvk-jory': 'hy-valttdk-kandi-kojot',
|
||||
'hy-valttdk-yk-jory': 'hy-valttdk-kandi-kojot',
|
||||
'hy-valttdk-sosk-jory': 'hy-valttdk-kandi-kojot',
|
||||
'hy-valttdk-ecok-jory': 'hy-valttdk-kandi-kojot',
|
||||
'hy-valttdk-film-jory': 'hy-valttdk-maisteri-kojot',
|
||||
'hy-valttdk-pvm-jory': 'hy-valttdk-maisteri-kojot',
|
||||
'hy-valttdk-gpc-jory': 'hy-valttdk-maisteri-kojot',
|
||||
'hy-valttdk-ym-jory': 'hy-valttdk-maisteri-kojot',
|
||||
'hy-valttdk-cos-jory': 'hy-valttdk-maisteri-kojot',
|
||||
'hy-valttdk-ens-jory': 'hy-valttdk-maisteri-kojot',
|
||||
'hy-valttdk-msv-jory': 'hy-valttdk-maisteri-kojot',
|
||||
'hy-valttdk-sosm-jory': 'hy-valttdk-maisteri-kojot',
|
||||
'hy-valttdk-econ-jory': 'hy-valttdk-maisteri-kojot',
|
||||
'hy-valttdk-sote-jory': 'hy-valttdk-maisteri-kojot',
|
||||
'hy-sskh-ksv-jory': 'hy-sskh-kandi-kojot',
|
||||
'hy-mmtdk-maatk-jory': 'hy-mmtdk-kandi-kojot',
|
||||
'hy-mmtdk-metsak-jory': 'hy-mmtdk-kandi-kojot',
|
||||
'hy-mmtdk-etk-jory': 'hy-mmtdk-kandi-kojot',
|
||||
'hy-mmtdk-yet-jory': 'hy-mmtdk-kandi-kojot',
|
||||
'hy-mmtdk-agri-jory': 'hy-mmtdk-maisteri-kojot',
|
||||
'hy-mmtdk-agere-jory': 'hy-mmtdk-maisteri-kojot',
|
||||
'hy-mmtdk-for-jory': 'hy-mmtdk-maisteri-kojot',
|
||||
'hy-mmtdk-food-jory': 'hy-mmtdk-maisteri-kojot',
|
||||
'hy-mmtdk-hnfb-jory': 'hy-mmtdk-maisteri-kojot',
|
||||
'hy-mmtdk-ekm-jory': 'hy-mmtdk-maisteri-kojot',
|
||||
'hy-mmtdk-mmb-jory': 'hy-mmtdk-maisteri-kojot',
|
||||
'hy-eltdk-elk-jory': 'hy-eltdk-kandi-kojot',
|
||||
'hy-eltdk-ell-jory': 'hy-eltdk-maisteri-kojot',
|
||||
'hy-dp-dptheol-jory': 'hy-tohtoriohjelma-johtajat',
|
||||
'hy-dp-dplaw-jory': 'hy-tohtoriohjelma-johtajat',
|
||||
'hy-dp-philartsoc-jory': 'hy-tohtoriohjelma-johtajat',
|
||||
'hy-dp-dphistcult-jory': 'hy-tohtoriohjelma-johtajat',
|
||||
'hy-dp-dplang-jory': 'hy-tohtoriohjelma-johtajat',
|
||||
'hy-dp-sky-jory': 'hy-tohtoriohjelma-johtajat',
|
||||
'hy-dp-dpsocs-jory': 'hy-tohtoriohjelma-johtajat',
|
||||
'hy-dp-pyam-jory': 'hy-tohtoriohjelma-johtajat',
|
||||
'hy-dp-dpe-jory': 'hy-tohtoriohjelma-johtajat',
|
||||
'hy-dp-seduce-jory': 'hy-tohtoriohjelma-johtajat',
|
||||
'hy-dp-clic-jory': 'hy-tohtoriohjelma-johtajat',
|
||||
'hy-dp-dpbm-jory': 'hy-tohtoriohjelma-johtajat',
|
||||
'hy-dp-klto-jory': 'hy-tohtoriohjelma-johtajat',
|
||||
'hy-dp-docpop-jory': 'hy-tohtoriohjelma-johtajat',
|
||||
'hy-dp-findos-jory': 'hy-tohtoriohjelma-johtajat',
|
||||
'hy-dp-dpdr-jory': 'hy-tohtoriohjelma-johtajat',
|
||||
'hy-dp-ils-jory': 'hy-tohtoriohjelma-johtajat',
|
||||
'hy-dp-bandm-jory': 'hy-tohtoriohjelma-johtajat',
|
||||
'hy-dp-cvm-jory': 'hy-tohtoriohjelma-johtajat',
|
||||
'hy-dp-dphub-jory': 'hy-tohtoriohjelma-johtajat',
|
||||
'hy-dp-luova-jory': 'hy-tohtoriohjelma-johtajat',
|
||||
'hy-dp-dpps-jory': 'hy-tohtoriohjelma-johtajat',
|
||||
'hy-dp-denvi-jory': 'hy-tohtoriohjelma-johtajat',
|
||||
'hy-dp-agforee-jory': 'hy-tohtoriohjelma-johtajat',
|
||||
'hy-dp-mbdp-jory': 'hy-tohtoriohjelma-johtajat',
|
||||
'hy-dp-foodhealth-jory': 'hy-tohtoriohjelma-johtajat',
|
||||
'hy-dp-papu-jory': 'hy-tohtoriohjelma-johtajat',
|
||||
'hy-dp-geodoc-jory': 'hy-tohtoriohjelma-johtajat',
|
||||
'hy-dp-atm-dp-jory': 'hy-tohtoriohjelma-johtajat',
|
||||
'hy-dp-chems-jory': 'hy-tohtoriohjelma-johtajat',
|
||||
'hy-dp-domast-jory': 'hy-tohtoriohjelma-johtajat',
|
||||
'hy-dp-matrena-jory': 'hy-tohtoriohjelma-johtajat',
|
||||
'hy-dp-docs-jory': 'hy-tohtoriohjelma-johtajat',
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps kosu IAM to the name of TDKs, defined in data.js facultyMap
|
||||
*/
|
||||
const kosuFacultyMap = {
|
||||
'hy-ypa-opa-kosu-kumpula': 'matemaattis-luonnontieteellinen',
|
||||
'hy-ypa-opa-kosu-meilahti': ['lääketieteellinen'],
|
||||
'hy-ypa-opa-kosu-kruununhaka': [
|
||||
'oikeustieteellinen',
|
||||
'valtiotieteellinen',
|
||||
'kasvatustieteellinen',
|
||||
'humanistinen',
|
||||
'teologinen',
|
||||
'svenska',
|
||||
],
|
||||
'hy-ypa-opa-kosu-metsatalo': [
|
||||
'oikeustieteellinen',
|
||||
'valtiotieteellinen',
|
||||
'kasvatustieteellinen',
|
||||
'humanistinen',
|
||||
'teologinen',
|
||||
'svenska',
|
||||
],
|
||||
'hy-ypa-opa-kosu-porthania': [
|
||||
'oikeustieteellinen',
|
||||
'valtiotieteellinen',
|
||||
'kasvatustieteellinen',
|
||||
'humanistinen',
|
||||
'teologinen',
|
||||
'svenska',
|
||||
],
|
||||
'hy-ypa-opa-kosu-siltavuori': [
|
||||
'oikeustieteellinen',
|
||||
'valtiotieteellinen',
|
||||
'kasvatustieteellinen',
|
||||
'humanistinen',
|
||||
'teologinen',
|
||||
'svenska',
|
||||
],
|
||||
'hy-ypa-opa-kosu-viikki': [
|
||||
'bio- ja ympäristötieteellinen',
|
||||
'farmasia',
|
||||
'maatalous -metsätieteellinen',
|
||||
'eläinlääketieteellinen',
|
||||
],
|
||||
}
|
||||
|
||||
const doctoralIams = ['hy-tohtorikoulutus-johtoryhma', 'hy-tine']
|
||||
|
||||
const doctoralSchoolMap = {
|
||||
'hy-tutkijakoulut-hymy-jory': [
|
||||
'T920101',
|
||||
'T920102',
|
||||
'T920103',
|
||||
'T920104',
|
||||
'T920105',
|
||||
'T920106',
|
||||
'T920107',
|
||||
'T920108',
|
||||
'T920109',
|
||||
'T920110',
|
||||
'T920111',
|
||||
],
|
||||
'hy-tutkijakoulut-dshealth-jory': [
|
||||
'T921101',
|
||||
'T921102',
|
||||
'T921103',
|
||||
'T921104',
|
||||
'T921105',
|
||||
'T921106',
|
||||
'T921107',
|
||||
'T921108',
|
||||
'T921109',
|
||||
],
|
||||
'hy-tutkijakoulut-yeb-jory': ['T922101', 'T922102', 'T922103', 'T922104', 'T922105', 'T922106'],
|
||||
'hy-tutkijakoulut-donasci-jory': ['T923101', 'T923102', 'T923103', 'T923104', 'T923105', 'T923106', 'T923107'],
|
||||
}
|
||||
|
||||
const opetusVaradekaani = 'hy-varadekaanit-opetus'
|
||||
|
||||
const universityServices = 'hy-ypa-opa-dojo'
|
||||
|
||||
const dekaaniFacultyMap = {
|
||||
'hy-ttdk-dekanaatti': 'teologinen',
|
||||
'hy-oiktdk-dekanaatti': 'oikeustieteellinen',
|
||||
'hy-ltdk-dekanaatti': 'lääketieteellinen',
|
||||
'hy-humtdk-dekanaatti': 'humanistinen',
|
||||
'hy-mltdk-dekanaatti': 'matemaattis-luonnontieteellinen',
|
||||
'hy-ftdk-dekanaatti': 'farmasia',
|
||||
'hy-bytdk-dekanaatti': 'bio- ja ympäristötieteellinen',
|
||||
'hy-ktdk-dekanaatti': 'kasvatustieteellinen',
|
||||
'hy-valttdk-dekanaatti': 'valtiotieteellinen',
|
||||
'hy-sskh-rehtoraatti': 'svenska',
|
||||
'hy-mmtdk-dekanaatti': 'maatalous -metsätieteellinen',
|
||||
'hy-eltdk-dekanaatti': 'eläinlääketieteellinen',
|
||||
}
|
||||
|
||||
const oodikoneIams = [
|
||||
'grp-oodikone-staging-users',
|
||||
'grp-oodikone-basic-staging-users',
|
||||
'grp-oodikone-users',
|
||||
'grp-oodikone-basic-users',
|
||||
]
|
||||
|
||||
const universityWideGroups = ['hy-ypa-opa-opintoasiainpaallikot', 'hy-rehtoraatti']
|
||||
|
||||
const superAdminGroups = ['grp-toska']
|
||||
|
||||
const adminGroups = ['grp-ospa']
|
||||
|
||||
const employeeGroups = ['hy-employees']
|
||||
|
||||
const isSuperAdminIam = iam => superAdminGroups.includes(iam)
|
||||
|
||||
const isAdminIam = iam => adminGroups.includes(iam)
|
||||
|
||||
const isUniversityWideIam = iam => universityWideGroups.includes(iam)
|
||||
|
||||
const isDoctoralIam = iam => doctoralIams.includes(iam)
|
||||
|
||||
const isEmployeeIam = iam => employeeGroups.includes(iam)
|
||||
|
||||
const iamToDoctoralSchool = iam => doctoralSchoolMap[iam]
|
||||
|
||||
const getStudyLeaderGroup = iam => kojoMap[iam]
|
||||
|
||||
const iamToOrganisationCode = iam => {
|
||||
const organisationCodes = joryMap[iam]
|
||||
if (Array.isArray(organisationCodes)) {
|
||||
return organisationCodes
|
||||
}
|
||||
return [organisationCodes]
|
||||
}
|
||||
|
||||
const kosuIamToFaculties = iam => {
|
||||
const faculties = kosuFacultyMap[iam]
|
||||
if (!Array.isArray(faculties) || !faculties.length) return []
|
||||
|
||||
return faculties.map(f => facultyMap[f])
|
||||
}
|
||||
|
||||
const dekaaniIamToFaculty = iam => {
|
||||
const faculty = dekaaniFacultyMap[iam]
|
||||
if (!faculty) return null
|
||||
return facultyMap[faculty]
|
||||
}
|
||||
|
||||
const relevantIAMs = []
|
||||
.concat(Object.keys(joryMap))
|
||||
.concat(Object.keys(kojoMap))
|
||||
.concat(Object.values(kojoMap))
|
||||
.concat(Object.keys(kosuFacultyMap))
|
||||
.concat(Object.keys(dekaaniFacultyMap))
|
||||
.concat(opetusVaradekaani)
|
||||
.concat(universityServices)
|
||||
.concat(doctoralIams)
|
||||
.concat(Object.keys(doctoralSchoolMap))
|
||||
.concat(universityWideGroups)
|
||||
.concat(superAdminGroups)
|
||||
.concat(adminGroups)
|
||||
.concat(employeeGroups)
|
||||
.concat(oodikoneIams)
|
||||
|
||||
module.exports = {
|
||||
isSuperAdminIam,
|
||||
isAdminIam,
|
||||
isUniversityWideIam,
|
||||
isDoctoralIam,
|
||||
isEmployeeIam,
|
||||
iamToDoctoralSchool,
|
||||
iamToOrganisationCode,
|
||||
kosuIamToFaculties,
|
||||
dekaaniIamToFaculty,
|
||||
opetusVaradekaani,
|
||||
getStudyLeaderGroup,
|
||||
relevantIAMs,
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
const mapToDegreeCode = organisationCode => {
|
||||
if (!organisationCode) return ''
|
||||
if (organisationCode.length < 7) return ''
|
||||
const doctoral = organisationCode[0] === 'T'
|
||||
if (doctoral) {
|
||||
return organisationCode
|
||||
}
|
||||
|
||||
const [start, end] = organisationCode.split('-')
|
||||
if (end && end.length < 3) return ''
|
||||
if (start.length < 2) return ''
|
||||
const masters = end[0] === 'M'
|
||||
const code = `${masters ? 'M' : 'K'}H${start.substr(0, 2)}_${end.substr(-3)}`
|
||||
return code
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
mapToDegreeCode,
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -125,7 +125,7 @@ const codes = {
|
||||
T922106: 'foodhealth',
|
||||
T923101: 'papu',
|
||||
T923102: 'geodoc',
|
||||
T923103: 'atm',
|
||||
T923103: 'atm-dp',
|
||||
T923104: 'chems',
|
||||
T923105: 'domast',
|
||||
T923106: 'matrena',
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
FROM node:14.17-alpine3.13
|
||||
|
||||
ENV TZ="Europe/Helsinki"
|
||||
|
||||
WORKDIR /opt/app-root/src
|
||||
|
||||
EXPOSE 8080
|
||||
|
||||
ENV NODE_ENV=development
|
||||
|
||||
COPY ./package* ./
|
||||
RUN npm ci
|
||||
COPY . .
|
||||
|
||||
CMD ["npm", "run", "dev"]
|
||||
Generated
+99
-4177
File diff suppressed because it is too large
Load Diff
@@ -4,7 +4,7 @@
|
||||
"scripts": {
|
||||
"start": "node --max_old_space_size=4096 index.js",
|
||||
"dev": "nodemon --max_old_space_size=4096 index.js",
|
||||
"rapodiff": "node /usr/src/app/src/rapodiff.js"
|
||||
"rapodiff": "node /opt/app-root/src/src/rapodiff.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@sentry/node": "^5.30.0",
|
||||
@@ -28,7 +28,8 @@
|
||||
"umzug": "^2.3.0",
|
||||
"unfuck-utf8-headers-middleware": "^1.0.1",
|
||||
"uuid": "^3.4.0",
|
||||
"winston": "^3.8.2"
|
||||
"winston": "^3.8.2",
|
||||
"winston-gelf-transporter": "^1.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"nodemon": "^1.19.4"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Node env to use
|
||||
const isStaging = process.env.NODE_ENV === 'staging'
|
||||
const isDev = process.env.NODE_ENV === 'development'
|
||||
const isProduction = process.env.NODE_ENV === 'production'
|
||||
const isStaging = process.env.REACT_APP_STAGING === 'true'
|
||||
const isProduction = !isStaging && process.env.NODE_ENV === 'production'
|
||||
|
||||
// Sentry
|
||||
const sentryRelease = process.env.SENTRY_RELEASE || ''
|
||||
@@ -16,7 +16,11 @@ const requiredGroup = isStaging
|
||||
// Pate
|
||||
const pateToken = process.env.PATE_API_TOKEN || ''
|
||||
|
||||
// Jami
|
||||
const jamiUrl = process.env.JAMI_URL || ''
|
||||
|
||||
// Importer client
|
||||
const importerUrl = process.env.IMPORTER_DB_API_URL || ''
|
||||
const importerToken = process.env.IMPORTER_DB_API_TOKEN || ''
|
||||
|
||||
// Networking: Urls & ports
|
||||
@@ -24,16 +28,6 @@ const baseUrl = isDev ? '/api' : ''
|
||||
const frontUrl = process.env.FRONT_URL
|
||||
const backendPort = 8080
|
||||
|
||||
// Analytics
|
||||
const ANALYTICS_INFLUXDB_URL = process.env.ANALYTICS_INFLUXDB_URL
|
||||
const ANALYTICS_INFLUXDB_USER = process.env.ANALYTICS_INFLUXDB_USER
|
||||
const ANALYTICS_INFLUXDB_PASSWORD = process.env.ANALYTICS_INFLUXDB_PASSWORD
|
||||
const ANALYTICS_INFLUXDB_DB = process.env.ANALYTICS_INFLUXDB_DB
|
||||
|
||||
// Matomo
|
||||
const MATOMO_SITE_ID = process.env.MATOMO_SITE_ID
|
||||
const MATOMO_URL = process.env.MATOMO_URL
|
||||
|
||||
// Other stuff
|
||||
const DB_URL_KONE = process.env.DB_URL_KONE
|
||||
const DB_URL_USER = process.env.DB_URL_USER
|
||||
@@ -52,11 +46,9 @@ const SECRET_TOKEN = process.env.SECRET_TOKEN
|
||||
const DB_SCHEMA_KONE = process.env.DB_SCHEMA_KONE || 'public'
|
||||
const SIS_UPDATER_URL = process.env.SIS_UPDATER_URL
|
||||
|
||||
const formatURL = url => {
|
||||
return !!url && !url.startsWith('http') ? `http://${url}` : url
|
||||
}
|
||||
|
||||
const addSlashToEnd = url => (url.endsWith('/') ? url : url + '/')
|
||||
const KONE_PASSWORD = process.env.KONE_PASSWORD || 'postgres'
|
||||
const USER_PASSWORD = process.env.USER_PASSWORD || 'postgres'
|
||||
const SIS_PASSWORD = process.env.SIS_PASSWORD || 'postgres'
|
||||
|
||||
module.exports = {
|
||||
DB_URL_USER,
|
||||
@@ -66,21 +58,20 @@ module.exports = {
|
||||
redis,
|
||||
SECRET_TOKEN,
|
||||
DB_SCHEMA_KONE,
|
||||
KONE_PASSWORD,
|
||||
USER_PASSWORD,
|
||||
SIS_PASSWORD,
|
||||
requiredGroup,
|
||||
SIS_UPDATER_URL,
|
||||
ANALYTICS_INFLUXDB_URL: formatURL(ANALYTICS_INFLUXDB_URL),
|
||||
ANALYTICS_INFLUXDB_USER,
|
||||
ANALYTICS_INFLUXDB_PASSWORD,
|
||||
ANALYTICS_INFLUXDB_DB,
|
||||
MATOMO_SITE_ID,
|
||||
MATOMO_URL: MATOMO_URL && addSlashToEnd(MATOMO_URL),
|
||||
runningInCI,
|
||||
sentryRelease,
|
||||
sentryEnvironment,
|
||||
isProduction,
|
||||
pateToken,
|
||||
jamiUrl,
|
||||
baseUrl,
|
||||
frontUrl,
|
||||
backendPort,
|
||||
importerUrl,
|
||||
importerToken,
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ class DbConnection extends EventEmitter {
|
||||
idle: 300000000,
|
||||
},
|
||||
logging: false,
|
||||
password: conf.SIS_PASSWORD,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -44,12 +45,14 @@ const sequelizeKone = new Sequelize(conf.DB_URL_KONE, {
|
||||
schema: conf.DB_SCHEMA_KONE,
|
||||
searchPath: conf.DB_SCHEMA_KONE,
|
||||
logging: false,
|
||||
password: conf.KONE_PASSWORD,
|
||||
})
|
||||
|
||||
sequelizeKone.query(`SET SESSION search_path to ${conf.DB_SCHEMA_KONE}`)
|
||||
|
||||
const sequelizeUser = new Sequelize(conf.DB_URL_USER, {
|
||||
logging: false,
|
||||
password: conf.USER_PASSWORD,
|
||||
})
|
||||
|
||||
const initializeDatabaseConnection = async () => {
|
||||
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
const { INTEGER, STRING, ARRAY, DATE } = require('sequelize')
|
||||
|
||||
module.exports = {
|
||||
up: async queryInterface => {
|
||||
await queryInterface.createTable('progress_criteria', {
|
||||
code: {
|
||||
type: STRING,
|
||||
primaryKey: true,
|
||||
},
|
||||
courses_year_one: {
|
||||
type: ARRAY(STRING),
|
||||
},
|
||||
courses_year_two: {
|
||||
type: ARRAY(STRING),
|
||||
},
|
||||
courses_year_three: {
|
||||
type: ARRAY(STRING),
|
||||
},
|
||||
credits_year_one: {
|
||||
type: INTEGER,
|
||||
},
|
||||
credits_year_two: {
|
||||
type: INTEGER,
|
||||
},
|
||||
credits_year_three: {
|
||||
type: INTEGER,
|
||||
},
|
||||
created_at: DATE,
|
||||
updated_at: DATE,
|
||||
})
|
||||
},
|
||||
down: () => {},
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
module.exports = {
|
||||
up: async (queryInterface, Sequelize) => {
|
||||
await queryInterface.addColumn('progress_criteria', 'courses_year_four', {
|
||||
type: Sequelize.ARRAY(Sequelize.STRING),
|
||||
})
|
||||
await queryInterface.addColumn('progress_criteria', 'courses_year_five', {
|
||||
type: Sequelize.ARRAY(Sequelize.STRING),
|
||||
})
|
||||
await queryInterface.addColumn('progress_criteria', 'courses_year_six', {
|
||||
type: Sequelize.ARRAY(Sequelize.STRING),
|
||||
})
|
||||
await queryInterface.addColumn('progress_criteria', 'credits_year_four', {
|
||||
type: Sequelize.INTEGER,
|
||||
})
|
||||
await queryInterface.addColumn('progress_criteria', 'credits_year_five', {
|
||||
type: Sequelize.INTEGER,
|
||||
})
|
||||
await queryInterface.addColumn('progress_criteria', 'credits_year_six', {
|
||||
type: Sequelize.INTEGER,
|
||||
})
|
||||
},
|
||||
down: async () => {},
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
module.exports = {
|
||||
up: async queryInterface => {
|
||||
await queryInterface.dropTable('mandatory_courses')
|
||||
await queryInterface.dropTable('mandatory_course_labels')
|
||||
},
|
||||
down: async () => {},
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
module.exports = {
|
||||
up: async queryInterface => {
|
||||
await queryInterface.sequelize.query('DROP SEQUENCE "mandatory_courses_id_seq"')
|
||||
await queryInterface.sequelize.query('DROP SEQUENCE "mandatory_course_labels_id_seq"')
|
||||
},
|
||||
down: async () => {},
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
module.exports = {
|
||||
up: async (queryInterface, Sequelize) => {
|
||||
await queryInterface.sequelize.query('TRUNCATE TABLE progress_criteria')
|
||||
await queryInterface.sequelize.query('TRUNCATE TABLE excluded_courses')
|
||||
await queryInterface.removeConstraint('excluded_courses', 'excluded_courses_programme_code_course_code_key')
|
||||
await queryInterface.addColumn('progress_criteria', 'curriculum_version', {
|
||||
type: Sequelize.STRING,
|
||||
})
|
||||
await queryInterface.addColumn('excluded_courses', 'curriculum_version', {
|
||||
type: Sequelize.STRING,
|
||||
})
|
||||
},
|
||||
down: async queryInterface => {
|
||||
await queryInterface.removeColumn('progress_criteria', 'curriculum_version')
|
||||
await queryInterface.removeColumn('excluded_courses', 'curriculum_version')
|
||||
},
|
||||
}
|
||||
@@ -6,10 +6,12 @@ const { getAllProgrammes } = require('./services/studyrights')
|
||||
const { updateBasicView, updateStudytrackView } = require('./services/studyprogrammeUpdates')
|
||||
const { findAndSaveTeachers } = require('./services/topteachers')
|
||||
const { faculties } = require('./services/organisations')
|
||||
const { combinedStudyprogrammes } = require('./services/studyprogrammeHelpers')
|
||||
const { updateFacultyOverview, updateFacultyProgressOverview } = require('./services/faculty/facultyUpdates')
|
||||
const { isProduction } = require('./conf-backend')
|
||||
const { getCurrentSemester } = require('./services/semesters')
|
||||
const logger = require('./util/logger')
|
||||
const { getAssociations } = require('./services/studyrights')
|
||||
|
||||
const schedule = (cronTime, func) => new CronJob({ cronTime, onTick: func, start: true, timeZone: 'Europe/Helsinki' })
|
||||
|
||||
@@ -29,28 +31,48 @@ const refreshFaculties = async () => {
|
||||
await updateFacultyProgressOverview(faculty.code)
|
||||
ready += 1
|
||||
} catch (e) {
|
||||
logger.error({ message: `Failed to update stats for faculty ${faculty?.code} with type ALL`, meta: e })
|
||||
logger.error({ message: `Failed to update stats for faculty ${faculty?.code} with type ALL`, meta: `${e}` })
|
||||
}
|
||||
logger.info(`${ready}/${facultyList.length} faculties done`)
|
||||
logger.info(`${ready}/${facultyList.length} faculties done; faculty ${faculty?.code}`)
|
||||
}
|
||||
logger.info('Faculty stats refreshed!')
|
||||
}
|
||||
|
||||
const refreshNewOverviews = async () => {
|
||||
logger.info('Refreshing studyprogramme and studytrack overview statistics for all programmes')
|
||||
// Filters out old programmes and special ones like Fitech studies. Filters also out the programmes starting with
|
||||
// 2_ or ending with _2. Those programmes are mapped to correct programme (studentProgrammeModuleFixer.js)
|
||||
const codes = (await getAllProgrammes())
|
||||
.map(p => p.code)
|
||||
.filter(code => code.includes('KH') || code.includes('MH') || /^(T)[0-9]{6}$/.test(code))
|
||||
.filter(
|
||||
code =>
|
||||
(code.includes('KH') && !code.startsWith('2_KH') && !code.endsWith('_2')) ||
|
||||
(code.includes('MH') && !code.startsWith('2_MH') && !code.endsWith('_2')) ||
|
||||
/^(T)[0-9]{6}$/.test(code)
|
||||
)
|
||||
const associations = await getAssociations()
|
||||
let ready = 0
|
||||
for (const code of codes) {
|
||||
let combinedProgramme = ''
|
||||
// If combined programme is given, this updates only the bachelor programme
|
||||
try {
|
||||
await updateBasicView(code)
|
||||
await updateStudytrackView(code)
|
||||
await updateBasicView(code, combinedProgramme)
|
||||
await updateStudytrackView(code, combinedProgramme, associations)
|
||||
ready += 1
|
||||
} catch (e) {
|
||||
logger.error({ message: `Failed to update overview stats for programme ${code}`, meta: e })
|
||||
logger.error({ message: `Failed to update overview stats for programme ${code}`, meta: `${e}` })
|
||||
}
|
||||
logger.info(`${ready}/${codes.length} programmes done`)
|
||||
combinedProgramme = combinedStudyprogrammes[code] || ''
|
||||
// If bachelor programme is combined with master programme, then update the combination.
|
||||
if (combinedProgramme) {
|
||||
try {
|
||||
await updateBasicView(code, combinedProgramme)
|
||||
await updateStudytrackView(code, combinedProgramme, associations)
|
||||
} catch (e) {
|
||||
logger.error({ message: `Failed to update overview stats for combined programme ${code}`, meta: `${e}` })
|
||||
}
|
||||
}
|
||||
logger.info(`${ready}/${codes.length} programmes done: code ${code}`)
|
||||
}
|
||||
logger.info('Studyprogramme and studytrack overview stats refreshed!')
|
||||
}
|
||||
@@ -91,7 +113,7 @@ const refreshUberToRedis = async () => {
|
||||
await refreshUber(oldAttainmentsQuery)
|
||||
logger.info(`Refreshing CDS uber year ${formattedYear} doned`)
|
||||
} catch (e) {
|
||||
logger.error({ message: `Error when refreshing CDS uber year ${formattedYear}`, meta: e })
|
||||
logger.error({ message: `Error when refreshing CDS uber year ${formattedYear}`, meta: `${e}` })
|
||||
}
|
||||
}
|
||||
logger.info(`Refreshing CDS Uber data doned`)
|
||||
@@ -132,4 +154,5 @@ module.exports = {
|
||||
refreshStatistics,
|
||||
refreshTrends,
|
||||
refreshFaculties,
|
||||
refreshNewOverviews,
|
||||
}
|
||||
|
||||
@@ -3,8 +3,8 @@ const { ApplicationError } = require('../util/customErrors')
|
||||
const { getUser, getMockedUser } = require('../services/userService')
|
||||
const { requiredGroup } = require('../conf-backend')
|
||||
const _ = require('lodash')
|
||||
const { relevantIAMs } = require('../../config/IAMConfig')
|
||||
const { getOrganizationAccess } = require('../util/organizationAccess')
|
||||
const logger = require('../util/logger')
|
||||
|
||||
const parseIamGroups = iamGroups => iamGroups?.split(';').filter(Boolean) ?? []
|
||||
|
||||
@@ -27,11 +27,12 @@ const currentUserMiddleware = async (req, _res, next) => {
|
||||
throw new ApplicationError('Not enough data in request headers', 403, { logoutUrl })
|
||||
}
|
||||
|
||||
const iamGroups = parseIamGroups(hygroupcn).filter(iam => relevantIAMs.includes(iam))
|
||||
|
||||
const iamRights = Object.keys(await getOrganizationAccess({ iamGroups }))
|
||||
const iamGroups = parseIamGroups(hygroupcn)
|
||||
const { access = {}, specialGroup = {} } = await getOrganizationAccess({ id: sisId, iamGroups })
|
||||
const iamRights = Object.keys(access)
|
||||
|
||||
if (!hasRequiredIamGroup(iamGroups, iamRights)) {
|
||||
logger.error({ message: 'User does not have required iam group', meta: { iamGroups, iamRights } })
|
||||
throw new ApplicationError('User does not have required iam group', 403, { logoutUrl })
|
||||
}
|
||||
|
||||
@@ -40,6 +41,8 @@ const currentUserMiddleware = async (req, _res, next) => {
|
||||
name,
|
||||
email,
|
||||
iamGroups,
|
||||
iamRights,
|
||||
specialGroup,
|
||||
sisId,
|
||||
})
|
||||
|
||||
|
||||
@@ -16,6 +16,9 @@ ExcludedCourse.init(
|
||||
course_code: {
|
||||
type: STRING,
|
||||
},
|
||||
curriculum_version: {
|
||||
type: STRING,
|
||||
},
|
||||
created_at: {
|
||||
type: DATE,
|
||||
},
|
||||
|
||||
@@ -67,6 +67,9 @@ Studyright.belongsTo(Organization, { foreignKey: 'facultyCode', targetKey: 'code
|
||||
Studyplan.belongsTo(Student, { foreignKey: 'studentnumber', targetKey: 'studentnumber' })
|
||||
Student.hasMany(Studyplan, { foreignKey: 'studentnumber', sourceKey: 'studentnumber' })
|
||||
|
||||
Studyplan.belongsTo(Studyright, { foreignKey: 'studyrightid', targetKey: 'studyrightid' })
|
||||
Studyright.hasMany(Studyplan, { foreignKey: 'studyrightid', sourceKey: 'studyrightid' })
|
||||
|
||||
Studyright.belongsTo(Student, { foreignKey: 'studentStudentnumber', targetKey: 'studentnumber' })
|
||||
Student.hasMany(Studyright, { foreignKey: 'studentStudentnumber', sourceKey: 'studentnumber' })
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
const { STRING } = require('sequelize')
|
||||
const Sequelize = require('sequelize')
|
||||
const { sequelizeKone } = require('../database/connection')
|
||||
|
||||
@@ -17,6 +18,9 @@ const ExcludedCourse = sequelizeKone.define(
|
||||
programme_code: {
|
||||
type: Sequelize.STRING,
|
||||
},
|
||||
curriculum_version: {
|
||||
type: STRING,
|
||||
},
|
||||
course_code: {
|
||||
type: Sequelize.STRING,
|
||||
},
|
||||
@@ -32,18 +36,6 @@ const ExcludedCourse = sequelizeKone.define(
|
||||
}
|
||||
)
|
||||
|
||||
const MandatoryCourse = sequelizeKone.define('mandatory_courses', {
|
||||
course_code: {
|
||||
type: Sequelize.STRING,
|
||||
},
|
||||
studyprogramme_id: {
|
||||
type: Sequelize.STRING,
|
||||
},
|
||||
label: {
|
||||
type: Sequelize.BIGINT,
|
||||
},
|
||||
})
|
||||
|
||||
const TagStudent = sequelizeKone.define(
|
||||
'tag_student',
|
||||
{
|
||||
@@ -87,23 +79,6 @@ const Tag = sequelizeKone.define(
|
||||
}
|
||||
)
|
||||
|
||||
const MandatoryCourseLabels = sequelizeKone.define('mandatory_course_labels', {
|
||||
id: {
|
||||
primaryKey: true,
|
||||
type: Sequelize.BIGINT,
|
||||
autoIncrement: true,
|
||||
},
|
||||
studyprogramme_id: {
|
||||
type: Sequelize.STRING,
|
||||
},
|
||||
label: {
|
||||
type: Sequelize.STRING,
|
||||
},
|
||||
orderNumber: {
|
||||
type: Sequelize.INTEGER,
|
||||
},
|
||||
})
|
||||
|
||||
const CustomPopulationSearch = sequelizeKone.define('custom_population_searches', {
|
||||
id: {
|
||||
primaryKey: true,
|
||||
@@ -121,6 +96,7 @@ const CustomPopulationSearch = sequelizeKone.define('custom_population_searches'
|
||||
},
|
||||
})
|
||||
|
||||
// also used in completed courses search feature
|
||||
const OpenUniPopulationSearch = sequelizeKone.define(
|
||||
'open_uni_population_searches',
|
||||
{
|
||||
@@ -143,6 +119,56 @@ const OpenUniPopulationSearch = sequelizeKone.define(
|
||||
{ underscored: true, timestamps: true }
|
||||
)
|
||||
|
||||
const ProgressCriteria = sequelizeKone.define(
|
||||
'progress_criteria',
|
||||
{
|
||||
code: {
|
||||
primaryKey: true,
|
||||
type: STRING,
|
||||
},
|
||||
curriculumVersion: {
|
||||
type: STRING,
|
||||
},
|
||||
coursesYearOne: {
|
||||
type: Sequelize.ARRAY(Sequelize.STRING),
|
||||
},
|
||||
coursesYearTwo: {
|
||||
type: Sequelize.ARRAY(Sequelize.STRING),
|
||||
},
|
||||
coursesYearThree: {
|
||||
type: Sequelize.ARRAY(Sequelize.STRING),
|
||||
},
|
||||
coursesYearFour: {
|
||||
type: Sequelize.ARRAY(Sequelize.STRING),
|
||||
},
|
||||
coursesYearFive: {
|
||||
type: Sequelize.ARRAY(Sequelize.STRING),
|
||||
},
|
||||
coursesYearSix: {
|
||||
type: Sequelize.ARRAY(Sequelize.STRING),
|
||||
},
|
||||
creditsYearOne: {
|
||||
type: Sequelize.INTEGER,
|
||||
},
|
||||
creditsYearTwo: {
|
||||
type: Sequelize.INTEGER,
|
||||
},
|
||||
creditsYearThree: {
|
||||
type: Sequelize.INTEGER,
|
||||
},
|
||||
creditsYearFour: {
|
||||
type: Sequelize.INTEGER,
|
||||
},
|
||||
creditsYearFive: {
|
||||
type: Sequelize.INTEGER,
|
||||
},
|
||||
creditsYearSix: {
|
||||
type: Sequelize.INTEGER,
|
||||
},
|
||||
},
|
||||
{ underscored: true, timestamps: true }
|
||||
)
|
||||
|
||||
const StudyGuidanceGroupTag = sequelizeKone.define(
|
||||
'study_guidance_group_tags',
|
||||
{
|
||||
@@ -165,27 +191,17 @@ const StudyGuidanceGroupTag = sequelizeKone.define(
|
||||
{ underscored: true, timestamps: true }
|
||||
)
|
||||
|
||||
MandatoryCourse.belongsTo(MandatoryCourseLabels, {
|
||||
foreignKey: 'label',
|
||||
sourceKey: 'id',
|
||||
})
|
||||
MandatoryCourseLabels.hasMany(MandatoryCourse, {
|
||||
foreignKey: 'label',
|
||||
sourceKey: 'id',
|
||||
})
|
||||
|
||||
TagStudent.belongsTo(Tag, { foreignKey: 'tag_id', sourceKey: 'tag_id' })
|
||||
Tag.hasMany(TagStudent, { foreignKey: 'tag_id', sourceKey: 'tag_id' })
|
||||
|
||||
module.exports = {
|
||||
MandatoryCourse,
|
||||
TagStudent,
|
||||
Tag,
|
||||
ThesisTypeEnums,
|
||||
MandatoryCourseLabels,
|
||||
CustomPopulationSearch,
|
||||
OpenUniPopulationSearch,
|
||||
sequelizeKone,
|
||||
ExcludedCourse,
|
||||
StudyGuidanceGroupTag,
|
||||
ProgressCriteria,
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const { Model, STRING, DATE, JSONB } = require('sequelize')
|
||||
const { Model, STRING, DATE, INTEGER, JSONB, ARRAY, TEXT } = require('sequelize')
|
||||
const { dbConnections } = require('../database/connection')
|
||||
|
||||
class ProgrammeModule extends Model {}
|
||||
@@ -9,6 +9,9 @@ ProgrammeModule.init(
|
||||
primaryKey: true,
|
||||
type: STRING,
|
||||
},
|
||||
group_id: {
|
||||
type: STRING,
|
||||
},
|
||||
code: {
|
||||
type: STRING,
|
||||
},
|
||||
@@ -18,6 +21,9 @@ ProgrammeModule.init(
|
||||
type: {
|
||||
type: STRING,
|
||||
},
|
||||
order: {
|
||||
type: INTEGER,
|
||||
},
|
||||
studyLevel: {
|
||||
type: STRING,
|
||||
},
|
||||
@@ -28,6 +34,15 @@ ProgrammeModule.init(
|
||||
key: 'id',
|
||||
},
|
||||
},
|
||||
valid_from: {
|
||||
type: DATE,
|
||||
},
|
||||
valid_to: {
|
||||
type: DATE,
|
||||
},
|
||||
curriculum_period_ids: {
|
||||
type: ARRAY(TEXT),
|
||||
},
|
||||
createdAt: {
|
||||
type: DATE,
|
||||
},
|
||||
|
||||
@@ -5,19 +5,19 @@ class ProgrammeModuleChild extends Model {}
|
||||
|
||||
ProgrammeModuleChild.init(
|
||||
{
|
||||
parent_id: {
|
||||
composite: {
|
||||
type: STRING,
|
||||
primaryKey: true,
|
||||
},
|
||||
parentId: {
|
||||
type: STRING,
|
||||
references: {
|
||||
model: 'programme_modules',
|
||||
key: 'id',
|
||||
},
|
||||
},
|
||||
child_id: {
|
||||
childId: {
|
||||
type: STRING,
|
||||
references: {
|
||||
model: 'programme_modules',
|
||||
key: 'id',
|
||||
},
|
||||
},
|
||||
createdAt: {
|
||||
type: DATE,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const { Model, STRING, DATE, INTEGER, BOOLEAN } = require('sequelize')
|
||||
const { Model, STRING, DATE, INTEGER } = require('sequelize')
|
||||
const { dbConnections } = require('../database/connection')
|
||||
|
||||
class Student extends Model {}
|
||||
@@ -18,15 +18,15 @@ Student.init(
|
||||
email: { type: STRING },
|
||||
secondary_email: { type: STRING },
|
||||
national_student_number: { type: STRING },
|
||||
phone_number: { type: STRING },
|
||||
country_fi: { type: STRING },
|
||||
country_sv: { type: STRING },
|
||||
country_en: { type: STRING },
|
||||
home_country_fi: { type: STRING },
|
||||
home_country_sv: { type: STRING },
|
||||
home_country_en: { type: STRING },
|
||||
gender_code: { type: INTEGER },
|
||||
gender_code: { type: STRING },
|
||||
sis_person_id: { type: STRING },
|
||||
dissemination_info_allowed: { type: BOOLEAN },
|
||||
createdAt: {
|
||||
type: DATE,
|
||||
},
|
||||
|
||||
@@ -20,6 +20,16 @@ Studyplan.init(
|
||||
onDelete: 'cascade',
|
||||
allowNull: false,
|
||||
},
|
||||
studyrightid: {
|
||||
type: STRING,
|
||||
references: {
|
||||
model: 'studyright',
|
||||
key: 'studyrightid',
|
||||
},
|
||||
onUpdate: 'cascade',
|
||||
onDelete: 'cascade',
|
||||
allowNull: false,
|
||||
},
|
||||
programme_code: {
|
||||
type: STRING,
|
||||
allowNull: false,
|
||||
@@ -30,6 +40,9 @@ Studyplan.init(
|
||||
completed_credits: {
|
||||
type: INTEGER,
|
||||
},
|
||||
sisu_id: {
|
||||
type: STRING,
|
||||
},
|
||||
createdAt: {
|
||||
type: DATE,
|
||||
},
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const { Model, STRING, DATE, INTEGER } = require('sequelize')
|
||||
const { Model, STRING, DATE, INTEGER, BOOLEAN } = require('sequelize')
|
||||
const { dbConnections } = require('../database/connection')
|
||||
|
||||
class Studyright extends Model {}
|
||||
@@ -27,7 +27,9 @@ Studyright.init(
|
||||
active: {
|
||||
type: INTEGER,
|
||||
},
|
||||
// irtisanomisperuste
|
||||
cancelled: {
|
||||
type: BOOLEAN,
|
||||
},
|
||||
studentStudentnumber: {
|
||||
type: STRING,
|
||||
references: {
|
||||
|
||||
@@ -3,11 +3,10 @@ const express = require('express')
|
||||
const cors = require('cors')
|
||||
const { frontUrl } = require('./conf-backend')
|
||||
const shibbolethCharsetMiddleware = require('./middleware/shibbolethCharsetMiddleware')
|
||||
const matomoInit = require('./routes/matomo-init')
|
||||
const currentUserMiddleware = require('./middleware/currentUserMiddleware')
|
||||
const accessLogger = require('./middleware/accessLogger')
|
||||
const auth = require('./middleware/auth')
|
||||
|
||||
const changelog = require('./routes/changelog')
|
||||
const courses = require('./routes/courses')
|
||||
const students = require('./routes/students')
|
||||
const population = require('./routes/population')
|
||||
@@ -18,23 +17,22 @@ const elementdetails = require('./routes/elementdetails')
|
||||
const teachers = require('./routes/teachers')
|
||||
const providers = require('./routes/providers')
|
||||
const faculties = require('./routes/faculties')
|
||||
const courseExclusions = require('./routes/courseExclusions')
|
||||
const semesters = require('./routes/semesters')
|
||||
const mandatoryCourses = require('./routes/mandatorycourses')
|
||||
const mandatoryCourseLabels = require('./routes/mandatorycourselabels')
|
||||
const feedback = require('./routes/feedback')
|
||||
const tags = require('./routes/tags')
|
||||
const updater = require('./routes/updater')
|
||||
const tsaAnalytics = require('./routes/tsaAnalytics')
|
||||
const customPopulationSearch = require('./routes/customPopulationSearch')
|
||||
const languageCenterData = require('./routes/languageCenterData')
|
||||
const trends = require('./routes/trends')
|
||||
const programmeModules = require('./routes/programmeModules')
|
||||
const studyGuidanceGroups = require('./routes/studyGuidanceGroups')
|
||||
const studyProgramme = require('./routes/studyProgramme')
|
||||
const customOpenUniSearch = require('./routes/customOpenUniSearch')
|
||||
const studyProgrammeCriteria = require('./routes/studyProgrammeCriteria')
|
||||
const initializeSentry = require('./util/sentry')
|
||||
|
||||
const completedCoursesSearch = require('./routes/completedCoursesSearch')
|
||||
const errorMiddleware = require('./middleware/errorMiddleware')
|
||||
|
||||
module.exports = (app, url) => {
|
||||
initializeSentry(app)
|
||||
app.use(Sentry.Handlers.requestHandler())
|
||||
@@ -44,7 +42,6 @@ module.exports = (app, url) => {
|
||||
app.use(express.json())
|
||||
|
||||
app.use(shibbolethCharsetMiddleware)
|
||||
app.use(url, matomoInit)
|
||||
app.use(currentUserMiddleware)
|
||||
app.use(accessLogger)
|
||||
app.use(url, login)
|
||||
@@ -57,16 +54,18 @@ module.exports = (app, url) => {
|
||||
app.use(url, semesters)
|
||||
app.use(url, tags)
|
||||
app.use(url, programmeModules)
|
||||
app.use(url, courseExclusions)
|
||||
app.use(url, studyProgramme)
|
||||
app.use(`${url}/programmecriteria`, studyProgrammeCriteria)
|
||||
app.use(`${url}/openunisearch`, auth.roles(['openUniSearch']), customOpenUniSearch)
|
||||
app.use(`${url}/changelog`, changelog)
|
||||
app.use(`${url}/completedcoursessearch`, completedCoursesSearch)
|
||||
app.use(`${url}/languagecenterdata`, languageCenterData)
|
||||
app.use(`${url}/faculties`, auth.roles(['facultyStatistics']), faculties)
|
||||
app.use(`${url}/updater`, auth.roles(['admin']), updater)
|
||||
app.use(`${url}/teachers`, auth.roles(['teachers']), teachers)
|
||||
app.use(`${url}/users`, auth.roles(['admin']), users)
|
||||
app.use(`${url}/feedback`, feedback)
|
||||
app.use(`${url}/mandatory_courses`, mandatoryCourses)
|
||||
app.use(`${url}/mandatory-course-labels`, mandatoryCourseLabels)
|
||||
app.use(`${url}/tsa`, tsaAnalytics)
|
||||
app.use(`${url}/custom-population-search`, customPopulationSearch)
|
||||
app.use(`${url}/cool-data-science`, trends)
|
||||
app.use(`${url}/studyguidancegroups`, auth.roles(['studyGuidanceGroups']), studyGuidanceGroups)
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
const router = require('express').Router()
|
||||
const axios = require('axios')
|
||||
|
||||
router.get('/', async (req, res) => {
|
||||
const result = await axios.get('https://api.github.com/repos/UniversityOfHelsinkiCS/oodikone/releases')
|
||||
const formattedResult = result.data.map(r => ({ description: r.body, time: r.created_at, title: r.name }))
|
||||
res.status(200).send(formattedResult)
|
||||
})
|
||||
|
||||
module.exports = router
|
||||
@@ -0,0 +1,101 @@
|
||||
const router = require('express').Router()
|
||||
const { getCompletedCourses } = require('../services/completedCoursesSearch')
|
||||
const { getImporterClient } = require('../util/importerClient')
|
||||
const importerClient = getImporterClient()
|
||||
const _ = require('lodash')
|
||||
const {
|
||||
getOpenUniSearches,
|
||||
createNewSearch,
|
||||
deleteSearch,
|
||||
updateSearch,
|
||||
} = require('../services/openUni/openUniManageSearches')
|
||||
const logger = require('../util/logger')
|
||||
|
||||
router.get('/', async (req, res) => {
|
||||
const studentNumbers = JSON.parse(req.query?.studentlist) || []
|
||||
const courseCodes = JSON.parse(req.query?.courselist) || []
|
||||
const {
|
||||
user: { isAdmin, studentsUserCanAccess, sisPersonId: teacherId },
|
||||
} = req
|
||||
if (!Array.isArray(studentNumbers)) return res.status(400).json({ error: 'Student numbers must be of type array' })
|
||||
if (!Array.isArray(courseCodes)) return res.status(400).json({ error: 'Courses must be of type array' })
|
||||
|
||||
const answerTimeout = new Promise(resolve => setTimeout(resolve, 6000))
|
||||
|
||||
try {
|
||||
// Teachers also can get rights to students via importer if the students
|
||||
// have enrolled to their courses in last 8 months
|
||||
// (acual logic a bit different, see importer)
|
||||
const teacherRightsToStudents = await Promise.race([
|
||||
importerClient.post(`/teacher-rights/`, { teacherId, studentNumbers }),
|
||||
answerTimeout,
|
||||
])
|
||||
if (teacherRightsToStudents && Array.isArray(teacherRightsToStudents.data))
|
||||
studentsUserCanAccess.push(...teacherRightsToStudents.data)
|
||||
} catch (e) {
|
||||
logger.error(`Importer teacher-rights request failed with message: ${e?.message}`)
|
||||
}
|
||||
|
||||
const filteredStudentNumbers = isAdmin ? studentNumbers : _.intersection(studentNumbers, studentsUserCanAccess)
|
||||
const completedCourses = await getCompletedCourses(filteredStudentNumbers, courseCodes)
|
||||
const discardedStudentNumbers = _.difference(
|
||||
studentNumbers,
|
||||
completedCourses.students.map(s => s.studentNumber)
|
||||
)
|
||||
return res.json({ discardedStudentNumbers, ...completedCourses })
|
||||
})
|
||||
|
||||
router.get('/searches', async (req, res) => {
|
||||
const userId = req.user.id
|
||||
const foundSearches = await getOpenUniSearches(userId)
|
||||
return res.json(foundSearches)
|
||||
})
|
||||
|
||||
router.post('/searches', async (req, res) => {
|
||||
const courseCodes = req.body?.courselist || []
|
||||
const userId = req.user.id
|
||||
const name = req.body?.name
|
||||
|
||||
if (!name) return res.status(400).json({ error: 'Courselist name missing' })
|
||||
if (courseCodes && !Array.isArray(courseCodes))
|
||||
return res.status(400).json({ error: 'Course codes must be type of array' })
|
||||
|
||||
const createdSearch = await createNewSearch(userId, name, courseCodes)
|
||||
if (!createdSearch) return res.status(400).json({ error: 'Failed to create courselist' })
|
||||
return res.status(201).json({
|
||||
id: createdSearch.id,
|
||||
userId: createdSearch.userId,
|
||||
name: createdSearch.name,
|
||||
courseList: createdSearch.courseCodes,
|
||||
updatedAt: createdSearch.updatedAt,
|
||||
})
|
||||
})
|
||||
|
||||
router.put('/searches/:id', async (req, res) => {
|
||||
const id = req.params?.id
|
||||
const courseCodes = req.body?.courselist || []
|
||||
const userId = req.user.id
|
||||
if (!id || !userId) return res.status(422).end()
|
||||
const updatedSearch = await updateSearch(userId, id, courseCodes)
|
||||
|
||||
if (!updatedSearch) return res.status(404).json({ error: 'Courselist could not be found' })
|
||||
return res.json({
|
||||
id: updatedSearch.id,
|
||||
userId: updatedSearch.userId,
|
||||
name: updatedSearch.name,
|
||||
courseList: updatedSearch.courseCodes,
|
||||
updatedAt: updatedSearch.updatedAt,
|
||||
})
|
||||
})
|
||||
|
||||
router.delete('/searches/:id', async (req, res) => {
|
||||
const id = req.params?.id
|
||||
const userId = req.user.id
|
||||
if (!id || !userId) return res.status(422).end()
|
||||
|
||||
const deletedSearch = await deleteSearch(userId, id)
|
||||
if (!deletedSearch) return res.status(404).json({ error: 'Courselist could not be found' })
|
||||
return res.json(id)
|
||||
})
|
||||
|
||||
module.exports = router
|
||||
@@ -0,0 +1,25 @@
|
||||
const router = require('express').Router()
|
||||
const { addExcludedCourses, removeExcludedCourses } = require('../services/excludedCourses')
|
||||
|
||||
router.delete('/v3/course_exclusions/:code', async (req, res) => {
|
||||
const { programmeCode, curriculumVersion, courseCodes } = req.body
|
||||
const result = await removeExcludedCourses({ programmeCode, curriculumVersion, courseCodes })
|
||||
if (!result) {
|
||||
res.status(400).end()
|
||||
return
|
||||
}
|
||||
res.json(result)
|
||||
})
|
||||
|
||||
router.post('/v3/course_exclusions/:code', async (req, res) => {
|
||||
const { code } = req.params
|
||||
const { courseCodes, curriculumVersion } = req.body
|
||||
const result = await addExcludedCourses(code, courseCodes, curriculumVersion.join(','))
|
||||
if (!result) {
|
||||
res.status(400).end()
|
||||
return
|
||||
}
|
||||
res.json(result)
|
||||
})
|
||||
|
||||
module.exports = router
|
||||
@@ -1,5 +1,5 @@
|
||||
const router = require('express').Router()
|
||||
var moment = require('moment-timezone')
|
||||
const moment = require('moment-timezone')
|
||||
const { getCustomOpenUniCourses } = require('../services/openUni/openUniStats')
|
||||
const {
|
||||
getOpenUniSearches,
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
const router = require('express').Router()
|
||||
const MandatoryCourses = require('../services/mandatoryCourses')
|
||||
const { getAllProgrammes, getAllElementDetails } = require('../services/studyrights')
|
||||
|
||||
router.get('/elementdetails/all', async (req, res) => {
|
||||
@@ -12,13 +11,4 @@ router.get('/studyprogrammes', async (req, res) => {
|
||||
res.json(studyrights)
|
||||
})
|
||||
|
||||
router.get('/v2/studyprogrammes/:id/mandatory_courses', async (req, res) => {
|
||||
if (req.params.id) {
|
||||
const codes = await MandatoryCourses.byStudyprogramme(req.params.id)
|
||||
res.json(codes)
|
||||
} else {
|
||||
res.status(422).end()
|
||||
}
|
||||
})
|
||||
|
||||
module.exports = router
|
||||
|
||||
@@ -2,7 +2,6 @@ const router = require('express').Router()
|
||||
const { faculties } = require('../services/organisations')
|
||||
const { combineFacultyBasics } = require('../services/faculty/facultyBasics')
|
||||
const { combineFacultyCredits } = require('../services/faculty/facultyCredits')
|
||||
const { findFacultyProgrammeCodes } = require('../services/faculty/faculty')
|
||||
const { combineFacultyThesisWriters } = require('../services/faculty/facultyThesisWriters')
|
||||
const { countGraduationTimes } = require('../services/faculty/facultyGraduationTimes')
|
||||
const { updateFacultyOverview, updateFacultyProgressOverview } = require('../services/faculty/facultyUpdates')
|
||||
@@ -10,8 +9,7 @@ const { combineFacultyStudentProgress } = require('../services/faculty/facultySt
|
||||
const { combineFacultyStudents } = require('../services/faculty/facultyStudents')
|
||||
|
||||
const {
|
||||
getFacultyProgrammes,
|
||||
setFacultyProgrammes,
|
||||
getProgrammes,
|
||||
getBasicStats,
|
||||
setBasicStats,
|
||||
getCreditStats,
|
||||
@@ -30,15 +28,6 @@ const logger = require('../util/logger')
|
||||
// Faculty uses a lot of tools designed for Study programme.
|
||||
// Some of them have been copied here and slightly edited for faculty purpose.
|
||||
|
||||
const getProgrammes = async (code, programmeFilter) => {
|
||||
const programmes = await getFacultyProgrammes(code, programmeFilter)
|
||||
if (programmes) return programmes
|
||||
let updatedProgrammes = await findFacultyProgrammeCodes(code, programmeFilter)
|
||||
if (updatedProgrammes) updatedProgrammes = await setFacultyProgrammes(code, updatedProgrammes, programmeFilter)
|
||||
|
||||
return updatedProgrammes
|
||||
}
|
||||
|
||||
router.get('/', async (req, res) => {
|
||||
const ignore = ['Y', 'H99', 'Y01', 'H92', 'H930']
|
||||
const facultyList = (await faculties()).filter(f => !ignore.includes(f.code))
|
||||
@@ -84,11 +73,10 @@ router.get('/:id/basicstats', async (req, res) => {
|
||||
router.get('/:id/creditstats', async (req, res) => {
|
||||
const code = req.params.id
|
||||
const yearType = req.query?.year_type
|
||||
const specialGroups = req.query?.special_groups
|
||||
const programmeFilter = req.query?.programme_filter
|
||||
|
||||
if (!code) return res.status(422).end()
|
||||
const data = await getCreditStats(code, yearType, programmeFilter, specialGroups)
|
||||
const data = await getCreditStats(code, yearType, programmeFilter)
|
||||
if (data) return res.json(data)
|
||||
|
||||
const programmes = await getProgrammes(code, programmeFilter)
|
||||
@@ -97,9 +85,9 @@ router.get('/:id/creditstats', async (req, res) => {
|
||||
// list of all programmes is also needed in classification of credits
|
||||
const allProgrammes = programmeFilter === 'ALL_PROGRAMMES' ? programmes : await getProgrammes(code, 'ALL_PROGRAMMES')
|
||||
|
||||
let updatedStats = await combineFacultyCredits(code, programmes.data, allProgrammes.data, yearType, specialGroups)
|
||||
let updatedStats = await combineFacultyCredits(code, programmes.data, allProgrammes.data, yearType)
|
||||
if (updatedStats) {
|
||||
updatedStats = await setCreditStats(updatedStats, yearType, programmeFilter, specialGroups)
|
||||
updatedStats = await setCreditStats(updatedStats, yearType, programmeFilter)
|
||||
}
|
||||
|
||||
return res.json(updatedStats)
|
||||
@@ -141,15 +129,15 @@ router.get('/:id/graduationtimes', async (req, res) => {
|
||||
|
||||
router.get('/:id/progressstats', async (req, res) => {
|
||||
const code = req.params.id
|
||||
const programmeFilter = req.query.programme_filter
|
||||
const specialGroups = req.query?.special_groups
|
||||
const graduated = req.query?.graduated
|
||||
const programmes = await getProgrammes(code, 'NEW_STUDY_PROGRAMMES')
|
||||
if (!programmes) return res.status(422).end()
|
||||
|
||||
if (!code) return res.status(422).end()
|
||||
const data = await getFacultyProgressStats(code, specialGroups, graduated)
|
||||
if (data) return res.json(data)
|
||||
const programmes = await getProgrammes(code, programmeFilter)
|
||||
if (!programmes) return res.status(422).end()
|
||||
|
||||
let updateStats = await combineFacultyStudentProgress(code, programmes.data, specialGroups, graduated)
|
||||
if (updateStats) {
|
||||
updateStats = await setFacultyProgressStats(updateStats, specialGroups, graduated)
|
||||
@@ -159,16 +147,16 @@ router.get('/:id/progressstats', async (req, res) => {
|
||||
|
||||
router.get('/:id/studentstats', async (req, res) => {
|
||||
const code = req.params.id
|
||||
const programmeFilter = req.query.programme_filter
|
||||
const specialGroups = req.query?.special_groups
|
||||
const graduated = req.query?.graduated
|
||||
|
||||
if (!code) return res.status(422).end()
|
||||
const data = await getFacultyStudentStats(code, specialGroups, graduated)
|
||||
if (data) return res.json(data)
|
||||
const programmes = await getProgrammes(code, programmeFilter)
|
||||
if (!programmes) return res.status(422).end()
|
||||
let updateStats = await combineFacultyStudents(code, programmes.data, specialGroups, graduated)
|
||||
const newProgrammes = await getProgrammes(code, 'NEW_STUDY_PROGRAMMES')
|
||||
if (!newProgrammes) return res.status(422).end()
|
||||
|
||||
let updateStats = await combineFacultyStudents(code, newProgrammes.data, specialGroups, graduated)
|
||||
if (updateStats) {
|
||||
updateStats = await setFacultyStudentStats(updateStats, specialGroups, graduated)
|
||||
}
|
||||
@@ -179,12 +167,7 @@ router.get('/:id/update_basicview', async (req, res) => {
|
||||
const code = req.params.id
|
||||
const statsType = req.query?.stats_type
|
||||
if (code) {
|
||||
let result = null
|
||||
try {
|
||||
result = await updateFacultyOverview(code, statsType)
|
||||
} catch (e) {
|
||||
logger.error(`Failed to update faculty ${code} basic tab stats for ${statsType}: ${e}`)
|
||||
}
|
||||
const result = await updateFacultyOverview(code, statsType)
|
||||
return res.json(result)
|
||||
}
|
||||
return res.status(422).end()
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
const router = require('express').Router()
|
||||
const { getLanguageCenterData } = require('../services/languageCenterData')
|
||||
|
||||
router.get('/', async (req, res) => {
|
||||
const { iamGroups, isAdmin } = req.user
|
||||
if (!isAdmin && !iamGroups.includes('grp-kielikeskus-esihenkilot')) {
|
||||
return res.status(403).json({ error: 'Request failed because of missing rights' })
|
||||
}
|
||||
const result = await getLanguageCenterData()
|
||||
return res.json(result)
|
||||
})
|
||||
|
||||
module.exports = router
|
||||
@@ -1,71 +0,0 @@
|
||||
const router = require('express').Router()
|
||||
const MandatoryCourseLabels = require('../services/mandatoryCourseLabels')
|
||||
const logger = require('../util/logger')
|
||||
|
||||
router.get('/:programme/labels', async (req, res) => {
|
||||
const { programme } = req.params
|
||||
if (programme) {
|
||||
try {
|
||||
const labels = await MandatoryCourseLabels.labelsByStudyprogramme(programme)
|
||||
res.status(200).json(labels)
|
||||
} catch (err) {
|
||||
logger.error(err)
|
||||
res.status(400).json(err.message)
|
||||
}
|
||||
} else {
|
||||
res.status(422).end()
|
||||
}
|
||||
})
|
||||
|
||||
router.delete('/:programme/labels', async (req, res) => {
|
||||
const { programme } = req.params
|
||||
const { label } = req.body
|
||||
if (programme && label) {
|
||||
try {
|
||||
await MandatoryCourseLabels.destroy(programme, label)
|
||||
const labels = await MandatoryCourseLabels.labelsByStudyprogramme(programme)
|
||||
res.status(200).json(labels)
|
||||
} catch (err) {
|
||||
logger.error(err)
|
||||
res.status(400).json(err.message)
|
||||
}
|
||||
} else {
|
||||
res.status(422).end()
|
||||
}
|
||||
})
|
||||
|
||||
router.post('/:programme/move', async (req, res) => {
|
||||
const { programme } = req.params
|
||||
const { label, direction } = req.body
|
||||
if (programme && label) {
|
||||
try {
|
||||
await MandatoryCourseLabels.move(programme, label, direction)
|
||||
const labels = await MandatoryCourseLabels.labelsByStudyprogramme(programme)
|
||||
res.status(200).json(labels)
|
||||
} catch (err) {
|
||||
logger.error(err)
|
||||
res.status(400).json(err.message)
|
||||
}
|
||||
} else {
|
||||
res.status(422).end()
|
||||
}
|
||||
})
|
||||
|
||||
router.post('/:programme/labels', async (req, res) => {
|
||||
const { programme } = req.params
|
||||
const { label } = req.body
|
||||
if (programme && label) {
|
||||
try {
|
||||
await MandatoryCourseLabels.create(programme, label)
|
||||
const labels = await MandatoryCourseLabels.labelsByStudyprogramme(programme)
|
||||
res.status(200).json(labels)
|
||||
} catch (err) {
|
||||
logger.error(err)
|
||||
res.status(400).json(err.message)
|
||||
}
|
||||
} else {
|
||||
res.status(422).end()
|
||||
}
|
||||
})
|
||||
|
||||
module.exports = router
|
||||
@@ -1,36 +0,0 @@
|
||||
const router = require('express').Router()
|
||||
const MandatoryCourses = require('../services/mandatoryCourses')
|
||||
|
||||
router.delete('/:programme', async (req, res) => {
|
||||
const { programme } = req.params
|
||||
const { course } = req.body
|
||||
if (!programme || !course) return res.status(400).end()
|
||||
await MandatoryCourses.remove(programme, course)
|
||||
res.status(200).json(course)
|
||||
})
|
||||
|
||||
router.post('/:programme', async (req, res) => {
|
||||
const { programme } = req.params
|
||||
const { course } = req.body
|
||||
if (!programme || !course) return res.status(400).end()
|
||||
await MandatoryCourses.create(programme, course)
|
||||
const newCourse = await MandatoryCourses.find(programme, course)
|
||||
if (!newCourse) return res.status(400).end()
|
||||
res.status(201).json({ name: newCourse.course.name, code: newCourse.course_code, label: newCourse.label })
|
||||
})
|
||||
|
||||
router.post('/:programme/label/:course', async (req, res) => {
|
||||
const { programme, course } = req.params
|
||||
const { label } = req.body
|
||||
if (!programme || !course) return res.status(400).end()
|
||||
await MandatoryCourses.updateLabel(programme, course, label)
|
||||
const updatedCourse = await MandatoryCourses.find(programme, course)
|
||||
if (!updatedCourse) return res.status(400).end()
|
||||
res.status(200).json({
|
||||
name: updatedCourse.course.name,
|
||||
code: updatedCourse.course_code,
|
||||
label: updatedCourse.mandatory_course_label,
|
||||
})
|
||||
})
|
||||
|
||||
module.exports = router
|
||||
@@ -1,36 +0,0 @@
|
||||
const router = require('express').Router()
|
||||
const config = require('../conf-backend')
|
||||
|
||||
// Produces the Matomo tracker initialization script for the frontend.
|
||||
// Matomo URL and site id are baked here instead of in the frontend
|
||||
// so we can fix them on the fly instead of rebuilding the entire frontend.
|
||||
|
||||
const acual = `
|
||||
var _paq = window._paq || [];
|
||||
/* tracker methods like "setCustomDimension" should be called before "trackPageView" */
|
||||
_paq.push(["trackPageView"]);
|
||||
_paq.push(["enableLinkTracking"]);
|
||||
(function() {
|
||||
var u = "${config.MATOMO_URL}";
|
||||
_paq.push(["setTrackerUrl", u + "matomo.php"]);
|
||||
_paq.push(["setSiteId", "${config.MATOMO_SITE_ID}"]);
|
||||
_paq.push(["enableHeartBeatTimer"]);
|
||||
var d = document,
|
||||
g = d.createElement("script"),
|
||||
s = d.getElementsByTagName("script")[0];
|
||||
g.type = "text/javascript";
|
||||
g.async = true;
|
||||
g.defer = true;
|
||||
g.src = u + "matomo.js";
|
||||
s.parentNode.insertBefore(g, s);
|
||||
})();
|
||||
`
|
||||
|
||||
const script = config.MATOMO_URL ? acual : ''
|
||||
|
||||
router.get('/matomo-init', (_, res) => {
|
||||
res.contentType('text/javascript; charset=UTF-8')
|
||||
res.send(script)
|
||||
})
|
||||
|
||||
module.exports = router
|
||||
@@ -82,60 +82,10 @@ router.post('/v2/populationstatistics/courses', async (req, res) => {
|
||||
return
|
||||
}
|
||||
|
||||
//These cause problems in front (where they were originally calculated)
|
||||
// Do them here for the moment
|
||||
for (const course of result.coursestatistics) {
|
||||
// course.students.notParticipated = {}
|
||||
course.students.didNotPass = {}
|
||||
|
||||
for (const student of Object.keys(course.students.all)) {
|
||||
if (
|
||||
!Object.keys(course.students.passed).includes(student) &&
|
||||
!Object.keys(course.students.retryPassed).includes(student)
|
||||
)
|
||||
course.students.didNotPass[student] = true
|
||||
}
|
||||
}
|
||||
res.json(result)
|
||||
}
|
||||
})
|
||||
|
||||
router.post('/v2/populationstatistics/coursesbycoursecode', async (req, res) => {
|
||||
const { from, to, coursecodes } = req.body
|
||||
if (!from || !to || !coursecodes) {
|
||||
return res.status(400).json({ error: 'The body should have a yearcode and coursecode defined' })
|
||||
}
|
||||
const maxYearsToCreatePopulationFrom = await CourseService.maxYearsToCreatePopulationFrom(coursecodes)
|
||||
if (Math.abs(to - from + 1) > maxYearsToCreatePopulationFrom) {
|
||||
return res.status(400).json({ error: `Max years to create population from is ${maxYearsToCreatePopulationFrom}` })
|
||||
}
|
||||
|
||||
const {
|
||||
user: { isAdmin, studentsUserCanAccess },
|
||||
} = req
|
||||
|
||||
const studentnumbers = await Student.findByCourseAndSemesters(coursecodes, from, to)
|
||||
const studentnumberlist = isAdmin ? studentnumbers : _.intersection(studentnumbers, studentsUserCanAccess)
|
||||
|
||||
const result = await Population.bottlenecksOf(
|
||||
{
|
||||
year: 1900,
|
||||
studyRights: [],
|
||||
semesters: ['FALL', 'SPRING'],
|
||||
months: 10000,
|
||||
},
|
||||
studentnumberlist
|
||||
)
|
||||
|
||||
if (result.error) {
|
||||
Sentry.captureException(new Error(result.error))
|
||||
res.status(400).json(result)
|
||||
return
|
||||
}
|
||||
|
||||
res.json(result)
|
||||
})
|
||||
|
||||
router.post('/v2/populationstatistics/coursesbytag', async (req, res) => {
|
||||
const { tag } = req.body
|
||||
if (!tag) {
|
||||
@@ -171,7 +121,7 @@ router.post('/v2/populationstatistics/coursesbytag', async (req, res) => {
|
||||
router.post('/v2/populationstatistics/coursesbystudentnumberlist', async (req, res) => {
|
||||
const {
|
||||
user: { isAdmin, studentsUserCanAccess },
|
||||
body: { studentnumberlist: studentnumbersInReq, studentCountLimit, ...query },
|
||||
body: { studentnumberlist: studentnumbersInReq, ...query },
|
||||
} = req
|
||||
|
||||
if (!studentnumbersInReq) {
|
||||
@@ -179,14 +129,12 @@ router.post('/v2/populationstatistics/coursesbystudentnumberlist', async (req, r
|
||||
}
|
||||
|
||||
const studentnumberlist = isAdmin ? studentnumbersInReq : _.intersection(studentnumbersInReq, studentsUserCanAccess)
|
||||
|
||||
const result = await Population.bottlenecksOf(
|
||||
{
|
||||
year: query?.year ?? 1900,
|
||||
studyRights: query?.studyRights ?? [],
|
||||
semesters: query?.semesters ?? ['FALL', 'SPRING'],
|
||||
months: query?.months ?? 10000,
|
||||
studentCountLimit,
|
||||
},
|
||||
studentnumberlist
|
||||
)
|
||||
@@ -214,7 +162,13 @@ router.get('/v3/populationstatistics', async (req, res) => {
|
||||
} = req
|
||||
|
||||
try {
|
||||
if (!isAdmin && !rights.includes(studyRights.programme) && !iamRights.includes(studyRights.programme)) {
|
||||
if (
|
||||
!isAdmin &&
|
||||
!rights.includes(studyRights.programme) &&
|
||||
!rights.includes(studyRights.combinedProgramme) &&
|
||||
!iamRights.includes(studyRights.programme) &&
|
||||
!iamRights.includes(studyRights.combinedProgramme)
|
||||
) {
|
||||
res.status(403).json([])
|
||||
return
|
||||
}
|
||||
@@ -223,7 +177,7 @@ router.get('/v3/populationstatistics', async (req, res) => {
|
||||
return
|
||||
}
|
||||
|
||||
if (req.query.months == null) {
|
||||
if (req.query.months === null) {
|
||||
req.query.months = 12
|
||||
}
|
||||
|
||||
@@ -232,12 +186,15 @@ router.get('/v3/populationstatistics', async (req, res) => {
|
||||
const multipopulationstudentPromises = Promise.all(
|
||||
req.query.years.map(year => {
|
||||
const newMonths = (upperYearBound - Number(year)) * 12
|
||||
const populationStudents = Population.optimizedStatisticsOf({
|
||||
...req.query,
|
||||
studyRights,
|
||||
year,
|
||||
months: newMonths,
|
||||
})
|
||||
const populationStudents = Population.optimizedStatisticsOf(
|
||||
{
|
||||
...req.query,
|
||||
studyRights: { programme: studyRights.programme },
|
||||
year,
|
||||
months: newMonths,
|
||||
},
|
||||
null
|
||||
)
|
||||
return populationStudents
|
||||
})
|
||||
)
|
||||
@@ -253,7 +210,11 @@ router.get('/v3/populationstatistics', async (req, res) => {
|
||||
|
||||
res.json(filterPersonalTags(result, user.id))
|
||||
} else {
|
||||
const result = await Population.optimizedStatisticsOf({ ...req.query, studyRights })
|
||||
const result = await Population.optimizedStatisticsOf({
|
||||
...req.query,
|
||||
studyRights: { programme: studyRights.programme },
|
||||
})
|
||||
|
||||
if (result.error) {
|
||||
Sentry.captureException(new Error(result.error))
|
||||
res.status(400).end()
|
||||
@@ -261,7 +222,7 @@ router.get('/v3/populationstatistics', async (req, res) => {
|
||||
}
|
||||
|
||||
// Obfuscate if user has only iam rights
|
||||
if (!isAdmin && !rights.includes(studyRights.programme)) {
|
||||
if (!isAdmin && !rights.includes(studyRights.programme) && !rights.includes(studyRights.combinedProgramme)) {
|
||||
result.students = result.students.map(student => {
|
||||
const { iv, encryptedData } = encrypt(student.studentNumber)
|
||||
const obfuscatedBirthDate = new Date(new Date(student.birthdate).setMonth(0, 0)) // only birth year for age distribution
|
||||
@@ -269,6 +230,7 @@ router.get('/v3/populationstatistics', async (req, res) => {
|
||||
...student,
|
||||
firstnames: '',
|
||||
lastname: '',
|
||||
phone_number: '',
|
||||
started: null,
|
||||
iv,
|
||||
studentNumber: encryptedData,
|
||||
@@ -381,6 +343,7 @@ router.get('/v3/populationstatisticsbycourse', async (req, res) => {
|
||||
studentNumber: crypto.createHash('md5').update(`${studentNumber}${randomHash}`).digest('hex'),
|
||||
firstnames: '',
|
||||
lastname: '',
|
||||
phone_number: '',
|
||||
name: '',
|
||||
email: '',
|
||||
started: null,
|
||||
@@ -401,17 +364,20 @@ router.get('/v3/populationstatisticsbycourse', async (req, res) => {
|
||||
})
|
||||
|
||||
router.post('/v3/populationstatisticsbystudentnumbers', async (req, res) => {
|
||||
const { studentnumberlist } = req.body
|
||||
const { studentnumberlist, tags } = req.body
|
||||
const {
|
||||
user: { isAdmin, id, studentsUserCanAccess },
|
||||
} = req
|
||||
|
||||
const filteredStudentNumbers = isAdmin ? studentnumberlist : _.intersection(studentnumberlist, studentsUserCanAccess)
|
||||
|
||||
const studyProgrammeCode =
|
||||
tags?.studyProgramme && tags?.studyProgramme.includes('+')
|
||||
? tags?.studyProgramme.split('+')[0]
|
||||
: tags?.studyProgramme
|
||||
const result = await Population.optimizedStatisticsOf(
|
||||
{
|
||||
year: 1900,
|
||||
studyRights: [],
|
||||
year: tags?.year || 1900,
|
||||
studyRights: studyProgrammeCode ? [studyProgrammeCode] : [],
|
||||
semesters: ['FALL', 'SPRING'],
|
||||
months: 10000,
|
||||
},
|
||||
@@ -421,7 +387,12 @@ router.post('/v3/populationstatisticsbystudentnumbers', async (req, res) => {
|
||||
Sentry.captureException(new Error(result.error))
|
||||
return
|
||||
}
|
||||
res.status(200).json(filterPersonalTags(result, id))
|
||||
|
||||
result.studyProgramme = tags?.studyProgramme
|
||||
|
||||
const discardedStudentNumbers = _.difference(studentnumberlist, filteredStudentNumbers)
|
||||
|
||||
res.status(200).json({ ...filterPersonalTags(result, id), discardedStudentNumbers })
|
||||
})
|
||||
|
||||
router.get('/v3/populationstatistics/studyprogrammes', async (req, res) => {
|
||||
@@ -433,7 +404,11 @@ router.get('/v3/populationstatistics/studyprogrammes', async (req, res) => {
|
||||
mapCodesToIds(studyrights.programmes)
|
||||
res.json(studyrights)
|
||||
} else {
|
||||
const studyrights = await StudyrightService.getFilteredAssociations(rights.concat(iamRights))
|
||||
const allRights = rights.concat(iamRights)
|
||||
// For combined programme
|
||||
// If more programmes are combined, then a function might be a better idea to add moar rights
|
||||
if (allRights.includes('KH90_001') || allRights.includes('MH90_001')) allRights.push('KH90_001', 'MH90_001')
|
||||
const studyrights = await StudyrightService.getFilteredAssociations(allRights)
|
||||
mapCodesToIds(studyrights.programmes)
|
||||
res.json(studyrights)
|
||||
}
|
||||
|
||||
@@ -1,43 +1,21 @@
|
||||
const router = require('express').Router()
|
||||
const {
|
||||
byProgrammeCode,
|
||||
addExcludedCourses,
|
||||
removeExcludedCourses,
|
||||
modulesByProgrammeCode,
|
||||
} = require('../services/programmeModules')
|
||||
const { getCoursesAndModules, getCurriculumVersions } = require('../services/programmeModules')
|
||||
|
||||
router.get('/v3/programme_modules/:code', async (req, res) => {
|
||||
router.get('/v3/get_curriculum_options/:code', async (req, res) => {
|
||||
const { code } = req.params
|
||||
const result = await byProgrammeCode(code)
|
||||
const result = await getCurriculumVersions(code)
|
||||
res.json(result)
|
||||
})
|
||||
|
||||
router.get('/v3/programme_modules/:code/modules', async (req, res) => {
|
||||
const { code } = req.params
|
||||
const result = await modulesByProgrammeCode(code)
|
||||
res.json(result)
|
||||
})
|
||||
|
||||
router.delete('/v3/programme_modules', async (req, res) => {
|
||||
const { programmecode, ids } = req.body
|
||||
await removeExcludedCourses(ids)
|
||||
const result = await byProgrammeCode(programmecode)
|
||||
if (!result) {
|
||||
res.status(400).end()
|
||||
return
|
||||
}
|
||||
res.json(result)
|
||||
})
|
||||
|
||||
router.post('/v3/programme_modules/:programmecode/', async (req, res) => {
|
||||
const { programmecode, coursecodes } = req.body
|
||||
await addExcludedCourses(programmecode, coursecodes)
|
||||
const result = await byProgrammeCode(programmecode)
|
||||
if (!result) {
|
||||
res.status(400).end()
|
||||
return
|
||||
}
|
||||
res.json(result)
|
||||
router.get('/v3/get_curriculum/:code/:period_ids', async (req, res) => {
|
||||
const { code, period_ids } = req.params
|
||||
const result = await getCoursesAndModules(code, period_ids.replace(' ', '').split(','))
|
||||
res.json({
|
||||
defaultProgrammeCourses: result.defaultProgrammeCourses.courses,
|
||||
defaultProgrammeModules: result.defaultProgrammeCourses.modules,
|
||||
secondProgrammeCourses: result.secondProgrammeCourses?.courses,
|
||||
secondProgrammeModules: result.secondProgrammeCourses?.modules,
|
||||
})
|
||||
})
|
||||
|
||||
module.exports = router
|
||||
|
||||
@@ -44,9 +44,9 @@ router.get('/students/:id', async (req, res) => {
|
||||
if (!isAdmin && !studentsUserCanAccess.includes(studentId)) {
|
||||
throw new ApplicationError('Error finding student', 400)
|
||||
}
|
||||
|
||||
const student = await Student.withId(studentId)
|
||||
res.json(filterStudentTags(student, id))
|
||||
const filteredTags = filterStudentTags(student, id)
|
||||
res.json(filteredTags)
|
||||
})
|
||||
|
||||
module.exports = router
|
||||
|
||||
@@ -15,20 +15,35 @@ const {
|
||||
setStudytrackStats,
|
||||
} = require('../services/analyticsService')
|
||||
const { updateBasicView, updateStudytrackView } = require('../services/studyprogrammeUpdates')
|
||||
const { getProgrammeName } = require('../services/studyprogramme')
|
||||
const logger = require('../util/logger')
|
||||
const { getAssociations } = require('../services/studyrights')
|
||||
const { ElementDetail } = require('../models')
|
||||
|
||||
// For grafana statistics (idea stolen from Norppa)
|
||||
const logInfoForGrafana = async (code, combinedProgramme) => {
|
||||
const programme = await ElementDetail.findOne({ where: { code: code } })
|
||||
const programmeCode = combinedProgramme ? `${programme.code}-${combinedProgramme}` : programme.code
|
||||
logger.info('Study Programme', {
|
||||
studyprogrammeName: combinedProgramme ? `${programme.name.fi} + maisteri` : programme.name.fi,
|
||||
studyprogrammeCode: programmeCode,
|
||||
})
|
||||
}
|
||||
|
||||
router.get('/v2/studyprogrammes/:id/basicstats', async (req, res) => {
|
||||
const code = req.params.id
|
||||
const yearType = req.query?.year_type
|
||||
const specialGroups = req.query?.special_groups
|
||||
|
||||
const combinedProgramme = req.query?.combined_programme
|
||||
if (!code) return res.status(422).end()
|
||||
|
||||
const data = await getBasicStats(code, yearType, specialGroups)
|
||||
logInfoForGrafana(code, combinedProgramme)
|
||||
const data = await getBasicStats(code, combinedProgramme, yearType, specialGroups)
|
||||
if (data) return res.json(data)
|
||||
|
||||
const updated = await getBasicStatsForStudytrack({
|
||||
studyprogramme: req.params.id,
|
||||
combinedProgramme,
|
||||
settings: {
|
||||
isAcademicYear: yearType === 'ACADEMIC_YEAR',
|
||||
includeAllSpecials: specialGroups === 'SPECIAL_INCLUDED',
|
||||
@@ -42,13 +57,15 @@ router.get('/v2/studyprogrammes/:id/creditstats', async (req, res) => {
|
||||
const code = req.params.id
|
||||
const yearType = req.query?.year_type
|
||||
const specialGroups = req.query?.special_groups
|
||||
const combinedProgramme = req.query?.combined_programme
|
||||
|
||||
if (!code) return res.status(422).end()
|
||||
|
||||
const data = await getCreditStats(code, yearType, specialGroups)
|
||||
const data = await getCreditStats(code, combinedProgramme, yearType, specialGroups)
|
||||
if (data) return res.json(data)
|
||||
const updatedStats = await getCreditStatsForStudytrack({
|
||||
studyprogramme: req.params.id,
|
||||
combinedProgramme,
|
||||
settings: {
|
||||
isAcademicYear: yearType === 'ACADEMIC_YEAR',
|
||||
includeAllSpecials: specialGroups === 'SPECIAL_INCLUDED',
|
||||
@@ -62,13 +79,15 @@ router.get('/v2/studyprogrammes/:id/graduationstats', async (req, res) => {
|
||||
const code = req.params.id
|
||||
const yearType = req.query?.year_type
|
||||
const specialGroups = req.query?.special_groups
|
||||
const combinedProgramme = req.query?.combined_programme
|
||||
|
||||
if (!code) return res.status(422).end()
|
||||
|
||||
const data = await getGraduationStats(code, yearType, specialGroups)
|
||||
const data = await getGraduationStats(code, combinedProgramme, yearType, specialGroups)
|
||||
if (data) return res.json(data)
|
||||
const updatedStats = await getGraduationStatsForStudytrack({
|
||||
studyprogramme: req.params.id,
|
||||
combinedProgramme,
|
||||
settings: {
|
||||
isAcademicYear: yearType === 'ACADEMIC_YEAR',
|
||||
includeAllSpecials: specialGroups === 'SPECIAL_INCLUDED',
|
||||
@@ -81,14 +100,16 @@ router.get('/v2/studyprogrammes/:id/graduationstats', async (req, res) => {
|
||||
router.get('/v2/studyprogrammes/:id/coursestats', async (req, res) => {
|
||||
const code = req.params.id
|
||||
const showByYear = req.query.academicyear
|
||||
const combinedProgramme = req.query?.combined_programme
|
||||
const date = new Date()
|
||||
date.setHours(23, 59, 59, 999)
|
||||
|
||||
logInfoForGrafana(code, combinedProgramme)
|
||||
try {
|
||||
const data = await getStudyprogrammeCoursesForStudytrack(date.getTime(), code, showByYear)
|
||||
const data = await getStudyprogrammeCoursesForStudytrack(date.getTime(), code, showByYear, combinedProgramme)
|
||||
return res.json(data)
|
||||
} catch (e) {
|
||||
logger.error(`Failed to get code ${code} programme courses stats: ${e}`)
|
||||
logger.error({ message: `Failed to get code ${code} programme courses stats`, meta: `${e}` })
|
||||
}
|
||||
})
|
||||
|
||||
@@ -96,18 +117,23 @@ router.get('/v2/studyprogrammes/:id/studytrackstats', async (req, res) => {
|
||||
const code = req.params.id
|
||||
const graduated = req.query?.graduated
|
||||
const specialGroups = req.query?.special_groups
|
||||
const combinedProgramme = req.query?.combined_programme
|
||||
|
||||
if (!code) return res.status(422).end()
|
||||
|
||||
const data = await getStudytrackStats(code, graduated, specialGroups)
|
||||
logInfoForGrafana(code, combinedProgramme)
|
||||
const data = await getStudytrackStats(code, combinedProgramme, graduated, specialGroups)
|
||||
if (data) return res.json(data)
|
||||
|
||||
const associations = await getAssociations()
|
||||
const updated = await getStudytrackStatsForStudyprogramme({
|
||||
studyprogramme: code,
|
||||
combinedProgramme,
|
||||
settings: {
|
||||
graduated: graduated === 'GRADUATED_INCLUDED',
|
||||
specialGroups: specialGroups === 'SPECIAL_INCLUDED',
|
||||
},
|
||||
associations,
|
||||
})
|
||||
if (updated) await setStudytrackStats(updated, graduated, specialGroups)
|
||||
return res.json(updated)
|
||||
@@ -115,12 +141,13 @@ router.get('/v2/studyprogrammes/:id/studytrackstats', async (req, res) => {
|
||||
|
||||
router.get('/v2/studyprogrammes/:id/update_basicview', async (req, res) => {
|
||||
const code = req.params.id
|
||||
const combinedProgramme = req.query?.combined_programme
|
||||
if (code) {
|
||||
let result = null
|
||||
try {
|
||||
result = await updateBasicView(code)
|
||||
result = await updateBasicView(code, combinedProgramme)
|
||||
} catch (e) {
|
||||
logger.error(`Failed to update code ${code} basic stats: ${e}`)
|
||||
logger.error({ message: `Failed to update code ${code} ${combinedProgramme} basic stats`, meta: `${e}` })
|
||||
}
|
||||
return res.json(result)
|
||||
} else {
|
||||
@@ -130,12 +157,14 @@ router.get('/v2/studyprogrammes/:id/update_basicview', async (req, res) => {
|
||||
|
||||
router.get('/v2/studyprogrammes/:id/update_studytrackview', async (req, res) => {
|
||||
const code = req.params.id
|
||||
const combinedProgramme = req.query?.combined_programme
|
||||
if (code) {
|
||||
let result = null
|
||||
try {
|
||||
result = await updateStudytrackView(code)
|
||||
const associations = await getAssociations()
|
||||
result = await updateStudytrackView(code, combinedProgramme, associations)
|
||||
} catch (e) {
|
||||
logger.error(`Failed to update code ${code} studytrack stats: ${e}`)
|
||||
logger.error({ message: `Failed to update code ${code} ${combinedProgramme} studytrack stats`, meta: `${e}` })
|
||||
}
|
||||
return res.json(result)
|
||||
} else {
|
||||
@@ -143,4 +172,66 @@ router.get('/v2/studyprogrammes/:id/update_studytrackview', async (req, res) =>
|
||||
}
|
||||
})
|
||||
|
||||
router.get('/v2/studyprogrammes/:id/evaluationstats', async (req, res) => {
|
||||
const code = req.params.id
|
||||
const yearType = req.query?.year_type
|
||||
const specialGroups = req.query?.special_groups
|
||||
const graduated = req.query?.graduated
|
||||
|
||||
if (!code) return res.status(422).end()
|
||||
|
||||
// Statistics for Tilannekuvalomake view
|
||||
const combinedProgramme = ''
|
||||
let gradData = await getGraduationStats(code, combinedProgramme, yearType, specialGroups)
|
||||
if (!gradData) {
|
||||
const updatedStats = await getGraduationStatsForStudytrack({
|
||||
studyprogramme: req.params.id,
|
||||
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 associations = await getAssociations()
|
||||
const updated = await getStudytrackStatsForStudyprogramme({
|
||||
studyprogramme: code,
|
||||
combinedProgramme,
|
||||
settings: {
|
||||
graduated: graduated === 'GRADUATED_INCLUDED',
|
||||
specialGroups: specialGroups === 'SPECIAL_INCLUDED',
|
||||
},
|
||||
associations,
|
||||
})
|
||||
if (updated) {
|
||||
await setStudytrackStats(updated, graduated, specialGroups)
|
||||
progressData = updated
|
||||
}
|
||||
}
|
||||
|
||||
const programmeName = await getProgrammeName(code)
|
||||
|
||||
delete gradData.tableStats
|
||||
delete gradData.graphStats
|
||||
delete gradData.titles
|
||||
const data = {
|
||||
id: code,
|
||||
programmeName: programmeName?.name,
|
||||
status: gradData?.status,
|
||||
lastUpdated: gradData.lastUpdated,
|
||||
graduations: gradData,
|
||||
creditCounts: progressData?.creditCounts,
|
||||
years: progressData?.years,
|
||||
}
|
||||
|
||||
return res.json(data)
|
||||
})
|
||||
|
||||
module.exports = router
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
const {
|
||||
getCriteria,
|
||||
saveYearlyCourseCriteria,
|
||||
saveYearlyCreditCriteria,
|
||||
} = require('../services/studyProgrammeCriteria')
|
||||
|
||||
const router = require('express').Router()
|
||||
|
||||
router.get('/', async (req, res) => {
|
||||
const studyProgramme = req.query?.programmecode
|
||||
if (studyProgramme !== '' && !studyProgramme) return res.status(422).end()
|
||||
const studyProgrammeCriteria = await getCriteria(studyProgramme)
|
||||
return res.json(studyProgrammeCriteria)
|
||||
})
|
||||
|
||||
router.post('/courses', async (req, res) => {
|
||||
const { code, courses, year } = req.body
|
||||
if (!code || !courses || !year) return res.status(400).end()
|
||||
const studyProgrammeCriteria = await saveYearlyCourseCriteria(code, courses, year)
|
||||
return res.json(studyProgrammeCriteria)
|
||||
})
|
||||
|
||||
router.post('/credits', async (req, res) => {
|
||||
const { code, credits } = req.body
|
||||
if (!code || !credits) return res.status(400).end()
|
||||
const studyProgrammeCriteria = await saveYearlyCreditCriteria(code, credits)
|
||||
return res.json(studyProgrammeCriteria)
|
||||
})
|
||||
|
||||
module.exports = router
|
||||
@@ -22,8 +22,9 @@ router.get('/tags/:studytrack', async (req, res) => {
|
||||
const {
|
||||
user: { rights, roles, id },
|
||||
} = req
|
||||
|
||||
if (!rights.includes(studytrack) && !roles?.includes('admin')) return res.status(403).end()
|
||||
const programmeCodes = studytrack.includes('KH') && studytrack.includes('MH') ? studytrack.split('-') : [studytrack]
|
||||
if (!rights.includes(programmeCodes[0]) && !rights.includes(programmeCodes[1]) && !roles?.includes('admin'))
|
||||
return res.status(403).end()
|
||||
|
||||
const tags = await Tags.findTagsByStudytrack(studytrack)
|
||||
res.status(200).json(filterRelevantTags(tags, id))
|
||||
@@ -37,7 +38,9 @@ router.post('/tags', async (req, res) => {
|
||||
user: { rights, roles, id },
|
||||
} = req
|
||||
|
||||
if (!rights.includes(studytrack) && !roles?.includes('admin')) return res.status(403).end()
|
||||
const programmeCodes = studytrack.includes('KH') && studytrack.includes('MH') ? studytrack.split('-') : [studytrack]
|
||||
if (!rights.includes(programmeCodes[0]) && !rights.includes(programmeCodes[1]) && !roles?.includes('admin'))
|
||||
return res.status(403).end()
|
||||
|
||||
await Tags.createNewTag({ studytrack, tagname, year, personal_user_id })
|
||||
const tags = await Tags.findTagsByStudytrack(studytrack)
|
||||
@@ -50,7 +53,15 @@ router.delete('/tags', async (req, res) => {
|
||||
user: { rights, roles, id },
|
||||
} = req
|
||||
|
||||
if (!rights.includes(tag.studytrack) && !roles?.includes('admin') && !(tag.personal_user_id === id))
|
||||
const programmeCodes =
|
||||
tag.studytrack.includes('KH') && tag.studytrack.includes('MH') ? tag.studytrack.split('-') : [tag.studytrack]
|
||||
|
||||
if (
|
||||
!rights.includes(programmeCodes[0]) &&
|
||||
!rights.includes(programmeCodes[1]) &&
|
||||
!roles?.includes('admin') &&
|
||||
!(tag.personal_user_id === id)
|
||||
)
|
||||
return res.status(403).end()
|
||||
|
||||
await Tags.deleteTag(tag)
|
||||
@@ -69,7 +80,9 @@ router.get('/studenttags/:studytrack', async (req, res) => {
|
||||
user: { rights, roles, id },
|
||||
} = req
|
||||
|
||||
if (!rights.includes(studytrack) && !roles?.includes('admin')) return res.status(403).end()
|
||||
const programmeCodes = studytrack.includes('KH') && studytrack.includes('MH') ? studytrack.split('-') : [studytrack]
|
||||
if (!rights.includes(programmeCodes[0]) && !rights.includes(programmeCodes[1]) && !roles?.includes('admin'))
|
||||
return res.status(403).end()
|
||||
|
||||
const result = await TagStudent.getStudentTagsByStudytrack(studytrack)
|
||||
res.status(200).json(filterRelevantStudentTags(result, id))
|
||||
@@ -83,33 +96,38 @@ router.get('/studenttags/:studentnumber', async (req, res) => {
|
||||
})
|
||||
|
||||
router.post('/studenttags/:studentnumber', async (req, res) => {
|
||||
const { tag, studytrack } = req.body
|
||||
const { tag, studytrack, combinedProgramme } = req.body
|
||||
const {
|
||||
user: { roles, rights, id },
|
||||
} = req
|
||||
|
||||
if (!rights.includes(studytrack) && !roles?.includes('admin')) return res.status(403).end()
|
||||
if (!rights.includes(studytrack) && !rights.includes(combinedProgramme) && !roles?.includes('admin'))
|
||||
return res.status(403).end()
|
||||
|
||||
await TagStudent.createStudentTag(tag)
|
||||
const result = await TagStudent.getStudentTagsByStudytrack(studytrack)
|
||||
const studytrackCode = combinedProgramme ? `${studytrack}-${combinedProgramme}` : studytrack
|
||||
const result = await TagStudent.getStudentTagsByStudytrack(studytrackCode)
|
||||
res.status(200).json(filterRelevantStudentTags(result, id))
|
||||
})
|
||||
|
||||
router.post('/studenttags', async (req, res) => {
|
||||
const { tags, studytrack } = req.body
|
||||
const { tags, studytrack, combinedProgramme } = req.body
|
||||
const {
|
||||
user: { rights, roles, id },
|
||||
} = req
|
||||
|
||||
if (!rights.includes(studytrack) && !roles?.includes('admin')) return res.status(403).end()
|
||||
if (!rights.includes(studytrack) && !rights.includes(combinedProgramme) && !roles?.includes('admin'))
|
||||
return res.status(403).end()
|
||||
|
||||
const existingtags = await Tags.findTagsByStudytrack(studytrack)
|
||||
const existingTagids = existingtags.map(t => t.tag_id)
|
||||
const studytrackCode = combinedProgramme ? `${studytrack}-${combinedProgramme}` : studytrack
|
||||
const existingTags = await Tags.findTagsByStudytrack(studytrackCode)
|
||||
const existingTagids = existingTags.map(t => t.tag_id)
|
||||
const tagids = [...new Set(tags.map(t => t.tag_id))]
|
||||
if (!tagids.find(t => existingTagids.includes(t))) return res.status(400).json({ error: 'The tag does not exist' })
|
||||
|
||||
const studentnumbers = tags.map(t => t.studentnumber)
|
||||
const students = await Students.filterStudentnumbersByAccessrights(studentnumbers, [studytrack])
|
||||
const studentFromProgrammes = combinedProgramme ? [studytrack, combinedProgramme] : [studytrack]
|
||||
const students = await Students.filterStudentnumbersByAccessrights(studentnumbers, studentFromProgrammes)
|
||||
const missingStudents = difference(studentnumbers, students)
|
||||
if (missingStudents.length !== 0)
|
||||
return res
|
||||
@@ -117,39 +135,41 @@ router.post('/studenttags', async (req, res) => {
|
||||
.json({ error: `Could not find the following students from the programme: ${missingStudents.join(', ')}` })
|
||||
|
||||
await TagStudent.createMultipleStudentTags(tags)
|
||||
const result = await TagStudent.getStudentTagsByStudytrack(studytrack)
|
||||
const result = await TagStudent.getStudentTagsByStudytrack(studytrackCode)
|
||||
res.status(200).json(filterRelevantStudentTags(result, id))
|
||||
})
|
||||
|
||||
router.delete('/studenttags/delete_one', async (req, res) => {
|
||||
const { tag_id, studentnumber, studytrack } = req.body
|
||||
const { tag_id, studentnumber, studytrack, combinedProgramme } = req.body
|
||||
const {
|
||||
user: { rights, roles, id },
|
||||
} = req
|
||||
|
||||
if (!rights.includes(studytrack) && !roles?.includes('admin')) return res.status(403).end()
|
||||
|
||||
const tags = await Tags.findTagsFromStudytrackById(studytrack, [tag_id])
|
||||
if (!rights.includes(studytrack) && !rights.includes(combinedProgramme) && !roles?.includes('admin'))
|
||||
return res.status(403).end()
|
||||
const studytrackCode = combinedProgramme ? `${studytrack}-${combinedProgramme}` : studytrack
|
||||
const tags = await Tags.findTagsFromStudytrackById(studytrackCode, [tag_id])
|
||||
if (tags.length === 0) return res.status(403).json({ error: 'The tag does not exist' })
|
||||
|
||||
await TagStudent.deleteStudentTag(studentnumber, tag_id)
|
||||
const result = await TagStudent.getStudentTagsByStudytrack(studytrack)
|
||||
const result = await TagStudent.getStudentTagsByStudytrack(studytrackCode)
|
||||
res.status(200).json(filterRelevantStudentTags(result, id))
|
||||
})
|
||||
|
||||
router.delete('/studenttags/delete_many', async (req, res) => {
|
||||
const { tagId, studentnumbers, studytrack } = req.body
|
||||
const { tagId, studentnumbers, studytrack, combinedProgramme } = req.body
|
||||
const {
|
||||
user: { rights, roles, id },
|
||||
} = req
|
||||
|
||||
if (!rights.includes(studytrack) && !roles?.includes('admin')) return res.status(403).end()
|
||||
|
||||
const tags = await Tags.findTagsFromStudytrackById(studytrack, [tagId])
|
||||
if (!rights.includes(studytrack) && !rights.includes(combinedProgramme) && !roles?.includes('admin'))
|
||||
return res.status(403).end()
|
||||
const studytrackCode = combinedProgramme ? `${studytrack}-${combinedProgramme}` : studytrack
|
||||
const tags = await Tags.findTagsFromStudytrackById(studytrackCode, [tagId])
|
||||
if (tags.length === 0) return res.status(403).json({ error: 'The tag does not exist' })
|
||||
|
||||
await TagStudent.deleteMultipleStudentTags(tagId, studentnumbers)
|
||||
const result = await TagStudent.getStudentTagsByStudytrack(studytrack)
|
||||
const result = await TagStudent.getStudentTagsByStudytrack(studytrackCode)
|
||||
res.status(200).json(filterRelevantStudentTags(result, id))
|
||||
})
|
||||
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
const Sentry = require('@sentry/node')
|
||||
const router = require('express').Router()
|
||||
const tsaService = require('../services/tsaService')
|
||||
|
||||
router.post('/event', (req, res) => {
|
||||
const { group, name, label, value } = req.body
|
||||
|
||||
if (!group || !name || !value) {
|
||||
return res.status(400).json({ error: 'group, name and value are required' })
|
||||
}
|
||||
|
||||
const {
|
||||
user: { userId },
|
||||
} = req
|
||||
|
||||
// don't await here because frontend doesn't care if it succeeds
|
||||
tsaService.sendTsaEvent(userId, { group, name, label, value }).catch(e => {
|
||||
Sentry.withScope(scope => {
|
||||
// if error was caused by a 400, dig out the JSON response for Sentry
|
||||
if (e.isAxiosError && e.response && e.response.data) {
|
||||
scope.setExtra({ error: e.response.data })
|
||||
}
|
||||
|
||||
Sentry.captureException(e)
|
||||
})
|
||||
})
|
||||
|
||||
res.status(200).end()
|
||||
})
|
||||
|
||||
module.exports = router
|
||||
@@ -8,7 +8,7 @@ const {
|
||||
abort,
|
||||
updateCoursesByCourseCode,
|
||||
} = require('../services/sisUpdaterService')
|
||||
const { refreshStatistics, refreshTrends } = require('../events')
|
||||
const { refreshStatistics, refreshTrends, refreshFaculties, refreshNewOverviews } = require('../events')
|
||||
|
||||
router.get('/update/v2/meta', async (req, res) => {
|
||||
const response = await updateSISMetadata(req)
|
||||
@@ -54,12 +54,22 @@ router.get('/refresh_redis_cache', async (req, res) => {
|
||||
|
||||
router.post('/refresh_statistic_v2', async (req, res) => {
|
||||
await refreshStatistics()
|
||||
res.status(200).json('Refreshing sis statistics')
|
||||
res.status(200).json('Teacher and study programme statistics refreshed')
|
||||
})
|
||||
|
||||
router.post('/refresh_study_programmes_v2', async (req, res) => {
|
||||
await refreshNewOverviews()
|
||||
res.status(200).json('New study programme overviews refreshed')
|
||||
})
|
||||
|
||||
router.post('/refresh_faculties_v2', async (req, res) => {
|
||||
await refreshFaculties()
|
||||
res.status(200).json('Faculties refreshed')
|
||||
})
|
||||
|
||||
router.post('/refresh_trends', async (req, res) => {
|
||||
await refreshTrends()
|
||||
res.status(200).json('Refreshing trends')
|
||||
res.status(200).json('Trends refreshed')
|
||||
})
|
||||
|
||||
router.get('/abort', async (req, res) => {
|
||||
|
||||
@@ -10,9 +10,10 @@ const createRedisKeyForGraduationStats = (id, yearType, specialGroups) =>
|
||||
const createRedisKeyForStudytrackStats = (id, graduated, specialGroups) =>
|
||||
`STUDYTRACK_STATS_${id}_${graduated}_${specialGroups}`
|
||||
|
||||
const getBasicStats = async (id, yearType, specialGroups) => {
|
||||
const getBasicStats = async (id, combinedProgramme, yearType, specialGroups) => {
|
||||
if (!isUpdatedNewProgramme(id)) return null
|
||||
const redisKey = createRedisKeyForBasicStats(id, yearType, specialGroups)
|
||||
let searchkey = combinedProgramme ? `${id}-${combinedProgramme}` : id
|
||||
const redisKey = createRedisKeyForBasicStats(searchkey, yearType, specialGroups)
|
||||
const dataFromRedis = await redisClient.getAsync(redisKey)
|
||||
if (!dataFromRedis) return null
|
||||
return JSON.parse(dataFromRedis)
|
||||
@@ -32,9 +33,10 @@ const setBasicStats = async (data, yearType, specialGroups) => {
|
||||
return dataToRedis
|
||||
}
|
||||
|
||||
const getCreditStats = async (id, yearType, specialGroups) => {
|
||||
const getCreditStats = async (id, combinedProgramme, yearType, specialGroups) => {
|
||||
if (!isUpdatedNewProgramme(id)) return null
|
||||
const redisKey = createRedisKeyForCreditStats(id, yearType, specialGroups)
|
||||
let searchkey = combinedProgramme ? `${id}-${combinedProgramme}` : id
|
||||
const redisKey = createRedisKeyForCreditStats(searchkey, yearType, specialGroups)
|
||||
const dataFromRedis = await redisClient.getAsync(redisKey)
|
||||
if (!dataFromRedis) return null
|
||||
return JSON.parse(dataFromRedis)
|
||||
@@ -54,9 +56,10 @@ const setCreditStats = async (data, yearType, specialGroups) => {
|
||||
return dataToRedis
|
||||
}
|
||||
|
||||
const getGraduationStats = async (id, yearType, specialGroups) => {
|
||||
const getGraduationStats = async (id, combinedProgramme, yearType, specialGroups) => {
|
||||
if (!isUpdatedNewProgramme(id)) return null
|
||||
const redisKey = createRedisKeyForGraduationStats(id, yearType, specialGroups)
|
||||
let searchkey = combinedProgramme ? `${id}-${combinedProgramme}` : id
|
||||
const redisKey = createRedisKeyForGraduationStats(searchkey, yearType, specialGroups)
|
||||
const dataFromRedis = await redisClient.getAsync(redisKey)
|
||||
if (!dataFromRedis) return null
|
||||
return JSON.parse(dataFromRedis)
|
||||
@@ -76,9 +79,10 @@ const setGraduationStats = async (data, yearType, specialGroups) => {
|
||||
return dataToRedis
|
||||
}
|
||||
|
||||
const getStudytrackStats = async (id, graduated, specialGroups) => {
|
||||
const getStudytrackStats = async (id, combinedProgramme, graduated, specialGroups) => {
|
||||
if (!isUpdatedNewProgramme(id)) return null
|
||||
const redisKey = createRedisKeyForStudytrackStats(id, graduated, specialGroups)
|
||||
let searchkey = combinedProgramme ? `${id}-${combinedProgramme}` : id
|
||||
const redisKey = createRedisKeyForStudytrackStats(searchkey, graduated, specialGroups)
|
||||
const dataFromRedis = await redisClient.getAsync(redisKey)
|
||||
if (!dataFromRedis) return null
|
||||
return JSON.parse(dataFromRedis)
|
||||
|
||||
@@ -0,0 +1,164 @@
|
||||
const { Credit, Enrollment, Student, Course } = require('../models')
|
||||
const { Op } = require('sequelize')
|
||||
const _ = require('lodash')
|
||||
|
||||
const getCompletedCourses = async (studentNumbers, courseCodes) => {
|
||||
let courses = await Course.findAll({
|
||||
attributes: ['code', 'name', 'substitutions'],
|
||||
where: {
|
||||
code: {
|
||||
[Op.in]: courseCodes,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
let fullCourseCodes = [
|
||||
...courseCodes,
|
||||
...new Set([...courses.reduce((acc, course) => [...acc, course.code, ...course.substitutions], [])]),
|
||||
]
|
||||
|
||||
let credits = await Credit.findAll({
|
||||
attributes: ['course_code', 'student_studentnumber', 'credittypecode', 'attainment_date'],
|
||||
where: {
|
||||
course_code: {
|
||||
[Op.in]: fullCourseCodes,
|
||||
},
|
||||
student_studentnumber: {
|
||||
[Op.in]: studentNumbers,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
let enrollments = await Enrollment.findAll({
|
||||
attributes: ['course_code', 'enrollment_date_time', 'studentnumber', 'state'],
|
||||
where: {
|
||||
course_code: {
|
||||
[Op.in]: fullCourseCodes,
|
||||
},
|
||||
studentnumber: {
|
||||
[Op.in]: studentNumbers,
|
||||
},
|
||||
state: {
|
||||
[Op.eq]: 'ENROLLED',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const studentInfo = await Student.findAll({
|
||||
attributes: ['studentnumber', 'firstnames', 'lastname', 'email', 'sis_person_id'],
|
||||
where: {
|
||||
studentnumber: {
|
||||
[Op.in]: studentNumbers,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
courses = courses.map(course => {
|
||||
const { code, name, substitutions } = course
|
||||
return { code, name, substitutions }
|
||||
})
|
||||
|
||||
credits = credits.map(credit => {
|
||||
const { course_code, student_studentnumber, credittypecode, attainment_date } = credit
|
||||
const originalCode = courseCodes.includes(course_code)
|
||||
? null
|
||||
: courses.find(course => course.substitutions.includes(course_code))?.code
|
||||
return {
|
||||
courseCode: originalCode ? originalCode : course_code,
|
||||
substitution: originalCode ? course_code : null,
|
||||
studentNumber: student_studentnumber,
|
||||
creditType: credittypecode,
|
||||
date: attainment_date,
|
||||
}
|
||||
})
|
||||
|
||||
enrollments = enrollments.map(enrollment => {
|
||||
const originalCode = courses.find(course => course.substitutions.includes(enrollment.course_code))?.code
|
||||
return {
|
||||
courseCode: originalCode ? originalCode : enrollment.course_code,
|
||||
substitution: originalCode ? enrollment.course_code : null,
|
||||
studentNumber: enrollment.studentnumber,
|
||||
date: enrollment.enrollment_date_time,
|
||||
}
|
||||
})
|
||||
|
||||
const studentCredits = {}
|
||||
|
||||
studentInfo.forEach(s => {
|
||||
if (!studentCredits[s.studentnumber]) {
|
||||
studentCredits[s.studentnumber] = { credits: [], enrollments: [] }
|
||||
}
|
||||
studentCredits[s.studentnumber].firstnames = s.firstnames
|
||||
studentCredits[s.studentnumber].lastname = s.lastname
|
||||
studentCredits[s.studentnumber].email = s.email
|
||||
studentCredits[s.studentnumber].sis_person_id = s.sis_person_id
|
||||
})
|
||||
|
||||
credits.forEach(credit => {
|
||||
if (credit.creditType === 10) {
|
||||
return
|
||||
}
|
||||
const previous = studentCredits[credit.studentNumber].credits?.find(c => credit.courseCode === c.courseCode)
|
||||
if (previous && previous.date > credit.date) {
|
||||
return
|
||||
} else if (previous) {
|
||||
studentCredits[credit.studentNumber].credits = studentCredits[credit.studentNumber].credits.filter(
|
||||
c => credit.courseCode !== c.courseCode
|
||||
)
|
||||
}
|
||||
studentCredits[credit.studentNumber].credits.push({
|
||||
date: credit.date,
|
||||
courseCode: credit.courseCode,
|
||||
creditType: credit.creditType,
|
||||
substitution: credit.substitution,
|
||||
})
|
||||
})
|
||||
|
||||
enrollments.forEach(enrollment => {
|
||||
if (
|
||||
credits.find(
|
||||
credit => credit.courseCode === enrollment.courseCode && credit.studentNumber === enrollment.studentNumber
|
||||
)
|
||||
) {
|
||||
return
|
||||
}
|
||||
studentCredits[enrollment.studentNumber].enrollments.push({
|
||||
date: enrollment.date,
|
||||
courseCode: enrollment.courseCode,
|
||||
state: enrollment.state,
|
||||
})
|
||||
})
|
||||
|
||||
const students = Object.keys(studentCredits).reduce(
|
||||
(acc, student) => [
|
||||
...acc,
|
||||
{
|
||||
studentNumber: student,
|
||||
sis_person_id: studentCredits[student].sis_person_id,
|
||||
credits: studentCredits[student].credits,
|
||||
enrollments: studentCredits[student].enrollments,
|
||||
firstnames: studentCredits[student].firstnames,
|
||||
lastname: studentCredits[student].lastname,
|
||||
email: studentCredits[student].email,
|
||||
},
|
||||
],
|
||||
[]
|
||||
)
|
||||
|
||||
students.forEach(student => {
|
||||
courseCodes.forEach(courseCode => {
|
||||
let [hits, rest] = _.partition(student.enrollments, e => e.courseCode === courseCode)
|
||||
if (hits?.length === 0) return
|
||||
let latest = hits[0]
|
||||
for (let hit of hits) {
|
||||
if (hit > latest) {
|
||||
latest = hit
|
||||
}
|
||||
}
|
||||
student.enrollments = [latest, ...rest]
|
||||
})
|
||||
})
|
||||
return { students, courses }
|
||||
}
|
||||
|
||||
module.exports = { getCompletedCourses }
|
||||
@@ -26,7 +26,9 @@ class CourseStatsCounter {
|
||||
failedMany: {},
|
||||
improvedPassedGrade: {},
|
||||
markedToSemester: {},
|
||||
enrolledNoGrade: {},
|
||||
}
|
||||
|
||||
this.stats = {
|
||||
students: 0,
|
||||
passed: 0,
|
||||
@@ -38,10 +40,10 @@ class CourseStatsCounter {
|
||||
percentage: undefined,
|
||||
passedOfPopulation: undefined,
|
||||
triedOfPopulation: undefined,
|
||||
perStudent: undefined,
|
||||
passingSemesters: this.initializePassingSemesters(),
|
||||
}
|
||||
this.enrollments = {
|
||||
enrolledNoGrade: new Set(),
|
||||
semesters: this.initializePassingSemesters({}),
|
||||
}
|
||||
this.grades = {}
|
||||
@@ -57,7 +59,6 @@ class CourseStatsCounter {
|
||||
passingSemesters[fall[i]] = initialValue
|
||||
passingSemesters[spring[i]] = initialValue
|
||||
}
|
||||
|
||||
return passingSemesters
|
||||
}
|
||||
|
||||
@@ -184,11 +185,20 @@ class CourseStatsCounter {
|
||||
getFinalStats() {
|
||||
const stats = this.stats
|
||||
const students = this.students
|
||||
const allStudents = Object.keys(students.all)
|
||||
const totalEnrolledNoGrade = [...(this.enrollments.ENROLLED || [])].filter(
|
||||
student => !allStudents.includes(student)
|
||||
).length
|
||||
const studs = this.enrollments
|
||||
? Object.keys(this.enrollments)
|
||||
.filter(key => key !== 'ENROLLED' && key !== 'semesters')
|
||||
.reduce((acc, key) => [...acc, ...[...this.enrollments[key]].map(studentnumber => studentnumber)], [])
|
||||
: []
|
||||
const allStudents = Object.keys(students.all).map(student => student)
|
||||
const fileredEnrolledNoGrade = this.enrollments['ENROLLED']
|
||||
? [...this.enrollments['ENROLLED']]
|
||||
.filter(student => !studs.includes(student) && !allStudents.includes(student))
|
||||
.reduce((acc, student) => ({ ...acc, [student]: true }), {})
|
||||
: {}
|
||||
|
||||
students.all = { ...students.all, ...fileredEnrolledNoGrade }
|
||||
students.enrolledNoGrade = fileredEnrolledNoGrade
|
||||
stats.students = lengthOf(students.all)
|
||||
stats.passed = lengthOf(students.passed)
|
||||
stats.failed = lengthOf(students.failed)
|
||||
@@ -200,10 +210,9 @@ class CourseStatsCounter {
|
||||
stats.triedOfPopulation = percentageOf(stats.students, this.studentsInTotal)
|
||||
stats.perStudent = stats.attempts / (stats.passed + stats.failed)
|
||||
stats.passingSemestersCumulative = this.getPassingSemestersCumulative()
|
||||
stats.totalStudents = stats.students + totalEnrolledNoGrade
|
||||
stats.totalEnrolledNoGrade = totalEnrolledNoGrade
|
||||
stats.totalStudents = stats.students
|
||||
stats.totalEnrolledNoGrade = lengthOf(fileredEnrolledNoGrade)
|
||||
stats.percentageWithEnrollments = percentageOf(stats.passed, stats.totalStudents)
|
||||
|
||||
return {
|
||||
stats,
|
||||
students,
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
const Sequelize = require('sequelize')
|
||||
const { ExcludedCourse } = require('../models/models_kone')
|
||||
const { Op } = Sequelize
|
||||
|
||||
const addExcludedCourses = async (programmecode, coursecodes, curriculum) => {
|
||||
return ExcludedCourse.bulkCreate(
|
||||
coursecodes.map(c => ({ programme_code: programmecode, curriculum_version: curriculum, course_code: c }))
|
||||
)
|
||||
}
|
||||
|
||||
const removeExcludedCourses = async ({ programmeCode, curriculumVersion, courseCodes }) => {
|
||||
return ExcludedCourse.destroy({
|
||||
where: {
|
||||
programme_code: programmeCode,
|
||||
curriculum_version: curriculumVersion,
|
||||
course_code: { [Op.in]: courseCodes },
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = { addExcludedCourses, removeExcludedCourses }
|
||||
@@ -4,10 +4,9 @@ const {
|
||||
ElementDetail,
|
||||
Organization,
|
||||
ProgrammeModule,
|
||||
Semester,
|
||||
SemesterEnrollment,
|
||||
Studyright,
|
||||
StudyrightElement,
|
||||
SemesterEnrollment,
|
||||
Student,
|
||||
Transfer,
|
||||
Credit,
|
||||
@@ -19,36 +18,59 @@ const {
|
||||
formatFacultyTransfer,
|
||||
formatFacultyThesisWriter,
|
||||
formatOrganization,
|
||||
formatAbsence,
|
||||
isNewProgramme,
|
||||
mapCodesToIds,
|
||||
facultyProgrammeStudents,
|
||||
} = require('./facultyHelpers')
|
||||
|
||||
const {
|
||||
dbConnections: { sequelize },
|
||||
} = require('../../database/connection')
|
||||
const getTransferredToAndAway = async (programmeCodes, allProgrammeCodes, since) => {
|
||||
const awayTransfers = (await transferredAway(programmeCodes, allProgrammeCodes, since)).map(t => t.studyrightid)
|
||||
const toTransfers = (await transferredTo(programmeCodes, allProgrammeCodes, since)).map(t => t.studyrightid)
|
||||
const transferredToOrAway = [...toTransfers, ...awayTransfers]
|
||||
return transferredToOrAway
|
||||
const awayTransfers = await transferredAway(programmeCodes, allProgrammeCodes, since)
|
||||
const toTransfers = await transferredTo(programmeCodes, allProgrammeCodes, since)
|
||||
return [...toTransfers, ...awayTransfers]
|
||||
}
|
||||
|
||||
const getTransferredInside = async (programmeCodes, allProgrammeCodes, since) => {
|
||||
const transfersInside = (await transferredInsideFaculty(programmeCodes, allProgrammeCodes, since)).map(
|
||||
t => t.studyrightid
|
||||
)
|
||||
return transfersInside
|
||||
return await transferredInsideFaculty(programmeCodes, allProgrammeCodes, since)
|
||||
}
|
||||
const startedStudyrights = async (faculty, since, studyRightWhere) =>
|
||||
|
||||
const getTransfersOut = async (programmeCode, start, end) => {
|
||||
return await transferredFaculty([], [programmeCode], start, end)
|
||||
}
|
||||
|
||||
const getTransfersIn = async (programmeCode, start, end) => {
|
||||
return await transferredFaculty([programmeCode], [], start, end)
|
||||
}
|
||||
const transferredFaculty = async (programmeCodeIn, programmeCodeOut, start, end) =>
|
||||
(
|
||||
await Transfer.findAll({
|
||||
where: {
|
||||
transferdate: {
|
||||
[Op.between]: [start, end],
|
||||
},
|
||||
[Op.or]: [
|
||||
{
|
||||
sourcecode: programmeCodeOut,
|
||||
},
|
||||
{
|
||||
targetcode: programmeCodeIn,
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
).map(formatFacultyTransfer)
|
||||
|
||||
const startedStudyrights = async (faculty, code, since, studyRightWhere) =>
|
||||
(
|
||||
await Studyright.findAll({
|
||||
include: [
|
||||
{
|
||||
model: Student,
|
||||
attributes: ['studentnumber'],
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
model: StudyrightElement,
|
||||
required: true,
|
||||
where: {
|
||||
code: code,
|
||||
},
|
||||
include: {
|
||||
model: ElementDetail,
|
||||
required: true,
|
||||
@@ -56,28 +78,25 @@ const startedStudyrights = async (faculty, since, studyRightWhere) =>
|
||||
},
|
||||
],
|
||||
where: {
|
||||
faculty_code: faculty,
|
||||
studystartdate: {
|
||||
facultyCode: faculty,
|
||||
startdate: {
|
||||
[Op.gte]: since,
|
||||
},
|
||||
student_studentnumber: { [Op.not]: null },
|
||||
...studyRightWhere,
|
||||
},
|
||||
})
|
||||
).map(facultyFormatStudyright)
|
||||
|
||||
const graduatedStudyrights = async (faculty, since, studyrightWhere) =>
|
||||
const graduatedStudyrights = async (faculty, code, since, studyrightWhere) =>
|
||||
(
|
||||
await Studyright.findAll({
|
||||
include: [
|
||||
{
|
||||
model: Student,
|
||||
attributes: ['studentnumber'],
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
model: StudyrightElement,
|
||||
required: true,
|
||||
where: {
|
||||
code: code,
|
||||
},
|
||||
include: {
|
||||
model: ElementDetail,
|
||||
required: true,
|
||||
@@ -85,28 +104,26 @@ const graduatedStudyrights = async (faculty, since, studyrightWhere) =>
|
||||
},
|
||||
],
|
||||
where: {
|
||||
faculty_code: faculty,
|
||||
facultyCode: faculty,
|
||||
enddate: {
|
||||
[Op.gte]: since,
|
||||
},
|
||||
graduated: 1,
|
||||
student_studentnumber: { [Op.not]: null },
|
||||
...studyrightWhere,
|
||||
},
|
||||
})
|
||||
).map(facultyFormatStudyright)
|
||||
|
||||
const studyrightsByRightStartYear = async (faculty, since, graduated = 1) =>
|
||||
const studyrightsByRightStartYear = async (faculty, code, since, graduated = 1) =>
|
||||
(
|
||||
await Studyright.findAll({
|
||||
include: [
|
||||
{
|
||||
model: Student,
|
||||
attributes: ['studentnumber'],
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
model: StudyrightElement,
|
||||
attributes: ['code', 'startdate'],
|
||||
where: {
|
||||
code: code,
|
||||
},
|
||||
required: true,
|
||||
include: {
|
||||
model: ElementDetail,
|
||||
@@ -115,25 +132,98 @@ const studyrightsByRightStartYear = async (faculty, since, graduated = 1) =>
|
||||
},
|
||||
],
|
||||
where: {
|
||||
faculty_code: faculty,
|
||||
facultyCode: faculty,
|
||||
startdate: {
|
||||
[Op.gte]: since,
|
||||
},
|
||||
graduated: graduated,
|
||||
student_studentnumber: { [Op.not]: null },
|
||||
},
|
||||
})
|
||||
).map(facultyFormatStudyright)
|
||||
|
||||
const bachelorStudyright = async id => {
|
||||
return await Studyright.findOne({
|
||||
attributes: ['studystartdate'],
|
||||
where: {
|
||||
studyrightid: id,
|
||||
extentcode: 1,
|
||||
},
|
||||
})
|
||||
}
|
||||
const getStudyRightsByExtent = async (faculty, startDate, endDate, code, extents, graduated) =>
|
||||
(
|
||||
await Studyright.findAll({
|
||||
include: {
|
||||
model: StudyrightElement,
|
||||
attributes: [],
|
||||
required: true,
|
||||
where: {
|
||||
code: code,
|
||||
},
|
||||
include: {
|
||||
model: ElementDetail,
|
||||
attributes: [],
|
||||
},
|
||||
},
|
||||
group: [sequelize.col('studyright.studyrightid')],
|
||||
where: {
|
||||
facultyCode: faculty,
|
||||
extentcode: {
|
||||
[Op.in]: extents,
|
||||
},
|
||||
prioritycode: {
|
||||
[Op.not]: 6,
|
||||
},
|
||||
graduated: {
|
||||
[Op.in]: graduated,
|
||||
},
|
||||
[Op.and]: [
|
||||
sequelize.where(
|
||||
sequelize.fn(
|
||||
'GREATEST',
|
||||
sequelize.col('studyright_elements.startdate'),
|
||||
sequelize.col('studyright.startdate')
|
||||
),
|
||||
{
|
||||
[Op.between]: [startDate, endDate],
|
||||
}
|
||||
),
|
||||
],
|
||||
},
|
||||
})
|
||||
).map(facultyFormatStudyright)
|
||||
|
||||
const getStudyRightsByBachelorStart = async (faculty, startDate, endDate, code, extents, graduated) =>
|
||||
(
|
||||
await Studyright.findAll({
|
||||
include: {
|
||||
model: StudyrightElement,
|
||||
required: true,
|
||||
where: {
|
||||
code: code,
|
||||
},
|
||||
},
|
||||
where: {
|
||||
facultyCode: faculty,
|
||||
extentcode: {
|
||||
[Op.in]: extents,
|
||||
},
|
||||
prioritycode: {
|
||||
[Op.not]: 6,
|
||||
},
|
||||
graduated: {
|
||||
[Op.in]: graduated,
|
||||
},
|
||||
startdate: {
|
||||
[Op.between]: [startDate, endDate],
|
||||
},
|
||||
},
|
||||
})
|
||||
).map(facultyFormatStudyright)
|
||||
|
||||
const getStudentsByStudentnumbers = async studentnumbers =>
|
||||
(
|
||||
await Student.findAll({
|
||||
where: {
|
||||
studentnumber: studentnumbers,
|
||||
},
|
||||
include: {
|
||||
model: SemesterEnrollment,
|
||||
attributes: ['semestercode', 'enrollmenttype'],
|
||||
},
|
||||
})
|
||||
).map(facultyProgrammeStudents)
|
||||
|
||||
const hasMasterRight = async id => {
|
||||
return await Studyright.findOne({
|
||||
@@ -199,6 +289,7 @@ const degreeProgrammesOfFaculty = async facultyCode =>
|
||||
},
|
||||
},
|
||||
},
|
||||
group: ['programme_module.code', 'programme_module.name', 'organization.id'],
|
||||
})
|
||||
).map(facultyFormatProgramme)
|
||||
|
||||
@@ -256,29 +347,6 @@ const thesisWriters = async (provider, since, thesisTypes, students) =>
|
||||
})
|
||||
).map(formatFacultyThesisWriter)
|
||||
|
||||
const statutoryAbsences = async (studentnumber, startdate, enddate) =>
|
||||
(
|
||||
await SemesterEnrollment.findAll({
|
||||
attributes: ['semestercode'],
|
||||
include: {
|
||||
model: Semester,
|
||||
attributes: ['startdate', 'enddate'],
|
||||
where: {
|
||||
startdate: {
|
||||
[Op.gte]: startdate,
|
||||
},
|
||||
enddate: {
|
||||
[Op.lte]: enddate,
|
||||
},
|
||||
},
|
||||
},
|
||||
where: {
|
||||
studentnumber: studentnumber,
|
||||
statutory_absence: true,
|
||||
},
|
||||
})
|
||||
).map(formatAbsence)
|
||||
|
||||
// Some programme modules are not directly associated to a faculty (organization).
|
||||
// Some have intermediate organizations, such as department, so the connection must be digged up
|
||||
const findFacultyProgrammeCodes = async (faculty, programmeFilter) => {
|
||||
@@ -336,7 +404,6 @@ module.exports = {
|
||||
startedStudyrights,
|
||||
graduatedStudyrights,
|
||||
studyrightsByRightStartYear,
|
||||
bachelorStudyright,
|
||||
hasMasterRight,
|
||||
transferredInsideFaculty,
|
||||
transferredAway,
|
||||
@@ -345,7 +412,11 @@ module.exports = {
|
||||
thesisWriters,
|
||||
findFacultyProgrammeCodes,
|
||||
facultyOgranizationId,
|
||||
statutoryAbsences,
|
||||
getTransferredToAndAway,
|
||||
getTransferredInside,
|
||||
getStudyRightsByExtent,
|
||||
getStudyRightsByBachelorStart,
|
||||
getStudentsByStudentnumbers,
|
||||
getTransfersOut,
|
||||
getTransfersIn,
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
const { indexOf } = require('lodash')
|
||||
const { indexOf, isArray } = require('lodash')
|
||||
const moment = require('moment')
|
||||
const {
|
||||
startedStudyrights,
|
||||
graduatedStudyrights,
|
||||
@@ -7,6 +8,8 @@ const {
|
||||
transferredTo,
|
||||
getTransferredToAndAway,
|
||||
getTransferredInside,
|
||||
studyrightsByRightStartYear,
|
||||
getTransfersIn,
|
||||
} = require('./faculty')
|
||||
const { getStatsBasis, getYearsArray, defineYear } = require('../studyprogrammeHelpers')
|
||||
const { findRightProgramme, isNewProgramme, checkTransfers, getExtentFilter } = require('./facultyHelpers')
|
||||
@@ -14,17 +17,12 @@ const { codes } = require('../../../config/programmeCodes')
|
||||
|
||||
const filterDuplicateStudyrights = studyrights => {
|
||||
// bachelor+master students have two studyrights (separated by two last digits in studyrightid)
|
||||
// choose only the earlier started one, so we don't count start of masters as starting in faculty
|
||||
// choose only bachelor one, so we don't count start of masters as starting in faculty
|
||||
let rightsToCount = {}
|
||||
|
||||
studyrights.forEach(right => {
|
||||
const id = right.studyrightid.slice(0, -2)
|
||||
const start = new Date(right.studystartdate)
|
||||
if (id in rightsToCount) {
|
||||
if (new Date(rightsToCount[id].studystartdate) > start) {
|
||||
rightsToCount[id] = right
|
||||
}
|
||||
} else {
|
||||
if (right.studyrightid.slice(-2) === '-1') {
|
||||
rightsToCount[id] = right
|
||||
}
|
||||
})
|
||||
@@ -58,6 +56,7 @@ const addProgramme = async (
|
||||
|
||||
const getFacultyStarters = async (
|
||||
faculty,
|
||||
programmes,
|
||||
transfersToOrAwayStudyrights,
|
||||
insideTransfersStudyrights,
|
||||
since,
|
||||
@@ -73,43 +72,50 @@ const getFacultyStarters = async (
|
||||
) => {
|
||||
const startedGraphStats = [...graphStats]
|
||||
const startedTableStats = { ...tableStats }
|
||||
const studyrightWhere = getExtentFilter(includeAllSpecials)
|
||||
const studyrights = await startedStudyrights(faculty, since, studyrightWhere)
|
||||
let filteredStudyrights = filterDuplicateStudyrights(studyrights)
|
||||
const keys = Object.keys(codes)
|
||||
const programmeTableStats = {}
|
||||
if (!includeAllSpecials) {
|
||||
filteredStudyrights = filteredStudyrights.filter(
|
||||
s => !checkTransfers(s, insideTransfersStudyrights, transfersToOrAwayStudyrights)
|
||||
)
|
||||
const studyrightWhere = getExtentFilter(includeAllSpecials)
|
||||
const start = new Date('2017-01-01').toUTCString()
|
||||
const end = new Date()
|
||||
for (const code of programmes) {
|
||||
const studyrights = await startedStudyrights(faculty, code, since, studyrightWhere)
|
||||
let filteredStudyrights = filterDuplicateStudyrights(studyrights)
|
||||
const insideTransfers = await getTransfersIn(code, start, end)
|
||||
filteredStudyrights = filteredStudyrights.filter(s => !checkTransfers(s, insideTransfers, insideTransfers))
|
||||
const keys = Object.keys(codes)
|
||||
|
||||
if (!includeAllSpecials) {
|
||||
// We do not include inside transferred studyrights into faculty starters, but function below expect two arguments
|
||||
filteredStudyrights = filteredStudyrights.filter(
|
||||
s => !checkTransfers(s, insideTransfersStudyrights, transfersToOrAwayStudyrights)
|
||||
)
|
||||
}
|
||||
|
||||
filteredStudyrights.forEach(({ startdate, studyrightElements }) => {
|
||||
const { programme, programmeName } = findRightProgramme(studyrightElements, code)
|
||||
let programmeId = programme
|
||||
|
||||
if (keys.includes(programme)) {
|
||||
programmeId = codes[programme].toUpperCase()
|
||||
}
|
||||
|
||||
if (programmeFilter === 'ALL_PROGRAMMES' || isNewProgramme(programme)) {
|
||||
const startYear = defineYear(startdate, isAcademicYear)
|
||||
|
||||
startedGraphStats[indexOf(yearsArray, startYear)] += 1
|
||||
startedTableStats[startYear] += 1
|
||||
|
||||
if (!(programmeId in programmeTableStats)) {
|
||||
programmeTableStats[programmeId] = { ...tableStats }
|
||||
}
|
||||
programmeTableStats[programmeId][startYear] += 1
|
||||
|
||||
if (!(programmeId in allBasics.programmeNames)) {
|
||||
allBasics.programmeNames[programmeId] = { ...programmeName, code: programme }
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
filteredStudyrights.forEach(({ studystartdate, studyrightElements }) => {
|
||||
const { programme, programmeName } = findRightProgramme(studyrightElements, 'started')
|
||||
let programmeId = programme
|
||||
|
||||
if (keys.includes(programme)) {
|
||||
programmeId = codes[programme].toUpperCase()
|
||||
}
|
||||
|
||||
if (programmeFilter === 'ALL_PROGRAMMES' || isNewProgramme(programme)) {
|
||||
const startYear = defineYear(studystartdate, isAcademicYear)
|
||||
|
||||
startedGraphStats[indexOf(yearsArray, startYear)] += 1
|
||||
startedTableStats[startYear] += 1
|
||||
|
||||
if (!(programmeId in programmeTableStats)) {
|
||||
programmeTableStats[programmeId] = { ...tableStats }
|
||||
}
|
||||
programmeTableStats[programmeId][startYear] += 1
|
||||
|
||||
if (!(programmeId in allBasics.programmeNames)) {
|
||||
allBasics.programmeNames[programmeId] = { ...programmeName, code: programme }
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
allBasics.studentInfo.graphStats.push({ name: 'Started studying', data: startedGraphStats })
|
||||
allBasics.studentInfo.graphStats.push({ name: 'Started studying (new in faculty)', data: startedGraphStats })
|
||||
programmeData['started'] = programmeTableStats
|
||||
|
||||
Object.keys(startedTableStats).forEach(year => {
|
||||
@@ -119,6 +125,7 @@ const getFacultyStarters = async (
|
||||
|
||||
const getFacultyGraduates = async (
|
||||
faculty,
|
||||
programmes,
|
||||
transfersToOrAwayStudyrights,
|
||||
insideTransfersStudyrights,
|
||||
since,
|
||||
@@ -135,61 +142,63 @@ const getFacultyGraduates = async (
|
||||
) => {
|
||||
const graduatedGraphStats = [[...graphStats], [...graphStats], [...graphStats], [...graphStats], [...graphStats]]
|
||||
const graduatedTableStats = {}
|
||||
const programmeTableStats = {}
|
||||
Object.keys(tableStats).forEach(year => (graduatedTableStats[year] = [0, 0, 0, 0, 0]))
|
||||
const keys = Object.keys(codes)
|
||||
let studyrightWhere = getExtentFilter(includeAllSpecials)
|
||||
let graduatedRights = await graduatedStudyrights(faculty, since, studyrightWhere)
|
||||
const programmeTableStats = {}
|
||||
for (const code of programmes) {
|
||||
let graduatedRights = await graduatedStudyrights(faculty, code, since, studyrightWhere)
|
||||
if (!includeAllSpecials) {
|
||||
graduatedRights = graduatedRights.filter(
|
||||
s => !checkTransfers(s, insideTransfersStudyrights, transfersToOrAwayStudyrights)
|
||||
)
|
||||
}
|
||||
graduatedRights.forEach(({ enddate, extentcode, studyrightElements }) => {
|
||||
const { programme, programmeName } = findRightProgramme(studyrightElements, code)
|
||||
if (!studyrightElements.find(sre => new Date(sre.enddate).getTime() === new Date(enddate).getTime())) return
|
||||
let programmeId = programme
|
||||
|
||||
Object.keys(tableStats).forEach(year => (graduatedTableStats[year] = [0, 0, 0, 0, 0]))
|
||||
if (!includeAllSpecials) {
|
||||
graduatedRights = graduatedRights.filter(
|
||||
s => !checkTransfers(s, insideTransfersStudyrights, transfersToOrAwayStudyrights)
|
||||
)
|
||||
if (keys.includes(programme)) {
|
||||
programmeId = codes[programme].toUpperCase()
|
||||
}
|
||||
|
||||
if (programmeFilter === 'ALL_PROGRAMMES' || isNewProgramme(programme)) {
|
||||
const endYear = defineYear(enddate, isAcademicYear)
|
||||
graduatedGraphStats[0][indexOf(yearsArray, endYear)] += 1
|
||||
graduatedTableStats[endYear][0] += 1
|
||||
|
||||
if (!(programmeId in programmeTableStats)) {
|
||||
programmeTableStats[programmeId] = {}
|
||||
Object.keys(tableStats).forEach(year => (programmeTableStats[programmeId][year] = [0, 0, 0, 0, 0]))
|
||||
}
|
||||
programmeTableStats[programmeId][endYear][0] += 1
|
||||
|
||||
if (extentcode === 1) {
|
||||
graduatedGraphStats[1][indexOf(yearsArray, endYear)] += 1
|
||||
graduatedTableStats[endYear][1] += 1
|
||||
programmeTableStats[programmeId][endYear][1] += 1
|
||||
} else if (extentcode === 2) {
|
||||
graduatedGraphStats[2][indexOf(yearsArray, endYear)] += 1
|
||||
graduatedTableStats[endYear][2] += 1
|
||||
programmeTableStats[programmeId][endYear][2] += 1
|
||||
} else if (extentcode === 4) {
|
||||
graduatedGraphStats[3][indexOf(yearsArray, endYear)] += 1
|
||||
graduatedTableStats[endYear][3] += 1
|
||||
programmeTableStats[programmeId][endYear][3] += 1
|
||||
} else {
|
||||
graduatedGraphStats[4][indexOf(yearsArray, endYear)] += 1
|
||||
graduatedTableStats[endYear][4] += 1
|
||||
programmeTableStats[programmeId][endYear][4] += 1
|
||||
}
|
||||
|
||||
if (!(programmeId in allBasics.programmeNames)) {
|
||||
allBasics.programmeNames[programmeId] = { code: programme, ...programmeName }
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
graduatedRights.forEach(({ enddate, extentcode, studyrightElements }) => {
|
||||
const { programme, programmeName } = findRightProgramme(studyrightElements, 'graduated')
|
||||
let programmeId = programme
|
||||
|
||||
if (keys.includes(programme)) {
|
||||
programmeId = codes[programme].toUpperCase()
|
||||
}
|
||||
if (programmeFilter === 'ALL_PROGRAMMES' || isNewProgramme(programme)) {
|
||||
const endYear = defineYear(enddate, isAcademicYear)
|
||||
graduatedGraphStats[0][indexOf(yearsArray, endYear)] += 1
|
||||
graduatedTableStats[endYear][0] += 1
|
||||
|
||||
if (!(programmeId in programmeTableStats)) {
|
||||
programmeTableStats[programmeId] = {}
|
||||
Object.keys(tableStats).forEach(year => (programmeTableStats[programmeId][year] = [0, 0, 0, 0, 0]))
|
||||
}
|
||||
programmeTableStats[programmeId][endYear][0] += 1
|
||||
|
||||
if (extentcode === 1) {
|
||||
graduatedGraphStats[1][indexOf(yearsArray, endYear)] += 1
|
||||
graduatedTableStats[endYear][1] += 1
|
||||
programmeTableStats[programmeId][endYear][1] += 1
|
||||
} else if (extentcode === 2) {
|
||||
graduatedGraphStats[2][indexOf(yearsArray, endYear)] += 1
|
||||
graduatedTableStats[endYear][2] += 1
|
||||
programmeTableStats[programmeId][endYear][2] += 1
|
||||
} else if (extentcode === 4) {
|
||||
graduatedGraphStats[3][indexOf(yearsArray, endYear)] += 1
|
||||
graduatedTableStats[endYear][3] += 1
|
||||
programmeTableStats[programmeId][endYear][3] += 1
|
||||
} else {
|
||||
graduatedGraphStats[4][indexOf(yearsArray, endYear)] += 1
|
||||
graduatedTableStats[endYear][4] += 1
|
||||
programmeTableStats[programmeId][endYear][4] += 1
|
||||
}
|
||||
|
||||
if (!(programmeId in allBasics.programmeNames)) {
|
||||
allBasics.programmeNames[programmeId] = { code: programme, ...programmeName }
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
allBasics.studentInfo.graphStats.push({ name: 'Graduated', data: graduatedGraphStats[0] })
|
||||
|
||||
allBasics.graduationInfo.graphStats.push({ name: 'Bachelors', data: graduatedGraphStats[1] })
|
||||
allBasics.graduationInfo.graphStats.push({ name: 'Masters', data: graduatedGraphStats[2] })
|
||||
allBasics.graduationInfo.graphStats.push({ name: 'Doctors', data: graduatedGraphStats[3] })
|
||||
@@ -202,7 +211,18 @@ const getFacultyGraduates = async (
|
||||
countsGraduations[year] = graduatedTableStats[year]
|
||||
})
|
||||
}
|
||||
|
||||
// If includeAllSpecial false, inside transfers should contain only students started after 1.8.2017.
|
||||
const getInsideTransfers = async (programmeCodes, allProgrammeCodes, since, includeAllSpecials, faculty) => {
|
||||
if (includeAllSpecials) {
|
||||
return await transferredInsideFaculty(programmeCodes, allProgrammeCodes, since)
|
||||
}
|
||||
const insiders = await transferredInsideFaculty(programmeCodes, allProgrammeCodes, since)
|
||||
const studyrights = (
|
||||
await studyrightsByRightStartYear(faculty, new Date(moment('2017-08-01', 'YYYY-MM-DD')).toUTCString())
|
||||
).map(sr => sr.studyrightid)
|
||||
const filteredTransfers = insiders.filter(tr => studyrights.includes(tr.studyrightid))
|
||||
return filteredTransfers
|
||||
}
|
||||
const getFacultyTransfers = async (
|
||||
programmes,
|
||||
programmeCodes,
|
||||
@@ -215,10 +235,16 @@ const getFacultyTransfers = async (
|
||||
allBasics,
|
||||
counts,
|
||||
programmeData,
|
||||
includeAllSpecials
|
||||
includeAllSpecials,
|
||||
faculty
|
||||
) => {
|
||||
const insideTransfers = await transferredInsideFaculty(programmeCodes, allProgrammeCodes, since)
|
||||
// Filter still away those that have started studying before 1.8.2017, when include all specials false.
|
||||
const insideTransfers = await getInsideTransfers(
|
||||
programmeCodes,
|
||||
allProgrammeCodes,
|
||||
since,
|
||||
includeAllSpecials,
|
||||
faculty
|
||||
)
|
||||
let transferGraphStats = []
|
||||
let transferTableStats = {}
|
||||
if (includeAllSpecials) {
|
||||
@@ -234,17 +260,19 @@ const getFacultyTransfers = async (
|
||||
for (const { transferdate, targetcode } of insideTransfers) {
|
||||
const transferYear = defineYear(transferdate, isAcademicYear)
|
||||
transferGraphStats[0][indexOf(yearsArray, transferYear)] += 1
|
||||
transferTableStats[transferYear][0] += 1
|
||||
await addProgramme(
|
||||
programmeTableStats,
|
||||
targetcode,
|
||||
tableStats,
|
||||
transferYear,
|
||||
0,
|
||||
allBasics,
|
||||
programmes,
|
||||
includeAllSpecials
|
||||
)
|
||||
if (isArray(transferTableStats[transferYear])) {
|
||||
transferTableStats[transferYear][0] += 1
|
||||
await addProgramme(
|
||||
programmeTableStats,
|
||||
targetcode,
|
||||
tableStats,
|
||||
transferYear,
|
||||
0,
|
||||
allBasics,
|
||||
programmes,
|
||||
includeAllSpecials
|
||||
)
|
||||
}
|
||||
}
|
||||
if (includeAllSpecials) {
|
||||
const awayTransfers = await transferredAway(programmeCodes, allProgrammeCodes, since)
|
||||
@@ -252,34 +280,38 @@ const getFacultyTransfers = async (
|
||||
|
||||
for (const { transferdate, sourcecode } of awayTransfers) {
|
||||
const transferYear = defineYear(transferdate, isAcademicYear)
|
||||
transferGraphStats[1][indexOf(yearsArray, transferYear)] += 1
|
||||
transferTableStats[transferYear][1] += 1
|
||||
await addProgramme(
|
||||
programmeTableStats,
|
||||
sourcecode,
|
||||
tableStats,
|
||||
transferYear,
|
||||
1,
|
||||
allBasics,
|
||||
programmes,
|
||||
includeAllSpecials
|
||||
)
|
||||
if (isArray(transferTableStats[transferYear])) {
|
||||
transferGraphStats[1][indexOf(yearsArray, transferYear)] += 1
|
||||
transferTableStats[transferYear][1] += 1
|
||||
await addProgramme(
|
||||
programmeTableStats,
|
||||
sourcecode,
|
||||
tableStats,
|
||||
transferYear,
|
||||
1,
|
||||
allBasics,
|
||||
programmes,
|
||||
includeAllSpecials
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
for (const { transferdate, targetcode } of toTransfers) {
|
||||
const transferYear = defineYear(transferdate, isAcademicYear)
|
||||
transferGraphStats[2][indexOf(yearsArray, transferYear)] += 1
|
||||
transferTableStats[transferYear][2] += 1
|
||||
await addProgramme(
|
||||
programmeTableStats,
|
||||
targetcode,
|
||||
tableStats,
|
||||
transferYear,
|
||||
2,
|
||||
allBasics,
|
||||
programmes,
|
||||
includeAllSpecials
|
||||
)
|
||||
if (isArray(transferTableStats[transferYear])) {
|
||||
transferGraphStats[2][indexOf(yearsArray, transferYear)] += 1
|
||||
transferTableStats[transferYear][2] += 1
|
||||
await addProgramme(
|
||||
programmeTableStats,
|
||||
targetcode,
|
||||
tableStats,
|
||||
transferYear,
|
||||
2,
|
||||
allBasics,
|
||||
programmes,
|
||||
includeAllSpecials
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -332,6 +364,7 @@ const combineFacultyBasics = async (faculty, programmes, yearType, allProgrammeC
|
||||
// Started studying in faculty
|
||||
await getFacultyStarters(
|
||||
faculty,
|
||||
wantedProgrammeCodes,
|
||||
transfersToAwayStudyrights,
|
||||
transferInsideStudyrights,
|
||||
since,
|
||||
@@ -349,6 +382,7 @@ const combineFacultyBasics = async (faculty, programmes, yearType, allProgrammeC
|
||||
// Graduated in faculty
|
||||
await getFacultyGraduates(
|
||||
faculty,
|
||||
wantedProgrammeCodes,
|
||||
transfersToAwayStudyrights,
|
||||
transferInsideStudyrights,
|
||||
since,
|
||||
@@ -377,7 +411,8 @@ const combineFacultyBasics = async (faculty, programmes, yearType, allProgrammeC
|
||||
allBasics,
|
||||
counts,
|
||||
programmeData,
|
||||
includeAllSpecials
|
||||
includeAllSpecials,
|
||||
faculty
|
||||
)
|
||||
|
||||
// combine tableStats from all categories
|
||||
@@ -423,7 +458,11 @@ const combineFacultyBasics = async (faculty, programmes, yearType, allProgrammeC
|
||||
if (code in programmeData.transferred) {
|
||||
studentInfo[code][year] = studentInfo[code][year].concat(programmeData.transferred[code][year])
|
||||
} else {
|
||||
studentInfo[code][year] = studentInfo[code][year].concat([0, 0, 0])
|
||||
if (includeAllSpecials) {
|
||||
studentInfo[code][year] = studentInfo[code][year].concat([0, 0, 0])
|
||||
} else {
|
||||
studentInfo[code][year] = studentInfo[code][year].concat([0])
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const { indexOf } = require('lodash')
|
||||
const { indexOf, uniqBy } = require('lodash')
|
||||
const { mapToProviders } = require('../../util/utils')
|
||||
const {
|
||||
getStatsBasis,
|
||||
@@ -7,27 +7,25 @@ const {
|
||||
isNonMajorCredit,
|
||||
getYearsArray,
|
||||
getStartDate,
|
||||
isSpecialGroupCredit,
|
||||
} = require('../studyprogrammeHelpers')
|
||||
const {
|
||||
getStudyRights,
|
||||
getCreditsForStudyProgramme,
|
||||
allTransfers,
|
||||
getCourseCodesForStudyProgramme,
|
||||
} = require('../studyprogramme')
|
||||
const { getStudyRights, getCreditsForStudyProgramme, getCourseCodesForStudyProgramme } = require('../studyprogramme')
|
||||
|
||||
const isFacultyNonMajorCredit = (studyrights, attainment_date, facultyProgrammes) => {
|
||||
const isFacultyNonMajorCredit = (studyrights, attainmentDate, facultyProgrammes, facultyCode) => {
|
||||
let right = ''
|
||||
studyrights.forEach(studyright => {
|
||||
if (studyright) {
|
||||
if (!studyright.graduated && new Date(attainment_date) >= new Date(studyright.studystartdate)) {
|
||||
if (studyright && studyright.facultyCode === facultyCode) {
|
||||
const startDate =
|
||||
studyright.studyrightid.slice(-2) === '-2' && studyright.extentcode === 2
|
||||
? studyright.studystartdate
|
||||
: studyright.startdate
|
||||
if (!studyright.graduated && new Date(attainmentDate) >= new Date(startDate)) {
|
||||
if (!right) right = studyright.code
|
||||
else if (right && !facultyProgrammes.includes(right) && facultyProgrammes.includes(studyright.code))
|
||||
right = studyright.code
|
||||
} else {
|
||||
if (
|
||||
new Date(attainment_date) >= new Date(studyright.studystartdate) &&
|
||||
new Date(attainment_date) <= new Date(studyright.enddate)
|
||||
new Date(attainmentDate) >= new Date(startDate) &&
|
||||
new Date(attainmentDate) <= new Date(studyright.enddate)
|
||||
) {
|
||||
if (!right) right = studyright.code
|
||||
else if (right && !facultyProgrammes.includes(right) && facultyProgrammes.includes(studyright.code))
|
||||
@@ -38,58 +36,58 @@ const isFacultyNonMajorCredit = (studyrights, attainment_date, facultyProgrammes
|
||||
})
|
||||
return facultyProgrammes.includes(right)
|
||||
}
|
||||
const isFacultyOtherCredit = (studyrights, attainmentDate) => {
|
||||
return studyrights.some(studyright => {
|
||||
return (
|
||||
studyright &&
|
||||
[6, 7, 23, 34, 99].includes(studyright.extentcode) &&
|
||||
new Date(attainmentDate) >= new Date(studyright.startdate)
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
// Fetches all the credits for the studyprogramme and divides them into major-students, non-major faculty students,
|
||||
// non-major other faculty students adn non-degree student credits
|
||||
// non-major other faculty students, other (exhange students) and non-degree student credits.
|
||||
// Division is done on the basis that whether the student had a primary studyright to the programme on the attainment_date
|
||||
const getFacultyRegularCreditStats = async ({
|
||||
studyprogramme,
|
||||
since,
|
||||
years,
|
||||
isAcademicYear,
|
||||
includeAllSpecials,
|
||||
facultyProgrammes,
|
||||
facultyCode,
|
||||
}) => {
|
||||
const providercode = mapToProviders([studyprogramme])[0]
|
||||
const courses = await getCourseCodesForStudyProgramme(providercode)
|
||||
const credits = await getCreditsForStudyProgramme(courses, since)
|
||||
const students = [...new Set(credits.map(({ student_studentnumber }) => student_studentnumber))]
|
||||
const providedByProgramme = await getCourseCodesForStudyProgramme(providercode)
|
||||
const allCredits = await getCreditsForStudyProgramme(providedByProgramme, since)
|
||||
const credits = uniqBy(allCredits, 'id')
|
||||
const students = [...new Set(credits.map(({ studentNumber }) => studentNumber))]
|
||||
|
||||
let studyrights = await getStudyRights(students)
|
||||
const transfers = (await allTransfers(studyprogramme, since)).map(t => t.studyrightid).filter(s => s !== 'undefined')
|
||||
if (!includeAllSpecials) {
|
||||
studyrights = studyrights.filter(s => !transfers.includes(s.studyrightid))
|
||||
}
|
||||
let studyRights = await getStudyRights(students)
|
||||
let majors = getStatsBasis(years) // Has studyright to programme
|
||||
let facultyNonMajors = getStatsBasis(years) // Has studyright in the same faculty but not in the programme from which credits are
|
||||
let facultyOthers = getStatsBasis(years) // Exchange students, specialization education in Medicine, Dentistry and vetenary medicine, continuous education
|
||||
let otherNonMajors = getStatsBasis(years) // Has studyright in different faculty
|
||||
let nonDegree = getStatsBasis(years) // open uni, non-degree study rights, contract studies, (other) specialisation education, credits outside of the studyright
|
||||
|
||||
let majors = getStatsBasis(years)
|
||||
let allNonMajors = getStatsBasis(years)
|
||||
let facultyNonMajors = getStatsBasis(years)
|
||||
let otherNonMajors = getStatsBasis(years)
|
||||
let nonDegree = getStatsBasis(years)
|
||||
credits.forEach(({ studentNumber, attainmentDate, credits }) => {
|
||||
const studentStudyrights = studyRights.filter(studyright => studyright.studentNumber === studentNumber)
|
||||
const attainmentYear = defineYear(attainmentDate, isAcademicYear)
|
||||
|
||||
credits.forEach(({ student_studentnumber, attainment_date, credits }) => {
|
||||
const studentStudyrights = studyrights.filter(studyright => studyright.studentnumber === student_studentnumber)
|
||||
const attainmentYear = defineYear(attainment_date, isAcademicYear)
|
||||
|
||||
if (!includeAllSpecials && isSpecialGroupCredit(studentStudyrights, attainment_date, transfers)) {
|
||||
return
|
||||
}
|
||||
|
||||
if (isMajorStudentCredit(studentStudyrights, attainment_date, studyprogramme)) {
|
||||
if (isMajorStudentCredit(studentStudyrights, attainmentDate, studyprogramme)) {
|
||||
majors.graphStats[indexOf(years, attainmentYear)] += credits || 0
|
||||
majors.tableStats[attainmentYear] += credits || 0
|
||||
} else if (isNonMajorCredit(studentStudyrights, attainment_date)) {
|
||||
// for comparison
|
||||
allNonMajors.graphStats[indexOf(years, attainmentYear)] += credits || 0
|
||||
allNonMajors.tableStats[attainmentYear] += credits || 0
|
||||
|
||||
if (isFacultyNonMajorCredit(studentStudyrights, attainment_date, facultyProgrammes)) {
|
||||
} else if (isNonMajorCredit(studentStudyrights, attainmentDate)) {
|
||||
if (isFacultyNonMajorCredit(studentStudyrights, attainmentDate, facultyProgrammes, facultyCode)) {
|
||||
facultyNonMajors.graphStats[indexOf(years, attainmentYear)] += credits || 0
|
||||
facultyNonMajors.tableStats[attainmentYear] += credits || 0
|
||||
} else {
|
||||
otherNonMajors.graphStats[indexOf(years, attainmentYear)] += credits || 0
|
||||
otherNonMajors.tableStats[attainmentYear] += credits || 0
|
||||
}
|
||||
} else if (isFacultyOtherCredit(studentStudyrights, attainmentDate)) {
|
||||
facultyOthers.graphStats[indexOf(years, attainmentYear)] += credits || 0
|
||||
facultyOthers.tableStats[attainmentYear] += credits || 0
|
||||
} else {
|
||||
nonDegree.graphStats[indexOf(years, attainmentYear)] += credits || 0
|
||||
nonDegree.tableStats[attainmentYear] += credits || 0
|
||||
@@ -110,66 +108,67 @@ const getFacultyRegularCreditStats = async ({
|
||||
}
|
||||
}
|
||||
|
||||
return { majors, facultyNonMajors, otherNonMajors, nonDegree }
|
||||
return { majors, facultyNonMajors, otherNonMajors, facultyOthers, nonDegree }
|
||||
}
|
||||
|
||||
// Fetches all credits for the studytrack and combines them into statistics
|
||||
// These are for the faculty view which differs from study programme overview
|
||||
// (no transfers, non majors split to in- and outside-faculty)
|
||||
const getFacultyCreditStatsForStudytrack = async ({ studyprogramme, facultyProgrammes, settings }) => {
|
||||
const { isAcademicYear, includeAllSpecials } = settings
|
||||
const getFacultyCreditStatsForStudytrack = async ({
|
||||
studyprogramme,
|
||||
facultyProgrammes,
|
||||
isAcademicYear,
|
||||
facultyCode,
|
||||
}) => {
|
||||
const since = getStartDate(studyprogramme, isAcademicYear)
|
||||
const years = getYearsArray(since.getFullYear(), isAcademicYear)
|
||||
|
||||
const queryParameters = { studyprogramme, since, years, isAcademicYear, includeAllSpecials, facultyProgrammes }
|
||||
const { majors, facultyNonMajors, otherNonMajors, nonDegree } = await getFacultyRegularCreditStats(queryParameters)
|
||||
const queryParameters = { studyprogramme, since, years, isAcademicYear, facultyProgrammes, facultyCode }
|
||||
const { majors, facultyNonMajors, otherNonMajors, facultyOthers, nonDegree } = await getFacultyRegularCreditStats(
|
||||
queryParameters
|
||||
)
|
||||
|
||||
const reversedYears = getYearsArray(since.getFullYear(), isAcademicYear).reverse()
|
||||
const dataFound = [majors, facultyNonMajors, otherNonMajors].some(d => d.graphStats.length) // , transferred
|
||||
|
||||
if (!dataFound) return null
|
||||
|
||||
const tableStats = reversedYears.map(year =>
|
||||
includeAllSpecials
|
||||
? [
|
||||
year,
|
||||
majors.tableStats[year] +
|
||||
facultyNonMajors.tableStats[year] +
|
||||
otherNonMajors.tableStats[year] +
|
||||
nonDegree.tableStats[year],
|
||||
majors.tableStats[year],
|
||||
facultyNonMajors.tableStats[year],
|
||||
otherNonMajors.tableStats[year],
|
||||
nonDegree.tableStats[year],
|
||||
]
|
||||
: [year, majors.tableStats[year], majors.tableStats[year]]
|
||||
)
|
||||
const tableStats = reversedYears.map(year => [
|
||||
year,
|
||||
majors.tableStats[year] +
|
||||
facultyNonMajors.tableStats[year] +
|
||||
otherNonMajors.tableStats[year] +
|
||||
nonDegree.tableStats[year] +
|
||||
facultyOthers.tableStats[year],
|
||||
majors.tableStats[year],
|
||||
facultyNonMajors.tableStats[year],
|
||||
otherNonMajors.tableStats[year],
|
||||
nonDegree.tableStats[year],
|
||||
facultyOthers.tableStats[year],
|
||||
])
|
||||
|
||||
const graphStats = includeAllSpecials
|
||||
? [
|
||||
{
|
||||
name: 'Major students credits',
|
||||
data: majors.graphStats,
|
||||
},
|
||||
{
|
||||
name: 'Non-major faculty students credits',
|
||||
data: facultyNonMajors.graphStats,
|
||||
},
|
||||
{
|
||||
name: 'Non-major other faculty students credits 2',
|
||||
data: otherNonMajors.graphStats,
|
||||
},
|
||||
{
|
||||
name: 'Non-degree credits',
|
||||
data: nonDegree.graphStats,
|
||||
},
|
||||
]
|
||||
: [
|
||||
{
|
||||
name: 'Major students credits',
|
||||
data: majors.graphStats,
|
||||
},
|
||||
]
|
||||
const graphStats = [
|
||||
{
|
||||
name: 'Major students credits',
|
||||
data: majors.graphStats,
|
||||
},
|
||||
{
|
||||
name: 'Non-major faculty students credits',
|
||||
data: facultyNonMajors.graphStats,
|
||||
},
|
||||
{
|
||||
name: 'Non-major other faculty students credits 2',
|
||||
data: otherNonMajors.graphStats,
|
||||
},
|
||||
{
|
||||
name: 'Non-degree credits',
|
||||
data: nonDegree.graphStats,
|
||||
},
|
||||
{
|
||||
other: 'Other credits',
|
||||
data: facultyOthers.graphStats,
|
||||
},
|
||||
]
|
||||
|
||||
return {
|
||||
id: studyprogramme,
|
||||
@@ -179,33 +178,29 @@ const getFacultyCreditStatsForStudytrack = async ({ studyprogramme, facultyProgr
|
||||
}
|
||||
}
|
||||
|
||||
const getProgrammeCredits = async (code, yearType, specialGroups, facultyProgrammes) => {
|
||||
const getProgrammeCredits = async (code, yearType, facultyProgrammes, faculty) => {
|
||||
const updatedStats = await getFacultyCreditStatsForStudytrack({
|
||||
facultyCode: faculty,
|
||||
studyprogramme: code,
|
||||
facultyProgrammes,
|
||||
settings: {
|
||||
isAcademicYear: yearType === 'ACADEMIC_YEAR',
|
||||
includeAllSpecials: specialGroups === 'SPECIAL_INCLUDED',
|
||||
},
|
||||
isAcademicYear: yearType === 'ACADEMIC_YEAR',
|
||||
})
|
||||
|
||||
return updatedStats
|
||||
}
|
||||
|
||||
const combineFacultyCredits = async (faculty, programmes, allProgrammes, yearType, specialGroups) => {
|
||||
const combineFacultyCredits = async (faculty, programmes, allProgrammes, yearType) => {
|
||||
let counts = {}
|
||||
let years = []
|
||||
const titles =
|
||||
specialGroups === 'SPECIAL_INCLUDED'
|
||||
? [
|
||||
'',
|
||||
'Total',
|
||||
'Major students credits',
|
||||
'Non-major faculty students credits',
|
||||
'Non-major other faculty students credits',
|
||||
'Non-degree student credits',
|
||||
]
|
||||
: ['', 'Total', 'Major students credits']
|
||||
const titles = [
|
||||
'',
|
||||
'Total',
|
||||
'Major students credits',
|
||||
'Non-major faculty students credits',
|
||||
'Non-major other faculty students credits',
|
||||
'Non-degree student credits',
|
||||
'Other non-degree credits',
|
||||
]
|
||||
|
||||
let allCredits = {
|
||||
id: faculty,
|
||||
@@ -219,7 +214,8 @@ const combineFacultyCredits = async (faculty, programmes, allProgrammes, yearTyp
|
||||
|
||||
const progCodes = allProgrammes.map(p => p.code)
|
||||
for (const prog of programmes) {
|
||||
const data = await getProgrammeCredits(prog.code, yearType, specialGroups, progCodes)
|
||||
if (prog.code === 'MH70_008_2') continue
|
||||
const data = await getProgrammeCredits(prog.code, yearType, progCodes, faculty)
|
||||
if (data) {
|
||||
allCredits.programmeNames[prog.progId] = { code: prog.code, ...prog.name }
|
||||
allCredits.programmeTableStats[prog.progId] = data.tableStats
|
||||
@@ -250,25 +246,24 @@ const combineFacultyCredits = async (faculty, programmes, allProgrammes, yearTyp
|
||||
let majors = []
|
||||
let facultyNonMajor = []
|
||||
let otherNonMajor = []
|
||||
let facultyOthers = []
|
||||
let nonDegree = []
|
||||
|
||||
years.forEach(year => {
|
||||
majors.push(counts[year][1])
|
||||
facultyNonMajor.push(counts[year][2])
|
||||
otherNonMajor.push(counts[year][3])
|
||||
nonDegree.push(counts[year][4])
|
||||
facultyOthers.push(counts[year][4])
|
||||
nonDegree.push(counts[year][5])
|
||||
})
|
||||
|
||||
allCredits.graphStats =
|
||||
specialGroups === 'SPECIAL_INCLUDED'
|
||||
? [
|
||||
{ name: 'Major students credits', data: majors },
|
||||
{ name: 'Non-major faculty students credits', data: facultyNonMajor },
|
||||
{ name: 'Non-major other faculty students credits', data: otherNonMajor },
|
||||
{ name: 'Non-degree credits', data: nonDegree },
|
||||
]
|
||||
: [{ name: 'Major students credits', data: majors }]
|
||||
|
||||
allCredits.graphStats = [
|
||||
{ name: 'Major students credits', data: majors },
|
||||
{ name: 'Non-major faculty students credits', data: facultyNonMajor },
|
||||
{ name: 'Non-major other faculty students credits', data: otherNonMajor },
|
||||
{ name: 'Non-degree credits', data: nonDegree },
|
||||
{ name: 'Other non-degree credits', data: facultyOthers },
|
||||
]
|
||||
return allCredits
|
||||
}
|
||||
|
||||
|
||||
@@ -1,14 +1,10 @@
|
||||
const moment = require('moment')
|
||||
const {
|
||||
graduatedStudyrights,
|
||||
studyrightsByRightStartYear,
|
||||
bachelorStudyright,
|
||||
hasMasterRight,
|
||||
statutoryAbsences,
|
||||
} = require('./faculty')
|
||||
const { graduatedStudyrights, studyrightsByRightStartYear, hasMasterRight } = require('./faculty')
|
||||
const { findRightProgramme, isNewProgramme } = require('./facultyHelpers')
|
||||
const { getYearsArray, getYearsObject, getMean, getMedian, defineYear } = require('../studyprogrammeHelpers')
|
||||
const { getYearsArray, getYearsObject, getMedian, defineYear } = require('../studyprogrammeHelpers')
|
||||
const { codes } = require('../../../config/programmeCodes')
|
||||
const { bachelorStudyright, countTimeCategories, getStatutoryAbsences } = require('../graduationHelpers')
|
||||
const { getProgrammes } = require('./facultyService')
|
||||
|
||||
const sortProgrammes = data => {
|
||||
const check = name => {
|
||||
@@ -26,7 +22,8 @@ const getSortedProgIds = progs => {
|
||||
|
||||
for (const prog of progs) {
|
||||
if (Object.keys(codes).includes(prog)) {
|
||||
programmes = [...programmes, { name: codes[prog].toUpperCase(), code: prog }]
|
||||
const name = prog === 'T923103' ? codes[prog].toUpperCase() + '-DP' : codes[prog].toUpperCase()
|
||||
programmes = [...programmes, { name: name, code: prog }]
|
||||
} else {
|
||||
programmes = [...programmes, { name: prog, code: prog }]
|
||||
}
|
||||
@@ -37,7 +34,7 @@ const getSortedProgIds = progs => {
|
||||
|
||||
const findBachelorStartdate = async id => {
|
||||
const studyright = await bachelorStudyright(id)
|
||||
if (studyright) return studyright.studystartdate
|
||||
if (studyright) return studyright.startdate
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -48,25 +45,6 @@ const getProgrammeObjectBasis = (years, levels, emptyObject = true) => {
|
||||
)
|
||||
}
|
||||
|
||||
const getStatutoryAbsences = async (studentnumber, startdate, enddate) => {
|
||||
const absences = await statutoryAbsences(studentnumber, startdate, enddate)
|
||||
if (absences.length) {
|
||||
const absentMonths = absences.reduce((sum, ab) => sum + moment(ab.end).diff(moment(ab.start), 'months'), 0)
|
||||
return absentMonths
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
const countTimeCategories = (times, goal) => {
|
||||
const statistics = { onTime: 0, yearOver: 0, wayOver: 0 }
|
||||
times.forEach(time => {
|
||||
if (time <= goal) statistics.onTime += 1
|
||||
else if (time <= goal + 12) statistics.yearOver += 1
|
||||
else statistics.wayOver += 1
|
||||
})
|
||||
return statistics
|
||||
}
|
||||
|
||||
const hasMS = async (programme, elements, studyrightid) => {
|
||||
// Only bachelor studyright: socCom, kasvatustiede - varhaiskasvatus track, farmasia - applied only to Bc = farmaseutti,
|
||||
if (['KH74_001', '620050-ba', '620030-ba', '520091-ba'].includes(programme)) {
|
||||
@@ -135,80 +113,93 @@ const addGraduation = async (
|
||||
]
|
||||
}
|
||||
|
||||
const getClassSizes = async (faculty, since, classSizes, programmeFilter, years) => {
|
||||
const studyrights = await studyrightsByRightStartYear(faculty, since, [0, 1])
|
||||
const getClassSizes = async (faculty, programmeCodes, since, classSizes, programmeFilter, years) => {
|
||||
for (const code of programmeCodes) {
|
||||
const studyrights = await studyrightsByRightStartYear(faculty, code, since, [0, 1])
|
||||
|
||||
// get all received studyrights for each year
|
||||
// a transferred student is counted into new programmes class size i.e.
|
||||
// students who received studyright on year X and graduated/will be graduating from programme Y
|
||||
// if we only counted those who started fresh in the new programme we could get a bigger number of graduates
|
||||
// than the class size, as the graduatation time count only looks at a degree as whole studyright
|
||||
for (const right of studyrights) {
|
||||
const { startdate, studyrightid, extentcode, studyrightElements } = right
|
||||
const { programme } = findRightProgramme(studyrightElements, 'graduated')
|
||||
if (programmeFilter === 'NEW_STUDY_PROGRAMMES' && !isNewProgramme(programme)) continue
|
||||
// get all received studyrights for each year
|
||||
// a transferred student is counted into new programmes class size i.e.
|
||||
// students who received studyright on year X and graduated/will be graduating from programme Y
|
||||
// if we only counted those who started fresh in the new programme we could get a bigger number of graduates
|
||||
// than the class size, as the graduatation time count only looks at a degree as whole studyright
|
||||
for (const right of studyrights) {
|
||||
const { startdate, studyrightid, extentcode, studyrightElements } = right
|
||||
if (programmeFilter === 'NEW_STUDY_PROGRAMMES' && !isNewProgramme(code)) continue
|
||||
|
||||
let actualStartdate = startdate
|
||||
let level = null
|
||||
let actualStartdate = startdate
|
||||
let level = null
|
||||
|
||||
if (programme === 'MH30_001' || programme === 'MH30_003') {
|
||||
level = 'bcMsCombo'
|
||||
} else if (extentcode === 1) {
|
||||
level = 'bachelor'
|
||||
} else if (extentcode === 2) {
|
||||
if (studyrightid.slice(-2) === '-2') {
|
||||
if (code === 'MH30_001' || code === 'MH30_003') {
|
||||
level = 'bcMsCombo'
|
||||
actualStartdate = await findBachelorStartdate(studyrightid.replace(/-2$/, '-1'))
|
||||
} else if (extentcode === 1) {
|
||||
level = 'bachelor'
|
||||
} else if (extentcode === 2) {
|
||||
if (studyrightid.slice(-2) === '-2') {
|
||||
level = 'bcMsCombo'
|
||||
actualStartdate = await findBachelorStartdate(studyrightid.replace(/-2$/, '-1'))
|
||||
} else {
|
||||
if (code.includes('KH')) {
|
||||
level = 'bachelor' // Some rare faculties have one/two outliers with extent 2 for bachelor
|
||||
} else {
|
||||
level = 'master'
|
||||
}
|
||||
}
|
||||
} else if (extentcode === 4) {
|
||||
level = 'doctor'
|
||||
} else if (extentcode === 3) {
|
||||
level = 'licentiate'
|
||||
} else {
|
||||
level = 'master'
|
||||
continue
|
||||
}
|
||||
} else if (extentcode === 4) {
|
||||
level = 'doctor'
|
||||
} else if (extentcode === 3) {
|
||||
level = 'licentiate'
|
||||
} else {
|
||||
continue
|
||||
}
|
||||
const startYear = defineYear(actualStartdate, false)
|
||||
|
||||
const startYear = defineYear(actualStartdate, false)
|
||||
|
||||
// On faculty level, count started bachelor rights to BcMsCombo as well --> useful class size even if master studies have yet not started
|
||||
// On programme level BcMsCombo sizes are iffy as many students haven't started masters yet --> no Ms programme element to count
|
||||
if (level !== 'bcMsCombo') {
|
||||
classSizes[level][startYear] += 1
|
||||
if (level === 'bachelor' && (await hasMS(programme, studyrightElements, studyrightid))) {
|
||||
classSizes['bcMsCombo'][startYear] += 1
|
||||
// On faculty level, count started bachelor rights to BcMsCombo as well --> useful class size even if master studies have yet not started
|
||||
// On programme level BcMsCombo sizes are iffy as many students haven't started masters yet --> no Ms programme element to count
|
||||
if (level !== 'bcMsCombo') {
|
||||
classSizes[level][startYear] += 1
|
||||
if (level === 'bachelor' && (await hasMS(code, studyrightElements, studyrightid))) {
|
||||
classSizes['bcMsCombo'][startYear] += 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!(programme in classSizes.programmes)) {
|
||||
if (extentcode === 2) {
|
||||
classSizes.programmes[programme] = {}
|
||||
classSizes.programmes[programme]['bcMsCombo'] = getYearsObject({ years, emptyArrays: false })
|
||||
classSizes.programmes[programme]['master'] = getYearsObject({ years, emptyArrays: false })
|
||||
if (!(code in classSizes.programmes)) {
|
||||
if (extentcode === 2) {
|
||||
classSizes.programmes[code] = {}
|
||||
classSizes.programmes[code]['bcMsCombo'] = getYearsObject({ years, emptyArrays: false })
|
||||
classSizes.programmes[code]['master'] = getYearsObject({ years, emptyArrays: false })
|
||||
} else {
|
||||
classSizes.programmes[code] = getYearsObject({ years, emptyArrays: false })
|
||||
}
|
||||
}
|
||||
|
||||
if (extentcode === 2 && ['master', 'bcMsCombo'].includes(level)) {
|
||||
classSizes.programmes[code][level][startYear] += 1
|
||||
} else {
|
||||
classSizes.programmes[programme] = getYearsObject({ years, emptyArrays: false })
|
||||
classSizes.programmes[code][startYear] += 1
|
||||
}
|
||||
}
|
||||
|
||||
if (extentcode === 2) {
|
||||
classSizes.programmes[programme][level][startYear] += 1
|
||||
} else {
|
||||
classSizes.programmes[programme][startYear] += 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const count = async (faculty, since, years, yearsList, levels, programmeFilter, programmeNames, goals, mode) => {
|
||||
const count = async (
|
||||
faculty,
|
||||
programmeCodes,
|
||||
since,
|
||||
years,
|
||||
yearsList,
|
||||
levels,
|
||||
programmeFilter,
|
||||
programmeNames,
|
||||
goals,
|
||||
mode
|
||||
) => {
|
||||
let graduationAmounts = {}
|
||||
let graduationTimes = {}
|
||||
let programmes = getProgrammeObjectBasis(yearsList, levels)
|
||||
let data = {
|
||||
means: {},
|
||||
medians: {},
|
||||
programmes: {
|
||||
medians: getProgrammeObjectBasis(yearsList, levels),
|
||||
means: getProgrammeObjectBasis(yearsList, levels),
|
||||
},
|
||||
}
|
||||
|
||||
@@ -216,62 +207,57 @@ const count = async (faculty, since, years, yearsList, levels, programmeFilter,
|
||||
graduationAmounts[level] = getYearsObject({ years: yearsList })
|
||||
graduationTimes[level] = getYearsObject({ years: yearsList, emptyArrays: true })
|
||||
data.medians[level] = []
|
||||
data.means[level] = []
|
||||
})
|
||||
|
||||
let studyrights = null
|
||||
if (mode === 'gradYear') {
|
||||
studyrights = await graduatedStudyrights(faculty, since)
|
||||
} else {
|
||||
studyrights = await studyrightsByRightStartYear(faculty, since)
|
||||
}
|
||||
|
||||
for (const right of studyrights) {
|
||||
const { enddate, startdate, studyrightid, extentcode, studyrightElements, studentnumber } = right
|
||||
const { programme, programmeName } = findRightProgramme(studyrightElements, 'graduated')
|
||||
if (programmeFilter === 'NEW_STUDY_PROGRAMMES' && !isNewProgramme(programme)) continue
|
||||
|
||||
if (!(programme in programmeNames)) {
|
||||
programmeNames[programme] = programmeName
|
||||
}
|
||||
|
||||
let year = null
|
||||
for (const code of programmeCodes) {
|
||||
let studyrights = null
|
||||
if (mode === 'gradYear') {
|
||||
year = defineYear(enddate, false)
|
||||
studyrights = await graduatedStudyrights(faculty, code, since)
|
||||
} else {
|
||||
year = defineYear(startdate, false)
|
||||
studyrights = await studyrightsByRightStartYear(faculty, code, since)
|
||||
}
|
||||
for (const right of studyrights) {
|
||||
const { enddate, startdate, studyrightid, extentcode, studyrightElements, studentnumber } = right
|
||||
const { programme, programmeName } = findRightProgramme(studyrightElements, code)
|
||||
if (programmeFilter === 'NEW_STUDY_PROGRAMMES' && !isNewProgramme(programme)) continue
|
||||
|
||||
await addGraduation(
|
||||
extentcode,
|
||||
startdate,
|
||||
studyrightid,
|
||||
enddate,
|
||||
studentnumber,
|
||||
graduationAmounts,
|
||||
graduationTimes,
|
||||
year,
|
||||
programmes,
|
||||
programme
|
||||
)
|
||||
if (!(programme in programmeNames)) {
|
||||
programmeNames[programme] = programmeName
|
||||
}
|
||||
|
||||
let year = null
|
||||
if (mode === 'gradYear') {
|
||||
year = defineYear(enddate, false)
|
||||
} else {
|
||||
year = defineYear(startdate, false)
|
||||
}
|
||||
|
||||
await addGraduation(
|
||||
extentcode,
|
||||
startdate,
|
||||
studyrightid,
|
||||
enddate,
|
||||
studentnumber,
|
||||
graduationAmounts,
|
||||
graduationTimes,
|
||||
year,
|
||||
programmes,
|
||||
programme
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
years.forEach(year => {
|
||||
levels.forEach(level => {
|
||||
const median = getMedian(graduationTimes[level][year])
|
||||
const mean = getMean(graduationTimes[level][year])
|
||||
const statistics = { onTime: 0, yearOver: 0, wayOver: 0 }
|
||||
|
||||
// Programme level breakdown
|
||||
data.programmes.medians[level][year] = { programmes: [], data: [] }
|
||||
data.programmes.means[level][year] = { programmes: [], data: [] }
|
||||
const programmeCodes = Object.keys(programmes[level][year])
|
||||
const programmeIds = getSortedProgIds(programmeCodes)
|
||||
|
||||
if (programmeIds.length > 0) {
|
||||
for (const prog of programmeIds) {
|
||||
const progMedian = getMedian(programmes[level][year][prog.code].graduationTimes)
|
||||
const progMean = getMean(programmes[level][year][prog.code].graduationTimes)
|
||||
|
||||
let goal = goals[level]
|
||||
if (faculty === 'H30' && Object.keys(goals.exceptions).includes(prog.code)) {
|
||||
@@ -295,28 +281,12 @@ const count = async (faculty, since, years, yearsList, levels, programmeFilter,
|
||||
code: prog.code,
|
||||
},
|
||||
]
|
||||
|
||||
data.programmes.means[level][year].programmes = [...data.programmes.means[level][year].programmes, prog.code]
|
||||
data.programmes.means[level][year].data = [
|
||||
...data.programmes.means[level][year].data,
|
||||
{
|
||||
y: progMean,
|
||||
amount: programmes[level][year][prog.code].graduationAmounts,
|
||||
statistics: progStatistics,
|
||||
name: prog.name,
|
||||
code: prog.code,
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
data.medians[level] = [
|
||||
...data.medians[level],
|
||||
{ y: median, amount: graduationAmounts[level][year], statistics, name: year },
|
||||
]
|
||||
data.means[level] = [
|
||||
...data.means[level],
|
||||
{ y: mean, amount: graduationAmounts[level][year], statistics, name: year },
|
||||
]
|
||||
})
|
||||
})
|
||||
|
||||
@@ -324,7 +294,8 @@ const count = async (faculty, since, years, yearsList, levels, programmeFilter,
|
||||
}
|
||||
|
||||
const countGraduationTimes = async (faculty, programmeFilter) => {
|
||||
const isAcademicYear = false // = yearType === 'ACADEMIC_YEAR'
|
||||
const programmes = (await getProgrammes(faculty, programmeFilter)).data
|
||||
const isAcademicYear = false
|
||||
const since = isAcademicYear ? new Date('2017-08-01') : new Date('2017-01-01')
|
||||
const yearsList = getYearsArray(since.getFullYear(), isAcademicYear)
|
||||
const levels = ['bachelor', 'bcMsCombo', 'master', 'doctor', 'licentiate']
|
||||
@@ -343,7 +314,7 @@ const countGraduationTimes = async (faculty, programmeFilter) => {
|
||||
MH30_001: 12,
|
||||
'320011-ma': 12,
|
||||
'320001-ma': 12,
|
||||
MH30_003: 12,
|
||||
MH30_003: 6,
|
||||
'320002-ma': 12,
|
||||
},
|
||||
}
|
||||
@@ -355,9 +326,10 @@ const countGraduationTimes = async (faculty, programmeFilter) => {
|
||||
// We count studyrights (vs. studyright_elements)
|
||||
// This way we get the whole time for a degree, even if the student was transferred to a new programme
|
||||
// E.g. started 8/2016 in old Bc, transferred to new 10/2020, graduated from new 1/2021 --> total 53 months (not 3)
|
||||
|
||||
const programmeCodes = programmes.map(prog => prog.code)
|
||||
const byGradYear = await count(
|
||||
faculty,
|
||||
programmeCodes,
|
||||
since,
|
||||
years,
|
||||
yearsList,
|
||||
@@ -367,8 +339,10 @@ const countGraduationTimes = async (faculty, programmeFilter) => {
|
||||
goals,
|
||||
'gradYear'
|
||||
)
|
||||
|
||||
const byStartYear = await count(
|
||||
faculty,
|
||||
programmeCodes,
|
||||
since,
|
||||
years,
|
||||
yearsList,
|
||||
@@ -381,7 +355,7 @@ const countGraduationTimes = async (faculty, programmeFilter) => {
|
||||
|
||||
const classSizes = getProgrammeObjectBasis(yearsList, levels, false)
|
||||
classSizes['programmes'] = {}
|
||||
await getClassSizes(faculty, since, classSizes, programmeFilter, years)
|
||||
await getClassSizes(faculty, programmeCodes, since, classSizes, programmeFilter, years)
|
||||
|
||||
return { id: faculty, goals, byGradYear, byStartYear, programmeNames, classSizes }
|
||||
}
|
||||
|
||||
@@ -2,26 +2,19 @@ const Sequelize = require('sequelize')
|
||||
const { Op } = Sequelize
|
||||
const { codes } = require('../../../config/programmeCodes')
|
||||
|
||||
const findRightProgramme = (studyrightElements, mode) => {
|
||||
const findRightProgramme = (studyrightElements, code) => {
|
||||
let programme = ''
|
||||
let programmeName = ''
|
||||
let studyRightElement = null
|
||||
|
||||
if (studyrightElements) {
|
||||
if (mode === 'started') {
|
||||
studyRightElement = studyrightElements
|
||||
.filter(sre => sre.element_detail.type === 20)
|
||||
.sort((a, b) => new Date(a.startdate) - new Date(b.startdate))[0]
|
||||
// this way round counts to old programmes, other way around to new programmes
|
||||
} else {
|
||||
// graduated
|
||||
studyRightElement = studyrightElements
|
||||
.filter(sre => sre.element_detail.type === 20)
|
||||
.sort((a, b) => new Date(b.startdate) - new Date(a.startdate))[0]
|
||||
}
|
||||
if (studyRightElement) {
|
||||
programme = studyRightElement.code
|
||||
programmeName = studyRightElement.element_detail.name
|
||||
studyRightElement = studyrightElements
|
||||
.filter(sre => sre.element_detail.type === 20)
|
||||
.filter(sre => sre.code === code)
|
||||
|
||||
if (studyRightElement.length > 0) {
|
||||
programme = studyRightElement[0].code
|
||||
programmeName = studyRightElement[0].element_detail.name
|
||||
}
|
||||
}
|
||||
return { programme, programmeName }
|
||||
@@ -37,9 +30,10 @@ const facultyFormatStudyright = studyright => {
|
||||
active,
|
||||
prioritycode,
|
||||
extentcode,
|
||||
student,
|
||||
studentStudentnumber,
|
||||
studyright_elements,
|
||||
startdate,
|
||||
facultyCode,
|
||||
} = studyright
|
||||
|
||||
return {
|
||||
@@ -52,18 +46,28 @@ const facultyFormatStudyright = studyright => {
|
||||
active,
|
||||
prioritycode,
|
||||
extentcode,
|
||||
studentnumber: student.studentnumber,
|
||||
facultyCode,
|
||||
studentnumber: studentStudentnumber,
|
||||
studyrightElements: studyright_elements,
|
||||
}
|
||||
}
|
||||
|
||||
const facultyProgrammeStudents = student => {
|
||||
const { studentnumber, home_country_en, gender_code, semester_enrollments } = student
|
||||
return {
|
||||
stundetNumber: studentnumber,
|
||||
homeCountryEn: home_country_en,
|
||||
genderCode: gender_code,
|
||||
semesters: semester_enrollments.map(s => s.dataValues),
|
||||
}
|
||||
}
|
||||
const formatFacultyTransfer = transfer => {
|
||||
const { sourcecode, targetcode, transferdate, studyrightid } = transfer
|
||||
const { sourcecode, targetcode, transferdate, studyrightid, studentnumber } = transfer
|
||||
return {
|
||||
sourcecode,
|
||||
targetcode,
|
||||
transferdate,
|
||||
studyrightid,
|
||||
studentnumber,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,11 +92,6 @@ const formatOrganization = org => {
|
||||
return { id, name, code, parentId: parent_id }
|
||||
}
|
||||
|
||||
const formatAbsence = absence => {
|
||||
const { semestercode, semester } = absence
|
||||
return { semestercode, start: semester.startdate, end: semester.enddate }
|
||||
}
|
||||
|
||||
const newProgrammes = [/^KH/, /^MH/, /^T/, /^LI/, /^K-/, /^FI/, /^00901$/, /^00910$/]
|
||||
|
||||
const isNewProgramme = code => {
|
||||
@@ -105,16 +104,17 @@ const isNewProgramme = code => {
|
||||
}
|
||||
|
||||
const checkTransfers = (s, insideTransfersStudyrights, transfersToOrAwayStudyrights) => {
|
||||
if (transfersToOrAwayStudyrights.includes(s.studyrightid)) return true
|
||||
if (insideTransfersStudyrights.includes(s.studyrightid) && new Date(s.studystartdate) < new Date('2017-08-01'))
|
||||
return true
|
||||
return false
|
||||
const allTransfers = [
|
||||
...insideTransfersStudyrights.map(sr => sr.studentnumber),
|
||||
...transfersToOrAwayStudyrights.map(sr => sr.studentnumber),
|
||||
]
|
||||
return allTransfers.includes(s.studentnumber)
|
||||
}
|
||||
|
||||
const getExtentFilter = includeAllSpecials => {
|
||||
const filteredExtents = [16] // always filter out secondary subject students
|
||||
if (!includeAllSpecials) {
|
||||
filteredExtents.push(7, 34, 22, 99, 14, 13, 9)
|
||||
filteredExtents.push(6, 7, 9, 13, 14, 18, 22, 23, 34, 99)
|
||||
}
|
||||
let studyrightWhere = {
|
||||
extentcode: {
|
||||
@@ -130,6 +130,9 @@ const mapCodesToIds = data => {
|
||||
for (const prog of data) {
|
||||
if (keys.includes(prog.code)) {
|
||||
prog.progId = codes[prog.code].toUpperCase()
|
||||
if (prog.code === 'T923103' && !prog.progId.includes('-DP')) {
|
||||
prog.progId = prog.progId + '-DP'
|
||||
}
|
||||
} else {
|
||||
prog.progId = prog.code
|
||||
}
|
||||
@@ -143,9 +146,9 @@ module.exports = {
|
||||
formatFacultyTransfer,
|
||||
formatFacultyThesisWriter,
|
||||
formatOrganization,
|
||||
formatAbsence,
|
||||
isNewProgramme,
|
||||
checkTransfers,
|
||||
getExtentFilter,
|
||||
mapCodesToIds,
|
||||
facultyProgrammeStudents,
|
||||
}
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
const { redisClient } = require('../redis')
|
||||
const moment = require('moment')
|
||||
const { findFacultyProgrammeCodes } = require('./faculty')
|
||||
|
||||
const createRedisKeyForFacultyProgrammes = (id, programmeFilter) => `FACULTY_PROGRAMMES_${id}_${programmeFilter}`
|
||||
const createRedisKeyForBasicStats = (id, yearType, programmeFilter, specialGroups) =>
|
||||
`FACULTY_BASIC_STATS_${id}_${yearType}_${programmeFilter}_${specialGroups}`
|
||||
const createRedisKeyForCreditStats = (id, yearType, programmeFilter, specialGroups) =>
|
||||
`FACULTY_CREDIT_STATS_${id}_${yearType}_${programmeFilter}_${specialGroups}`
|
||||
const createRedisKeyForCreditStats = (id, yearType, programmeFilter) =>
|
||||
`FACULTY_CREDIT_STATS_${id}_${yearType}_${programmeFilter}`
|
||||
const createRedisKeyForThesiswriters = (id, yearType, programmeFilter, specialGroups) =>
|
||||
`FACULTY_THESIS_WRITERS_STATS_${id}_${yearType}_${programmeFilter}_${specialGroups}`
|
||||
const createRedisKeyForGraduationTimeStats = (id, programmeFilter) =>
|
||||
@@ -27,7 +28,16 @@ const setFacultyProgrammes = async (id, data, programmeFilter) => {
|
||||
return dataToRedis
|
||||
}
|
||||
|
||||
const getFacultyProgrammes = async (id, programmeFilter) => {
|
||||
const getProgrammes = async (code, programmeFilter) => {
|
||||
const programmes = await getFacultyProgrammesFromRedis(code, programmeFilter)
|
||||
if (programmes) return programmes
|
||||
let updatedProgrammes = await findFacultyProgrammeCodes(code, programmeFilter)
|
||||
if (updatedProgrammes) updatedProgrammes = await setFacultyProgrammes(code, updatedProgrammes, programmeFilter)
|
||||
|
||||
return updatedProgrammes
|
||||
}
|
||||
|
||||
const getFacultyProgrammesFromRedis = async (id, programmeFilter) => {
|
||||
const redisKey = createRedisKeyForFacultyProgrammes(id, programmeFilter)
|
||||
const dataFromRedis = await redisClient.getAsync(redisKey)
|
||||
if (!dataFromRedis) return null
|
||||
@@ -54,9 +64,9 @@ const getBasicStats = async (id, yearType, programmeFilter, specialGroups) => {
|
||||
return JSON.parse(dataFromRedis)
|
||||
}
|
||||
|
||||
const setCreditStats = async (data, yearType, programmeFilter, specialGroups) => {
|
||||
const setCreditStats = async (data, yearType, programmeFilter) => {
|
||||
const { id } = data
|
||||
const redisKey = createRedisKeyForCreditStats(id, yearType, programmeFilter, specialGroups)
|
||||
const redisKey = createRedisKeyForCreditStats(id, yearType, programmeFilter)
|
||||
const dataToRedis = {
|
||||
...data,
|
||||
status: 'DONE',
|
||||
@@ -67,8 +77,8 @@ const setCreditStats = async (data, yearType, programmeFilter, specialGroups) =>
|
||||
return dataToRedis
|
||||
}
|
||||
|
||||
const getCreditStats = async (id, yearType, programmeFilter, specialGroups) => {
|
||||
const redisKey = createRedisKeyForCreditStats(id, yearType, programmeFilter, specialGroups)
|
||||
const getCreditStats = async (id, yearType, programmeFilter) => {
|
||||
const redisKey = createRedisKeyForCreditStats(id, yearType, programmeFilter)
|
||||
const dataFromRedis = await redisClient.getAsync(redisKey)
|
||||
if (!dataFromRedis) return null
|
||||
return JSON.parse(dataFromRedis)
|
||||
@@ -155,7 +165,7 @@ const setFacultyStudentStats = async (data, specialGroups, graduated) => {
|
||||
|
||||
module.exports = {
|
||||
setFacultyProgrammes,
|
||||
getFacultyProgrammes,
|
||||
getProgrammes,
|
||||
setBasicStats,
|
||||
getBasicStats,
|
||||
setCreditStats,
|
||||
|
||||
@@ -1,45 +1,52 @@
|
||||
const { indexOf } = require('lodash')
|
||||
const moment = require('moment')
|
||||
const { getAcademicYearDates } = require('../../util/semester')
|
||||
const {
|
||||
getCreditThresholds,
|
||||
getBachelorCreditGraphStats,
|
||||
getMasterCreditGraphStats,
|
||||
getDoctoralCreditGraphStats,
|
||||
getYearsArray,
|
||||
getCorrectStudentnumbers,
|
||||
tableTitles,
|
||||
getOnlyMasterCreditGraphStats,
|
||||
getOnlyMasterThresholds,
|
||||
} = require('../studyprogrammeHelpers')
|
||||
const { studytrackStudents, allStudyrights } = require('../studyprogramme')
|
||||
const { getCreditThresholds, getYearsArray } = require('../studyprogrammeHelpers')
|
||||
const { studytrackStudents } = require('../studyprogramme')
|
||||
const { getStudyRightsByExtent, getStudyRightsByBachelorStart, getTransfersIn, getTransfersOut } = require('./faculty')
|
||||
const { checkTransfers } = require('./facultyHelpers')
|
||||
|
||||
const getStudentData = (startDate, students, thresholdKeys, thresholdAmounts, limits, limitKeys, prog) => {
|
||||
const getStudentData = (
|
||||
startDate,
|
||||
students,
|
||||
prog,
|
||||
limits = [],
|
||||
limitKeys = [],
|
||||
thresholdKeys = [],
|
||||
thresholdAmounts = []
|
||||
) => {
|
||||
let data = {}
|
||||
let programmeData = {}
|
||||
const creditCounts = []
|
||||
thresholdKeys.forEach(t => (data[t] = 0))
|
||||
limitKeys.forEach(t => (programmeData[t] = 0))
|
||||
students.forEach(({ credits }) => {
|
||||
const creditcount = credits
|
||||
.filter(credit => moment(credit.attainment_date).isAfter(startDate))
|
||||
.reduce((prev, curr) => (prev += curr.credits), 0)
|
||||
|
||||
data[thresholdKeys[0]] += creditcount < thresholdAmounts[0] ? 1 : 0
|
||||
data[thresholdKeys[1]] += creditcount >= thresholdAmounts[0] && creditcount < thresholdAmounts[1] ? 1 : 0
|
||||
data[thresholdKeys[2]] += creditcount >= thresholdAmounts[1] && creditcount < thresholdAmounts[2] ? 1 : 0
|
||||
data[thresholdKeys[3]] += creditcount >= thresholdAmounts[2] && creditcount < thresholdAmounts[3] ? 1 : 0
|
||||
data[thresholdKeys[4]] += creditcount >= thresholdAmounts[3] && creditcount < thresholdAmounts[4] ? 1 : 0
|
||||
if (thresholdKeys.length === 8) {
|
||||
data[thresholdKeys[5]] += creditcount >= thresholdAmounts[4] && creditcount < thresholdAmounts[5] ? 1 : 0
|
||||
data[thresholdKeys[6]] += creditcount >= thresholdAmounts[5] && creditcount < thresholdAmounts[6] ? 1 : 0
|
||||
data[thresholdKeys[7]] += creditcount >= thresholdAmounts[6] ? 1 : 0
|
||||
} else if (thresholdKeys.length === 7) {
|
||||
data[thresholdKeys[5]] += creditcount >= thresholdAmounts[4] && creditcount < thresholdAmounts[5] ? 1 : 0
|
||||
data[thresholdKeys[6]] += creditcount >= thresholdAmounts[5] ? 1 : 0
|
||||
.filter(credit => moment(credit.attainment_date).isSameOrAfter(startDate))
|
||||
.reduce((prev, curr) => prev + curr.credits, 0)
|
||||
creditCounts.push(creditcount)
|
||||
// Data is only used for doctoral programmes, otherwise only programmeData is needed
|
||||
if (prog === 'T') {
|
||||
data[thresholdKeys[0]] += creditcount < thresholdAmounts[0] ? 1 : 0
|
||||
data[thresholdKeys[1]] += creditcount >= thresholdAmounts[0] && creditcount < thresholdAmounts[1] ? 1 : 0
|
||||
data[thresholdKeys[2]] += creditcount >= thresholdAmounts[1] && creditcount < thresholdAmounts[2] ? 1 : 0
|
||||
data[thresholdKeys[3]] += creditcount >= thresholdAmounts[2] && creditcount < thresholdAmounts[3] ? 1 : 0
|
||||
if (thresholdKeys.length === 8) {
|
||||
data[thresholdKeys[4]] += creditcount >= thresholdAmounts[3] && creditcount < thresholdAmounts[4] ? 1 : 0
|
||||
data[thresholdKeys[5]] += creditcount >= thresholdAmounts[4] && creditcount < thresholdAmounts[5] ? 1 : 0
|
||||
data[thresholdKeys[6]] += creditcount >= thresholdAmounts[5] && creditcount < thresholdAmounts[6] ? 1 : 0
|
||||
data[thresholdKeys[7]] += creditcount >= thresholdAmounts[6] ? 1 : 0
|
||||
} else if (thresholdKeys.length === 7) {
|
||||
data[thresholdKeys[4]] += creditcount >= thresholdAmounts[3] && creditcount < thresholdAmounts[4] ? 1 : 0
|
||||
data[thresholdKeys[5]] += creditcount >= thresholdAmounts[4] && creditcount < thresholdAmounts[5] ? 1 : 0
|
||||
data[thresholdKeys[6]] += creditcount >= thresholdAmounts[5] ? 1 : 0
|
||||
} else if (thresholdKeys.length === 6) {
|
||||
data[thresholdKeys[4]] += creditcount >= thresholdAmounts[3] && creditcount < thresholdAmounts[4] ? 1 : 0
|
||||
data[thresholdKeys[5]] += creditcount >= thresholdAmounts[4] ? 1 : 0
|
||||
} else {
|
||||
data[thresholdKeys[4]] += creditcount >= thresholdAmounts[3] ? 1 : 0
|
||||
}
|
||||
} else {
|
||||
data[thresholdKeys[5]] += creditcount >= thresholdAmounts[4] ? 1 : 0
|
||||
}
|
||||
if (['KH', 'MH'].includes(prog)) {
|
||||
programmeData[limitKeys[0]] += creditcount <= limits[5][0] ? 1 : 0
|
||||
programmeData[limitKeys[1]] += limits[4][0] <= creditcount && limits[4][1] > creditcount ? 1 : 0
|
||||
programmeData[limitKeys[2]] += limits[3][0] <= creditcount && limits[3][1] > creditcount ? 1 : 0
|
||||
@@ -49,275 +56,238 @@ const getStudentData = (startDate, students, thresholdKeys, thresholdAmounts, li
|
||||
}
|
||||
})
|
||||
|
||||
return [data, programmeData]
|
||||
return { data, progData: programmeData, creditCounts }
|
||||
}
|
||||
|
||||
const createLimits = (months, creditsToAdd) => {
|
||||
return [
|
||||
[Math.ceil(months * (60 / 12)) + creditsToAdd, null],
|
||||
[Math.ceil(months * (45 / 12)) + creditsToAdd, Math.ceil(months * (60 / 12)) + creditsToAdd],
|
||||
[Math.ceil(months * (30 / 12)) + creditsToAdd, Math.ceil(months * (45 / 12)) + creditsToAdd],
|
||||
[Math.ceil(months * (15 / 12)) + creditsToAdd, Math.ceil(months * (30 / 12)) + creditsToAdd],
|
||||
[creditsToAdd + 1, Math.ceil(months * (15 / 12)) + creditsToAdd],
|
||||
[creditsToAdd, null],
|
||||
]
|
||||
}
|
||||
|
||||
const createYearlyTitles = (start, limitList) => {
|
||||
return [
|
||||
`${start} Credits`,
|
||||
`${limitList[4][0]} ≤ Credits < ${limitList[4][1]}`,
|
||||
`${limitList[3][0]} ≤ Credits < ${limitList[3][1]}`,
|
||||
`${limitList[2][0]} ≤ Credits < ${limitList[2][1]}`,
|
||||
`${limitList[1][0]} ≤ Credits < ${limitList[1][1]}`,
|
||||
`${limitList[0][0]} ≤ Credits`,
|
||||
]
|
||||
}
|
||||
|
||||
const filterOutTransfers = async (studyrights, programmeCode, startDate, endDate) => {
|
||||
const transferredIn = await getTransfersIn(programmeCode, startDate, endDate)
|
||||
const transferredOut = await getTransfersOut(programmeCode, startDate, endDate)
|
||||
const filteredStudyrights = studyrights.filter(sr => !checkTransfers(sr, transferredIn, transferredOut))
|
||||
return filteredStudyrights
|
||||
}
|
||||
|
||||
const combineFacultyStudentProgress = async (faculty, programmes, specialGroups, graduated) => {
|
||||
const since = new Date('2017-08-01')
|
||||
const isAcademicYear = true
|
||||
const includeYearsCombined = true
|
||||
const includeAllSpecials = specialGroups === 'SPECIAL_INCLUDED'
|
||||
const includeGraduated = graduated === 'GRADUATED_INCLUDED'
|
||||
const yearsArray = getYearsArray(since.getFullYear(), isAcademicYear, includeYearsCombined)
|
||||
|
||||
let allBachelorGraphStats = getBachelorCreditGraphStats(yearsArray)
|
||||
let allBsMsGraphstats = getMasterCreditGraphStats(yearsArray)
|
||||
let allMastersGraphStats = getOnlyMasterCreditGraphStats(yearsArray)
|
||||
let allDoctoralGraphStats = getDoctoralCreditGraphStats(yearsArray)
|
||||
|
||||
let bachelorsProgrammeStats = {}
|
||||
let bcMsProgrammeStats = {}
|
||||
let mastersProgrammeStats = {}
|
||||
let doctoralProgrammeStats = {}
|
||||
|
||||
let bachelorsTableStats = new Array(yearsArray.length)
|
||||
let bachelorMastersTableStats = new Array(yearsArray.length)
|
||||
let mastersTableStats = new Array(yearsArray.length)
|
||||
let doctoralTableStats = new Array(yearsArray.length)
|
||||
|
||||
let yearlyBachelorTitles = []
|
||||
let yearlyBcMsTitles = []
|
||||
let yearlyMasterTitles = []
|
||||
let graduationStatus = [0]
|
||||
if (includeGraduated) {
|
||||
graduationStatus = [0, 1]
|
||||
}
|
||||
const yearsArray = getYearsArray(since.getFullYear(), true, true)
|
||||
const progressStats = {
|
||||
id: faculty,
|
||||
years: yearsArray,
|
||||
yearlyBachelorTitles: [],
|
||||
yearlyBcMsTitles: [],
|
||||
yearlyMasterTitles: [],
|
||||
yearlyLicentiateTitles: [],
|
||||
programmeNames: {},
|
||||
bachelorsProgStats: {},
|
||||
bcMsProgStats: {},
|
||||
mastersProgStats: {},
|
||||
doctoralProgStats: {},
|
||||
creditCounts: {
|
||||
bachelor: {},
|
||||
bachelorMaster: {},
|
||||
master: {},
|
||||
licentiate: {}, // only used for programmes MH30_001 and MH30_003
|
||||
doctor: {},
|
||||
},
|
||||
}
|
||||
|
||||
const reversedYears = [...yearsArray].reverse()
|
||||
reversedYears.forEach(year => {
|
||||
bachelorsTableStats[indexOf(reversedYears, year)] = [year, 0, 0, 0, 0, 0, 0, 0, 0, 0]
|
||||
bachelorMastersTableStats[indexOf(reversedYears, year)] = [year, 0, 0, 0, 0, 0, 0, 0, 0]
|
||||
mastersTableStats[indexOf(reversedYears, year)] = [year, 0, 0, 0, 0, 0, 0, 0]
|
||||
doctoralTableStats[indexOf(reversedYears, year)] = [year, 0, 0, 0, 0, 0, 0, 0, 0]
|
||||
})
|
||||
|
||||
const limitKeys = ['zero', 't5', 't4', 't3', 't2', 't1']
|
||||
let bachelorlimits = []
|
||||
let masterlimits = []
|
||||
let bcmslimits = []
|
||||
let licentiatelimits = []
|
||||
const bachelorProgress = {}
|
||||
const masterProgress = {}
|
||||
const bcmsProgress = {}
|
||||
const licentiateProgress = {}
|
||||
const doctoralProgress = {}
|
||||
for (const year of reversedYears) {
|
||||
if (year === 'Total') continue
|
||||
const { startDate, endDate } = getAcademicYearDates(year, since)
|
||||
const today = moment()
|
||||
const lastDayOfMonth = moment(today).endOf('month')
|
||||
const lastDayOfMonth = moment().endOf('month')
|
||||
let months = Math.round(moment.duration(moment(lastDayOfMonth).diff(moment(startDate))).asMonths())
|
||||
|
||||
if (months >= 36) {
|
||||
bachelorlimits = [
|
||||
[Math.ceil(36 * (60 / 12)), null],
|
||||
[Math.ceil(36 * (45 / 12)), Math.ceil(36 * (60 / 12))],
|
||||
[Math.ceil(36 * (30 / 12)), Math.ceil(36 * (45 / 12))],
|
||||
[Math.ceil(36 * (15 / 12)), Math.ceil(36 * (30 / 12))],
|
||||
[1, Math.ceil(36 * (15 / 12))],
|
||||
[0, null],
|
||||
]
|
||||
bachelorlimits = createLimits(36, 0)
|
||||
} else {
|
||||
bachelorlimits = [
|
||||
[Math.ceil(months * (60 / 12)), null],
|
||||
[Math.ceil(months * (45 / 12)), Math.ceil(months * (60 / 12))],
|
||||
[Math.ceil(months * (30 / 12)), Math.ceil(months * (45 / 12))],
|
||||
[Math.ceil(months * (15 / 12)), Math.ceil(months * (30 / 12))],
|
||||
[1, Math.ceil(months * (15 / 12))],
|
||||
[0, null],
|
||||
]
|
||||
bachelorlimits = createLimits(months, 0)
|
||||
}
|
||||
if (months >= 24) {
|
||||
masterlimits = [
|
||||
[Math.ceil(24 * (60 / 12)), null],
|
||||
[Math.ceil(24 * (45 / 12)), Math.ceil(24 * (60 / 12))],
|
||||
[Math.ceil(24 * (30 / 12)), Math.ceil(24 * (45 / 12))],
|
||||
[Math.ceil(24 * (15 / 12)), Math.ceil(24 * (30 / 12))],
|
||||
[1, Math.ceil(20 * (15 / 12))],
|
||||
[0, null],
|
||||
]
|
||||
masterlimits = createLimits(24, 0)
|
||||
} else {
|
||||
masterlimits = [
|
||||
[Math.ceil(months * (60 / 12)), null],
|
||||
[Math.ceil(months * (45 / 12)), Math.ceil(months * (60 / 12))],
|
||||
[Math.ceil(months * (30 / 12)), Math.ceil(months * (45 / 12))],
|
||||
[Math.ceil(months * (15 / 12)), Math.ceil(months * (30 / 12))],
|
||||
[1, Math.ceil(months * (15 / 12))],
|
||||
[0, null],
|
||||
]
|
||||
masterlimits = createLimits(months, 0)
|
||||
}
|
||||
if (months >= 60) {
|
||||
bcmslimits = [
|
||||
[Math.ceil(60 * (60 / 12)) + 180, null],
|
||||
[Math.ceil(60 * (45 / 12)) + 180, Math.ceil(60 * (60 / 12)) + 180],
|
||||
[Math.ceil(60 * (30 / 12)) + 180, Math.ceil(60 * (45 / 12)) + 180],
|
||||
[Math.ceil(60 * (15 / 12)) + 180, Math.ceil(60 * (30 / 12)) + 180],
|
||||
[181, Math.ceil(60 * (15 / 12)) + 180],
|
||||
[180, null],
|
||||
]
|
||||
if (months >= 72 && faculty === 'H90') {
|
||||
bcmslimits = createLimits(72, 0)
|
||||
} else if (months >= 60) {
|
||||
bcmslimits = createLimits(60, 0)
|
||||
} else {
|
||||
bcmslimits = [
|
||||
[Math.ceil(months * (60 / 12)) + 180, null],
|
||||
[Math.ceil(months * (45 / 12)) + 180, Math.ceil(months * (60 / 12)) + 180],
|
||||
[Math.ceil(months * (30 / 12)) + 180, Math.ceil(months * (45 / 12)) + 180],
|
||||
[Math.ceil(months * (15 / 12)) + 180, Math.ceil(months * (30 / 12)) + 180],
|
||||
[181, Math.ceil(months * (15 / 12)) + 180],
|
||||
[180, null],
|
||||
]
|
||||
bcmslimits = createLimits(months, 0)
|
||||
}
|
||||
if (months >= 72 && faculty === 'H30') {
|
||||
licentiatelimits = createLimits(72, 0)
|
||||
} else if (months >= 60) {
|
||||
licentiatelimits = createLimits(60, 0)
|
||||
} else {
|
||||
licentiatelimits = createLimits(months, 0)
|
||||
}
|
||||
progressStats.yearlyBachelorTitles = [...progressStats.yearlyBachelorTitles, createYearlyTitles(0, bachelorlimits)]
|
||||
progressStats.yearlyMasterTitles = [...progressStats.yearlyMasterTitles, createYearlyTitles(0, masterlimits)]
|
||||
progressStats.yearlyLicentiateTitles = [
|
||||
...progressStats.yearlyLicentiateTitles,
|
||||
createYearlyTitles(0, licentiatelimits),
|
||||
]
|
||||
progressStats.yearlyBcMsTitles = [...progressStats.yearlyBcMsTitles, createYearlyTitles(0, bcmslimits)]
|
||||
|
||||
yearlyBachelorTitles.push([
|
||||
'0 Credits',
|
||||
`${bachelorlimits[4][0]} ≤ Credits < ${bachelorlimits[4][1]}`,
|
||||
`${bachelorlimits[3][0]} ≤ Credits < ${bachelorlimits[3][1]}`,
|
||||
`${bachelorlimits[2][0]} ≤ Credits < ${bachelorlimits[2][1]}`,
|
||||
`${bachelorlimits[1][0]} ≤ Credits < ${bachelorlimits[1][1]}`,
|
||||
`${bachelorlimits[0][0]} ≤ Credits`,
|
||||
])
|
||||
yearlyMasterTitles.push([
|
||||
'0 Credits',
|
||||
`${masterlimits[4][0]} ≤ Credits < ${masterlimits[4][1]}`,
|
||||
`${masterlimits[3][0]} ≤ Credits < ${masterlimits[3][1]}`,
|
||||
`${masterlimits[2][0]} ≤ Credits < ${masterlimits[2][1]}`,
|
||||
`${masterlimits[1][0]} ≤ Credits < ${masterlimits[1][1]}`,
|
||||
`${masterlimits[0][0]} ≤ Credits`,
|
||||
])
|
||||
yearlyBcMsTitles.push([
|
||||
'180 Credits',
|
||||
`${bcmslimits[4][0]} ≤ Credits < ${bcmslimits[4][1]}`,
|
||||
`${bcmslimits[3][0]} ≤ Credits < ${bcmslimits[3][1]}`,
|
||||
`${bcmslimits[2][0]} ≤ Credits < ${bcmslimits[2][1]}`,
|
||||
`${bcmslimits[1][0]} ≤ Credits < ${bcmslimits[1][1]}`,
|
||||
`${bcmslimits[0][0]} ≤ Credits`,
|
||||
])
|
||||
for (const { progId, code } of programmes) {
|
||||
let { creditThresholdKeys, creditThresholdAmounts } = getCreditThresholds(code)
|
||||
if (!creditThresholdKeys) return
|
||||
const studentnumbers = await getCorrectStudentnumbers({
|
||||
codes: [code],
|
||||
startDate,
|
||||
endDate,
|
||||
includeAllSpecials,
|
||||
includeGraduated,
|
||||
})
|
||||
progressStats.creditCounts.bachelor[year] = []
|
||||
progressStats.creditCounts.bachelorMaster[year] = []
|
||||
progressStats.creditCounts.master[year] = []
|
||||
progressStats.creditCounts.licentiate[year] = []
|
||||
progressStats.creditCounts.doctor[year] = []
|
||||
|
||||
const all = await allStudyrights(code, studentnumbers)
|
||||
const students = await studytrackStudents(studentnumbers)
|
||||
for (const { progId, code, name } of programmes) {
|
||||
let extentcodes = [1, 2, 3, 4]
|
||||
if (includeAllSpecials) {
|
||||
extentcodes = [...extentcodes, ...[6, 7, 9, 13, 14, 22, 23, 34, 99]]
|
||||
}
|
||||
// For bachelor-master we want to have the start of the bachelor programme of the student.
|
||||
let studyrights = code.includes('MH')
|
||||
? await getStudyRightsByBachelorStart(faculty, startDate, endDate, code, extentcodes, graduationStatus)
|
||||
: await getStudyRightsByExtent(faculty, startDate, endDate, code, extentcodes, graduationStatus)
|
||||
|
||||
// Get credit threshold values: 'combined study programme' is false and 'only master studyright' is true
|
||||
|
||||
if (!(progId in progressStats.programmeNames)) {
|
||||
progressStats.programmeNames[progId] = { code: code, ...name }
|
||||
}
|
||||
if (!includeAllSpecials) {
|
||||
studyrights = await filterOutTransfers(studyrights, code, startDate, endDate)
|
||||
}
|
||||
if (code.includes('KH')) {
|
||||
const data = getStudentData(
|
||||
startDate,
|
||||
students,
|
||||
creditThresholdKeys,
|
||||
creditThresholdAmounts,
|
||||
bachelorlimits,
|
||||
limitKeys,
|
||||
'KH'
|
||||
)
|
||||
const studentData = data[0]
|
||||
const programmeData = data[1]
|
||||
if (!(progId in bachelorsProgrammeStats)) {
|
||||
bachelorsProgrammeStats[progId] = new Array(reversedYears.length - 1)
|
||||
}
|
||||
bachelorsTableStats[indexOf(reversedYears, year)][1] += all.length || 0
|
||||
if (year !== 'Total') {
|
||||
bachelorsProgrammeStats[progId][indexOf(reversedYears, year)] = limitKeys.map(key => programmeData[key])
|
||||
}
|
||||
for (const key of Object.keys(studentData)) {
|
||||
bachelorsTableStats[indexOf(reversedYears, year)][indexOf(creditThresholdKeys, key) + 2] +=
|
||||
studentData[key] || 0
|
||||
allBachelorGraphStats[key].data[indexOf(yearsArray, year)] += studentData[key] || 0
|
||||
const allBachelors = studyrights.filter(sr => sr.extentcode === 1).map(sr => sr.studentnumber)
|
||||
const students = await studytrackStudents(allBachelors)
|
||||
const { progData, creditCounts } = getStudentData(startDate, students, 'KH', bachelorlimits, limitKeys)
|
||||
|
||||
progressStats.creditCounts.bachelor[year] = [...progressStats.creditCounts.bachelor[year], ...creditCounts]
|
||||
|
||||
if (!(progId in bachelorProgress)) {
|
||||
bachelorProgress[progId] = new Array(reversedYears.length - 1)
|
||||
}
|
||||
bachelorProgress[progId][indexOf(reversedYears, year)] = limitKeys.map(key => progData[key])
|
||||
} else if (code.includes('MH')) {
|
||||
const all_BcMs = all.filter(sr => sr.studyrightid.slice(-2) === '-2')
|
||||
const all_Ms = all.filter(sr => sr.studyrightid.slice(-2) !== '-2')
|
||||
const all_MsStudentnumbers = all_Ms.map(sr => sr.studentnumber)
|
||||
const all_BcMsStudentnumbers = all_BcMs.map(sr => sr.studentnumber)
|
||||
mastersTableStats[indexOf(reversedYears, year)][1] += all_Ms.length || 0
|
||||
bachelorMastersTableStats[indexOf(reversedYears, year)][1] += all_BcMs.length || 0
|
||||
const allMsStudents = studyrights
|
||||
.filter(sr => sr.extentcode === 2 && sr.studyrightid.slice(-2) !== '-2')
|
||||
.map(sr => sr.studentnumber)
|
||||
const allBcMsStudents = studyrights
|
||||
.filter(sr => sr.extentcode === 2 && sr.studyrightid.slice(-2) === '-2')
|
||||
.map(sr => sr.studentnumber)
|
||||
const bcMsStudents = await studytrackStudents(allBcMsStudents)
|
||||
const msStudents = await studytrackStudents(allMsStudents)
|
||||
|
||||
const msStudents = students.filter(student => all_MsStudentnumbers.includes(student.studentnumber))
|
||||
const bcMsStudents = students.filter(student => all_BcMsStudentnumbers.includes(student.studentnumber))
|
||||
|
||||
const data = getStudentData(
|
||||
const { progData: bcMsProgdata, creditCounts: creditCountsBcMs } = getStudentData(
|
||||
startDate,
|
||||
bcMsStudents,
|
||||
creditThresholdKeys,
|
||||
creditThresholdAmounts,
|
||||
'MH',
|
||||
bcmslimits,
|
||||
limitKeys,
|
||||
'MH'
|
||||
limitKeys
|
||||
)
|
||||
const bcMsStudentdata = data[0]
|
||||
const bcMsProgrammedata = data[1]
|
||||
const { msOnlyCreditThresholdKeys, msOnlyCreditThresholdAmount } = getOnlyMasterThresholds()
|
||||
const data2 = getStudentData(
|
||||
const { progData: msProgdata, creditCounts: creditCountsMaster } = getStudentData(
|
||||
startDate,
|
||||
msStudents,
|
||||
msOnlyCreditThresholdKeys,
|
||||
msOnlyCreditThresholdAmount,
|
||||
'MH',
|
||||
masterlimits,
|
||||
limitKeys,
|
||||
'MH'
|
||||
limitKeys
|
||||
)
|
||||
const msStudentdata = data2[0]
|
||||
const msProgrammedata = data2[1]
|
||||
if (!(progId in mastersProgrammeStats)) {
|
||||
mastersProgrammeStats[progId] = new Array(reversedYears.length - 1)
|
||||
}
|
||||
if (!(progId in bcMsProgrammeStats)) {
|
||||
bcMsProgrammeStats[progId] = new Array(reversedYears.length - 1)
|
||||
}
|
||||
if (year !== 'Total') {
|
||||
mastersProgrammeStats[progId][indexOf(reversedYears, year)] = limitKeys.map(key => msProgrammedata[key])
|
||||
bcMsProgrammeStats[progId][indexOf(reversedYears, year)] = limitKeys.map(key => bcMsProgrammedata[key])
|
||||
}
|
||||
for (const key of Object.keys(msStudentdata)) {
|
||||
mastersTableStats[indexOf(reversedYears, year)][indexOf(msOnlyCreditThresholdKeys, key) + 2] +=
|
||||
msStudentdata[key] || 0
|
||||
allMastersGraphStats[key].data[indexOf(yearsArray, year)] += msStudentdata[key] || 0
|
||||
}
|
||||
for (const key of Object.keys(bcMsStudentdata)) {
|
||||
bachelorMastersTableStats[indexOf(reversedYears, year)][indexOf(creditThresholdKeys, key) + 2] +=
|
||||
bcMsStudentdata[key] || 0
|
||||
allBsMsGraphstats[key].data[indexOf(yearsArray, year)] += bcMsStudentdata[key] || 0
|
||||
if (['MH30_001', 'MH30_003'].includes(code)) {
|
||||
progressStats.creditCounts.licentiate[year] = [
|
||||
...progressStats.creditCounts.licentiate[year],
|
||||
...creditCountsMaster,
|
||||
]
|
||||
if (!(progId in licentiateProgress)) {
|
||||
licentiateProgress[progId] = new Array(reversedYears.length - 1)
|
||||
}
|
||||
licentiateProgress[progId][indexOf(reversedYears, year)] = limitKeys.map(key => msProgdata[key])
|
||||
} else {
|
||||
progressStats.creditCounts.bachelorMaster[year] = [
|
||||
...progressStats.creditCounts.bachelorMaster[year],
|
||||
...creditCountsBcMs,
|
||||
]
|
||||
progressStats.creditCounts.master[year] = [...progressStats.creditCounts.master[year], ...creditCountsMaster]
|
||||
if (!(progId in masterProgress)) {
|
||||
masterProgress[progId] = new Array(reversedYears.length - 1)
|
||||
bcmsProgress[progId] = new Array(reversedYears.length - 1)
|
||||
}
|
||||
masterProgress[progId][indexOf(reversedYears, year)] = limitKeys.map(key => msProgdata[key])
|
||||
bcmsProgress[progId][indexOf(reversedYears, year)] = limitKeys.map(key => bcMsProgdata[key])
|
||||
}
|
||||
} else {
|
||||
const data = getStudentData(startDate, students, creditThresholdKeys, creditThresholdAmounts, [], [], 'T')
|
||||
const studentData = data[0]
|
||||
doctoralTableStats[indexOf(reversedYears, year)][1] += all.length || 0
|
||||
if (!(progId in doctoralProgrammeStats)) {
|
||||
doctoralProgrammeStats[progId] = new Array(reversedYears.length - 1)
|
||||
}
|
||||
if (year !== 'Total') {
|
||||
doctoralProgrammeStats[progId][indexOf(reversedYears, year)] = creditThresholdKeys.map(
|
||||
key => studentData[key]
|
||||
)
|
||||
}
|
||||
for (const key of Object.keys(studentData)) {
|
||||
doctoralTableStats[indexOf(reversedYears, year)][indexOf(creditThresholdKeys, key) + 2] +=
|
||||
studentData[key] || 0
|
||||
allDoctoralGraphStats[key].data[indexOf(yearsArray, year)] += studentData[key] || 0
|
||||
const all = studyrights.filter(sr => sr.extentcode === 4 || sr.extentcode === 3).map(sr => sr.studentnumber)
|
||||
const doctoralStudents = await studytrackStudents(all)
|
||||
let { creditThresholdKeys, creditThresholdAmounts } = getCreditThresholds(code)
|
||||
|
||||
const { data, creditCounts } = getStudentData(
|
||||
startDate,
|
||||
doctoralStudents,
|
||||
'T',
|
||||
[],
|
||||
[],
|
||||
creditThresholdKeys,
|
||||
creditThresholdAmounts
|
||||
)
|
||||
|
||||
progressStats.creditCounts.doctor[year] = [...progressStats.creditCounts.doctor[year], ...creditCounts]
|
||||
|
||||
if (!(progId in doctoralProgress)) {
|
||||
doctoralProgress[progId] = new Array(reversedYears.length - 1)
|
||||
}
|
||||
doctoralProgress[progId][indexOf(reversedYears, year)] = creditThresholdKeys.map(key => data[key])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const facultyProgressStats = {
|
||||
id: faculty,
|
||||
years: yearsArray,
|
||||
bachelorsTableStats: bachelorsTableStats,
|
||||
bcMsTableStats: bachelorMastersTableStats,
|
||||
mastersTableStats: mastersTableStats,
|
||||
doctoralTableStats: doctoralTableStats,
|
||||
bachelorsGraphStats: allBachelorGraphStats,
|
||||
bcMsGraphStats: allBsMsGraphstats,
|
||||
mastersGraphStats: allMastersGraphStats,
|
||||
doctoralGraphStats: allDoctoralGraphStats,
|
||||
bachelorTitles: tableTitles.creditProgress.bachelor,
|
||||
bcMsTitles: tableTitles.creditProgress.master,
|
||||
mastersTitles: tableTitles.creditProgress.masterOnly,
|
||||
doctoralTitles: tableTitles.creditProgress.doctoral,
|
||||
yearlyBachelorTitles: yearlyBachelorTitles,
|
||||
yearlyBcMsTitles: yearlyBcMsTitles,
|
||||
yearlyMasterTitles: yearlyMasterTitles,
|
||||
programmeNames: programmes.reduce(
|
||||
(obj, dataItem) => ({ ...obj, [dataItem.progId]: { code: dataItem.code, ...dataItem.name } }),
|
||||
{}
|
||||
),
|
||||
bachelorsProgrammeStats: bachelorsProgrammeStats,
|
||||
bcMsProgrammeStats: bcMsProgrammeStats,
|
||||
mastersProgrammeStats: mastersProgrammeStats,
|
||||
doctoralProgrammeStats: doctoralProgrammeStats,
|
||||
}
|
||||
return facultyProgressStats
|
||||
progressStats.bachelorsProgStats = Object.keys(bachelorProgress)
|
||||
.filter(prog => !bachelorProgress[prog].every(year => year.every(value => value === 0)))
|
||||
.reduce((result, prog) => ({ ...result, [prog]: bachelorProgress[prog] }), {})
|
||||
progressStats.mastersProgStats = Object.keys(masterProgress)
|
||||
.filter(prog => !masterProgress[prog].every(year => year.every(value => value === 0)))
|
||||
.reduce((result, prog) => ({ ...result, [prog]: masterProgress[prog] }), {})
|
||||
progressStats.bcMsProgStats = Object.keys(bcmsProgress)
|
||||
.filter(prog => !bcmsProgress[prog].every(year => year.every(value => value === 0)))
|
||||
.reduce((result, prog) => ({ ...result, [prog]: bcmsProgress[prog] }), {})
|
||||
progressStats.licentiateProgStats = Object.keys(licentiateProgress)
|
||||
.filter(prog => !licentiateProgress[prog].every(year => year.every(value => value === 0)))
|
||||
.reduce((result, prog) => ({ ...result, [prog]: licentiateProgress[prog] }), {})
|
||||
progressStats.doctoralProgStats = Object.keys(doctoralProgress)
|
||||
.filter(prog => !doctoralProgress[prog].every(year => year.every(value => value === 0)))
|
||||
.reduce((result, prog) => ({ ...result, [prog]: doctoralProgress[prog] }), {})
|
||||
return progressStats
|
||||
}
|
||||
module.exports = { combineFacultyStudentProgress }
|
||||
|
||||
@@ -1,39 +1,11 @@
|
||||
const {
|
||||
getStartDate,
|
||||
getYearsArray,
|
||||
getPercentage,
|
||||
getYearsObject,
|
||||
getCorrectStudentnumbers,
|
||||
tableTitles,
|
||||
} = require('../studyprogrammeHelpers')
|
||||
const {
|
||||
studytrackStudents,
|
||||
allStudyrights,
|
||||
startedStudyrights,
|
||||
inactiveStudyrights,
|
||||
enrolledStudents,
|
||||
absentStudents,
|
||||
graduatedStudyRights,
|
||||
} = require('../studyprogramme')
|
||||
const { getStartDate, getYearsArray, getPercentage, getYearsObject, tableTitles } = require('../studyprogrammeHelpers')
|
||||
const { getAcademicYearDates } = require('../../util/semester')
|
||||
|
||||
const getStudentData = students => {
|
||||
let data = { female: 0, male: 0, finnish: 0 }
|
||||
|
||||
students.forEach(({ gender_code, home_country_en }) => {
|
||||
data.male += gender_code === '1' ? 1 : 0
|
||||
data.female += gender_code === '2' ? 1 : 0
|
||||
data.finnish += home_country_en === 'Finland' ? 1 : 0
|
||||
})
|
||||
return data
|
||||
}
|
||||
|
||||
// Goes through the faculty and its study programmes for the year
|
||||
const getFacultyDataForYear = async ({ programmes, since, settings, year, years, programmeTableStats }) => {
|
||||
const { includeAllSpecials, includeGraduated } = settings
|
||||
const { startDate, endDate } = getAcademicYearDates(year, since)
|
||||
// Totals for the year into table stats
|
||||
const totals = {
|
||||
const { getStudyRightsByExtent, getStudentsByStudentnumbers, getTransfersIn } = require('./faculty')
|
||||
const { checkTransfers } = require('./facultyHelpers')
|
||||
const { inactiveStudyrights, absentStudents, enrolledStudents } = require('../studyprogramme')
|
||||
const { graduatedStudyRights } = require('../studyprogramme')
|
||||
const emptyTotals = () => {
|
||||
return {
|
||||
total: 0,
|
||||
allStarted: 0,
|
||||
allEnrolled: 0,
|
||||
@@ -43,116 +15,223 @@ const getFacultyDataForYear = async ({ programmes, since, settings, year, years,
|
||||
allFinnish: 0,
|
||||
allFemale: 0,
|
||||
allMale: 0,
|
||||
allOtherUnknown: 0,
|
||||
allOtherCountries: 0,
|
||||
}
|
||||
}
|
||||
const toltalsCalculation = totals => {
|
||||
return [
|
||||
totals.total,
|
||||
totals.allStarted,
|
||||
getPercentage(totals.allStarted, totals.total),
|
||||
totals.allEnrolled,
|
||||
getPercentage(totals.allEnrolled, totals.total),
|
||||
totals.allAbsent,
|
||||
getPercentage(totals.allAbsent, totals.total),
|
||||
totals.allInactive,
|
||||
getPercentage(totals.allInactive, totals.total),
|
||||
totals.allGraduated,
|
||||
getPercentage(totals.allGraduated, totals.total),
|
||||
totals.allMale,
|
||||
getPercentage(totals.allMale, totals.total),
|
||||
totals.allFemale,
|
||||
getPercentage(totals.allFemale, totals.total),
|
||||
totals.allOtherUnknown,
|
||||
getPercentage(totals.allOtherUnknown, totals.total),
|
||||
totals.allFinnish,
|
||||
getPercentage(totals.allFinnish, totals.total),
|
||||
totals.allOtherCountries,
|
||||
getPercentage(totals.allOtherCountries, totals.total),
|
||||
]
|
||||
}
|
||||
const addTotals = (facultyTableStats, year, totals) => {
|
||||
facultyTableStats[year] = [year, ...toltalsCalculation(totals)]
|
||||
}
|
||||
const addTotalsProgramme = (programmeTableStats, progId, year, totals) => {
|
||||
programmeTableStats[progId][year] = toltalsCalculation(totals)
|
||||
}
|
||||
|
||||
for (const { progId, code } of programmes) {
|
||||
const studentnumbers = await getCorrectStudentnumbers({
|
||||
codes: [code],
|
||||
startDate,
|
||||
endDate,
|
||||
includeAllSpecials,
|
||||
includeGraduated,
|
||||
})
|
||||
const getStudentData = (students, facultyExtra, year, code) => {
|
||||
let data = { female: 0, male: 0, finnish: 0, otherCountries: 0, otherUnknown: 0 }
|
||||
students.forEach(({ genderCode, homeCountryEn }) => {
|
||||
data.male += genderCode === '1' ? 1 : 0
|
||||
data.female += genderCode === '2' ? 1 : 0
|
||||
data.otherUnknown += ['0', '3'].includes(genderCode) ? 1 : 0
|
||||
data.finnish += homeCountryEn === 'Finland' ? 1 : 0
|
||||
data.otherCountries += homeCountryEn !== 'Finland' ? 1 : 0
|
||||
if (!Object.keys(facultyExtra[year][code]).includes(homeCountryEn) && homeCountryEn !== 'Finland')
|
||||
facultyExtra[year][code][homeCountryEn] = 0
|
||||
if (!Object.keys(facultyExtra['Total'][code]).includes(homeCountryEn) && homeCountryEn !== 'Finland')
|
||||
facultyExtra['Total'][code][homeCountryEn] = 0
|
||||
if (homeCountryEn !== 'Finland') {
|
||||
facultyExtra[year][code][homeCountryEn] += 1
|
||||
facultyExtra['Total'][code][homeCountryEn] += 1
|
||||
}
|
||||
})
|
||||
return data
|
||||
}
|
||||
|
||||
// Goes through the faculty and its study programmes for the year
|
||||
const getFacultyDataForYear = async ({
|
||||
faculty,
|
||||
programmes,
|
||||
since,
|
||||
settings,
|
||||
year,
|
||||
years,
|
||||
programmeTableStats,
|
||||
facultyExtra,
|
||||
allTotals,
|
||||
progTotals,
|
||||
programmeNames,
|
||||
}) => {
|
||||
const { includeAllSpecials, includeGraduated } = settings
|
||||
const { startDate, endDate } = getAcademicYearDates(year, since)
|
||||
// Totals for the year into table stats
|
||||
let extents = [1, 2, 3, 4]
|
||||
let graduated = [0]
|
||||
if (includeAllSpecials) {
|
||||
extents = [...extents, ...[6, 7, 9, 13, 14, 18, 22, 23, 34, 99]] // 16 is always excluded
|
||||
}
|
||||
if (includeGraduated) {
|
||||
graduated = [...graduated, 1]
|
||||
}
|
||||
const totals = emptyTotals()
|
||||
for (const { progId, code, name } of programmes) {
|
||||
if (!Object.keys(facultyExtra[year]).includes(code)) facultyExtra[year][code] = {}
|
||||
if (!Object.keys(facultyExtra['Total']).includes(code)) facultyExtra['Total'][code] = {}
|
||||
|
||||
const toTransfers = await getTransfersIn(code, startDate, endDate)
|
||||
const allStudyrights = await getStudyRightsByExtent(faculty, startDate, endDate, code, extents, graduated)
|
||||
let allStudents = []
|
||||
if (includeAllSpecials) {
|
||||
allStudents = [...new Set([...allStudyrights.map(sr => sr.studentnumber)])]
|
||||
}
|
||||
let studyrights = allStudyrights.filter(s => !checkTransfers(s, toTransfers, toTransfers))
|
||||
|
||||
// Get all the studyrights and students for the calculations
|
||||
let all = await allStudyrights(code, studentnumbers)
|
||||
const students = await studytrackStudents(studentnumbers)
|
||||
const studentData = getStudentData(students)
|
||||
const started = await startedStudyrights(code, startDate, studentnumbers)
|
||||
const enrolled = await enrolledStudents(code, startDate, studentnumbers)
|
||||
const absent = await absentStudents(code, studentnumbers)
|
||||
const inactive = await inactiveStudyrights(code, studentnumbers)
|
||||
const graduated = await graduatedStudyRights(code, startDate, studentnumbers)
|
||||
|
||||
const studentNbrs = studyrights.map(sr => sr.studentnumber)
|
||||
allStudents = [...new Set([...studentNbrs, ...allStudents])]
|
||||
const students = await getStudentsByStudentnumbers(allStudents)
|
||||
const studentData = getStudentData(students, facultyExtra, year, code)
|
||||
const started = [...new Set(studentNbrs)]
|
||||
const inactive = await inactiveStudyrights(code, allStudents)
|
||||
const graduatedStudents = await graduatedStudyRights(code, startDate, allStudents)
|
||||
const absent = await absentStudents(code, allStudents)
|
||||
const enrolled = await enrolledStudents(code, allStudents)
|
||||
// Count stats for the programme
|
||||
if (allStudents.length === 0) continue
|
||||
if (!(progId in programmeTableStats)) {
|
||||
programmeTableStats[progId] = years.reduce((resultObj, yearKey) => {
|
||||
return { ...resultObj, [yearKey]: [] }
|
||||
}, {})
|
||||
progTotals[progId] = emptyTotals()
|
||||
}
|
||||
if (!(progId in programmeNames)) {
|
||||
programmeNames[progId] = { code: code, ...name }
|
||||
progTotals[progId] = emptyTotals()
|
||||
}
|
||||
|
||||
programmeTableStats[progId][year] = [
|
||||
all.length,
|
||||
started.length,
|
||||
getPercentage(started.length, all.length),
|
||||
enrolled.length,
|
||||
getPercentage(enrolled.length, all.length),
|
||||
absent.length,
|
||||
getPercentage(absent.length, all.length),
|
||||
inactive.length,
|
||||
getPercentage(inactive.length, all.length),
|
||||
graduated.length,
|
||||
getPercentage(graduated.length, all.length),
|
||||
studentData.male,
|
||||
getPercentage(studentData.male, all.length),
|
||||
studentData.female,
|
||||
getPercentage(studentData.female, all.length),
|
||||
studentData.finnish,
|
||||
getPercentage(studentData.finnish, all.length),
|
||||
]
|
||||
const yearTotals = {
|
||||
total: students.length,
|
||||
allStarted: started.length,
|
||||
allEnrolled: enrolled.length,
|
||||
allAbsent: absent.length,
|
||||
allInactive: inactive.length,
|
||||
allGraduated: graduatedStudents.length,
|
||||
allMale: studentData.male,
|
||||
allFemale: studentData.female,
|
||||
allOtherUnknown: studentData.otherUnknown,
|
||||
allFinnish: studentData.finnish,
|
||||
allOtherCountries: studentData.otherCountries,
|
||||
}
|
||||
addTotalsProgramme(programmeTableStats, progId, year, yearTotals)
|
||||
|
||||
totals.total += all.length
|
||||
totals.total += students.length
|
||||
totals.allStarted += started.length
|
||||
totals.allEnrolled += enrolled.length
|
||||
totals.allAbsent += absent.length
|
||||
totals.allInactive += inactive.length
|
||||
totals.allGraduated += graduated.length
|
||||
totals.allGraduated += graduatedStudents.length
|
||||
totals.allFemale += studentData.female
|
||||
totals.allMale += studentData.male
|
||||
totals.allOtherUnknown += studentData.otherUnknown
|
||||
totals.allFinnish += studentData.finnish
|
||||
}
|
||||
totals.allOtherCountries += studentData.otherCountries
|
||||
|
||||
allTotals.total += students.length
|
||||
allTotals.allStarted += started.length
|
||||
allTotals.allEnrolled += enrolled.length
|
||||
allTotals.allAbsent += absent.length
|
||||
allTotals.allInactive += inactive.length
|
||||
allTotals.allGraduated += graduatedStudents.length
|
||||
allTotals.allFemale += studentData.female
|
||||
allTotals.allMale += studentData.male
|
||||
allTotals.allOtherUnknown += studentData.otherUnknown
|
||||
allTotals.allFinnish += studentData.finnish
|
||||
allTotals.allOtherCountries += studentData.otherCountries
|
||||
|
||||
progTotals[progId].total += students.length
|
||||
progTotals[progId].allStarted += started.length
|
||||
progTotals[progId].allEnrolled += enrolled.length
|
||||
progTotals[progId].allAbsent += absent.length
|
||||
progTotals[progId].allInactive += inactive.length
|
||||
progTotals[progId].allGraduated += graduatedStudents.length
|
||||
progTotals[progId].allFemale += studentData.female
|
||||
progTotals[progId].allMale += studentData.male
|
||||
progTotals[progId].allOtherUnknown += studentData.otherUnknown
|
||||
progTotals[progId].allFinnish += studentData.finnish
|
||||
progTotals[progId].allOtherCountries += studentData.otherCountries
|
||||
}
|
||||
return totals
|
||||
}
|
||||
|
||||
// Combines all the data for faculty students table
|
||||
const combineFacultyStudents = async (code, programmes, specialGroups, graduated) => {
|
||||
// Only academic years are considered
|
||||
const isAcademicYear = true
|
||||
const includeAllSpecials = specialGroups === 'SPECIAL_INCLUDED'
|
||||
const includeGraduated = graduated === 'GRADUATED_INCLUDED'
|
||||
const includeYearsCombined = true
|
||||
const since = getStartDate(code, isAcademicYear)
|
||||
const years = getYearsArray(since.getFullYear(), isAcademicYear, includeYearsCombined)
|
||||
const since = getStartDate(code, true)
|
||||
const years = getYearsArray(since.getFullYear(), true, true)
|
||||
const settings = { includeAllSpecials, includeGraduated }
|
||||
let facultyTableStats = getYearsObject({ years, emptyArrays: true })
|
||||
let programmeTableStats = {}
|
||||
|
||||
const facultyTableStats = getYearsObject({ years, emptyArrays: true })
|
||||
const programmeTableStats = {}
|
||||
const facultyExtra = years.reduce((acc, year) => ({ ...acc, [year]: {} }), {})
|
||||
const progTotals = {}
|
||||
const allTotals = emptyTotals()
|
||||
const programmeNames = {}
|
||||
for (const year of years.reverse()) {
|
||||
const queryParams = { programmes, since, settings, year, years, programmeTableStats }
|
||||
const totals = await getFacultyDataForYear(queryParams)
|
||||
facultyTableStats[year] = [
|
||||
if (year === 'Total') continue
|
||||
const queryParams = {
|
||||
faculty: code,
|
||||
programmes,
|
||||
since,
|
||||
settings,
|
||||
year,
|
||||
totals.total,
|
||||
totals.allStarted,
|
||||
getPercentage(totals.allStarted, totals.total),
|
||||
totals.allEnrolled,
|
||||
getPercentage(totals.allEnrolled, totals.total),
|
||||
totals.allAbsent,
|
||||
getPercentage(totals.allAbsent, totals.total),
|
||||
totals.allInactive,
|
||||
getPercentage(totals.allInactive, totals.total),
|
||||
totals.allGraduated,
|
||||
getPercentage(totals.allGraduated, totals.total),
|
||||
totals.allMale,
|
||||
getPercentage(totals.allMale, totals.total),
|
||||
totals.allFemale,
|
||||
getPercentage(totals.allFemale, totals.total),
|
||||
totals.allFinnish,
|
||||
getPercentage(totals.allFinnish, totals.total),
|
||||
]
|
||||
years,
|
||||
programmeTableStats,
|
||||
facultyExtra,
|
||||
allTotals,
|
||||
progTotals,
|
||||
programmeNames,
|
||||
}
|
||||
const totals = await getFacultyDataForYear(queryParams)
|
||||
addTotals(facultyTableStats, year, totals)
|
||||
}
|
||||
|
||||
addTotals(facultyTableStats, 'Total', allTotals)
|
||||
Object.keys(progTotals).forEach(programme =>
|
||||
addTotalsProgramme(programmeTableStats, programme, 'Total', progTotals[programme])
|
||||
)
|
||||
|
||||
const studentsData = {
|
||||
id: code,
|
||||
years: years,
|
||||
facultyTableStats: facultyTableStats,
|
||||
facultyTableStatsExtra: facultyExtra,
|
||||
programmeStats: programmeTableStats,
|
||||
titles: tableTitles['studytracks'],
|
||||
programmeNames: programmes.reduce(
|
||||
(obj, dataItem) => ({ ...obj, [dataItem.progId]: { code: dataItem.code, ...dataItem.name } }),
|
||||
{}
|
||||
),
|
||||
titles: [...tableTitles.studytracksStart, ...tableTitles.studytracksBasic, ...tableTitles.studytracksEnd],
|
||||
programmeNames: programmeNames,
|
||||
}
|
||||
return studentsData
|
||||
}
|
||||
|
||||
@@ -11,28 +11,24 @@ const {
|
||||
} = require('../studyprogrammeHelpers')
|
||||
|
||||
const getFacultyThesisWriters = async ({ since, years, isAcademicYear, facultyProgrammes, includeAllSpecials }) => {
|
||||
const thesisTypes = [
|
||||
'urn:code:course-unit-type:bachelors-thesis',
|
||||
'urn:code:course-unit-type:masters-thesis',
|
||||
'urn:code:course-unit-type:doctors-thesis',
|
||||
'urn:code:course-unit-type:licentiate-thesis',
|
||||
]
|
||||
const thesisTypes = ['urn:code:course-unit-type:bachelors-thesis', 'urn:code:course-unit-type:masters-thesis']
|
||||
|
||||
let bachelors = getStatsBasis(years)
|
||||
let masters = getStatsBasis(years)
|
||||
let doctors = getStatsBasis(years)
|
||||
let licentiates = getStatsBasis(years)
|
||||
let programmeCounts = {}
|
||||
let programmeNames = {}
|
||||
|
||||
for (const { progId, code, name } of facultyProgrammes) {
|
||||
if (code === 'MH70_008_2' || code.startsWith('LIS') || code.startsWith('T')) continue
|
||||
const provider = mapToProviders([code])[0]
|
||||
const students = await getCorrectStudentnumbers({
|
||||
codes: [code],
|
||||
startDate: alltimeStartDate,
|
||||
endDate: alltimeEndDate,
|
||||
includeAllSpecials,
|
||||
includeTransferredTo: includeAllSpecials,
|
||||
})
|
||||
|
||||
const thesisCourseCodes = await thesisWriters(provider, since, thesisTypes, students)
|
||||
thesisCourseCodes?.forEach(({ attainment_date, courseUnitType }) => {
|
||||
const thesisYear = defineYear(attainment_date, isAcademicYear)
|
||||
@@ -40,7 +36,7 @@ const getFacultyThesisWriters = async ({ since, years, isAcademicYear, facultyPr
|
||||
if (!(progId in programmeCounts)) {
|
||||
programmeCounts[progId] = {}
|
||||
|
||||
Object.keys(bachelors.tableStats).forEach(year => (programmeCounts[progId][year] = [0, 0, 0, 0, 0]))
|
||||
Object.keys(bachelors.tableStats).forEach(year => (programmeCounts[progId][year] = [0, 0, 0]))
|
||||
programmeNames[progId] = { ...name, code: code }
|
||||
}
|
||||
programmeCounts[progId][thesisYear][0] += 1
|
||||
@@ -53,22 +49,14 @@ const getFacultyThesisWriters = async ({ since, years, isAcademicYear, facultyPr
|
||||
masters.graphStats[indexOf(years, thesisYear)] += 1
|
||||
masters.tableStats[thesisYear] += 1
|
||||
programmeCounts[progId][thesisYear][2] += 1
|
||||
} else if (courseUnitType === thesisTypes[2]) {
|
||||
doctors.graphStats[indexOf(years, thesisYear)] += 1
|
||||
doctors.tableStats[thesisYear] += 1
|
||||
programmeCounts[progId][thesisYear][3] += 1
|
||||
} else if (courseUnitType === thesisTypes[3]) {
|
||||
licentiates.graphStats[indexOf(years, thesisYear)] += 1
|
||||
licentiates.tableStats[thesisYear] += 1
|
||||
programmeCounts[progId][thesisYear][4] += 1
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return { bachelors, masters, doctors, licentiates, programmeCounts, programmeNames }
|
||||
return { bachelors, masters, programmeCounts, programmeNames }
|
||||
}
|
||||
|
||||
const getFacultyThesisWritersForStudyTrack = async (
|
||||
const getFacultyThesisWritersForProgrammes = async (
|
||||
allThesisWriters,
|
||||
facultyProgrammes,
|
||||
isAcademicYear,
|
||||
@@ -77,24 +65,18 @@ const getFacultyThesisWritersForStudyTrack = async (
|
||||
const since = isAcademicYear ? new Date('2017-08-01') : new Date('2017-01-01')
|
||||
const years = getYearsArray(since.getFullYear(), isAcademicYear)
|
||||
const queryParameters = { since, years, isAcademicYear, facultyProgrammes, includeAllSpecials }
|
||||
const { bachelors, masters, doctors, licentiates, programmeCounts, programmeNames } = await getFacultyThesisWriters(
|
||||
queryParameters
|
||||
)
|
||||
const { bachelors, masters, programmeCounts, programmeNames } = await getFacultyThesisWriters(queryParameters)
|
||||
|
||||
allThesisWriters.years = years.map(year => year.toString())
|
||||
const reversedYears = years.reverse()
|
||||
allThesisWriters.tableStats = reversedYears.map(year => [
|
||||
year,
|
||||
bachelors.tableStats[year] + masters.tableStats[year] + doctors.tableStats[year] + licentiates.tableStats[year],
|
||||
bachelors.tableStats[year] + masters.tableStats[year],
|
||||
bachelors.tableStats[year],
|
||||
masters.tableStats[year],
|
||||
doctors.tableStats[year],
|
||||
licentiates.tableStats[year],
|
||||
])
|
||||
allThesisWriters.graphStats.push({ name: 'Bachelors', data: bachelors.graphStats })
|
||||
allThesisWriters.graphStats.push({ name: 'Masters', data: masters.graphStats })
|
||||
allThesisWriters.graphStats.push({ name: 'Doctors', data: doctors.graphStats })
|
||||
allThesisWriters.graphStats.push({ name: 'Licentiates', data: licentiates.graphStats })
|
||||
|
||||
const programmes = programmeNames ? Object.keys(programmeNames) : facultyProgrammes.map(programme => programme.code)
|
||||
programmes.forEach(programmeId => {
|
||||
@@ -118,7 +100,7 @@ const combineFacultyThesisWriters = async (faculty, facultyProgrammes, yearType,
|
||||
tableStats: [],
|
||||
graphStats: [],
|
||||
programmeTableStats: {},
|
||||
titles: ['', 'All', 'Bachelors', 'Masters', 'Doctors', 'Licentiates'],
|
||||
titles: ['', 'All', 'Bachelors', 'Masters'],
|
||||
programmeNames: {},
|
||||
status: 'Done',
|
||||
lastUpdated: '',
|
||||
@@ -126,7 +108,7 @@ const combineFacultyThesisWriters = async (faculty, facultyProgrammes, yearType,
|
||||
const isAcademicYear = yearType === 'ACADEMIC_YEAR'
|
||||
const includeAllSpecials = specialGroups === 'SPECIAL_INCLUDED'
|
||||
|
||||
await getFacultyThesisWritersForStudyTrack(allThesisWriters, facultyProgrammes, isAcademicYear, includeAllSpecials)
|
||||
await getFacultyThesisWritersForProgrammes(allThesisWriters, facultyProgrammes, isAcademicYear, includeAllSpecials)
|
||||
|
||||
return allThesisWriters
|
||||
}
|
||||
|
||||
@@ -83,7 +83,7 @@ const updateFacultyOverview = async (faculty, statsType) => {
|
||||
newProgrammes = await setFacultyProgrammes(faculty, onlyNew, 'NEW_STUDY_PROGRAMMES')
|
||||
allProgrammes?.data.forEach(prog => allProgrammeCodes.push(prog.code))
|
||||
} catch (e) {
|
||||
logger.error(e)
|
||||
logger.error(`Faculty updates: programme stats failed with error: ${e}`)
|
||||
}
|
||||
|
||||
for (const option of options) {
|
||||
@@ -100,12 +100,12 @@ const updateFacultyOverview = async (faculty, statsType) => {
|
||||
)
|
||||
await setBasicStats(updatedStudentInfo, option.yearType, option.programmeFilter, option.specialGroups)
|
||||
}
|
||||
if (statsType === 'ALL' || statsType === 'CREDITS') {
|
||||
if ((statsType === 'ALL' || statsType === 'CREDITS') && specialGroups !== 'SPECIAL_EXCLUDED') {
|
||||
const updatedCredits = await combineFacultyCredits(
|
||||
faculty,
|
||||
programmeFilter === 'ALL_PROGRAMMES' ? allProgrammes.data : newProgrammes.data,
|
||||
yearType,
|
||||
specialGroups
|
||||
allProgrammes.data,
|
||||
yearType
|
||||
)
|
||||
await setCreditStats(updatedCredits, yearType, programmeFilter, specialGroups)
|
||||
}
|
||||
@@ -119,18 +119,14 @@ const updateFacultyOverview = async (faculty, statsType) => {
|
||||
await setThesisWritersStats(updateThesisWriters, yearType, programmeFilter, specialGroups)
|
||||
}
|
||||
} catch (e) {
|
||||
logger.error(e)
|
||||
logger.error(`Faculty updates: basic/thesis-writers/credits stats failed with error ${e}`)
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const updatedTimesAll = await countGraduationTimes(faculty, 'ALL_PROGRAMMES')
|
||||
await setGraduationStats(updatedTimesAll, 'ALL_PROGRAMMES')
|
||||
const updatedTimesNew = await countGraduationTimes(faculty, 'NEW_STUDY_PROGRAMMES')
|
||||
await setGraduationStats(updatedTimesNew, 'NEW_STUDY_PROGRAMMES')
|
||||
} catch (e) {
|
||||
logger.error(e)
|
||||
}
|
||||
const updatedTimesAll = await countGraduationTimes(faculty, 'ALL_PROGRAMMES')
|
||||
await setGraduationStats(updatedTimesAll, 'ALL_PROGRAMMES')
|
||||
const updatedTimesNew = await countGraduationTimes(faculty, 'NEW_STUDY_PROGRAMMES')
|
||||
await setGraduationStats(updatedTimesNew, 'NEW_STUDY_PROGRAMMES')
|
||||
return 'OK'
|
||||
}
|
||||
|
||||
@@ -158,7 +154,7 @@ const updateFacultyProgressOverview = async faculty => {
|
||||
const onlyNew = await findFacultyProgrammeCodes(faculty, 'NEW_STUDY_PROGRAMMES')
|
||||
newProgrammes = await setFacultyProgrammes(faculty, onlyNew, 'NEW_STUDY_PROGRAMMES')
|
||||
} catch (e) {
|
||||
logger.error(e)
|
||||
logger.error(`Faculty stats: all programme stats failed with error ${e}`)
|
||||
}
|
||||
|
||||
for (const option of options) {
|
||||
@@ -179,7 +175,7 @@ const updateFacultyProgressOverview = async faculty => {
|
||||
)
|
||||
await setFacultyProgressStats(updateFacultyProgressStats, specialGroups, graduated)
|
||||
} catch (e) {
|
||||
logger.error(e)
|
||||
logger.error(`Faculty stats: progress stats failed with error ${e}`)
|
||||
}
|
||||
}
|
||||
return 'OK'
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
const Sequelize = require('sequelize')
|
||||
const { Op } = Sequelize
|
||||
const moment = require('moment')
|
||||
const { Semester, SemesterEnrollment, Studyright } = require('../models')
|
||||
|
||||
// categorize graduation times
|
||||
|
||||
const countTimeCategories = (times, goal) => {
|
||||
const statistics = { onTime: 0, yearOver: 0, wayOver: 0 }
|
||||
times.forEach(time => {
|
||||
if (time <= goal) statistics.onTime += 1
|
||||
else if (time <= goal + 12) statistics.yearOver += 1
|
||||
else statistics.wayOver += 1
|
||||
})
|
||||
return statistics
|
||||
}
|
||||
|
||||
// discount staturory absences from graduation times
|
||||
|
||||
const formatAbsence = absence => {
|
||||
const { semestercode, semester } = absence
|
||||
return { semestercode, start: semester.startdate, end: semester.enddate }
|
||||
}
|
||||
|
||||
const statutoryAbsences = async (studentnumber, startdate, enddate) =>
|
||||
(
|
||||
await SemesterEnrollment.findAll({
|
||||
attributes: ['semestercode'],
|
||||
include: {
|
||||
model: Semester,
|
||||
attributes: ['startdate', 'enddate'],
|
||||
where: {
|
||||
startdate: {
|
||||
[Op.gte]: startdate,
|
||||
},
|
||||
enddate: {
|
||||
[Op.lte]: enddate,
|
||||
},
|
||||
},
|
||||
},
|
||||
where: {
|
||||
studentnumber: studentnumber,
|
||||
statutory_absence: true,
|
||||
},
|
||||
})
|
||||
).map(formatAbsence)
|
||||
|
||||
const getStatutoryAbsences = async (studentnumber, startdate, enddate) => {
|
||||
const absences = await statutoryAbsences(studentnumber, startdate, enddate)
|
||||
if (absences.length) {
|
||||
const absentMonths = absences.reduce((sum, ab) => sum + moment(ab.end).diff(moment(ab.start), 'months'), 0)
|
||||
return absentMonths
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// count bachelor time in Bc+Ms studyrights to master graduation times
|
||||
|
||||
const bachelorStudyright = async id => {
|
||||
return await Studyright.findOne({
|
||||
attributes: ['startdate'],
|
||||
where: {
|
||||
studyrightid: id,
|
||||
extentcode: 1,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
countTimeCategories,
|
||||
getStatutoryAbsences,
|
||||
bachelorStudyright,
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
const { Course, Credit, Enrollment, StudyrightElement } = require('../models')
|
||||
const { Op } = require('sequelize')
|
||||
const { redisClient } = require('./redis')
|
||||
const REDIS_KEY = 'LANGUAGE_CENTER_DATA'
|
||||
|
||||
const getLanguageCenterData = async () => {
|
||||
const dataOnRedis = await redisClient.getAsync(REDIS_KEY)
|
||||
if (dataOnRedis) return JSON.parse(dataOnRedis)
|
||||
const freshData = await computeLanguageCenterData()
|
||||
redisClient.setAsync(REDIS_KEY, JSON.stringify(freshData))
|
||||
return freshData
|
||||
}
|
||||
|
||||
const computeLanguageCenterData = async () => {
|
||||
const courses = await Course.findAll({
|
||||
attributes: ['code', 'name'],
|
||||
where: {
|
||||
[Op.or]: [{ code: { [Op.like]: 'KK%' } }, { code: { [Op.like]: 'AYKK%' } }],
|
||||
},
|
||||
})
|
||||
|
||||
const credits = await Credit.findAll({
|
||||
attributes: ['course_code', 'credittypecode', 'student_studentnumber', 'semestercode', 'createdate'],
|
||||
where: {
|
||||
[Op.or]: [{ course_code: { [Op.like]: 'KK%' } }, { course_code: { [Op.like]: 'AYKK%' } }],
|
||||
},
|
||||
})
|
||||
|
||||
const enrollments = await Enrollment.findAll({
|
||||
attributes: ['studentnumber', 'semestercode', 'course_code', 'enrollment_date_time'],
|
||||
where: {
|
||||
[Op.or]: [{ course_code: { [Op.like]: 'KK%' } }, { course_code: { [Op.like]: 'AYKK%' } }],
|
||||
},
|
||||
})
|
||||
|
||||
const studentList = new Set()
|
||||
const attemptsByStudents = {}
|
||||
|
||||
credits.forEach(c => {
|
||||
const sn = c.student_studentnumber
|
||||
|
||||
// ignoring 7 (improved), 9 (transferred) and 10 (failed)
|
||||
if (c.credittypecode !== 4) return
|
||||
studentList.add(sn)
|
||||
if (!attemptsByStudents[sn]) {
|
||||
attemptsByStudents[sn] = []
|
||||
}
|
||||
attemptsByStudents[sn].push({ studentNumber: sn, courseCode: c.course_code, completed: true, date: c.createdate })
|
||||
})
|
||||
|
||||
enrollments.forEach(e => {
|
||||
const sn = e.studentnumber
|
||||
if (attemptsByStudents[sn] && attemptsByStudents[sn].find(att => att.courseCode === e.course_code)) return
|
||||
studentList.add(sn)
|
||||
if (!attemptsByStudents[sn]) {
|
||||
attemptsByStudents[sn] = []
|
||||
}
|
||||
attemptsByStudents[sn].push({
|
||||
studentNumber: sn,
|
||||
courseCode: e.course_code,
|
||||
completed: false,
|
||||
date: e.enrollment_date_time,
|
||||
})
|
||||
})
|
||||
|
||||
const studyrightElements = await StudyrightElement.findAll({
|
||||
attributes: ['studentnumber', 'startdate', 'enddate', 'code'],
|
||||
where: {
|
||||
studentnumber: { [Op.in]: Array.from(studentList.values()) },
|
||||
},
|
||||
})
|
||||
const studyrightMap = studyrightElements.reduce((obj, cur) => {
|
||||
if (!obj[cur.studentnumber]) {
|
||||
obj[cur.studentnumber] = [{ startdate: cur.startdate, enddate: cur.enddate, code: cur.code }]
|
||||
} else {
|
||||
obj[cur.studentnumber].push({ startdate: cur.startdate, enddate: cur.enddate, code: cur.code })
|
||||
}
|
||||
return obj
|
||||
}, {})
|
||||
|
||||
const getFaculty = (studentNumber, date) =>
|
||||
studyrightMap[studentNumber]?.find(sr => sr.startdate <= date && sr.enddate >= date)?.code
|
||||
|
||||
const attemptsArray = []
|
||||
studentList.forEach(sn => attemptsArray.push(...attemptsByStudents[sn]))
|
||||
|
||||
attemptsArray.forEach(item => {
|
||||
const faculty = getFaculty(item.studentNumber, item.date)
|
||||
if (faculty?.startsWith('MH') || faculty?.startsWith('KH')) {
|
||||
item.faculty = faculty.substring(1, 4)
|
||||
} else {
|
||||
item.faculty = faculty
|
||||
}
|
||||
})
|
||||
return { attempts: attemptsArray, courses }
|
||||
}
|
||||
|
||||
module.exports = { getLanguageCenterData }
|
||||
@@ -3,7 +3,7 @@ const { isProduction, pateToken } = require('../conf-backend')
|
||||
const { ApplicationError } = require('../util/customErrors')
|
||||
|
||||
const pateClient = axios.create({
|
||||
baseURL: 'https://importer.cs.helsinki.fi/api/pate/',
|
||||
baseURL: 'https://api-toska.apps.ocp-prod-0.k8s.it.helsinki.fi/pate/',
|
||||
params: {
|
||||
token: pateToken,
|
||||
},
|
||||
|
||||
@@ -1,64 +0,0 @@
|
||||
const Sequelize = require('sequelize')
|
||||
const _ = require('lodash')
|
||||
const { MandatoryCourseLabels, sequelizeKone } = require('../models/models_kone')
|
||||
const { Op } = Sequelize
|
||||
|
||||
const create = async (studyprogramme_id, { label }) => {
|
||||
const labels = await labelsByStudyprogramme(studyprogramme_id)
|
||||
await MandatoryCourseLabels.create({ studyprogramme_id, label, orderNumber: labels.length + 1 })
|
||||
}
|
||||
|
||||
const destroy = (studyprogramme_id, { id }) => {
|
||||
return MandatoryCourseLabels.destroy({ where: { studyprogramme_id, id } })
|
||||
}
|
||||
|
||||
const move = async (studyprogramme_id, { id }, direction) => {
|
||||
const labels = await labelsByStudyprogramme(studyprogramme_id).map(e => e.get())
|
||||
const orderedlabels = _.sortBy(labels, ['orderNumber'])
|
||||
const indexToMove = orderedlabels.findIndex(e => e.id === id)
|
||||
const indexToSwap = direction === 'up' ? indexToMove - 1 : indexToMove + 1
|
||||
if (indexToSwap < 0 || indexToSwap >= orderedlabels.length) return null
|
||||
;[orderedlabels[indexToMove], orderedlabels[indexToSwap]] = [orderedlabels[indexToSwap], orderedlabels[indexToMove]]
|
||||
const newOrder = orderedlabels.map((e, index) => ({ ...e, orderNumber: index }))
|
||||
await sequelizeKone.transaction(async transaction => {
|
||||
for (let e of newOrder) {
|
||||
await MandatoryCourseLabels.update(
|
||||
{ orderNumber: e.orderNumber },
|
||||
{ where: { studyprogramme_id, id: e.id }, transaction }
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const find = (studyprogramme_id, { id }) => {
|
||||
return MandatoryCourseLabels.findOne({
|
||||
attributes: ['id', 'label', 'orderNumber'],
|
||||
where: {
|
||||
studyprogramme_id: {
|
||||
[Op.eq]: studyprogramme_id,
|
||||
},
|
||||
id: {
|
||||
[Op.eq]: id,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const labelsByStudyprogramme = studyProgrammeId => {
|
||||
return MandatoryCourseLabels.findAll({
|
||||
attributes: ['id', 'label', 'orderNumber'],
|
||||
where: {
|
||||
studyprogramme_id: {
|
||||
[Op.eq]: studyProgrammeId,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
labelsByStudyprogramme,
|
||||
create,
|
||||
destroy,
|
||||
move,
|
||||
find,
|
||||
}
|
||||
@@ -1,108 +0,0 @@
|
||||
const Sequelize = require('sequelize')
|
||||
const { Course } = require('../models')
|
||||
const { MandatoryCourse, MandatoryCourseLabels } = require('../models/models_kone')
|
||||
const { Op } = Sequelize
|
||||
|
||||
const byStudyprogramme = async studyProgrammeId => {
|
||||
const mandatoryCourses = await MandatoryCourse.findAll({
|
||||
attributes: ['course_code'],
|
||||
include: [
|
||||
{
|
||||
model: MandatoryCourseLabels,
|
||||
attributes: ['id', 'label', 'orderNumber'],
|
||||
},
|
||||
],
|
||||
where: {
|
||||
studyprogramme_id: {
|
||||
[Op.eq]: studyProgrammeId,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const courses = await Course.findAll({
|
||||
model: Course,
|
||||
where: {
|
||||
code: {
|
||||
[Op.in]: mandatoryCourses.map(c => c.course_code),
|
||||
},
|
||||
},
|
||||
})
|
||||
const courseCodeToCourse = courses.reduce((acc, c) => {
|
||||
acc[c.code] = c
|
||||
return acc
|
||||
}, {})
|
||||
|
||||
return mandatoryCourses.map(mc => ({
|
||||
name: courseCodeToCourse[mc.course_code].name,
|
||||
code: mc.course_code,
|
||||
label: mc.mandatory_course_label,
|
||||
}))
|
||||
}
|
||||
|
||||
const create = (studyProgrammeId, code) => {
|
||||
return MandatoryCourse.create({
|
||||
studyprogramme_id: studyProgrammeId,
|
||||
course_code: code,
|
||||
})
|
||||
}
|
||||
|
||||
const find = async (studyProgrammeId, code) => {
|
||||
const mandatoryCourse = await MandatoryCourse.findOne({
|
||||
attributes: ['course_code', 'label'],
|
||||
include: [
|
||||
{
|
||||
model: MandatoryCourseLabels,
|
||||
attributes: ['id', 'label', 'orderNumber'],
|
||||
},
|
||||
],
|
||||
where: {
|
||||
studyprogramme_id: {
|
||||
[Op.eq]: studyProgrammeId,
|
||||
},
|
||||
course_code: {
|
||||
[Op.eq]: code,
|
||||
},
|
||||
},
|
||||
})
|
||||
if (!mandatoryCourse) return null
|
||||
|
||||
const course = await Course.findOne({
|
||||
where: {
|
||||
code: mandatoryCourse.course_code,
|
||||
},
|
||||
})
|
||||
if (!course) return null
|
||||
|
||||
return { ...mandatoryCourse.get(), course: course.get() }
|
||||
}
|
||||
|
||||
const remove = (studyProgrammeId, code) => {
|
||||
return MandatoryCourse.destroy({
|
||||
where: {
|
||||
studyprogramme_id: studyProgrammeId,
|
||||
course_code: code,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const updateLabel = (studyProgrammeId, code, { id }) => {
|
||||
return MandatoryCourse.update(
|
||||
{
|
||||
label: id,
|
||||
},
|
||||
{
|
||||
where: {
|
||||
course_code: code,
|
||||
studyprogramme_id: studyProgrammeId,
|
||||
},
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
create,
|
||||
find,
|
||||
remove,
|
||||
byStudyprogramme,
|
||||
updateLabel,
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user