mirror of
https://github.com/UniversityOfHelsinkiCS/oodikone.git
synced 2026-10-12 19:03:39 +03:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4c3f583712 | ||
|
|
d32608a0f2 | ||
|
|
c920729d57 | ||
|
|
dae5c5181f | ||
|
|
60ab9a849d | ||
|
|
8d6c8d71ef | ||
|
|
478722b1ad | ||
|
|
2c1341fac7 | ||
|
|
76ffc929b0 | ||
|
|
99dfb4ed63 | ||
|
|
1d8db829cb | ||
|
|
5cb67f7f24 | ||
|
|
d6d0824b6c | ||
|
|
59b257fb90 | ||
|
|
d5e175f362 | ||
|
|
c2a2331013 | ||
|
|
fe99727a6f | ||
|
|
12c076740a | ||
|
|
a8b4aa3f1f | ||
|
|
f0549eb4ac | ||
|
|
eebbb2d1db | ||
|
|
389a0b8512 | ||
|
|
7775ef6c54 | ||
|
|
2e984bdb89 | ||
|
|
ab28becd97 | ||
|
|
5a5039318c | ||
|
|
433dd15020 | ||
|
|
a971433273 | ||
|
|
314642c1b5 | ||
|
|
44b4a43f16 | ||
|
|
f483a564e8 | ||
|
|
c112c27d59 | ||
|
|
fec8346766 | ||
|
|
9d152cac10 | ||
|
|
63fea99dfd | ||
|
|
46bbd79a88 | ||
|
|
1e65c70210 | ||
|
|
031d33561c | ||
|
|
11eab49a0f | ||
|
|
9b2b4bc6d8 | ||
|
|
29f94bb94e | ||
|
|
0310292531 | ||
|
|
6c9ac8cd2d | ||
|
|
5afe92d85e | ||
|
|
e7e19b3f1b | ||
|
|
6df4f8f8da | ||
|
|
b1d6648f7f | ||
|
|
80e0668a3c | ||
|
|
9b9a585057 | ||
|
|
c396f5437a | ||
|
|
5149bb3a62 | ||
|
|
acd4a34048 | ||
|
|
fba8d92c5a | ||
|
|
a888d171a2 | ||
|
|
e4c56e11b7 | ||
|
|
c938d2440d | ||
|
|
a4b32129f1 | ||
|
|
7cb8d710d7 | ||
|
|
038ffefa84 | ||
|
|
2a8176ebe2 | ||
|
|
2be93ef839 | ||
|
|
319a001c0e | ||
|
|
831b00ae75 | ||
|
|
be923fa80d | ||
|
|
596ebb66cb | ||
|
|
118315385e | ||
|
|
192b1eb54b | ||
|
|
ea9a8da78d | ||
|
|
bcd44aa644 | ||
|
|
6381363722 | ||
|
|
b8d075be42 | ||
|
|
b109accac8 | ||
|
|
0a98526f18 | ||
|
|
0f6cf38e73 | ||
|
|
2df92ec068 | ||
|
|
dae7bbb058 | ||
|
|
ab121b7ec6 | ||
|
|
a2df1944c6 | ||
|
|
62821e2f3b | ||
|
|
14bd681260 | ||
|
|
fc123276ff | ||
|
|
ed96ae4f95 | ||
|
|
c0f01fb224 | ||
|
|
e50f631495 | ||
|
|
2838099f0a | ||
|
|
9452ce08bd | ||
|
|
075385c962 | ||
|
|
87e6199093 | ||
|
|
d47d0c22dd | ||
|
|
240edec696 | ||
|
|
6dcdaae83e | ||
|
|
608670d65d | ||
|
|
50ed56471c | ||
|
|
1d54d41861 | ||
|
|
0904703a57 | ||
|
|
8a4063486b | ||
|
|
86cebeb628 | ||
|
|
6770850822 | ||
|
|
133edbad89 | ||
|
|
e8556d0741 | ||
|
|
b654a247ae | ||
|
|
bba5c7e946 | ||
|
|
0cbd1f2e16 | ||
|
|
7f79a95385 | ||
|
|
d6901a40b3 | ||
|
|
46d786f9e5 | ||
|
|
e059ee3f93 | ||
|
|
4b164b1f2a | ||
|
|
c44de1e2b5 | ||
|
|
a3d65ab87b | ||
|
|
79eec49a51 | ||
|
|
b15a7a2102 | ||
|
|
99abeae5ad | ||
|
|
3b65e39e35 | ||
|
|
1af0bd77e9 | ||
|
|
f51bdfe23c | ||
|
|
2d50786093 | ||
|
|
e8f899a96e | ||
|
|
dd0f8f966d | ||
|
|
2d684d1f70 | ||
|
|
ec2bbfb497 | ||
|
|
a07365c1a6 | ||
|
|
5e3605930d | ||
|
|
5ce24443f2 | ||
|
|
9e825f9915 | ||
|
|
69c7f02dca | ||
|
|
bf2c3638d3 | ||
|
|
d4ccc7a4dc | ||
|
|
2bcafbabfd | ||
|
|
bc5d44f18f | ||
|
|
51b9e23693 | ||
|
|
75f0dca9f1 | ||
|
|
b802f92420 | ||
|
|
bf54330c12 | ||
|
|
aba30641b2 | ||
|
|
5dc06a8f95 | ||
|
|
8dad3ef620 | ||
|
|
e23774023d | ||
|
|
abd31bc807 | ||
|
|
120a6fbc5b | ||
|
|
a6bc58b0fc | ||
|
|
4baf6d8509 | ||
|
|
f263913c03 | ||
|
|
e9f6eb744f | ||
|
|
cd6ef17a10 | ||
|
|
f6272a0df0 | ||
|
|
9b385afb36 | ||
|
|
b7a2d67bcb | ||
|
|
2f20cd7914 | ||
|
|
55565e1721 | ||
|
|
e26c35ccd6 | ||
|
|
034410e534 | ||
|
|
c0ee2477be | ||
|
|
e7b0beb2cf | ||
|
|
4f8dcc03c2 | ||
|
|
ff7bedccc1 | ||
|
|
3b153df6ef | ||
|
|
93b0e283fa | ||
|
|
f52fddf85c | ||
|
|
f0fca7f7ab | ||
|
|
44058ce537 | ||
|
|
48356cbb8a | ||
|
|
50b579ede2 | ||
|
|
16df458433 | ||
|
|
2c84a0f73d | ||
|
|
0152d0c549 | ||
|
|
fe03fa89c6 | ||
|
|
891cabfbd7 | ||
|
|
b7d2b7f804 | ||
|
|
b7740c3b9b | ||
|
|
620c6859ae | ||
|
|
5e30c279ed | ||
|
|
637f23a96f | ||
|
|
9b8db2096d | ||
|
|
6d174d0275 | ||
|
|
2e882ff8a0 | ||
|
|
f8a6f89199 | ||
|
|
b272f9e94a | ||
|
|
4cc73d5fd8 | ||
|
|
c19810fca8 | ||
|
|
298c9e9b4b | ||
|
|
58ed0ac417 | ||
|
|
cb40fd970c | ||
|
|
cdc7331667 | ||
|
|
2ffc222bcf | ||
|
|
2fcb5fd91d | ||
|
|
f4be37a3b8 | ||
|
|
67d7b05cc6 | ||
|
|
88ad8db7bb | ||
|
|
dda612f284 | ||
|
|
6a9cf26f4b | ||
|
|
2d7b679b04 | ||
|
|
5aa4c5d146 | ||
|
|
ff9aaa1119 | ||
|
|
f443f3b7db | ||
|
|
a6a47e5ec8 | ||
|
|
b414fd3501 | ||
|
|
88ce7bd2a5 | ||
|
|
eb35ea503d | ||
|
|
20de79a26f | ||
|
|
dab21ab390 | ||
|
|
6069dbc328 | ||
|
|
7cb8041ca0 | ||
|
|
729192cddc | ||
|
|
4b3b10d033 | ||
|
|
9c6812d7cf | ||
|
|
58ce626d97 | ||
|
|
0834b33778 | ||
|
|
cbce1d7dbd | ||
|
|
b8e214f8cd | ||
|
|
69d920015c | ||
|
|
cf64249d4f | ||
|
|
f372e4f362 | ||
|
|
dd619d375d | ||
|
|
0888eaf547 | ||
|
|
14275a3e6f | ||
|
|
956b9302d2 | ||
|
|
51f462ce7b | ||
|
|
a38bd1a3b0 | ||
|
|
316f3511c2 | ||
|
|
e36c7aa6ba | ||
|
|
ffda9af909 | ||
|
|
7b56978e7c | ||
|
|
b95d439836 | ||
|
|
1f7573a0ba | ||
|
|
0204b4890f | ||
|
|
5a19e48ff9 | ||
|
|
790386a394 | ||
|
|
e86045e8e6 | ||
|
|
b1fb0a7cc7 | ||
|
|
0086ae69d3 | ||
|
|
10d9731fab | ||
|
|
83ffd5389e | ||
|
|
fcb4328dba | ||
|
|
f196a0939d | ||
|
|
52a480d6fb | ||
|
|
0618a17830 | ||
|
|
d54b3c0782 | ||
|
|
28fa4e5310 | ||
|
|
df402c4823 | ||
|
|
9136d41bdd | ||
|
|
e1f63c2441 | ||
|
|
d797b766b1 | ||
|
|
03bed43077 | ||
|
|
6763905b2a | ||
|
|
994101732d | ||
|
|
b1c071a507 | ||
|
|
0e2bdc8d2d | ||
|
|
be0f14326a | ||
|
|
1cd6e2f435 | ||
|
|
0790003ae8 | ||
|
|
a77bf8b6a9 | ||
|
|
00cd57dc13 | ||
|
|
44a8f39cc1 | ||
|
|
4ecdb3237d | ||
|
|
6b544179ef | ||
|
|
9e29afedad | ||
|
|
8610bf1084 | ||
|
|
fe49a63408 | ||
|
|
a9100b78ec | ||
|
|
e3fb31ef8f | ||
|
|
f865796771 | ||
|
|
2532b5367e | ||
|
|
eeca5c52fc | ||
|
|
1e940fde73 | ||
|
|
38a2f0a9a8 | ||
|
|
991203e08e | ||
|
|
0c8698ab04 | ||
|
|
57e04434b0 | ||
|
|
f1fcf9661d | ||
|
|
61ab3d9849 | ||
|
|
52db993e1f | ||
|
|
66d4989fee | ||
|
|
0439efddbe | ||
|
|
453511fe5d | ||
|
|
bcaa0086e5 | ||
|
|
80a7072f73 | ||
|
|
33475304e9 | ||
|
|
8bc5c0c3b0 | ||
|
|
b852562bd2 | ||
|
|
6de3dc0bdb | ||
|
|
ef41c4e270 | ||
|
|
bd22b25cca | ||
|
|
0e7757ee3f | ||
|
|
89a8837e16 | ||
|
|
1dc305601f | ||
|
|
e4adda1cb3 | ||
|
|
9dfef1baff | ||
|
|
3599f3f96b | ||
|
|
1114732ac3 | ||
|
|
e2ed53a351 | ||
|
|
a9c354065c | ||
|
|
187ed14d30 | ||
|
|
191646bb99 | ||
|
|
d2e67be9c0 | ||
|
|
2ec2cebf3b | ||
|
|
69a8347a5a | ||
|
|
66b63decb7 | ||
|
|
4943f169f5 | ||
|
|
899421c88e | ||
|
|
a59ed4ce18 | ||
|
|
8e85bf898f | ||
|
|
2bd4cbeda4 | ||
|
|
4d4425147a | ||
|
|
b174cb852c | ||
|
|
2bdcaf7dff | ||
|
|
f27a755240 | ||
|
|
f1e8654f99 | ||
|
|
00a3bfbdac | ||
|
|
55419747d0 | ||
|
|
dfcf0788bd | ||
|
|
b9935a2000 | ||
|
|
a17af20323 | ||
|
|
c5fbc93a9d | ||
|
|
2e1bb6b179 | ||
|
|
7819d67d52 | ||
|
|
0b01e67c5e | ||
|
|
26e2060ad9 | ||
|
|
3751bfae3e | ||
|
|
e6d173674c | ||
|
|
19f7ba45ba | ||
|
|
0b69e7999c | ||
|
|
da678ed29b | ||
|
|
5250742acd | ||
|
|
2b2a8e7e59 | ||
|
|
ff3f170695 | ||
|
|
077a9b4807 | ||
|
|
90c60ffc27 | ||
|
|
38d9361d47 | ||
|
|
7961abc56c | ||
|
|
4c8df9f0c3 | ||
|
|
c35ab6dd5e | ||
|
|
e52a43856e | ||
|
|
1a5395e9d7 | ||
|
|
ff1322611e | ||
|
|
0bbdc9d6c0 | ||
|
|
cb732b60aa | ||
|
|
fb31093ce7 | ||
|
|
a554cf0257 | ||
|
|
2136b574c0 | ||
|
|
abb26bc34b | ||
|
|
ca1f86a4c2 | ||
|
|
ce972d4b0a | ||
|
|
02e4ee0892 | ||
|
|
719299f433 | ||
|
|
8cdefbf342 | ||
|
|
cf1c97b76d | ||
|
|
5af0486032 | ||
|
|
14e9f8d96d | ||
|
|
4a3fce3d1f | ||
|
|
91349936ad | ||
|
|
0ffabf30c3 | ||
|
|
cffad2b05e | ||
|
|
e0cf547246 | ||
|
|
c0ebd66b0d | ||
|
|
9eae226aa3 | ||
|
|
98281ef504 | ||
|
|
90b85bd343 | ||
|
|
bb0dee241f | ||
|
|
0d1f4967de | ||
|
|
1a9a0780d2 | ||
|
|
07b8a67d0e | ||
|
|
c6312c53f8 | ||
|
|
aa40257fc7 | ||
|
|
40e5cfe8af | ||
|
|
0303c741ce | ||
|
|
80bb7ce549 | ||
|
|
cbe21c5bfc | ||
|
|
fbb17894cb | ||
|
|
828577487a | ||
|
|
ae8c2ae5a2 | ||
|
|
cbed21ee1f | ||
|
|
756f5023ca | ||
|
|
7ec0f32355 | ||
|
|
b4a990003f | ||
|
|
26cf57e66d | ||
|
|
dcbed64611 | ||
|
|
37ae3bc60c | ||
|
|
f506989700 | ||
|
|
f75957f0d3 | ||
|
|
f58df8336b | ||
|
|
bfc83ae5b5 | ||
|
|
3366da3016 | ||
|
|
649f9bd821 | ||
|
|
b7e1a10e4d | ||
|
|
7d2776661f | ||
|
|
bc133c8a76 | ||
|
|
dac2892749 | ||
|
|
f004c458a1 | ||
|
|
17b6592d2a | ||
|
|
83486d2719 | ||
|
|
2652180f4b | ||
|
|
055492b76b | ||
|
|
c761899962 | ||
|
|
998bb3a453 | ||
|
|
b9e9c89376 | ||
|
|
943a2145e9 | ||
|
|
4fef6a8582 | ||
|
|
4cf3003dec | ||
|
|
ec47fab422 | ||
|
|
c391d7786b | ||
|
|
cb0e4eae16 | ||
|
|
3b10c19a81 | ||
|
|
c60955c133 | ||
|
|
14fba8d8ba | ||
|
|
9843e9fafa | ||
|
|
08a51b05b1 | ||
|
|
1a7df991e8 | ||
|
|
2391062e71 | ||
|
|
f0c179e23d | ||
|
|
b183c438bb | ||
|
|
60db28f4bb | ||
|
|
e8be70bf9f | ||
|
|
7b60bc220c | ||
|
|
44dfaf20a6 | ||
|
|
009a9f5397 | ||
|
|
5ac2db44e3 | ||
|
|
337b1f05e4 | ||
|
|
537d9ecc33 | ||
|
|
4e9d1cd190 | ||
|
|
fbe1cc93fb | ||
|
|
12c3c9430d | ||
|
|
6251b69feb | ||
|
|
6960b67bfb | ||
|
|
cb3da3d1bc | ||
|
|
79df421846 | ||
|
|
d32c3c9bd9 | ||
|
|
af38c9fc97 | ||
|
|
843fc4c8ec | ||
|
|
97105588df | ||
|
|
6a68dd7817 | ||
|
|
acfd918d80 | ||
|
|
3aea117f2f | ||
|
|
f290a1a3d3 | ||
|
|
93db8810a3 | ||
|
|
4675f52c2d | ||
|
|
73c29b2edf | ||
|
|
5102ba4ab3 | ||
|
|
87746582c3 | ||
|
|
2b739e9ff3 | ||
|
|
3d32244238 | ||
|
|
3380b41e60 | ||
|
|
a07129d704 | ||
|
|
5adfaa6858 | ||
|
|
07d6ffb7e2 | ||
|
|
b025526e6b | ||
|
|
cf4ef78cbe | ||
|
|
6d3ccea413 | ||
|
|
f2ca3b4d45 | ||
|
|
8fa6ce72e6 | ||
|
|
61dba16e6c | ||
|
|
9b1968cf2c | ||
|
|
a91494fc86 | ||
|
|
4a71ce4ee9 | ||
|
|
e90dd00d4d | ||
|
|
e24df11422 | ||
|
|
220d21066b | ||
|
|
b80b5e5097 | ||
|
|
88dfb372d3 | ||
|
|
dc8edb38e5 | ||
|
|
5edaa3abd9 | ||
|
|
20da53a22e | ||
|
|
f598536b6d | ||
|
|
d2862364db | ||
|
|
b5b032c33c | ||
|
|
f86dce3fb8 | ||
|
|
b34a38e479 | ||
|
|
ba7a0cd531 | ||
|
|
57d98d33aa | ||
|
|
f8d17179a9 | ||
|
|
c8c6565208 | ||
|
|
f207aee266 | ||
|
|
8c20daf686 | ||
|
|
8bddad1795 | ||
|
|
04b5627f8d | ||
|
|
d89799cb89 | ||
|
|
ca35323b50 | ||
|
|
3db15898d8 | ||
|
|
b264685b16 | ||
|
|
6713d45f98 | ||
|
|
89e8c4508f | ||
|
|
47f70469f6 | ||
|
|
ec1e77ddbb | ||
|
|
5446befb81 | ||
|
|
85a56083f4 | ||
|
|
137ca05095 | ||
|
|
36b8c81ec1 | ||
|
|
33c0210347 | ||
|
|
8da67b2998 | ||
|
|
f0864bcd33 | ||
|
|
5634d23a51 | ||
|
|
c2008b634c | ||
|
|
e7783af0f9 | ||
|
|
229904187e | ||
|
|
a0e92a1944 | ||
|
|
b4468d3713 | ||
|
|
e94ccaae3e | ||
|
|
0dfeaff1f9 | ||
|
|
39cc589acc | ||
|
|
88a14f235b | ||
|
|
67736b295b | ||
|
|
ee11c3b47d | ||
|
|
9dd9e41001 | ||
|
|
e467689e27 | ||
|
|
d0a23b6038 | ||
|
|
42a5bff039 | ||
|
|
d601dbe597 | ||
|
|
e356186152 | ||
|
|
4c6153c76d | ||
|
|
abebc5d410 | ||
|
|
3dcb5f299c | ||
|
|
0632fa94f7 | ||
|
|
ed85513290 | ||
|
|
07ad8cb434 | ||
|
|
901b719888 | ||
|
|
9850faf400 | ||
|
|
176eca36eb | ||
|
|
77d5435c30 | ||
|
|
3f99df4971 | ||
|
|
819ba0db15 | ||
|
|
e8871cc845 | ||
|
|
e2c152ddc1 | ||
|
|
63c9d316d0 | ||
|
|
6b1ff9e9cb | ||
|
|
7f913c9927 | ||
|
|
b7616ec2ed | ||
|
|
d0840a7de3 | ||
|
|
cc214179f6 | ||
|
|
cbfe5aedc1 | ||
|
|
c0b61d6d4e | ||
|
|
dd28ee3c48 | ||
|
|
3f5e1e53b9 | ||
|
|
a5ef63bc2f | ||
|
|
5f7f496472 | ||
|
|
8058f8ca0a | ||
|
|
6fd5304414 | ||
|
|
2fde626606 | ||
|
|
75ff72e545 | ||
|
|
8b80f11728 | ||
|
|
4648f49ebc | ||
|
|
92fbe3b2d3 | ||
|
|
1e6c418c7e | ||
|
|
c120b9c6a9 | ||
|
|
ec7d77dca0 | ||
|
|
4a15d97207 | ||
|
|
40bb1dbc3e | ||
|
|
ad95ab6ce3 | ||
|
|
4d0a62f26c | ||
|
|
f30b415f05 | ||
|
|
c0de93ce55 | ||
|
|
6560e7bf26 | ||
|
|
4d782f9ec0 | ||
|
|
62dcd0a4ca | ||
|
|
994cb180a0 | ||
|
|
a28a1b0a6c | ||
|
|
2a5c690ab5 | ||
|
|
d7e739e20e | ||
|
|
e2c77fe63b | ||
|
|
39e9a3084b | ||
|
|
13dc659d18 | ||
|
|
aeee083d0a | ||
|
|
cd56b74d25 | ||
|
|
77eb31f840 | ||
|
|
1988182134 | ||
|
|
1b248665c4 | ||
|
|
942b02c8e2 | ||
|
|
7a34ad66e8 | ||
|
|
4fc7b97813 | ||
|
|
77a87c5b1e | ||
|
|
c7d2a552c9 | ||
|
|
63205fbfa1 | ||
|
|
df68671181 | ||
|
|
00cee9f14b | ||
|
|
905d19f46f | ||
|
|
019d10c938 | ||
|
|
7a0d91a510 | ||
|
|
10169e92a6 | ||
|
|
c74112a356 | ||
|
|
9dee8d4669 | ||
|
|
fc875d30b6 | ||
|
|
47fc6ea215 | ||
|
|
26420f9a36 | ||
|
|
c8f461b7f5 | ||
|
|
c17e53cec5 | ||
|
|
9cff76f498 | ||
|
|
b91991ac37 | ||
|
|
9eff7057e7 | ||
|
|
4c096199d2 | ||
|
|
9f39ee6b87 | ||
|
|
d06462d727 | ||
|
|
e4f65407ed | ||
|
|
26d8f2befc | ||
|
|
2713545eda | ||
|
|
fa619fed38 | ||
|
|
6d4f49879b | ||
|
|
37aeb9ebe4 | ||
|
|
5f5423971c | ||
|
|
39b3fde1a0 | ||
|
|
382c900d28 | ||
|
|
5ac2cfb13c | ||
|
|
aa6d2b83f5 | ||
|
|
201250b8af | ||
|
|
ff36a0e8f7 | ||
|
|
71c1ce703a | ||
|
|
c94711234f | ||
|
|
563d2cb4b0 | ||
|
|
3727bd03dd | ||
|
|
efb0b98cdc | ||
|
|
ef5aec5fc1 | ||
|
|
cdbbf06c18 | ||
|
|
e471b1e73a | ||
|
|
1d7f9d250a | ||
|
|
0207e58fa3 | ||
|
|
aecca8426d | ||
|
|
ef0d0a5eb7 | ||
|
|
284c5b270d | ||
|
|
51f2a7573e | ||
|
|
0837e1d972 | ||
|
|
e468186af2 | ||
|
|
3fcfe56bd0 | ||
|
|
85ad68c97d | ||
|
|
ede3b8efac | ||
|
|
cd1d65b29b | ||
|
|
456fe927e7 | ||
|
|
df36d58288 | ||
|
|
918869b307 | ||
|
|
2ba06ae787 | ||
|
|
ac0dc90661 | ||
|
|
8622bd8e6d | ||
|
|
76988e3ec5 | ||
|
|
b0a9fb48d7 | ||
|
|
16d603538f | ||
|
|
7dd810cad6 | ||
|
|
42afea5e0c | ||
|
|
6d0c5e552b | ||
|
|
897c2e4c88 | ||
|
|
673ce9f9c0 | ||
|
|
77054187a0 | ||
|
|
a9c244b1f6 | ||
|
|
ed5e2da875 | ||
|
|
25d5599466 | ||
|
|
33c0aa71d9 | ||
|
|
2e4572c871 | ||
|
|
f6b14777fe | ||
|
|
f251f9456e | ||
|
|
f364eb4f67 | ||
|
|
4896f9e458 | ||
|
|
096535e470 | ||
|
|
7c949b47a7 | ||
|
|
2a6653c700 | ||
|
|
c3b7d96b06 | ||
|
|
9a3783bc45 | ||
|
|
4882d1d9d3 | ||
|
|
9575d86d9c | ||
|
|
85c7e86ed9 | ||
|
|
d527c6ba5f | ||
|
|
6d5b91782c | ||
|
|
b8762aae34 | ||
|
|
4da78df1e6 | ||
|
|
93258ad12a | ||
|
|
0028b69661 | ||
|
|
ef902b1c0c | ||
|
|
d8e3ee092f | ||
|
|
36e8c2f7f8 | ||
|
|
8763b1868e | ||
|
|
700d799b00 | ||
|
|
b0ecc3d4f5 | ||
|
|
e681d5a94f | ||
|
|
980cd8ecf0 | ||
|
|
357c0b3f44 | ||
|
|
6467f5046e | ||
|
|
21ff440ebd | ||
|
|
73228fb26f | ||
|
|
2c1807fe10 | ||
|
|
50091a3098 | ||
|
|
3ba7965402 | ||
|
|
067a53a072 | ||
|
|
9a3956832d | ||
|
|
831b638c99 | ||
|
|
38c343b8d2 | ||
|
|
c05346ad9a | ||
|
|
dd12cd3626 | ||
|
|
55eebe32a6 | ||
|
|
58592df2a0 | ||
|
|
dd110d21e3 | ||
|
|
88443f0117 | ||
|
|
accd5c9120 | ||
|
|
c11f5d78ba | ||
|
|
9f4466d246 | ||
|
|
5504f02c31 | ||
|
|
863e84e226 | ||
|
|
0ce965abe3 | ||
|
|
e9242b3063 | ||
|
|
6ccf320bb2 | ||
|
|
4e9b579843 | ||
|
|
985084864c | ||
|
|
8099dff1b6 | ||
|
|
1528ff3d80 | ||
|
|
71c61c3393 | ||
|
|
f3b1f639b1 | ||
|
|
e599e620c0 | ||
|
|
f5b081656f | ||
|
|
7af8e1a4df | ||
|
|
8c092c8d9f | ||
|
|
e0789186ba | ||
|
|
c798a4747f | ||
|
|
8f396a9913 | ||
|
|
148a5acc25 | ||
|
|
9cf1b87b3f | ||
|
|
82ed55e1f9 | ||
|
|
82041e673d | ||
|
|
1e4405aac9 | ||
|
|
dcefdd12cb | ||
|
|
fd775252b8 | ||
|
|
acd7dc65f6 | ||
|
|
a82d272a27 | ||
|
|
1bb5ddba96 | ||
|
|
ae14a55885 | ||
|
|
9308016cf3 | ||
|
|
b5252a01f8 | ||
|
|
4b22902fa4 | ||
|
|
267ebce457 | ||
|
|
ee8013cba2 | ||
|
|
4ce07eaf31 | ||
|
|
e0d4e4072d | ||
|
|
d4aeece78e | ||
|
|
02adcbc4f8 | ||
|
|
a9d44b3d76 | ||
|
|
3d310ee184 | ||
|
|
0adf1eae76 | ||
|
|
e10d3dd3c7 | ||
|
|
fce0e482d3 | ||
|
|
4d57d00ddb | ||
|
|
7cc90a4378 | ||
|
|
ff0f1a545f | ||
|
|
6eb69a036a | ||
|
|
3c8a47fc76 | ||
|
|
735ec7efa0 | ||
|
|
c33f842c9a | ||
|
|
2340e39fb4 | ||
|
|
0221d1f70e | ||
|
|
c6fe105b46 | ||
|
|
6ec1bc118d | ||
|
|
098dd5425e | ||
|
|
9eeed5094f | ||
|
|
ada4f66105 | ||
|
|
b25c90e338 | ||
|
|
47995056cb | ||
|
|
46ee5b7764 | ||
|
|
c8f0e421fe | ||
|
|
3db6a4443e | ||
|
|
77d20ad189 | ||
|
|
bd6b2adef0 | ||
|
|
7457ada25a | ||
|
|
ab94a0b27c | ||
|
|
41e86af663 | ||
|
|
19e752b7ca | ||
|
|
3d4f71dbad | ||
|
|
cb256976d7 | ||
|
|
1424461e26 | ||
|
|
db66880e25 |
@@ -1,4 +1,3 @@
|
||||
# Populate and change this to .env
|
||||
IMPORTER_DB_API_TOKEN=
|
||||
IMPORTER_DB_API_URL=https://importer.cs.helsinki.fi
|
||||
PATE_TOKEN=
|
||||
|
||||
+45
-13
@@ -1,15 +1,47 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: npm
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: weekly
|
||||
time: "03:00"
|
||||
open-pull-requests-limit: 10
|
||||
target-branch: "trunk"
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: weekly
|
||||
time: "03:00"
|
||||
target-branch: "trunk"
|
||||
- package-ecosystem: npm
|
||||
directory: '/'
|
||||
schedule:
|
||||
interval: weekly
|
||||
time: '03:00'
|
||||
labels:
|
||||
- 'dependencies'
|
||||
- 'infra'
|
||||
- package-ecosystem: npm
|
||||
directory: '/updater/sis-updater-scheduler'
|
||||
schedule:
|
||||
interval: weekly
|
||||
time: '03:00'
|
||||
labels:
|
||||
- 'dependencies'
|
||||
- 'updater'
|
||||
- package-ecosystem: npm
|
||||
directory: '/updater/sis-updater-worker'
|
||||
schedule:
|
||||
interval: weekly
|
||||
time: '03:00'
|
||||
labels:
|
||||
- 'dependencies'
|
||||
- 'updater'
|
||||
- package-ecosystem: npm
|
||||
directory: '/services/backend'
|
||||
schedule:
|
||||
interval: weekly
|
||||
time: '03:00'
|
||||
labels:
|
||||
- 'dependencies'
|
||||
- 'back'
|
||||
- package-ecosystem: npm
|
||||
directory: '/services/frontend'
|
||||
schedule:
|
||||
interval: weekly
|
||||
time: '03:00'
|
||||
labels:
|
||||
- 'dependencies'
|
||||
- 'front'
|
||||
- package-ecosystem: 'github-actions'
|
||||
directory: '/'
|
||||
schedule:
|
||||
interval: weekly
|
||||
time: '03:00'
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
name: "Release oodikone to production"
|
||||
name: 'Release oodikone to production'
|
||||
|
||||
on:
|
||||
release:
|
||||
@@ -6,23 +6,23 @@ on:
|
||||
|
||||
jobs:
|
||||
build_and_release_backend:
|
||||
name: "Build and release backend"
|
||||
name: 'Build and release backend'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: docker/setup-buildx-action@v1
|
||||
- name: "Login to DockerHub"
|
||||
- name: 'Login to DockerHub'
|
||||
uses: docker/login-action@v1
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
- name: "Extract release version"
|
||||
- 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 ::set-output name=VERSION::"${GITHUB_REF#refs/tags/}"
|
||||
- name: "Build and push"
|
||||
- name: 'Build and push'
|
||||
uses: docker/build-push-action@v2
|
||||
with:
|
||||
build-args: |
|
||||
@@ -38,26 +38,26 @@ jobs:
|
||||
target: production
|
||||
|
||||
build_and_release_frontend:
|
||||
name: "Build and release frontend"
|
||||
name: 'Build and release frontend'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: docker/setup-buildx-action@v1
|
||||
- name: "Login to DockerHub"
|
||||
- name: 'Login to DockerHub'
|
||||
uses: docker/login-action@v1
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
- name: "Get current time"
|
||||
- name: 'Get current time'
|
||||
id: get_current_time
|
||||
run: echo ::set-output name=NOW::"$(date --iso-8601=seconds)"
|
||||
- name: "Extract release version"
|
||||
- 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 ::set-output name=VERSION::"${GITHUB_REF#refs/tags/}"
|
||||
- name: "Build and push"
|
||||
- name: 'Build and push'
|
||||
uses: docker/build-push-action@v2
|
||||
with:
|
||||
build-args: |
|
||||
@@ -72,11 +72,11 @@ jobs:
|
||||
push: true
|
||||
tags: toska/oodikone2-frontend:latest
|
||||
target: production
|
||||
- name: "Create a container from built image "
|
||||
- name: 'Create a container from built image '
|
||||
run: docker create --name frontend toska/oodikone2-frontend:latest
|
||||
- name: "Copy assets from the container"
|
||||
- name: 'Copy assets from the container'
|
||||
run: docker cp frontend:/usr/src/app/build ./build
|
||||
- name: "Create Sentry release"
|
||||
- name: 'Create Sentry release'
|
||||
uses: getsentry/action-release@v1
|
||||
env:
|
||||
SENTRY_URL: ${{ secrets.SENTRY_URL }}
|
||||
@@ -90,49 +90,16 @@ jobs:
|
||||
url_prefix: '~/static/js'
|
||||
version: ${{ steps.extract_version.outputs.VERSION }}
|
||||
|
||||
build_and_release_userservice:
|
||||
name: "Build and release userservice"
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: docker/setup-buildx-action@v1
|
||||
- name: "Login to DockerHub"
|
||||
uses: docker/login-action@v1
|
||||
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 ::set-output name=VERSION::"${GITHUB_REF#refs/tags/}"
|
||||
- name: "Build and push"
|
||||
uses: docker/build-push-action@v2
|
||||
with:
|
||||
build-args: |
|
||||
SENTRY_RELEASE=${{ steps.extract_version.outputs.VERSION }}
|
||||
SENTRY_ENVIRONMENT=production
|
||||
cache-from: |
|
||||
type=registry,ref=toska/oodikone2-userservice:staging
|
||||
type=registry,ref=toska/oodikone2-userservice:latest
|
||||
cache-to: type=inline
|
||||
context: services/userservice
|
||||
push: true
|
||||
tags: toska/oodikone2-userservice:latest
|
||||
target: production
|
||||
|
||||
report_success:
|
||||
runs-on: ubuntu-latest
|
||||
name: "Report deployment success to oodikone notifications"
|
||||
name: 'Report deployment success to oodikone notifications'
|
||||
needs:
|
||||
- build_and_release_backend
|
||||
- build_and_release_frontend
|
||||
- build_and_release_userservice
|
||||
- build_and_release_backend
|
||||
- build_and_release_frontend
|
||||
if: success()
|
||||
steps:
|
||||
- name: "Send notification to slack"
|
||||
uses: UniversityOfHelsinkiCS/slackbot-action@v1.4.2
|
||||
- name: 'Send notification to slack'
|
||||
uses: UniversityOfHelsinkiCS/slackbot-action@v1.4.2
|
||||
with:
|
||||
webhook-url: ${{ secrets.WEBHOOK_URL }}
|
||||
message-type: deployment
|
||||
@@ -141,15 +108,14 @@ jobs:
|
||||
|
||||
report_failure:
|
||||
runs-on: ubuntu-latest
|
||||
name: "Report deployment failure to oodikone notifications"
|
||||
name: 'Report deployment failure to oodikone notifications'
|
||||
needs:
|
||||
- build_and_release_backend
|
||||
- build_and_release_frontend
|
||||
- build_and_release_userservice
|
||||
- build_and_release_backend
|
||||
- build_and_release_frontend
|
||||
if: failure()
|
||||
steps:
|
||||
- name: "Send notification to slack"
|
||||
uses: UniversityOfHelsinkiCS/slackbot-action@v1.4.2
|
||||
- name: 'Send notification to slack'
|
||||
uses: UniversityOfHelsinkiCS/slackbot-action@v1.4.2
|
||||
with:
|
||||
webhook-url: ${{ secrets.WEBHOOK_URL }}
|
||||
message-type: deployment-failure
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
name: "Release sis-updater-scheduler"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
paths:
|
||||
- '.github/workflows/release-sis-updater-scheduler.yaml'
|
||||
- 'updater/sis-updater-scheduler/**'
|
||||
|
||||
jobs:
|
||||
build_and_release:
|
||||
name: "Build and release sis-updater-scheduler"
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: docker/setup-buildx-action@v1
|
||||
- name: "Login to DockerHub"
|
||||
uses: docker/login-action@v1
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
- name: "Build and push"
|
||||
uses: docker/build-push-action@v2
|
||||
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: "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-scheduler
|
||||
|
||||
report_failure:
|
||||
runs-on: ubuntu-latest
|
||||
name: "Report deployment failure to oodikone notifications"
|
||||
needs:
|
||||
- build_and_release
|
||||
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
|
||||
softa-to-deploy: sis-updater-scheduler
|
||||
@@ -1,54 +0,0 @@
|
||||
name: "Release sis-updater-worker"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
paths:
|
||||
- '.github/workflows/release-sis-updater-worker.yaml'
|
||||
- 'updater/sis-updater-worker/**'
|
||||
|
||||
jobs:
|
||||
build_and_release:
|
||||
name: "Build and release sis-updater-worker"
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: docker/setup-buildx-action@v1
|
||||
- name: "Login to DockerHub"
|
||||
uses: docker/login-action@v1
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
- name: "Build and push"
|
||||
uses: docker/build-push-action@v2
|
||||
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: "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-worker
|
||||
|
||||
report_failure:
|
||||
runs-on: ubuntu-latest
|
||||
name: "Report deployment failure to oodikone notifications"
|
||||
needs:
|
||||
- build_and_release
|
||||
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
|
||||
softa-to-deploy: sis-updater-worker
|
||||
@@ -0,0 +1,158 @@
|
||||
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@v2
|
||||
- uses: docker/setup-buildx-action@v1
|
||||
- name: 'Build the image'
|
||||
uses: docker/build-push-action@v2
|
||||
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@v2
|
||||
with:
|
||||
path: /tmp/worker.tar
|
||||
|
||||
build_scheduler:
|
||||
name: 'Build sis-updater-scheduler'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: docker/setup-buildx-action@v1
|
||||
- name: 'Build the image'
|
||||
uses: docker/build-push-action@v2
|
||||
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@v2
|
||||
with:
|
||||
path: /tmp/scheduler.tar
|
||||
|
||||
test_updater:
|
||||
name: 'Test updater'
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- build_worker
|
||||
- build_scheduler
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: 'Login to toska docker registry'
|
||||
uses: docker/login-action@v1
|
||||
with:
|
||||
registry: registry.toska.cs.helsinki.fi
|
||||
username: ${{ secrets.TOSKAREGISTRY_USERNAME }}
|
||||
password: ${{ secrets.TOSKAREGISTRY_PASSWORD }}
|
||||
- name: 'Download built images'
|
||||
uses: actions/download-artifact@v2
|
||||
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 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:
|
||||
- build_worker
|
||||
- build_scheduler
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: docker/setup-buildx-action@v1
|
||||
- name: 'Login to DockerHub'
|
||||
uses: docker/login-action@v1
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
- name: 'Build and push scheduler'
|
||||
uses: docker/build-push-action@v2
|
||||
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@v2
|
||||
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: ${{ steps.extract_version.outputs.VERSION }}
|
||||
- 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
|
||||
@@ -9,6 +9,7 @@ on:
|
||||
- 'cypress/**'
|
||||
- 'cypress.json'
|
||||
- 'services/**'
|
||||
- '!services/backend/src/rapodiff.js'
|
||||
- 'docker-compose.ci.yml'
|
||||
- 'docker-compose.test.yml'
|
||||
- 'nginx.conf'
|
||||
@@ -45,21 +46,7 @@ jobs:
|
||||
'eslint --ext .jsx,.js services/frontend' \
|
||||
'stylelint services/frontend/src/**/*.css'
|
||||
|
||||
lint_userservice:
|
||||
name: 'Lint userservice'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: 14
|
||||
- name: 'Install dependencies'
|
||||
run: npm ci && cd services/userservice && npm ci
|
||||
- name: 'Run frontend linter'
|
||||
run: npm run eslint services/userservice
|
||||
|
||||
# === Build images ===
|
||||
|
||||
build_backend:
|
||||
name: 'Build backend'
|
||||
runs-on: ubuntu-latest
|
||||
@@ -116,50 +103,106 @@ jobs:
|
||||
with:
|
||||
path: /tmp/frontend.tar
|
||||
|
||||
build_userservice:
|
||||
name: 'Build userservice'
|
||||
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
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: docker/setup-buildx-action@v1
|
||||
- name: 'Build image and export it'
|
||||
uses: docker/build-push-action@v2
|
||||
- uses: dorny/paths-filter@v2
|
||||
id: check_migrations
|
||||
with:
|
||||
build-args: |
|
||||
SENTRY_RELEASE=${{ github.sha }}
|
||||
SENTRY_ENVIRONMENT=staging
|
||||
cache-from: |
|
||||
type=registry,ref=toska/oodikone2-userservice:staging
|
||||
type=registry,ref=toska/oodikone2-userservice:latest
|
||||
cache-to: type=inline
|
||||
context: services/userservice
|
||||
push: false
|
||||
tags: toska/oodikone2-userservice:staging
|
||||
target: production
|
||||
outputs: type=docker,dest=/tmp/userservice.tar
|
||||
- name: 'Upload image for other jobs'
|
||||
uses: actions/upload-artifact@v2
|
||||
filters: |
|
||||
migrations:
|
||||
- 'updater/sis-updater-worker/src/db/migrations/**'
|
||||
- '.github/workflows/test-oodikone.yaml'
|
||||
- name: 'Login to toska docker registry'
|
||||
uses: docker/login-action@v1
|
||||
with:
|
||||
path: /tmp/userservice.tar
|
||||
registry: registry.toska.cs.helsinki.fi
|
||||
username: ${{ secrets.TOSKAREGISTRY_USERNAME }}
|
||||
password: ${{ secrets.TOSKAREGISTRY_PASSWORD }}
|
||||
- name: 'Let sis-updater-worker run migrations, then push new version of image to toska hub'
|
||||
if: steps.check_migrations.outputs.migrations == 'true'
|
||||
run: |
|
||||
docker-compose --file docker-compose.ci.yml up --detach sis-updater-worker
|
||||
sleep 30 # wait for sis-updater-worker to start and run migrations
|
||||
./anonyymioodi/commit_and_push_to_toska_hub.sh sis-db
|
||||
|
||||
# === Integration tests ===
|
||||
#
|
||||
run_userservice_integration_tests:
|
||||
name: 'Run userservice integration tests'
|
||||
build_and_push_anon_kone_db:
|
||||
name: 'Build anon kone-db and push to toska hub if new migrations have been added'
|
||||
needs:
|
||||
- build_backend
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: 'Run tests included in docker build'
|
||||
run: docker-compose --file docker-compose.test.yml up --build userservice-test
|
||||
- uses: dorny/paths-filter@v2
|
||||
id: check_migrations
|
||||
with:
|
||||
filters: |
|
||||
migrations:
|
||||
- 'services/backend/src/database/migrations_kone/**'
|
||||
- '.github/workflows/test-oodikone.yaml'
|
||||
- name: 'Download images'
|
||||
uses: actions/download-artifact@v2
|
||||
with:
|
||||
path: /tmp/
|
||||
- name: 'Load built backend image'
|
||||
run: docker load --input /tmp/artifact/backend.tar
|
||||
- name: 'Login to toska docker registry'
|
||||
uses: docker/login-action@v1
|
||||
with:
|
||||
registry: registry.toska.cs.helsinki.fi
|
||||
username: ${{ secrets.TOSKAREGISTRY_USERNAME }}
|
||||
password: ${{ secrets.TOSKAREGISTRY_PASSWORD }}
|
||||
- name: 'Let backend run migrations, then push new version of image to toska hub'
|
||||
if: steps.check_migrations.outputs.migrations == 'true'
|
||||
run: |
|
||||
docker-compose --file docker-compose.ci.yml up --detach backend
|
||||
sleep 30 # wait for sis-updater-worker to start and run migrations
|
||||
./anonyymioodi/commit_and_push_to_toska_hub.sh kone-db
|
||||
|
||||
build_and_push_anon_user_db:
|
||||
name: 'Build anon user-db and push to toska hub if new migrations have been added'
|
||||
needs:
|
||||
- build_backend
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: dorny/paths-filter@v2
|
||||
id: check_migrations
|
||||
with:
|
||||
filters: |
|
||||
migrations:
|
||||
- 'services/backend/src/database/migrations_user/**'
|
||||
- '.github/workflows/test-oodikone.yaml'
|
||||
- name: 'Download images'
|
||||
uses: actions/download-artifact@v2
|
||||
with:
|
||||
path: /tmp/
|
||||
- name: 'Load built backend image'
|
||||
run: docker load --input /tmp/artifact/backend.tar
|
||||
- name: 'Login to toska docker registry'
|
||||
uses: docker/login-action@v1
|
||||
with:
|
||||
registry: registry.toska.cs.helsinki.fi
|
||||
username: ${{ secrets.TOSKAREGISTRY_USERNAME }}
|
||||
password: ${{ secrets.TOSKAREGISTRY_PASSWORD }}
|
||||
- name: 'Let backend run migrations, then push new version of image to toska hub'
|
||||
if: steps.check_migrations.outputs.migrations == 'true'
|
||||
run: |
|
||||
docker-compose --file docker-compose.ci.yml up --detach backend
|
||||
sleep 30 # wait for sis-updater-worker to start and run migrations
|
||||
./anonyymioodi/commit_and_push_to_toska_hub.sh user-db
|
||||
|
||||
# === End-to-end tests ===
|
||||
|
||||
test_course_statistics:
|
||||
name: 'Test course statistics'
|
||||
needs:
|
||||
- build_backend
|
||||
- build_frontend
|
||||
- build_userservice
|
||||
- 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@v2
|
||||
@@ -171,7 +214,6 @@ jobs:
|
||||
run: |
|
||||
docker load --input /tmp/artifact/backend.tar
|
||||
docker load --input /tmp/artifact/frontend.tar
|
||||
docker load --input /tmp/artifact/userservice.tar
|
||||
- name: 'Login to toska docker registry'
|
||||
uses: docker/login-action@v1
|
||||
with:
|
||||
@@ -202,7 +244,9 @@ jobs:
|
||||
needs:
|
||||
- build_backend
|
||||
- build_frontend
|
||||
- build_userservice
|
||||
- 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@v2
|
||||
@@ -214,7 +258,6 @@ jobs:
|
||||
run: |
|
||||
docker load --input /tmp/artifact/backend.tar
|
||||
docker load --input /tmp/artifact/frontend.tar
|
||||
docker load --input /tmp/artifact/userservice.tar
|
||||
- name: 'Login to toska docker registry'
|
||||
uses: docker/login-action@v1
|
||||
with:
|
||||
@@ -245,7 +288,9 @@ jobs:
|
||||
needs:
|
||||
- build_backend
|
||||
- build_frontend
|
||||
- build_userservice
|
||||
- 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@v2
|
||||
@@ -257,7 +302,6 @@ jobs:
|
||||
run: |
|
||||
docker load --input /tmp/artifact/backend.tar
|
||||
docker load --input /tmp/artifact/frontend.tar
|
||||
docker load --input /tmp/artifact/userservice.tar
|
||||
- name: 'Login to toska docker registry'
|
||||
uses: docker/login-action@v1
|
||||
with:
|
||||
@@ -288,7 +332,9 @@ jobs:
|
||||
needs:
|
||||
- build_backend
|
||||
- build_frontend
|
||||
- build_userservice
|
||||
- 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@v2
|
||||
@@ -300,7 +346,6 @@ jobs:
|
||||
run: |
|
||||
docker load --input /tmp/artifact/backend.tar
|
||||
docker load --input /tmp/artifact/frontend.tar
|
||||
docker load --input /tmp/artifact/userservice.tar
|
||||
- name: 'Login to toska docker registry'
|
||||
uses: docker/login-action@v1
|
||||
with:
|
||||
@@ -331,7 +376,9 @@ jobs:
|
||||
needs:
|
||||
- build_backend
|
||||
- build_frontend
|
||||
- build_userservice
|
||||
- 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@v2
|
||||
@@ -343,7 +390,6 @@ jobs:
|
||||
run: |
|
||||
docker load --input /tmp/artifact/backend.tar
|
||||
docker load --input /tmp/artifact/frontend.tar
|
||||
docker load --input /tmp/artifact/userservice.tar
|
||||
- name: 'Login to toska docker registry'
|
||||
uses: docker/login-action@v1
|
||||
with:
|
||||
@@ -374,7 +420,9 @@ jobs:
|
||||
needs:
|
||||
- build_backend
|
||||
- build_frontend
|
||||
- build_userservice
|
||||
- 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@v2
|
||||
@@ -386,7 +434,6 @@ jobs:
|
||||
run: |
|
||||
docker load --input /tmp/artifact/backend.tar
|
||||
docker load --input /tmp/artifact/frontend.tar
|
||||
docker load --input /tmp/artifact/userservice.tar
|
||||
- name: 'Login to toska docker registry'
|
||||
uses: docker/login-action@v1
|
||||
with:
|
||||
@@ -417,7 +464,9 @@ jobs:
|
||||
needs:
|
||||
- build_backend
|
||||
- build_frontend
|
||||
- build_userservice
|
||||
- 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@v2
|
||||
@@ -429,7 +478,6 @@ jobs:
|
||||
run: |
|
||||
docker load --input /tmp/artifact/backend.tar
|
||||
docker load --input /tmp/artifact/frontend.tar
|
||||
docker load --input /tmp/artifact/userservice.tar
|
||||
- name: 'Login to toska docker registry'
|
||||
uses: docker/login-action@v1
|
||||
with:
|
||||
@@ -464,7 +512,9 @@ jobs:
|
||||
needs:
|
||||
- build_backend
|
||||
- build_frontend
|
||||
- build_userservice
|
||||
- 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@v2
|
||||
@@ -476,7 +526,6 @@ jobs:
|
||||
run: |
|
||||
docker load --input /tmp/artifact/backend.tar
|
||||
docker load --input /tmp/artifact/frontend.tar
|
||||
docker load --input /tmp/artifact/userservice.tar
|
||||
- name: 'Login to toska docker registry'
|
||||
uses: docker/login-action@v1
|
||||
with:
|
||||
@@ -504,8 +553,6 @@ jobs:
|
||||
needs:
|
||||
- lint_backend
|
||||
- lint_frontend
|
||||
- lint_userservice
|
||||
- run_userservice_integration_tests
|
||||
- test_course_statistics
|
||||
- test_custom_population
|
||||
- test_filters
|
||||
@@ -524,7 +571,6 @@ jobs:
|
||||
run: |
|
||||
docker load --input /tmp/artifact/backend.tar
|
||||
docker load --input /tmp/artifact/frontend.tar
|
||||
docker load --input /tmp/artifact/userservice.tar
|
||||
- name: 'Login to DockerHub'
|
||||
uses: docker/login-action@v1
|
||||
with:
|
||||
@@ -551,7 +597,6 @@ jobs:
|
||||
run: |
|
||||
docker push toska/oodikone2-backend:staging
|
||||
docker push toska/oodikone2-frontend:staging
|
||||
docker push toska/oodikone2-userservice:staging
|
||||
- name: 'Send deployment success notification to slack'
|
||||
if: success()
|
||||
uses: UniversityOfHelsinkiCS/slackbot-action@v1.4.2
|
||||
@@ -574,8 +619,6 @@ jobs:
|
||||
needs:
|
||||
- lint_backend
|
||||
- lint_frontend
|
||||
- lint_userservice
|
||||
- run_userservice_integration_tests
|
||||
- test_course_statistics
|
||||
- test_custom_population
|
||||
- test_filters
|
||||
|
||||
+2
-2
@@ -7,8 +7,8 @@ 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-alpine hadolint ${relativeFilePaths(files)}`,
|
||||
'*.sh': files => `${dockerCmdBase} koalaman/shellcheck-alpine:v0.7.2 ${relativeFilePaths(files)} -x`,
|
||||
Dockerfile: files => `${dockerCmdBase} hadolint/hadolint:v2.7.0 hadolint ${relativeFilePaths(files)}`,
|
||||
'*.sh': files => `${dockerCmdBase} koalaman/shellcheck:v0.7.2 ${relativeFilePaths(files)} -x`,
|
||||
'.github/workflows/*': files => `${dockerCmdBase} rhysd/actionlint:1.6.2 ${relativeFilePaths(files)}`,
|
||||
'docker-compose*': files => {
|
||||
const composeFiles = file => {
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 106 KiB |
@@ -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 0.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.
|
||||
@@ -43,11 +43,15 @@ What different CLI options do:
|
||||
- Cleans up any previous real data databases
|
||||
- Downloads needed database dumps from production servers.
|
||||
- Creates real data databases and populates them with downloaded dumps.
|
||||
3. Reset sis-importer data
|
||||
- Like option 2, but only for sis-importer database. Useful when developing updater microservice.
|
||||
3. Reset single database
|
||||
- Like option 2, but only for single database. Leaves other databases untouched.
|
||||
|
||||
## ⌨️ Development
|
||||
|
||||
### Architecture
|
||||
|
||||

|
||||
|
||||
### Basics
|
||||
|
||||
The development environment is entirely configured in the `docker-compose.yml` file located in this repository. The file defines the services for oodikone's two main components: **oodikone** and **updater**.
|
||||
@@ -79,7 +83,7 @@ It is recommended to spend some time to become familiar with `docker` and `docke
|
||||
- `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 userservice sh`: open bash terminal inside userservice container
|
||||
- `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.
|
||||
|
||||
### User types for development
|
||||
@@ -124,12 +128,16 @@ Continuous integration (CI) works with Github actions and is defined in workflow
|
||||
|
||||
### Modules are missing after updating package.json
|
||||
|
||||
You should always install the dependencies **inside** the container to have the application **inside** the container access them. This might be the case when someone else installs a new library and you only pull the changes in package.json. Use `docker exec -it <service> sh` to get inside the container and run `npm ci` to install modules.
|
||||
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
|
||||
|
||||
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.
|
||||
|
||||
### 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_.
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
*.sqz
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
# Anonyymioodi - anonymous database setup for oodikone
|
||||
|
||||
(name anonyymioodi for :culture: reasons)
|
||||
|
||||
This folder contains a guide and scripts to create anonymous database images for oodikone, mainly used for testing.
|
||||
|
||||
## How to create / modify database images
|
||||
|
||||
Creating / modifying images varies a bit depending on the db. Generally, to modify images, you can either use `psql` directly in docker by running `docker exec -it <dbname> psql -U postgres <dbname>` or start adminer with oodikone's docker-compose.
|
||||
|
||||
By default, previous db images from Toska Hub are used as a base. If needed, comment out Toska hub image and uncomment corresponding clean postgres image in docker-compose before running other steps.
|
||||
|
||||
See more details on how to create / modify databases below.
|
||||
|
||||
### Sis-importer-db
|
||||
|
||||
The following steps are for building a clean sis-importer-db image from scratch. If you want to just modify the existing image a bit, follow the steps defined for user-db / kone-db.
|
||||
|
||||
- See [sis-importer/importer-db-staging-sampletaker](https://github.com/UniversityOfHelsinkiCS/sis-importer/tree/master/importer-db-staging-sampletaker) and create `sis-importer-db.sqz` dump
|
||||
- Move `sis-importer-db.sqz` to this folder
|
||||
- Run `./create_container_from_dump.sh sis-importer-db`
|
||||
- Enrich db with command in file `enrich_data.sql` either by piping command to container or by running command inside container.
|
||||
|
||||
### Sis-db:
|
||||
|
||||
The following steps are for building a clean sis-db image from scratch using oodikone's updater. If you want to just modify the existing image a bit, follow the steps defined for user-db / kone-db.
|
||||
|
||||
- Temporarily replace sis-db -service in oodikone's docker-compose.yml with sis-db -service from this repo's docker-compose.yml
|
||||
- Ensure the newest sis-importer-db image is pulled from toska hub in oodikone repo
|
||||
- Modify sis scheduler limit in config.js by setting: `module.exports.DEV_SCHEDULE_COUNT = null`
|
||||
- Start updater, see oodikone readme for more info.
|
||||
- Populate sis-db from sis-importer-db:
|
||||
|
||||
```
|
||||
curl --request GET --url 'http://localhost:8082/v1/meta?token=dev' && \
|
||||
curl --request GET --url 'http://localhost:8082/v1/programmes?token=dev' && \
|
||||
curl --request GET --url 'http://localhost:8082/v1/students?token=dev'
|
||||
```
|
||||
(or run these from frontend. You might also want to follow sis-updater-workers logs to ensure things are running smoothly.)
|
||||
- Clean up any extra stuff you don't want to keep in sis-db.
|
||||
- Bonus: check that cypress tests run okay
|
||||
|
||||
### User-db / kone-db
|
||||
|
||||
- Temporarily replace the db service in oodikone's docker-compose.yml with the same db service from this folder's docker-compose.yml
|
||||
- Ensure the newest user-db/kone-db image is pulled from the registry with `docker-compose pull`
|
||||
- Modify db to wanted state. Tips:
|
||||
- Run oodikone if you want to upgrade something from frontend (e.g. tags in kone-db, courses in sis-db etc.).
|
||||
- Easy way to create new users to user-db is to modify dev user details in the frontend API config. A new user is created during login if the given uid is not present in user-db.
|
||||
|
||||
## Publishing the image
|
||||
|
||||
After completing the steps above and making sure your local version has the correct data, run `./commit_and_push_to_toska_hub.sh <dbname>` to publish the db image to Toska Hub.
|
||||
|
||||
## How to update Postgres versions
|
||||
|
||||
When Postgres is updated on the server, anon db images need to be updated to match server versions. Do following:
|
||||
|
||||
- Ensure you're using toska hub image in docker-compose and that you've pulled the newest image from toska hub: `docker-compose pull <dbname>`.
|
||||
- Start database with docker-compose
|
||||
- Create dump from database by running `docker exec -i <dbname> pg_dump -Fc -U postgres <dbname> > <dbname>.sqz`
|
||||
- Close container with docker-compose down.
|
||||
- Update the Postgres version in docker-compose.yml in this folder.
|
||||
- Uncomment row with Postgres image and comment row with Toskahub image.
|
||||
- Run `./create_container_from_dump.sh <dbname>`, followed by `./commit_and_push_to_toska_hub.sh <dbname>`
|
||||
Executable
+48
@@ -0,0 +1,48 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# === Config ===
|
||||
|
||||
# Quit if failing
|
||||
set -euo pipefail
|
||||
|
||||
# Print messages and logs that are not script output to stderr
|
||||
msg() {
|
||||
echo >&2 -e "${1-}"
|
||||
}
|
||||
|
||||
# Some special types of messages with colours
|
||||
successmsg() {
|
||||
msg "${GREEN}$1${NOFORMAT}
|
||||
"
|
||||
}
|
||||
errormsg() {
|
||||
msg "${RED}$1${NOFORMAT}
|
||||
"
|
||||
}
|
||||
infomsg() {
|
||||
msg "${BLUE}$1${NOFORMAT}
|
||||
"
|
||||
}
|
||||
warningmsg() {
|
||||
msg "${ORANGE}$1${NOFORMAT}
|
||||
"
|
||||
}
|
||||
|
||||
# Setup colors for messages if running interactive shell
|
||||
if [[ -t 2 ]] && [[ "${TERM-}" != "dumb" ]]; then
|
||||
NOFORMAT='\033[0m' RED='\033[0;31m' GREEN='\033[0;32m' ORANGE='\033[0;33m' BLUE='\033[0;34m'
|
||||
else
|
||||
NOFORMAT='' RED='' GREEN='' ORANGE='' BLUE=''
|
||||
fi
|
||||
|
||||
args=("$@")
|
||||
if [[ ${#args[@]} -ne 1 ]]; then
|
||||
errormsg "No database given, exiting"
|
||||
exit 1
|
||||
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"
|
||||
successmsg "Succesfully pushed"
|
||||
Executable
+64
@@ -0,0 +1,64 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# === Config ===
|
||||
|
||||
# Quit if failing
|
||||
set -euo pipefail
|
||||
|
||||
# Print messages and logs that are not script output to stderr
|
||||
msg() {
|
||||
echo >&2 -e "${1-}"
|
||||
}
|
||||
|
||||
# Some special types of messages with colours
|
||||
successmsg() {
|
||||
msg "${GREEN}$1${NOFORMAT}
|
||||
"
|
||||
}
|
||||
errormsg() {
|
||||
msg "${RED}$1${NOFORMAT}
|
||||
"
|
||||
}
|
||||
infomsg() {
|
||||
msg "${BLUE}$1${NOFORMAT}
|
||||
"
|
||||
}
|
||||
warningmsg() {
|
||||
msg "${ORANGE}$1${NOFORMAT}
|
||||
"
|
||||
}
|
||||
|
||||
# Setup colors for messages if running interactive shell
|
||||
if [[ -t 2 ]] && [[ "${TERM-}" != "dumb" ]]; then
|
||||
NOFORMAT='\033[0m' RED='\033[0;31m' GREEN='\033[0;32m' ORANGE='\033[0;33m' BLUE='\033[0;34m'
|
||||
else
|
||||
NOFORMAT='' RED='' GREEN='' ORANGE='' BLUE=''
|
||||
fi
|
||||
|
||||
# === Create images ===
|
||||
|
||||
retry () {
|
||||
sleep 5
|
||||
for i in {1..60}; do
|
||||
"$@" && break || warningmsg "Retry attempt $i failed, waiting..." && sleep 5;
|
||||
done
|
||||
}
|
||||
|
||||
args=("$@")
|
||||
if [[ ${#args[@]} -ne 1 ]]; then
|
||||
errormsg "No database given, exiting"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
||||
infomsg "Restoring PostgreSQL dumps from backups. This might take a while."
|
||||
database=${args[0]}
|
||||
docker-compose down --remove-orphans
|
||||
docker-compose up -d "$database"
|
||||
|
||||
database_dump="$database.sqz"
|
||||
infomsg "Attempting to restore database $database from dump $database_dump"
|
||||
retry docker-compose exec "$database" pg_isready --dbname="$database"
|
||||
docker exec -i "$database" /bin/bash -c "pg_restore --username=postgres \
|
||||
--format=custom --dbname=$database --no-owner" < "$database_dump"
|
||||
successmsg "Database $database succesfully created"
|
||||
@@ -0,0 +1,50 @@
|
||||
x-defaults: &defaults
|
||||
restart: unless-stopped
|
||||
|
||||
services:
|
||||
kone-db:
|
||||
<<: *defaults
|
||||
container_name: kone-db
|
||||
environment:
|
||||
PGDATA: /data
|
||||
POSTGRES_DB: kone-db
|
||||
POSTGRES_HOST_AUTH_METHOD: trust
|
||||
image: registry.toska.cs.helsinki.fi/kone-db
|
||||
# image: postgres:13.4-alpine
|
||||
profiles:
|
||||
- oodikone
|
||||
|
||||
sis-db:
|
||||
<<: *defaults
|
||||
container_name: sis-db
|
||||
environment:
|
||||
PGDATA: /data
|
||||
POSTGRES_DB: sis-db
|
||||
POSTGRES_HOST_AUTH_METHOD: trust
|
||||
# image: registry.toska.cs.helsinki.fi/sis-db
|
||||
image: postgres:13.2
|
||||
shm_size: 2gb
|
||||
|
||||
sis-importer-db:
|
||||
<<: *defaults
|
||||
container_name: sis-importer-db
|
||||
environment:
|
||||
PGDATA: /data
|
||||
POSTGRES_DB: sis-importer-db
|
||||
POSTGRES_HOST_AUTH_METHOD: trust
|
||||
# image: registry.toska.cs.helsinki.fi/sis-importer-db
|
||||
image: postgres:13.1
|
||||
profiles:
|
||||
- updater
|
||||
|
||||
user-db:
|
||||
<<: *defaults
|
||||
container_name: user-db
|
||||
environment:
|
||||
PGDATA: /data
|
||||
POSTGRES_DB: user-db
|
||||
POSTGRES_HOST_AUTH_METHOD: trust
|
||||
image: registry.toska.cs.helsinki.fi/user-db
|
||||
# image: postgres:13.4-alpine
|
||||
profiles:
|
||||
- oodikone
|
||||
@@ -0,0 +1,133 @@
|
||||
-- add these to sis-importer db by running:
|
||||
-- docker exec -i sis-importer-db psql -U postgres sis-importer-db < enrich_sampledata.sql
|
||||
|
||||
-- if you need to run updater for these students only, see studentnumberlist in the end
|
||||
-- this file
|
||||
|
||||
-- Add admission types (valintaperusteet) to studyrights for some of the computer
|
||||
-- science 2018-2019 population students. See person_ids / student_number below
|
||||
-- (in case you need to update dumps).
|
||||
|
||||
-- TODO: this should be moved into the sampledata creator script in
|
||||
-- importer repo, but since this file contains studentnumbers and person ids, it's
|
||||
-- better to keep this here
|
||||
|
||||
-- KP (koepisteet)
|
||||
-- hy-hlo-1412620 | 013683126
|
||||
-- hy-hlo-1485246 | 014340523
|
||||
-- hy-hlo-32230770 | 010589388
|
||||
-- hy-hlo-35626233 | 010775888
|
||||
-- hy-hlo-40150144 | 011065995
|
||||
UPDATE studyrights
|
||||
SET admission_type_urn = 'urn:code:admission-type:kp'
|
||||
WHERE person_id IN (
|
||||
'hy-hlo-1412620',
|
||||
'hy-hlo-1485246',
|
||||
'hy-hlo-32230770',
|
||||
'hy-hlo-35626233',
|
||||
'hy-hlo-40150144'
|
||||
);
|
||||
|
||||
-- TV (todistusvalinta)
|
||||
-- hy-hlo-45783433 | 011321150
|
||||
-- hy-hlo-48537946 | 010764653
|
||||
-- hy-hlo-49801905 | 011194400
|
||||
-- hy-hlo-53321302 | 011338871
|
||||
-- hy-hlo-51223300 | 013951984
|
||||
-- hy-hlo-53310137 | 012520688
|
||||
-- hy-hlo-53621583 | 011196796
|
||||
-- hy-hlo-53863064 | 011736884
|
||||
-- hy-hlo-57711592 | 010262566
|
||||
-- hy-hlo-57732872 | 013670647
|
||||
UPDATE studyrights
|
||||
SET admission_type_urn = 'urn:code:admission-type:tv'
|
||||
WHERE person_id IN (
|
||||
'hy-hlo-45783433',
|
||||
'hy-hlo-48537946',
|
||||
'hy-hlo-49801905',
|
||||
'hy-hlo-53321302',
|
||||
'hy-hlo-51223300',
|
||||
'hy-hlo-53310137',
|
||||
'hy-hlo-53621583',
|
||||
'hy-hlo-53863064',
|
||||
'hy-hlo-57711592',
|
||||
'hy-hlo-57732872'
|
||||
);
|
||||
|
||||
-- YP:
|
||||
-- hy-hlo-67768022 | 011952325
|
||||
-- hy-hlo-73740025 | 011736606
|
||||
-- hy-hlo-74780917 | 012775433
|
||||
-- hy-hlo-79615371 | 014113941
|
||||
-- hy-hlo-78118455 | 014300615
|
||||
UPDATE studyrights
|
||||
SET admission_type_urn = 'urn:code:admission-type:yp'
|
||||
WHERE person_id IN (
|
||||
'hy-hlo-67768022',
|
||||
'hy-hlo-73740025',
|
||||
'hy-hlo-74780917',
|
||||
'hy-hlo-79615371',
|
||||
'hy-hlo-78118455'
|
||||
);
|
||||
-- AV:
|
||||
-- hy-hlo-78777265 | 010779392
|
||||
-- hy-hlo-81909550 | 011894153
|
||||
-- hy-hlo-84571890 | 013897158
|
||||
-- hy-hlo-80330834 | 015085926
|
||||
-- hy-hlo-84775722 | 011791272
|
||||
UPDATE studyrights
|
||||
SET admission_type_urn = 'urn:code:admission-type:av'
|
||||
WHERE person_id IN (
|
||||
'hy-hlo-78777265',
|
||||
'hy-hlo-81909550',
|
||||
'hy-hlo-84571890',
|
||||
'hy-hlo-80330834',
|
||||
'hy-hlo-84775722'
|
||||
);
|
||||
-- M:
|
||||
-- hy-hlo-87439827 | 011928456
|
||||
-- hy-hlo-96612371 | 014418361
|
||||
-- hy-hlo-84840261 | 010975895
|
||||
-- hy-hlo-90330681 | 010522688
|
||||
-- hy-hlo-96594603 | 013951573
|
||||
UPDATE studyrights
|
||||
SET admission_type_urn = 'urn:code:admission-type:m'
|
||||
WHERE person_id IN (
|
||||
'hy-hlo-87439827',
|
||||
'hy-hlo-96612371',
|
||||
'hy-hlo-84840261',
|
||||
'hy-hlo-90330681',
|
||||
'hy-hlo-96594603'
|
||||
);
|
||||
|
||||
-- all studentnumbers for running updater:
|
||||
013683126
|
||||
014340523
|
||||
010589388
|
||||
010775888
|
||||
011065995
|
||||
011321150
|
||||
010764653
|
||||
011194400
|
||||
011338871
|
||||
013951984
|
||||
012520688
|
||||
011196796
|
||||
011736884
|
||||
010262566
|
||||
013670647
|
||||
011952325
|
||||
011736606
|
||||
012775433
|
||||
014113941
|
||||
014300615
|
||||
010779392
|
||||
011894153
|
||||
013897158
|
||||
015085926
|
||||
011791272
|
||||
011928456
|
||||
014418361
|
||||
010975895
|
||||
010522688
|
||||
013951573
|
||||
@@ -44,20 +44,22 @@ will take care of setting up Oodikone for you. See README for more details."
|
||||
msg ""
|
||||
}
|
||||
|
||||
# Define custom shell prompt for the interactive select loop
|
||||
PS3="Please enter your choice: "
|
||||
set_custom_select_prompt() {
|
||||
PS3="Please enter your choice: "
|
||||
|
||||
options=(
|
||||
"Set up oodikone from scratch."
|
||||
"Reset all real data."
|
||||
"Reset sis-importer data."
|
||||
"Quit."
|
||||
)
|
||||
options=(
|
||||
"Set up oodikone from scratch."
|
||||
"Reset all real data."
|
||||
"Reset single database."
|
||||
"Quit."
|
||||
)
|
||||
}
|
||||
|
||||
# Run scripts before showing interactive prompt
|
||||
# Run scripts and set the prompt
|
||||
show_welcome
|
||||
init_dirs
|
||||
get_username
|
||||
set_custom_select_prompt
|
||||
|
||||
while true; do
|
||||
select opt in "${options[@]}"; do
|
||||
@@ -66,8 +68,10 @@ while true; do
|
||||
set_up_oodikone_from_scratch;;
|
||||
"Reset all real data.")
|
||||
reset_all_real_data;;
|
||||
"Reset sis-importer data.")
|
||||
reset_sis_importer_data;;
|
||||
"Reset single database.")
|
||||
reset_single_database
|
||||
set_custom_select_prompt # Set prompt back to initial values
|
||||
;;
|
||||
"Quit.")
|
||||
break 2;;
|
||||
*) msg "${RED}Invalid option:${NOFORMAT} $REPLY
|
||||
|
||||
@@ -20,15 +20,13 @@ describe('Course Statistics tests', () => {
|
||||
|
||||
cy.contains('.tabular.menu a', 'Table').click()
|
||||
cy.contains('All')
|
||||
cy.contains('.modeSelectorRow a', 'Attempts').click()
|
||||
cy.contains('.modeSelectorRow a', 'Students').click()
|
||||
cy.cs('viewMode-Attempts').click()
|
||||
cy.cs('viewMode-Students').click()
|
||||
|
||||
cy.contains('.tabular.menu a', 'Pass rate chart').click()
|
||||
cy.get('div.modeSelectorContainer').click()
|
||||
cy.contains('svg', 'Pass rate chart')
|
||||
|
||||
cy.contains('.tabular.menu a', 'Grade distribution chart').click()
|
||||
cy.get('div.modeSelectorContainer').click()
|
||||
cy.contains('svg', 'Grades')
|
||||
|
||||
cy.contains('a', 'New query').click()
|
||||
@@ -98,7 +96,7 @@ describe('Course Statistics tests', () => {
|
||||
cy.get("input[placeholder='Search by entering a course name']").type('tietokantojen perusteet')
|
||||
|
||||
cy.contains('Tietokantojen perusteet')
|
||||
cy.contains('Avoin yo: Tietokantojen perusteet')
|
||||
cy.contains('Tietokantojen perusteet')
|
||||
cy.contains('A581328')
|
||||
cy.contains('AYTKT10004')
|
||||
cy.contains('TKT10004, 581328')
|
||||
@@ -107,46 +105,39 @@ describe('Course Statistics tests', () => {
|
||||
cy.contains('Fetch statistics').should('be.enabled').click()
|
||||
cy.contains('Search for courses').should('not.exist')
|
||||
|
||||
cy.contains('TKT10004, 581328 Tietokantojen perusteet')
|
||||
cy.contains('TKT10004, 581328, AYTKT10004, A581328 Tietokantojen perusteet')
|
||||
cy.get('.right').click()
|
||||
cy.contains('No results')
|
||||
})
|
||||
|
||||
it('Provider organization toggle works', () => {
|
||||
cy.url().should('include', '/coursestatistics')
|
||||
cy.contains('Search for courses')
|
||||
cy.get("input[placeholder='Search by entering a course name']").type('tietokantojen perusteet')
|
||||
cy.contains('td', /^AYTKT10004/).click()
|
||||
|
||||
cy.contains('td', /^TKT10004/).click()
|
||||
cy.contains('Fetch statistics').should('be.enabled').click()
|
||||
cy.contains('Search for courses').should('not.exist')
|
||||
cy.contains('AYTKT10004 Avoin yo: Tietokantojen perusteet')
|
||||
|
||||
cy.contains('TKT10004, 581328, AYTKT10004, A581328 Tietokantojen perusteet')
|
||||
cy.get('[data-cy=unify_radio_reqular]').click()
|
||||
cy.contains('TKT10004, 581328 Tietokantojen perusteet')
|
||||
})
|
||||
|
||||
it('Searching course by name displays right courses, 10 credit courses', () => {
|
||||
cy.url().should('include', '/coursestatistics')
|
||||
cy.contains('Search for courses')
|
||||
cy.get("input[placeholder='Search by entering a course name']").type('tietorakenteet ja algoritmit')
|
||||
|
||||
cy.contains('Tietorakenteet ja algoritmit')
|
||||
cy.contains('Avoin yo: Tietorakenteet ja algoritmit')
|
||||
cy.contains('AYTKT20001')
|
||||
cy.contains('TKT20001, 58131')
|
||||
cy.contains('td', /^TKT20001/).click()
|
||||
|
||||
cy.contains('Fetch statistics').should('be.enabled').click()
|
||||
cy.contains('Search for courses').should('not.exist')
|
||||
|
||||
cy.contains('TKT20001, 58131 Tietorakenteet ja algoritmit')
|
||||
cy.contains('TKT20001, 58131, AYTKT20001 Tietorakenteet ja algoritmit')
|
||||
cy.get('.right').click()
|
||||
cy.contains('No results')
|
||||
|
||||
cy.get("input[placeholder='Search by entering a course name']").type('tietorakenteet ja algoritmit')
|
||||
cy.contains('td', /^AYTKT20001/).click()
|
||||
|
||||
cy.contains('Fetch statistics').should('be.enabled').click()
|
||||
cy.contains('Search for courses').should('not.exist')
|
||||
cy.contains('AYTKT20001 Avoin yo: Tietorakenteet ja algoritmit')
|
||||
|
||||
cy.get('.right').click()
|
||||
cy.contains('No results')
|
||||
cy.get("input[name='unifyOpenUniCourses']").parent().click()
|
||||
cy.get("input[placeholder='Search by entering a course name']").type('tietorakenteet ja algoritmit')
|
||||
cy.contains('td', 'TKT20001, 58131, AYTKT20001').click()
|
||||
|
||||
@@ -161,12 +152,11 @@ describe('Course Statistics tests', () => {
|
||||
cy.get("input[placeholder='Search by a course code']").type('TKT20003')
|
||||
cy.contains('tr', 'TKT20003').click()
|
||||
cy.contains('Fetch statistics').should('be.enabled').click()
|
||||
|
||||
cy.contains('Käyttöjärjestelmät')
|
||||
cy.contains('TKT20003')
|
||||
|
||||
cy.get(':nth-child(3) > :nth-child(1) > div > .item > .level').click()
|
||||
cy.contains('Population of course Käyttöjärjestelmät 2019-2020')
|
||||
cy.get(':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')
|
||||
|
||||
cy.contains('Students (127)').click()
|
||||
@@ -176,21 +166,33 @@ 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$/).click()
|
||||
cy.contains(/^TKT20001, AYTKT20001$/).click()
|
||||
cy.contains('Fetch statistics').should('be.enabled').click()
|
||||
cy.contains('TKT20001, 58131 Tietorakenteet ja algoritmit')
|
||||
cy.get(':nth-child(3) > :nth-child(1) > div > .item > .level').click()
|
||||
cy.contains('Population of course Tietorakenteet ja algoritmit 2018-2019')
|
||||
cy.contains('Students (197)').click()
|
||||
cy.contains('TKT20001, 58131, AYTKT20001 Tietorakenteet ja algoritmit')
|
||||
cy.get(':nth-child(3) > :nth-child(2) .level').click()
|
||||
cy.contains('Population of course Tietorakenteet ja algoritmit 2018-2019 (open and normal)')
|
||||
cy.contains('Students (204)').click()
|
||||
cy.contains('td', '010262566').siblings().contains('4')
|
||||
cy.contains('td', '010674989').siblings().contains('1')
|
||||
})
|
||||
|
||||
it('Language distribution is correct', () => {
|
||||
cy.get("input[placeholder='Search by a course code']").type('TKT20003')
|
||||
cy.contains(/^TKT20003$/).click()
|
||||
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.get(':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()
|
||||
cy.contains('td', 'finnish').siblings().contains('122')
|
||||
cy.contains('td', 'english').siblings().contains('5')
|
||||
})
|
||||
|
||||
describe('When searching unified course stats', () => {
|
||||
beforeEach(() => {
|
||||
cy.url().should('include', '/coursestatistics')
|
||||
cy.contains('Search for courses')
|
||||
cy.get("input[name='unifyOpenUniCourses']").parent().click()
|
||||
cy.get("input[placeholder='Search by a course code']").type('TKT10002')
|
||||
cy.contains('td', 'TKT10002, AYTKT10002').click()
|
||||
cy.contains('Fetch statistics').should('be.enabled').click()
|
||||
@@ -202,18 +204,28 @@ describe('Course Statistics tests', () => {
|
||||
const yearRange = { from: '2000-2001', to: '2020-2021' }
|
||||
const attemptsTableContents = [
|
||||
// [time, passed, failed, passrate]
|
||||
['Total', 511, 486, 25],
|
||||
['Total', null, 511, 486, 25],
|
||||
]
|
||||
|
||||
const attemptsTableContentsOpen = [
|
||||
// [time, passed, failed, passrate]
|
||||
['Total', null, 175, 175, 0],
|
||||
]
|
||||
|
||||
const attemptsTableContentsReqular = [
|
||||
// [time, passed, failed, passrate]
|
||||
['Total', null, 312, 298, 14],
|
||||
]
|
||||
|
||||
const gradesTableContents = [
|
||||
// [time, attempts, 0, 1, 2, 3, 4, 5, other passed]
|
||||
['Total', 511, 25, 11, 17, 21, 59, 365, 13],
|
||||
['2020-2021', 1, 0, 0, 0, 0, 0, 1, 0],
|
||||
['2019-2020', 54, 4, 3, 1, 3, 4, 39, 0],
|
||||
['2018-2019', 154, 7, 2, 6, 5, 15, 119, 0],
|
||||
['2017-2018', 186, 9, 3, 5, 6, 16, 139, 8],
|
||||
['2016-2017', 71, 2, 1, 2, 5, 13, 48, 0],
|
||||
['2015-2016', 21, 1, 1, 1, 0, 7, 8, 3],
|
||||
['Total', null, 511, 25, 11, 17, 21, 59, 365, 13],
|
||||
['2020-2021', null, 1, 0, 0, 0, 0, 0, 1, 0],
|
||||
['2019-2020', null, 54, 4, 3, 1, 3, 4, 39, 0],
|
||||
['2018-2019', null, 154, 7, 2, 6, 5, 15, 119, 0],
|
||||
['2017-2018', null, 186, 9, 3, 5, 6, 16, 139, 8],
|
||||
['2016-2017', null, 71, 2, 1, 2, 5, 13, 48, 0],
|
||||
['2015-2016', null, 21, 1, 1, 1, 0, 7, 8, 3],
|
||||
]
|
||||
|
||||
it('shows stats', () => {
|
||||
@@ -240,6 +252,7 @@ describe('Course Statistics tests', () => {
|
||||
.eq(trIndex)
|
||||
.within(() => {
|
||||
values.forEach((value, tdIndex) => {
|
||||
if (value === null) return
|
||||
cy.get('td').eq(tdIndex).contains(value)
|
||||
})
|
||||
})
|
||||
@@ -253,6 +266,7 @@ describe('Course Statistics tests', () => {
|
||||
.eq(trIndex)
|
||||
.within(() => {
|
||||
values.forEach((value, tdIndex) => {
|
||||
if (value === null) return
|
||||
cy.get('td').eq(tdIndex).contains(value)
|
||||
})
|
||||
})
|
||||
@@ -261,6 +275,44 @@ describe('Course Statistics tests', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('Toggling course provider changes stats correctly', () => {
|
||||
cy.get('[data-cy=unify_radio_reqular]').click()
|
||||
cy.get("div[name='toYear']").within(() => {
|
||||
cy.get("div[role='option']").first().should('have.text', '2020-2021')
|
||||
cy.contains("div[role='option']", yearRange.to).should('have.class', 'selected')
|
||||
cy.get("div[role='option']").last().should('have.text', yearRange.from)
|
||||
cy.get("div[role='option']").should('have.length', 21)
|
||||
})
|
||||
cy.contains('#CourseStatPanes a.item', 'Table').click()
|
||||
cy.contains('#CourseStatPanes a.item', 'Attempts').click()
|
||||
cy.get('#CourseStatPanes table>tbody').within(() => {
|
||||
attemptsTableContentsReqular.forEach((values, trIndex) => {
|
||||
cy.get('tr')
|
||||
.eq(trIndex)
|
||||
.within(() => {
|
||||
values.forEach((value, tdIndex) => {
|
||||
if (value === null) return
|
||||
cy.get('td').eq(tdIndex).contains(value)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
cy.get('[data-cy=unify_radio_open]').click()
|
||||
cy.get('#CourseStatPanes table>tbody').within(() => {
|
||||
attemptsTableContentsOpen.forEach((values, trIndex) => {
|
||||
cy.get('tr')
|
||||
.eq(trIndex)
|
||||
.within(() => {
|
||||
values.forEach((value, tdIndex) => {
|
||||
if (value === null) return
|
||||
cy.get('td').eq(tdIndex).contains(value)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
cy.get('[data-cy=unify_radio_unify]').click()
|
||||
})
|
||||
|
||||
it('after changing time range shows same stats', () => {
|
||||
const newYearRange = { from: '2016-2017', to: '2019-2020' }
|
||||
cy.get("div[name='fromYear']")
|
||||
@@ -290,6 +342,15 @@ describe('Course Statistics tests', () => {
|
||||
cy.contains('Show population').should('be.enabled')
|
||||
})
|
||||
})
|
||||
|
||||
it('If no data available, unify toggle is disabled', () => {
|
||||
cy.get("input[placeholder='Search by a course code']").type('AYCSM14111')
|
||||
cy.contains('AYCSM14111').click()
|
||||
cy.contains('Fetch statistics').should('be.enabled').click()
|
||||
cy.contains('AYCSM14111 Avoin yo: Full Stack -websovelluskehitys: React Native')
|
||||
cy.get('[data-cy=unify_radio_reqular]').find('input').should('be.disabled')
|
||||
cy.get('[data-cy=unify_radio_unify]').find('input').should('not.be.disabled')
|
||||
})
|
||||
})
|
||||
|
||||
it('Some features of Course Statistics are hidden for courseStatistics-users without other rights', () => {
|
||||
@@ -302,13 +363,17 @@ describe('Course Statistics tests', () => {
|
||||
cy.contains('Faculty statistics').should('not.exist')
|
||||
cy.contains('Show population').should('not.exist')
|
||||
|
||||
cy.cs('viewMode-Attempts').click()
|
||||
|
||||
const attemptsTableContents = [
|
||||
// [time, passed, failed, passrate]
|
||||
['Total*', 288, 213, 75, '73.96 %'],
|
||||
['2020-2021', '5 or less students', 'NA', 'NA', 'NA'],
|
||||
['2019-2020', 164, 121, 43, '73.78 %'],
|
||||
['2018-2019', 85, 60, 25, '70.59 %'],
|
||||
['2017-2018', 39, 32, 7, '82.05 %'],
|
||||
// ['Total', 288, 213, 75, '73.96 %'],
|
||||
// ['2020-2021', '5 or less students', 'NA', 'NA', 'NA'],
|
||||
['Total', null, 295, 216, 79, '73.22 %'],
|
||||
['2020-2021', null, 5, 2, 3, '40.00 %'],
|
||||
['2019-2020', null, 164, 121, 43, '73.78 %'],
|
||||
['2018-2019', null, 85, 60, 25, '70.59 %'],
|
||||
['2017-2018', null, 39, 32, 7, '82.05 %'],
|
||||
]
|
||||
|
||||
cy.contains('#CourseStatPanes a.item', 'Attempts').click()
|
||||
@@ -318,6 +383,7 @@ describe('Course Statistics tests', () => {
|
||||
.eq(trIndex)
|
||||
.within(() => {
|
||||
values.forEach((value, tdIndex) => {
|
||||
if (value === null) return
|
||||
cy.get('td').eq(tdIndex).contains(value)
|
||||
})
|
||||
})
|
||||
|
||||
+211
-113
@@ -9,30 +9,67 @@ const checkFilteringResult = studentCount => {
|
||||
// Semantic UI doesn't allow injection of data-cy:s for single multiple dropdown selection.
|
||||
// This function tries to click "x" on all selections inside dropdown matching given attribute
|
||||
const clearSemanticUIMultipleDropDownSelection = dataCyAttribute => {
|
||||
cy.cs(dataCyAttribute).find('i.delete').click()
|
||||
cy.cs(dataCyAttribute).find('i.delete').click({ force: true })
|
||||
cy.cs(dataCyAttribute).click()
|
||||
}
|
||||
|
||||
const clearSingleDropdownSelection = dataCyAttribute => {
|
||||
cy.cs(dataCyAttribute).find('i.clear').click()
|
||||
cy.cs(dataCyAttribute).find('i.clear').click({ force: true })
|
||||
}
|
||||
|
||||
const testRangeFilter = (parentEl, min, max, expected) => {
|
||||
cy.cs(parentEl)
|
||||
.cs('range-selector-min')
|
||||
.find('input')
|
||||
.invoke('val')
|
||||
.then(initialMin => {
|
||||
cy.cs(parentEl)
|
||||
.cs('range-selector-max')
|
||||
.find('input')
|
||||
.invoke('val')
|
||||
.then(initialMax => {
|
||||
cy.cs(parentEl).cs('range-selector-min').find('input').type(`{selectall}{backspace}${min}`).blur()
|
||||
cy.cs(parentEl).cs('range-selector-max').find('input').type(`{selectall}{backspace}${max}`).blur()
|
||||
|
||||
checkFilteringResult(expected)
|
||||
|
||||
cy.cs(parentEl).cs('range-selector-min').find('input').type(`{selectall}{backspace}${initialMin}`).blur()
|
||||
cy.cs(parentEl).cs('range-selector-max').find('input').type(`{selectall}{backspace}${initialMax}`).blur()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// Helper tool to create pre and post steps for each filter step. Created to avoid copypasting clicking and checking
|
||||
// to every it-function. Reason behind using test function wrapper is that Cypresses internal beforeEach / afterEach
|
||||
// functions don't take any parameters and using global object for matching test step name seemed overcomplicated.
|
||||
const createRunTestStepWithPreAndPostPartsFunction = amountWithoutFiltering => {
|
||||
return (dataCyAttributeOfHeaderToClick, testStepFunctionToRun) => {
|
||||
cy.cs(dataCyAttributeOfHeaderToClick).click()
|
||||
checkFilteringResult(amountWithoutFiltering)
|
||||
testStepFunctionToRun()
|
||||
checkFilteringResult(amountWithoutFiltering)
|
||||
cy.cs(dataCyAttributeOfHeaderToClick).click()
|
||||
return (filterName, testStepFunctionToRun) => {
|
||||
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') {
|
||||
getHeader().click({ force: true })
|
||||
}
|
||||
|
||||
checkFilteringResult(amountWithoutFiltering)
|
||||
testStepFunctionToRun()
|
||||
checkFilteringResult(amountWithoutFiltering)
|
||||
|
||||
if (open === 'false') {
|
||||
getHeader().click({ force: true })
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
describe('Population Statistics', () => {
|
||||
const pathToCSBach2018 =
|
||||
'/populations?months=36&semesters=FALL&semesters=SPRING&studyRights={%22programme%22%3A%22KH50_005%22}&tag&year=2018'
|
||||
const defaultAmountOfStudents = 148
|
||||
const defaultAmountOfStudents = 159
|
||||
const runTestStepWithPreAndPostParts = createRunTestStepWithPreAndPostPartsFunction(defaultAmountOfStudents)
|
||||
|
||||
before(() => {
|
||||
@@ -41,91 +78,116 @@ describe('Population Statistics', () => {
|
||||
|
||||
it('Transfer filter is on not transferred by default', () => {
|
||||
cy.contains('By default only students who have not transferred to this study programme are shown.')
|
||||
cy.cs('transferredToProgrammeFilter-havenot').find('input').should('be.checked')
|
||||
|
||||
runTestStepWithPreAndPostParts('TransferredToProgramme', () => {
|
||||
const card = cy.cs('TransferredToProgramme-filter-card')
|
||||
card.get('[data-cy="option-havenot"] input').should('be.checked')
|
||||
})
|
||||
})
|
||||
|
||||
it('Programme defaults to "Active Study Right" mode', () => {
|
||||
runTestStepWithPreAndPostParts('Programme', () => {
|
||||
cy.cs('Programme-filter-mode-selector').children().eq(0).should('contain', 'Active Study Right')
|
||||
})
|
||||
})
|
||||
|
||||
it('Programme filter works', () => {
|
||||
runTestStepWithPreAndPostParts('Programme', () => {
|
||||
const card = cy.cs('Programme-filter-card')
|
||||
const programmeDropdown = card.cs('Programme-filter-dropdown').selectFromDropdown(0)
|
||||
checkFilteringResult(5)
|
||||
programmeDropdown.get('i.delete').click()
|
||||
})
|
||||
})
|
||||
|
||||
// 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('graduatedFromProgrammeFilter-header', () => {
|
||||
runTestStepWithPreAndPostParts('GraduatedFromProgramme', () => {
|
||||
const getCard = () => cy.cs('GraduatedFromProgramme-filter-card')
|
||||
|
||||
const graduated = 11
|
||||
cy.cs('graduatedFromProgrammeFilter-graduated-true').click()
|
||||
getCard().cs('option-graduated-true').click()
|
||||
checkFilteringResult(graduated)
|
||||
cy.cs('graduatedFromProgrammeFilter-graduated-false').click()
|
||||
getCard().cs('option-graduated-false').click()
|
||||
checkFilteringResult(defaultAmountOfStudents - graduated)
|
||||
cy.cs('graduatedFromProgrammeFilter-all').click()
|
||||
getCard().cs('option-all').click()
|
||||
})
|
||||
})
|
||||
|
||||
it('Transfer filter works', () => {
|
||||
runTestStepWithPreAndPostParts('transferredToProgrammeFilter-header', () => {
|
||||
runTestStepWithPreAndPostParts('TransferredToProgramme', () => {
|
||||
const transferred = 1
|
||||
cy.cs('transferredToProgrammeFilter-have').click()
|
||||
cy.cs('TransferredToProgramme-filter-card').cs('option-have').click()
|
||||
checkFilteringResult(transferred)
|
||||
cy.cs('transferredToProgrammeFilter-all').click()
|
||||
cy.cs('TransferredToProgramme-filter-card').cs('option-all').click()
|
||||
checkFilteringResult(defaultAmountOfStudents + transferred)
|
||||
cy.cs('transferredToProgrammeFilter-havenot').click()
|
||||
cy.cs('TransferredToProgramme-filter-card').cs('option-havenot').click()
|
||||
})
|
||||
})
|
||||
|
||||
it('Enrollment filter works', () => {
|
||||
runTestStepWithPreAndPostParts('enrollmentStatusFilter-header', () => {
|
||||
cy.selectFromDropdown('enrollmentStatusFilter-status', 0)
|
||||
cy.selectFromDropdown('enrollmentStatusFilter-semesters', [0])
|
||||
checkFilteringResult(137)
|
||||
runTestStepWithPreAndPostParts('EnrollmentStatus', () => {
|
||||
cy.cs('enrollmentStatusFilter-status').selectFromDropdown(0)
|
||||
cy.cs('enrollmentStatusFilter-semesters').selectFromDropdown(0)
|
||||
checkFilteringResult(148)
|
||||
clearSemanticUIMultipleDropDownSelection('enrollmentStatusFilter-semesters')
|
||||
})
|
||||
})
|
||||
|
||||
it('Credit filter works', () => {
|
||||
runTestStepWithPreAndPostParts('credit-filter-header', () => {
|
||||
cy.cs('credit-filter-min').type('50')
|
||||
cy.cs('credit-filter-max').type('150')
|
||||
checkFilteringResult(116)
|
||||
cy.cs('credit-filter-min').find('input').clear()
|
||||
cy.cs('credit-filter-max').find('input').clear()
|
||||
runTestStepWithPreAndPostParts('CreditsEarned', () => {
|
||||
testRangeFilter('CreditsEarned-filter-card', 50, 150, 118)
|
||||
})
|
||||
})
|
||||
|
||||
it('Age filter works', () => {
|
||||
runTestStepWithPreAndPostParts('ageFilter-header', () => {
|
||||
cy.cs('ageFilter-min').type('20')
|
||||
cy.cs('ageFilter-max').type('40')
|
||||
checkFilteringResult(43)
|
||||
cy.cs('ageFilter-min').find('input').clear()
|
||||
cy.cs('ageFilter-max').find('input').clear()
|
||||
runTestStepWithPreAndPostParts('Age', () => {
|
||||
testRangeFilter('Age-filter-card', 20, 40, 49)
|
||||
})
|
||||
})
|
||||
|
||||
it('Gender filter works', () => {
|
||||
runTestStepWithPreAndPostParts('genderFilter-header', () => {
|
||||
cy.selectFromDropdown('genderFilter-dropdown', 0)
|
||||
checkFilteringResult(36)
|
||||
cy.selectFromDropdown('genderFilter-dropdown', 1)
|
||||
checkFilteringResult(112)
|
||||
cy.selectFromDropdown('genderFilter-dropdown', 2)
|
||||
runTestStepWithPreAndPostParts('Gender', () => {
|
||||
cy.cs('genderFilter-dropdown').selectFromDropdown(0)
|
||||
checkFilteringResult(42)
|
||||
cy.cs('genderFilter-dropdown').selectFromDropdown(1)
|
||||
checkFilteringResult(117)
|
||||
cy.cs('genderFilter-dropdown').selectFromDropdown(2)
|
||||
checkFilteringResult(0)
|
||||
cy.selectFromDropdown('genderFilter-dropdown', 3)
|
||||
cy.cs('genderFilter-dropdown').selectFromDropdown(3)
|
||||
checkFilteringResult(0)
|
||||
clearSingleDropdownSelection('genderFilter-dropdown')
|
||||
})
|
||||
})
|
||||
|
||||
it('Starting year filter works', () => {
|
||||
runTestStepWithPreAndPostParts('startYearAtUni-header', () => {
|
||||
cy.selectFromDropdown('startYearAtUni-dropdown', 0)
|
||||
runTestStepWithPreAndPostParts('StartYearAtUni', () => {
|
||||
cy.cs('startYearAtUni-dropdown').selectFromDropdown(0)
|
||||
checkFilteringResult(1)
|
||||
clearSemanticUIMultipleDropDownSelection('startYearAtUni-dropdown')
|
||||
cy.selectFromDropdown('startYearAtUni-dropdown', 14)
|
||||
checkFilteringResult(121)
|
||||
cy.cs('startYearAtUni-dropdown').selectFromDropdown(7)
|
||||
checkFilteringResult(152)
|
||||
clearSemanticUIMultipleDropDownSelection('startYearAtUni-dropdown')
|
||||
})
|
||||
})
|
||||
|
||||
// Return when we have +2020 population with admission types in data
|
||||
it.skip('Admission type filter works', () => {
|
||||
runTestStepWithPreAndPostParts('admissionTypeFilter-header', () => {
|
||||
cy.selectFromDropdown('admissionTypeFilter-dropdown', 0)
|
||||
runTestStepWithPreAndPostParts('AdmissionTypeFilter', () => {
|
||||
cy.cs('admissionTypeFilter-dropdown').selectFromDropdown(0)
|
||||
checkFilteringResult(5)
|
||||
cy.selectFromDropdown('admissionTypeFilter-dropdown', 2)
|
||||
cy.cs('admissionTypeFilter-dropdown').selectFromDropdown(2)
|
||||
checkFilteringResult(10)
|
||||
clearSingleDropdownSelection('admissionTypeFilter-dropdown')
|
||||
})
|
||||
@@ -135,31 +197,28 @@ describe('Population Statistics', () => {
|
||||
it.skip('Courses filter works', () => {
|
||||
// courses takes some time to load, wait for it to complete
|
||||
cy.wait(10000)
|
||||
runTestStepWithPreAndPostParts('courseFilter-header', () => {
|
||||
runTestStepWithPreAndPostParts('Courses', () => {
|
||||
cy.cs('courseFilter-course-dropdown').click()
|
||||
const courses = ['TKT20001', 'MAT11002']
|
||||
cy.cs('courseFilter-course-dropdown').click().contains(courses[0]).click()
|
||||
checkFilteringResult(140)
|
||||
cy.selectFromDropdown(`courseFilter-${courses[0]}-dropdown`, 1)
|
||||
cy.cs(`courseFilter-${courses[0]}-dropdown`).selectFromDropdown(1)
|
||||
checkFilteringResult(131)
|
||||
cy.cs('courseFilter-course-dropdown').click().contains(courses[1]).click()
|
||||
checkFilteringResult(56)
|
||||
cy.selectFromDropdown(`courseFilter-${courses[1]}-dropdown`, 2)
|
||||
cy.cs(`courseFilter-${courses[1]}-dropdown`).selectFromDropdown(2)
|
||||
checkFilteringResult(2)
|
||||
courses.forEach(course => cy.cs(`courseFilter-${course}-clear`).click())
|
||||
})
|
||||
})
|
||||
|
||||
it('Filter combinations work', () => {
|
||||
runTestStepWithPreAndPostParts('graduatedFromProgrammeFilter-header', () => {
|
||||
runTestStepWithPreAndPostParts('ageFilter-header', () => {
|
||||
cy.cs('graduatedFromProgrammeFilter-graduated-true').click()
|
||||
cy.cs('ageFilter-min').type('20')
|
||||
cy.cs('ageFilter-max').type('30')
|
||||
checkFilteringResult(11)
|
||||
cy.cs('ageFilter-min').find('input').clear()
|
||||
cy.cs('ageFilter-max').find('input').clear()
|
||||
cy.cs('graduatedFromProgrammeFilter-all').click()
|
||||
runTestStepWithPreAndPostParts('GraduatedFromProgramme', () => {
|
||||
runTestStepWithPreAndPostParts('Age', () => {
|
||||
const getCard = () => cy.cs('GraduatedFromProgramme-filter-card')
|
||||
getCard().cs('option-graduated-true').click()
|
||||
testRangeFilter('Age-filter-card', 20, 30, 11)
|
||||
getCard().cs('option-all').click()
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -167,7 +226,7 @@ describe('Population Statistics', () => {
|
||||
|
||||
describe('Course Statistics', () => {
|
||||
const pathToDSAndAlgoSpring2019 =
|
||||
'/coursepopulation?coursecodes=%5B%22TKT20001%22%2C%2258131%22%5D&from=138&separate=true&to=138&years=Kev%C3%A4t%202019'
|
||||
'/coursepopulation?coursecodes=%5B%22TKT20001%22%2C%2258131%22%5D&from=138&separate=true&to=138&years=Kev%C3%A4t%202019&unifyCourses=unifyStats'
|
||||
const defaultAmountOfStudents = 118
|
||||
const runTestStepWithPreAndPostParts = createRunTestStepWithPreAndPostPartsFunction(defaultAmountOfStudents)
|
||||
|
||||
@@ -176,7 +235,7 @@ describe('Course Statistics', () => {
|
||||
})
|
||||
|
||||
it('Grade filter works', () => {
|
||||
runTestStepWithPreAndPostParts('gradeFilter-header', () => {
|
||||
runTestStepWithPreAndPostParts('Grade', () => {
|
||||
cy.cs('gradeFilter-5').click()
|
||||
checkFilteringResult(20)
|
||||
cy.cs('gradeFilter-3').click()
|
||||
@@ -186,50 +245,69 @@ describe('Course Statistics', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('Programme defaults to "Attainment" mode', () => {
|
||||
runTestStepWithPreAndPostParts('Programme', () => {
|
||||
cy.cs('Programme-filter-mode-selector').children().eq(0).should('contain', 'Attainment')
|
||||
})
|
||||
})
|
||||
|
||||
it('Programme filter works', () => {
|
||||
runTestStepWithPreAndPostParts('Programme', () => {
|
||||
const card = cy.cs('Programme-filter-card')
|
||||
const programmeDropdown = card.cs('Programme-filter-dropdown').selectFromDropdown(1)
|
||||
checkFilteringResult(116)
|
||||
programmeDropdown.get('i.delete').click()
|
||||
})
|
||||
})
|
||||
|
||||
// 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('Age filter works', () => {
|
||||
runTestStepWithPreAndPostParts('ageFilter-header', () => {
|
||||
cy.cs('ageFilter-min').type('20')
|
||||
cy.cs('ageFilter-max').type('40')
|
||||
checkFilteringResult(33)
|
||||
cy.cs('ageFilter-min').find('input').clear()
|
||||
cy.cs('ageFilter-max').find('input').clear()
|
||||
runTestStepWithPreAndPostParts('Age', () => {
|
||||
testRangeFilter('Age-filter-card', 20, 40, 35)
|
||||
})
|
||||
})
|
||||
|
||||
it('Gender filter works', () => {
|
||||
runTestStepWithPreAndPostParts('genderFilter-header', () => {
|
||||
cy.selectFromDropdown('genderFilter-dropdown', 0)
|
||||
runTestStepWithPreAndPostParts('Gender', () => {
|
||||
cy.cs('genderFilter-dropdown').selectFromDropdown(0)
|
||||
checkFilteringResult(27)
|
||||
cy.selectFromDropdown('genderFilter-dropdown', 1)
|
||||
cy.cs('genderFilter-dropdown').selectFromDropdown(1)
|
||||
checkFilteringResult(91)
|
||||
cy.selectFromDropdown('genderFilter-dropdown', 2)
|
||||
cy.cs('genderFilter-dropdown').selectFromDropdown(2)
|
||||
checkFilteringResult(0)
|
||||
cy.selectFromDropdown('genderFilter-dropdown', 3)
|
||||
cy.cs('genderFilter-dropdown').selectFromDropdown(3)
|
||||
checkFilteringResult(0)
|
||||
clearSingleDropdownSelection('genderFilter-dropdown')
|
||||
})
|
||||
})
|
||||
|
||||
it('Starting year filter works', () => {
|
||||
runTestStepWithPreAndPostParts('startYearAtUni-header', () => {
|
||||
cy.selectFromDropdown('startYearAtUni-dropdown', [0])
|
||||
runTestStepWithPreAndPostParts('StartYearAtUni', () => {
|
||||
cy.cs('startYearAtUni-dropdown').selectFromDropdown([0])
|
||||
checkFilteringResult(1)
|
||||
clearSemanticUIMultipleDropDownSelection('startYearAtUni-dropdown')
|
||||
cy.selectFromDropdown('startYearAtUni-dropdown', 12)
|
||||
checkFilteringResult(29)
|
||||
cy.cs('startYearAtUni-dropdown').selectFromDropdown(3)
|
||||
checkFilteringResult(84)
|
||||
clearSemanticUIMultipleDropDownSelection('startYearAtUni-dropdown')
|
||||
})
|
||||
})
|
||||
|
||||
it('Filter combinations work', () => {
|
||||
runTestStepWithPreAndPostParts('gradeFilter-header', () => {
|
||||
runTestStepWithPreAndPostParts('ageFilter-header', () => {
|
||||
runTestStepWithPreAndPostParts('Grade', () => {
|
||||
runTestStepWithPreAndPostParts('Age', () => {
|
||||
cy.cs('gradeFilter-3').click()
|
||||
cy.cs('ageFilter-min').type('20')
|
||||
cy.cs('ageFilter-max').type('30')
|
||||
checkFilteringResult(1)
|
||||
cy.cs('ageFilter-min').find('input').clear()
|
||||
cy.cs('ageFilter-max').find('input').clear()
|
||||
testRangeFilter('Age-filter-card', 20, 30, 1)
|
||||
cy.cs('gradeFilter-3').click()
|
||||
})
|
||||
})
|
||||
@@ -245,51 +323,71 @@ describe('Custom Population Statistics', () => {
|
||||
cy.fixture('customPopulations').then(({ studentNumbersForCSStudentsSet1, studentNumbersForCSStudentsSet2 }) => {
|
||||
const students = [...studentNumbersForCSStudentsSet1, ...studentNumbersForCSStudentsSet2]
|
||||
runTestStepWithPreAndPostParts = createRunTestStepWithPreAndPostPartsFunction(students.length)
|
||||
cy.cs('student-no-input').click().type(students.join('/n'))
|
||||
cy.cs('student-no-input').click().type(students.join('\n'))
|
||||
cy.wait(10000)
|
||||
})
|
||||
cy.cs('search-button').click()
|
||||
})
|
||||
|
||||
it('Age filter works', () => {
|
||||
runTestStepWithPreAndPostParts('ageFilter-header', () => {
|
||||
cy.cs('ageFilter-min').type('20')
|
||||
cy.cs('ageFilter-max').type('30')
|
||||
checkFilteringResult(1)
|
||||
cy.cs('ageFilter-min').find('input').clear()
|
||||
cy.cs('ageFilter-max').find('input').clear()
|
||||
runTestStepWithPreAndPostParts('Age', () => {
|
||||
testRangeFilter('Age-filter-card', 20, 30, 1)
|
||||
})
|
||||
})
|
||||
|
||||
it('Programme defaults to "Active Study Right" mode', () => {
|
||||
runTestStepWithPreAndPostParts('Programme', () => {
|
||||
cy.cs('Programme-filter-mode-selector').children().eq(0).should('contain', 'Active Study Right')
|
||||
})
|
||||
})
|
||||
|
||||
it('Programme filter works', () => {
|
||||
runTestStepWithPreAndPostParts('Programme', () => {
|
||||
const card = cy.cs('Programme-filter-card')
|
||||
const programmeDropdown = card.cs('Programme-filter-dropdown').selectFromDropdown(0)
|
||||
checkFilteringResult(5)
|
||||
programmeDropdown.get('i.delete').click()
|
||||
})
|
||||
})
|
||||
|
||||
it('Gender filter works', () => {
|
||||
cy.cs('genderFilter-header').click()
|
||||
cy.selectFromDropdown('genderFilter-dropdown', 0)
|
||||
checkFilteringResult(3)
|
||||
cy.selectFromDropdown('genderFilter-dropdown', 1)
|
||||
checkFilteringResult(2)
|
||||
cy.selectFromDropdown('genderFilter-dropdown', 2)
|
||||
checkFilteringResult(0)
|
||||
clearSingleDropdownSelection('genderFilter-dropdown')
|
||||
checkFilteringResult(5, true)
|
||||
})
|
||||
|
||||
it('Starting year filter works', () => {
|
||||
runTestStepWithPreAndPostParts('startYearAtUni-header', () => {
|
||||
cy.selectFromDropdown('startYearAtUni-dropdown', [0])
|
||||
checkFilteringResult(1)
|
||||
clearSemanticUIMultipleDropDownSelection('startYearAtUni-dropdown')
|
||||
runTestStepWithPreAndPostParts('Gender', () => {
|
||||
cy.cs('genderFilter-dropdown').selectFromDropdown(0)
|
||||
checkFilteringResult(3)
|
||||
cy.cs('genderFilter-dropdown').selectFromDropdown(1)
|
||||
checkFilteringResult(2)
|
||||
cy.cs('genderFilter-dropdown').selectFromDropdown(2)
|
||||
checkFilteringResult(0)
|
||||
clearSingleDropdownSelection('genderFilter-dropdown')
|
||||
checkFilteringResult(5, true)
|
||||
})
|
||||
})
|
||||
|
||||
it('Courses filter works', () => {
|
||||
runTestStepWithPreAndPostParts('courseFilter-header', () => {
|
||||
it('Starting year filter works', () => {
|
||||
runTestStepWithPreAndPostParts('StartYearAtUni', () => {
|
||||
cy.cs('startYearAtUni-dropdown').selectFromDropdown([0])
|
||||
checkFilteringResult(1)
|
||||
clearSemanticUIMultipleDropDownSelection('startYearAtUni-dropdown')
|
||||
cy.cs('startYearAtUni-dropdown').click()
|
||||
})
|
||||
})
|
||||
|
||||
it.skip('Courses filter works', () => {
|
||||
runTestStepWithPreAndPostParts('Courses', () => {
|
||||
const courses = ['MAT11001', 'TKT20004']
|
||||
cy.cs('courseFilter-course-dropdown').click().contains(courses[0]).click()
|
||||
cy.cs('courseFilter-course-dropdown')
|
||||
.click()
|
||||
.contains(courses[0] + ' ')
|
||||
.click()
|
||||
checkFilteringResult(5)
|
||||
cy.selectFromDropdown(`courseFilter-${courses[0]}-dropdown`, 1)
|
||||
cy.cs(`courseFilter-${courses[0]}-dropdown`).selectFromDropdown(1)
|
||||
checkFilteringResult(5)
|
||||
cy.cs('courseFilter-course-dropdown').click().contains(courses[1]).click()
|
||||
cy.cs('courseFilter-course-dropdown')
|
||||
.click()
|
||||
.contains(courses[1] + ' ')
|
||||
.click()
|
||||
checkFilteringResult(3)
|
||||
cy.selectFromDropdown(`courseFilter-${courses[1]}-dropdown`, 3)
|
||||
cy.cs(`courseFilter-${courses[1]}-dropdown`).selectFromDropdown(3)
|
||||
checkFilteringResult(1)
|
||||
courses.forEach(course => cy.cs(`courseFilter-${course}-clear`).click())
|
||||
})
|
||||
|
||||
@@ -1,18 +1,23 @@
|
||||
/// <reference types="Cypress" />
|
||||
|
||||
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')
|
||||
}
|
||||
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'
|
||||
const pathToCSMaster2019 =
|
||||
'/populations?months=27&semesters=FALL&semesters=SPRING&studyRights=%7B%22programme%22%3A%22MH50_009%22%7D&tag&year=2019'
|
||||
describe('when using basic user', () => {
|
||||
beforeEach(() => {
|
||||
cy.init('/populations')
|
||||
@@ -59,31 +64,34 @@ describe('Population Statistics tests', () => {
|
||||
|
||||
it('Population statistics is usable on general level', () => {
|
||||
cy.visit(pathToCSBach2017)
|
||||
setPopStatsUntil('toukokuu 2020')
|
||||
cy.cs('filtered-students')
|
||||
// This advanced settings does not work correctly
|
||||
// see https://github.com/UniversityOfHelsinkiCS/oodikone/issues/3552
|
||||
// setPopStatsUntil('toukokuu 2020')
|
||||
|
||||
cy.get('.card').within(() => {
|
||||
cy.contains('Tietojenkäsittelytieteen kandiohjelma')
|
||||
cy.contains('Sample size: 149 students')
|
||||
cy.contains('Sample size: 170 students')
|
||||
cy.contains('Excludes exchange students')
|
||||
cy.contains("Excludes students who haven't enrolled present nor absent")
|
||||
cy.contains('Excludes students with non-degree study right')
|
||||
cy.contains('Excludes students who have transferred out of this programme')
|
||||
})
|
||||
cy.contains('Courses of population').click({ force: true })
|
||||
cy.cs('filtered-students')
|
||||
cy.contains('Courses of population').click()
|
||||
|
||||
cy.intercept('/api/v3/courseyearlystats**').as('coursePage')
|
||||
// eslint-disable-next-line cypress/no-unnecessary-waiting
|
||||
cy.wait(10000) // a bit hacky way, wait until ui is ready
|
||||
cy.get('[data-cy=expand-TKT1]').click()
|
||||
cy.get('[data-cy=coursestats-link-TKT10002]').click()
|
||||
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 Ohjelmoinnin perusteet')
|
||||
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 (149)').click()
|
||||
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()
|
||||
@@ -112,26 +120,72 @@ describe('Population Statistics tests', () => {
|
||||
|
||||
cy.contains('Fetch population').click()
|
||||
|
||||
cy.contains('Credit accumulation (for 149 students)')
|
||||
cy.contains('Credit accumulation (for 170 students)')
|
||||
|
||||
// spring + fall and include cancelled
|
||||
// spring + fall
|
||||
cy.get('[data-cy=advanced-toggle]').click()
|
||||
cy.cs('toggle-spring').click()
|
||||
cy.cs('toggle-cancelled').click()
|
||||
cy.contains('Fetch population').click()
|
||||
|
||||
cy.contains('Credit accumulation (for 170 students)')
|
||||
})
|
||||
|
||||
it('Credit Statistics, Statistics pane works', () => {
|
||||
it('Credit Statistics, Credits Gained tab works', () => {
|
||||
cy.selectStudyProgramme('Tietojenkäsittelytieteen kandiohjelma')
|
||||
cy.contains('Credit statistics').click()
|
||||
cy.get("[data-cy='credits-gained-main-table']").should('contain', 'All students of the population')
|
||||
|
||||
const months = Math.ceil(moment.duration(moment().diff(moment('2017-08-1'))).asMonths())
|
||||
|
||||
const limits = [1, ..._.range(1, 4).map(p => Math.ceil(months * ((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)')
|
||||
|
||||
for (const [start, end] of ranges) {
|
||||
let value = 'credits'
|
||||
|
||||
if (start !== null) {
|
||||
value = `${start} ≤ ${value}`
|
||||
}
|
||||
|
||||
if (end !== null) {
|
||||
value = `${value} < ${end}`
|
||||
}
|
||||
|
||||
cy.get('.credits-gained-table').should('contain', value)
|
||||
}
|
||||
|
||||
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)
|
||||
})
|
||||
|
||||
cy.get("[data-cy='credits-gained-table-Ei valintatapaa']").should('not.exist')
|
||||
})
|
||||
|
||||
it('Credit Statistics, Credits Gained tab shows stats by admissions', () => {
|
||||
cy.visit(pathToCSMaster2019)
|
||||
cy.cs('filtered-students')
|
||||
cy.contains('Credit statistics')
|
||||
cy.get("[data-cy='credit-statistics']").click()
|
||||
cy.get('.credits-gained-divider').click()
|
||||
cy.get("[data-cy='credits-gained-table-Avoin väylä']").should('exist')
|
||||
cy.get("[data-cy='credits-gained-table-Yhteispisteet']").should('exist')
|
||||
cy.get("[data-cy='credits-gained-table-Muu']").should('exist')
|
||||
cy.get("[data-cy='credits-gained-table-Ei valintatapaa']").should('exist')
|
||||
})
|
||||
|
||||
it('Credit Statistics, Statistics pane works', () => {
|
||||
cy.selectStudyProgramme('Tietojenkäsittelytieteen kandiohjelma')
|
||||
cy.contains('Credit statistics').click({ force: true })
|
||||
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 = 149 Students')
|
||||
cy.get("[data-cy='credit-stats-mean']").should('contain', '123.89')
|
||||
cy.get("[data-cy='credit-stats-stdev']").should('contain', '61.32')
|
||||
cy.get("[data-cy='credit-stats-min']").should('contain', '11')
|
||||
cy.get("[data-cy='credit-stats-table-name-header']").should('contain', 'Statistic for n = 170 Students')
|
||||
cy.get("[data-cy='credit-stats-mean']").should('contain', '114.75')
|
||||
cy.get("[data-cy='credit-stats-stdev']").should('contain', '65.53')
|
||||
cy.get("[data-cy='credit-stats-min']").should('contain', '5')
|
||||
cy.get("[data-cy='credit-stats-max']").should('contain', '314')
|
||||
})
|
||||
})
|
||||
@@ -144,7 +198,7 @@ describe('Population Statistics tests', () => {
|
||||
const existing = '010113437'
|
||||
const nonExisting = '66666666'
|
||||
cy.visit(pathToCSBach2017)
|
||||
cy.contains('Students (149)').click()
|
||||
cy.contains('Students (170)').click()
|
||||
cy.contains(existing)
|
||||
cy.contains(nonExisting).should('not.exist')
|
||||
cy.contains('button', 'Check studentnumbers').click()
|
||||
|
||||
@@ -13,7 +13,7 @@ describe('Student Statistics tests', () => {
|
||||
studentnumber: '010654019',
|
||||
}
|
||||
|
||||
it.only('Student statistics search form is usable', () => {
|
||||
it('Student statistics search form is usable', () => {
|
||||
cy.contains('Show student names')
|
||||
cy.url().should('include', '/students')
|
||||
cy.get('.prompt').type(student.lastname)
|
||||
@@ -37,8 +37,8 @@ describe('Student Statistics tests', () => {
|
||||
it('Can get student specific page by clicking student', () => {
|
||||
cy.url().should('include', '/students')
|
||||
cy.get('.prompt').type(student.studentnumber)
|
||||
cy.contains(student.studentnumber).click()
|
||||
cy.contains('Started: 01.08.2011')
|
||||
cy.contains('td a', student.studentnumber).click()
|
||||
cy.contains('Started: 01.08.2017')
|
||||
cy.contains('Tietojenkäsittelytieteen kandiohjelma (01.08.2017 - 01.08.2025')
|
||||
cy.contains(student.lastname).should('not.exist')
|
||||
cy.contains(student.firstnames).should('not.exist')
|
||||
@@ -54,7 +54,7 @@ describe('Student Statistics tests', () => {
|
||||
|
||||
it('Can get back to search menu', () => {
|
||||
cy.get('.prompt').type(student.studentnumber)
|
||||
cy.contains(student.studentnumber).click()
|
||||
cy.contains('td a', student.studentnumber).click()
|
||||
cy.get('.remove').click()
|
||||
cy.contains('Student number').should('not.exist')
|
||||
cy.contains('Credits').should('not.exist')
|
||||
@@ -62,7 +62,7 @@ describe('Student Statistics tests', () => {
|
||||
|
||||
it('Can jump to course', () => {
|
||||
cy.get('.prompt').type(student.studentnumber)
|
||||
cy.contains(student.studentnumber).click()
|
||||
cy.contains('td a', student.studentnumber).click()
|
||||
cy.contains('Introduction to Machine Learning (DATA11002)')
|
||||
.siblings()
|
||||
.within(() => {
|
||||
@@ -80,11 +80,21 @@ describe('Student Statistics tests', () => {
|
||||
cy.contains('Student number').should('not.exist')
|
||||
})
|
||||
|
||||
it.only('Can jump to population page', () => {
|
||||
it('Can jump to population page', () => {
|
||||
cy.get('.prompt').type(student.studentnumber)
|
||||
cy.contains(student.studentnumber).click()
|
||||
cy.contains('td a', student.studentnumber).click()
|
||||
cy.get('i.level.up.alternate.icon').eq(0).click()
|
||||
cy.contains('Population statistics')
|
||||
cy.contains('Tietojenkäsittelytieteen kandiohjelma')
|
||||
})
|
||||
})
|
||||
|
||||
// Use admin to see all students
|
||||
describe('Testing with admin rights', () => {
|
||||
it('Does not crash if student has no studyright or courses', () => {
|
||||
const studentNumber = '011825096'
|
||||
cy.init(`/students/${studentNumber}`, 'admin') // use admin to see all students
|
||||
cy.contains('Started: Unavailable')
|
||||
cy.contains('Credits: 0')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,172 +1,376 @@
|
||||
/// <reference types="Cypress" />
|
||||
|
||||
const deleteTag = name => {
|
||||
cy.contains(name).siblings().contains('Delete').click()
|
||||
cy.contains('td', name).siblings().contains('Delete').click()
|
||||
cy.contains('Are you sure you want to delete tag')
|
||||
cy.contains('Confirm').click()
|
||||
cy.contains(name).should('not.exist')
|
||||
cy.contains('td', name).should('not.exist')
|
||||
}
|
||||
|
||||
const getEmptyYears = isAcademicYear => {
|
||||
const thisYear = new Date().getFullYear()
|
||||
const years = []
|
||||
for (let year = thisYear; year > 2021; year--) {
|
||||
if (isAcademicYear) {
|
||||
years.push(`${year} - ${year + 1}`)
|
||||
} else {
|
||||
years.push(year)
|
||||
}
|
||||
}
|
||||
return years
|
||||
}
|
||||
|
||||
describe('Studyprogramme overview', () => {
|
||||
// check is used by couple of tests
|
||||
const checkProgressAndProductivity = (user = 'basic') => {
|
||||
// Column amount depends on user. Note that with dev user, you'll see even more
|
||||
// stuff than with admin or basic.
|
||||
const CSBachPopulationProgress2018ForAdmin = [
|
||||
'162',
|
||||
'44 (27%)',
|
||||
'118 (72%)',
|
||||
'161 (99%)',
|
||||
'161',
|
||||
'1',
|
||||
'11',
|
||||
'11',
|
||||
'152',
|
||||
'125',
|
||||
'78',
|
||||
'42',
|
||||
'16',
|
||||
]
|
||||
const CSBachPopulationProgress2018ForBasic = [
|
||||
'162',
|
||||
'44 (27%)',
|
||||
'118 (72%)',
|
||||
'161 (99%)',
|
||||
'161',
|
||||
'1',
|
||||
'11',
|
||||
'11',
|
||||
'152',
|
||||
'125',
|
||||
'78',
|
||||
'42',
|
||||
'16',
|
||||
] // cancelled and transferred from are missing
|
||||
|
||||
const population = user === 'basic' ? CSBachPopulationProgress2018ForBasic : CSBachPopulationProgress2018ForAdmin
|
||||
cy.contains('Population progress')
|
||||
cy.contains('Yearly productivity')
|
||||
|
||||
cy.contains('2018-2019')
|
||||
.siblings()
|
||||
.each((elem, index) => {
|
||||
cy.wrap(elem).contains(population[index])
|
||||
})
|
||||
const populationproductivity2019 = ['7388.0', '17', '7388.00', '0.00', '349.00']
|
||||
cy.get('table')
|
||||
.eq(1)
|
||||
.contains('2019')
|
||||
.siblings()
|
||||
.each((elem, index) => {
|
||||
cy.wrap(elem).contains(populationproductivity2019[index])
|
||||
})
|
||||
}
|
||||
|
||||
describe('when opening programme page with admin user', () => {
|
||||
/* Basic information overview -tests*/
|
||||
describe('Basic information -view works for basic user', () => {
|
||||
beforeEach(() => {
|
||||
cy.init('/study-programme/KH50_005', 'admin')
|
||||
cy.init('/study-programme')
|
||||
cy.viewport(1536, 960)
|
||||
})
|
||||
|
||||
it('progress should not be recalculating when opened for the first time', () => {
|
||||
cy.contains('Recalculating').should('not.exist')
|
||||
cy.contains('Last updated').should('exist')
|
||||
// If the backend breaks for one of the sections, the section header is not rendered and this will fail
|
||||
it('Basic information -tab loads', () => {
|
||||
cy.contains('a', 'Tietojenkäsittelytieteen kandiohjelma').click()
|
||||
cy.get('[data-cy=Section-StudentsOfTheStudyprogramme]')
|
||||
cy.get('[data-cy=Section-CreditsProducedByTheStudyprogramme]')
|
||||
cy.get('[data-cy=Section-GraduatedAndThesisWritersOfTheProgramme]')
|
||||
cy.get('[data-cy=Section-ProgrammesBeforeOrAfter]')
|
||||
cy.get('[data-cy=Section-AverageGraduationTimes]')
|
||||
})
|
||||
|
||||
// Following vars are used to compare values before and after stats calculation
|
||||
// Taken from https://docs.cypress.io/api/commands/should#Compare-text-values-of-two-elements
|
||||
const normalizeText = s => s.replace(/\s/g, '').toLowerCase()
|
||||
let originalProgressCalculatedText
|
||||
let originalProductivityCalculatedText
|
||||
const doRecalculation = () => {
|
||||
// Wait for page to load before clicking to admin page, TODO: change to cy.intercept
|
||||
cy.wait(5000)
|
||||
cy.get('.attached').contains('Admin').click()
|
||||
cy.contains('recalculate productivity').click()
|
||||
cy.contains('recalculate throughput').click()
|
||||
cy.get('.attached').contains('Overview').click() // TODO: add data-cy tags for tabs
|
||||
// Wait to "recalculating" to disappear, TODO: change this to cy.intercept
|
||||
cy.wait(5000)
|
||||
cy.reload()
|
||||
}
|
||||
it('Basic information contains correct students', () => {
|
||||
cy.contains('a', 'Tietojenkäsittelytieteen kandiohjelma').click()
|
||||
const years = getEmptyYears()
|
||||
const tableContents = [
|
||||
// [Year, Started, Graduated, Transferred Away, Transferred to]
|
||||
...years.map(year => [year, 0, 0, 0, 0]),
|
||||
[2021, 0, 0, 1, 0],
|
||||
[2020, 12, 36, 3, 2],
|
||||
[2019, 87, 17, 0, 1],
|
||||
[2018, 161, 1, 0, 0],
|
||||
[2017, 171, 0, 0, 0],
|
||||
]
|
||||
|
||||
it('renders progress and productivity tables with calculated status', () => {
|
||||
doRecalculation()
|
||||
|
||||
// check contains also study programme options for admin
|
||||
cy.contains('Study programme options')
|
||||
cy.get('table').should('have.length', 3)
|
||||
|
||||
checkProgressAndProductivity('admin')
|
||||
|
||||
// Grab update dates to be compared later
|
||||
cy.cs('throughputUpdateStatus')
|
||||
.invoke('text')
|
||||
.then(text => {
|
||||
originalProgressCalculatedText = normalizeText(text)
|
||||
expect(originalProgressCalculatedText).not.to.contain('recalculating')
|
||||
expect(originalProgressCalculatedText).not.to.contain('refresh')
|
||||
})
|
||||
|
||||
cy.cs('productivityUpdateStatus')
|
||||
.invoke('text')
|
||||
.then(text => {
|
||||
originalProductivityCalculatedText = normalizeText(text)
|
||||
expect(originalProductivityCalculatedText).not.to.contain('recalculating')
|
||||
expect(originalProgressCalculatedText).not.to.contain('refresh')
|
||||
})
|
||||
cy.checkTableStats(tableContents, 'StudentsOfTheStudyprogramme')
|
||||
})
|
||||
|
||||
it('renders progress and productivity tables with calculated status after recalculating stats again', () => {
|
||||
doRecalculation()
|
||||
checkProgressAndProductivity('admin')
|
||||
it('Basic information contains correct credits', () => {
|
||||
cy.contains('a', 'Tietojenkäsittelytieteen kandiohjelma').click()
|
||||
const years = getEmptyYears()
|
||||
const tableContents = [
|
||||
// [Year, Total, Major credits, Non-major credits, Transferred credits]
|
||||
...years.map(year => [year, 0, 0, 0, 0]),
|
||||
[2021, 0, 0, 0, 0],
|
||||
[2020, 4928, 548, 4213, 167],
|
||||
[2019, 7737, 1644, 5744, 349],
|
||||
[2018, 6730, 1820, 4638, 272],
|
||||
[2017, 2900, 788, 1724, 388],
|
||||
]
|
||||
|
||||
// Check new calculation statuses are reported
|
||||
const newProgressCalculatedTextElement = cy.cs('throughputUpdateStatus').invoke('text')
|
||||
const newProductivityCalculatedTextElement = cy.cs('productivityUpdateStatus').invoke('text')
|
||||
cy.checkTableStats(tableContents, 'CreditsProducedByTheStudyprogramme')
|
||||
})
|
||||
|
||||
newProgressCalculatedTextElement.should(text => {
|
||||
const newProgressCalculatedText = normalizeText(text)
|
||||
expect(newProgressCalculatedText).not.to.contain('recalculating')
|
||||
expect(newProgressCalculatedText).not.to.contain('refresh')
|
||||
expect(newProgressCalculatedText).not.to.contain(originalProgressCalculatedText)
|
||||
})
|
||||
it('Basic information contains correct thesis writers and graduates', () => {
|
||||
cy.contains('a', 'Tietojenkäsittelytieteen kandiohjelma').click()
|
||||
const years = getEmptyYears()
|
||||
const tableContents = [
|
||||
// [Year, Graduated, Wrote thesis]
|
||||
...years.map(year => [year, 0, 0]),
|
||||
[2021, 0, 0],
|
||||
[2020, 36, 35],
|
||||
[2019, 17, 25],
|
||||
[2018, 1, 5],
|
||||
[2017, 0, 0],
|
||||
]
|
||||
|
||||
newProductivityCalculatedTextElement.should(text => {
|
||||
const newProductivityCalculatedText = normalizeText(text)
|
||||
expect(newProductivityCalculatedText).not.to.contain('recalculating')
|
||||
expect(newProductivityCalculatedText).not.to.contain('refresh')
|
||||
expect(newProductivityCalculatedText).not.to.contain(originalProgressCalculatedText)
|
||||
})
|
||||
cy.checkTableStats(tableContents, 'GraduatedAndThesisWritersOfTheProgramme')
|
||||
})
|
||||
|
||||
it.skip('Special studyrights can be excluded and basic data changes accordingly', () => {
|
||||
cy.contains('a', 'Tietojenkäsittelytieteen maisteriohjelma').click()
|
||||
cy.get('[data-cy=StudentToggle]').click()
|
||||
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],
|
||||
]
|
||||
|
||||
cy.checkTableStats(studentTableContents, 'StudentsOfTheStudyprogramme')
|
||||
|
||||
const creditTableContents = [
|
||||
// [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],
|
||||
]
|
||||
|
||||
cy.checkTableStats(creditTableContents, 'CreditsProducedByTheStudyprogramme')
|
||||
|
||||
const graduatedTableContents = [
|
||||
// [Year, Graduated, Wrote thesis]
|
||||
...years.map(year => [year, 0, 0]),
|
||||
[2021, 0, 0],
|
||||
[2020, 0, 0],
|
||||
[2019, 0, 0],
|
||||
[2018, 0, 0],
|
||||
[2017, 0, 0],
|
||||
]
|
||||
|
||||
cy.checkTableStats(graduatedTableContents, 'GraduatedAndThesisWritersOfTheProgramme')
|
||||
cy.get('[data-cy=StudentToggle]').click()
|
||||
})
|
||||
|
||||
it('Year can be changed to academic year, and data changes accordingly', () => {
|
||||
cy.contains('a', 'Tietojenkäsittelytieteen maisteriohjelma').click()
|
||||
cy.get('[data-cy=YearToggle]').click()
|
||||
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', 5, 0, 0, 0],
|
||||
['2019 - 2020', 27, 0, 0, 0],
|
||||
['2018 - 2019', 4, 0, 0, 0],
|
||||
['2017 - 2018', 0, 0, 0, 0],
|
||||
]
|
||||
|
||||
cy.checkTableStats(studentTableContents, 'StudentsOfTheStudyprogramme')
|
||||
|
||||
const creditTableContents = [
|
||||
// [Year, Total, Major students credits, Non-major students credits, Transferred credits]
|
||||
...years.map(year => [year, 0, 0, 0, 0]),
|
||||
['2021 - 2022', 0, 0, 0, 0],
|
||||
['2020 - 2021', 0, 0, 0, 0],
|
||||
['2019 - 2020', 338.5, 0, 331, 7.5],
|
||||
['2018 - 2019', 173, 0, 173, 0],
|
||||
['2017 - 2018', 42, 0, 42, 0],
|
||||
]
|
||||
|
||||
cy.checkTableStats(creditTableContents, 'CreditsProducedByTheStudyprogramme')
|
||||
cy.get('[data-cy=YearToggle]').click()
|
||||
})
|
||||
|
||||
it('Basic information graphs render', () => {
|
||||
cy.contains('a', 'Tietojenkäsittelytieteen kandiohjelma').click()
|
||||
|
||||
cy.get('[data-cy=Graph-StudentsOfTheStudyprogramme')
|
||||
.should('contain', 'Started')
|
||||
.should('contain', 'Graduated')
|
||||
.should('contain', 'Transferred away')
|
||||
|
||||
cy.get('[data-cy=Graph-CreditsProducedByTheStudyprogramme')
|
||||
.should('contain', 'Major students credits')
|
||||
.should('contain', 'Non-major students credits')
|
||||
.should('contain', 'Transferred credits')
|
||||
.should('contain', 788)
|
||||
.should('contain', 1820)
|
||||
|
||||
cy.get('[data-cy=Graph-GraduatedAndThesisWritersOfTheProgramme')
|
||||
.should('contain', 'Graduated students')
|
||||
.should('contain', 'Wrote thesis')
|
||||
.should('contain', 1)
|
||||
.should('contain', 5)
|
||||
.should('contain', 17)
|
||||
.should('contain', 25)
|
||||
.should('contain', 36)
|
||||
.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=Graph-ProgrammesBeforeOrAfter')
|
||||
.should('contain', 'Tietojenkäsittelytieteen maisteriohjelma')
|
||||
.should('contain', 'Datatieteen maisteriohjelma')
|
||||
.should('contain', 'Matematiikan ja tilastotieteen maisteriohjelma')
|
||||
.should('contain', 1)
|
||||
.should('contain', 4)
|
||||
.should('contain', 13)
|
||||
})
|
||||
})
|
||||
|
||||
describe('when opening programme page with basic user', () => {
|
||||
/* Studytrack overview -tests*/
|
||||
describe('Studytrack overview works for basic user', () => {
|
||||
beforeEach(() => {
|
||||
cy.init('/study-programme/KH50_005')
|
||||
cy.init('/study-programme')
|
||||
})
|
||||
|
||||
it('renders progress and productivity tables with previosly calculated values', () => {
|
||||
// check doesn't contain study programme options for admin
|
||||
cy.contains('Study programme options').should('not.exist')
|
||||
cy.get('table').should('have.length', 2)
|
||||
checkProgressAndProductivity()
|
||||
// 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', () => {
|
||||
cy.contains('a', 'Tietojenkäsittelytieteen kandiohjelma').click()
|
||||
cy.get('.attached').contains('Studytracks and student populations').click()
|
||||
cy.get('[data-cy=Section-StudytrackOverview]')
|
||||
cy.get('[data-cy=Section-StudytrackProgress]')
|
||||
cy.get('[data-cy=Section-AverageGraduationTimes]')
|
||||
})
|
||||
|
||||
it('can open Thesis page', () => {
|
||||
cy.contains('Tietojenkäsittelytieteen kandiohjelma').click()
|
||||
cy.contains('Thesis Courses').click()
|
||||
cy.contains('Add thesis course').click()
|
||||
cy.contains('No results')
|
||||
it('Students of the studyprogramme are shown correctly', () => {
|
||||
cy.contains('a', 'Tietojenkäsittelytieteen kandiohjelma').click()
|
||||
cy.get('.attached').contains('Studytracks and student populations').click()
|
||||
const tableContents = [
|
||||
// [Year, All, Started studying, Currently enrolled, Absent, Cancelled, Graduated, Men, Women, Finnish]
|
||||
[
|
||||
'2020 - 2021',
|
||||
12,
|
||||
'100.0 %',
|
||||
10,
|
||||
'83.3 %',
|
||||
0,
|
||||
'0 %',
|
||||
0,
|
||||
'0 %',
|
||||
0,
|
||||
'0 %',
|
||||
0,
|
||||
'0 %',
|
||||
10,
|
||||
'83.3 %',
|
||||
2,
|
||||
'16.7 %',
|
||||
12,
|
||||
'100.0 %',
|
||||
],
|
||||
[
|
||||
'2019 - 2020',
|
||||
86,
|
||||
'100.0 %',
|
||||
86,
|
||||
'100.0 %',
|
||||
0,
|
||||
'0 %',
|
||||
0,
|
||||
'0 %',
|
||||
0,
|
||||
'0 %',
|
||||
1,
|
||||
'1.2 %',
|
||||
65,
|
||||
'75.6 %',
|
||||
21,
|
||||
'24.4 %',
|
||||
86,
|
||||
'100.0 %',
|
||||
],
|
||||
[
|
||||
'2018 - 2019',
|
||||
162,
|
||||
'100.0 %',
|
||||
161,
|
||||
'99.4 %',
|
||||
0,
|
||||
'0 %',
|
||||
0,
|
||||
'0 %',
|
||||
0,
|
||||
'0 %',
|
||||
11,
|
||||
'6.8 %',
|
||||
118,
|
||||
'72.8 %',
|
||||
44,
|
||||
'27.2 %',
|
||||
161,
|
||||
'99.4 %',
|
||||
],
|
||||
[
|
||||
'2017 - 2018',
|
||||
171,
|
||||
'100.0 %',
|
||||
171,
|
||||
'100.0 %',
|
||||
0,
|
||||
'0 %',
|
||||
0,
|
||||
'0 %',
|
||||
3,
|
||||
'1.8 %',
|
||||
42,
|
||||
'24.6 %',
|
||||
135,
|
||||
'78.9 %',
|
||||
36,
|
||||
'21.1 %',
|
||||
166,
|
||||
'97.1 %',
|
||||
],
|
||||
[
|
||||
'Total',
|
||||
431,
|
||||
'100.0 %',
|
||||
431,
|
||||
'100.0 %',
|
||||
0,
|
||||
'0 %',
|
||||
0,
|
||||
'0 %',
|
||||
3,
|
||||
'0.7 %',
|
||||
54,
|
||||
'12.5 %',
|
||||
328,
|
||||
'76.1 %',
|
||||
103,
|
||||
'23.9 %',
|
||||
425,
|
||||
'98.6 %',
|
||||
],
|
||||
]
|
||||
|
||||
cy.checkTableStats(tableContents, 'StudytrackOverview')
|
||||
})
|
||||
|
||||
it('can move to Population statistics page by clickin', () => {
|
||||
cy.get('i.level.up.alternate.icon').eq(0).click()
|
||||
cy.contains('Students (10)')
|
||||
it('Student progress data is shown correctly', () => {
|
||||
cy.contains('a', 'Tietojenkäsittelytieteen kandiohjelma').click()
|
||||
cy.get('.attached').contains('Studytracks and student populations').click()
|
||||
const tableContents = [
|
||||
['2020 - 2021', 12, 5, 2, 2, 0, 2, 0, 1],
|
||||
['2019 - 2020', 86, 13, 15, 32, 11, 2, 0, 13],
|
||||
['2018 - 2019', 162, 7, 16, 35, 33, 27, 18, 26],
|
||||
['2017 - 2018', 171, 10, 22, 18, 22, 24, 27, 48],
|
||||
['Total', 431, 35, 55, 87, 66, 55, 45, 88],
|
||||
]
|
||||
|
||||
cy.checkTableStats(tableContents, 'StudytrackProgress')
|
||||
})
|
||||
|
||||
it('Studytrack overview graphs render', () => {
|
||||
cy.contains('a', 'Tietojenkäsittelytieteen kandiohjelma').click()
|
||||
cy.get('.attached').contains('Studytracks and student populations').click()
|
||||
|
||||
cy.get('[data-cy=Graph-StudytrackProgress]')
|
||||
.should('contain', 'Less than 30 credits')
|
||||
.should('contain', '30-59 credits')
|
||||
.should('contain', 'More than 180 credits')
|
||||
.should('contain', '5.8%') // The percentage for less than 30 credits in 2017-2018, to check that the graph renders
|
||||
.should('contain', '4.3%') // The percentage for less than 30 credits in 2018-2019
|
||||
.should('contain', '15.1%') // The percentage for less than 30 credits in 2019-2020
|
||||
|
||||
cy.get('[data-cy=Graph-2018-AverageGraduationTimes]')
|
||||
.should('contain', '2018 - 2019')
|
||||
.should('contain', 'n = 11 / 162')
|
||||
.should('contain', '22 kk')
|
||||
})
|
||||
})
|
||||
|
||||
/* Tag-tests*/
|
||||
describe('Basic information -view works for basic user', () => {
|
||||
beforeEach(() => {
|
||||
cy.init('/study-programme')
|
||||
})
|
||||
|
||||
it('can create and delete tags for population', () => {
|
||||
const name = `tag-${new Date().getTime()}`
|
||||
cy.contains('Tietojenkäsittelytieteen kandiohjelma').click()
|
||||
cy.contains('a', 'Tietojenkäsittelytieteen kandiohjelma').click()
|
||||
cy.get('.attached').contains('Tags').click()
|
||||
cy.get('.tagNameSelectInput > .ui > input').type(name)
|
||||
cy.get('.yearSelectInput').type('2018')
|
||||
@@ -178,7 +382,7 @@ describe('Studyprogramme overview', () => {
|
||||
|
||||
it('can create personal tags', () => {
|
||||
const name = `tag-${new Date().getTime()}`
|
||||
cy.contains('Tietojenkäsittelytieteen kandiohjelma').click()
|
||||
cy.contains('a', 'Tietojenkäsittelytieteen kandiohjelma').click()
|
||||
cy.get('.attached').contains('Tags').click()
|
||||
cy.get('.tagNameSelectInput > .ui > input').type(name)
|
||||
cy.get('.yearSelectInput').type('2018')
|
||||
@@ -194,7 +398,7 @@ describe('Studyprogramme overview', () => {
|
||||
|
||||
const student = '010113437'
|
||||
|
||||
cy.contains('Tietojenkäsittelytieteen kandiohjelma').click()
|
||||
cy.contains('a', 'Tietojenkäsittelytieteen kandiohjelma').click()
|
||||
cy.get('.attached').contains('Tags').click()
|
||||
cy.get('.tagNameSelectInput > .ui > input').type(name)
|
||||
cy.get('.yearSelectInput').type('2018')
|
||||
@@ -202,7 +406,7 @@ describe('Studyprogramme overview', () => {
|
||||
cy.contains(name)
|
||||
|
||||
cy.contains('Add tags to students').click()
|
||||
cy.get('.form > .field > .dropdown').click().get('.ui > .search').type(name).click()
|
||||
cy.get('.form > .field > .dropdown').click().get('.ui > input.search').type(name).click()
|
||||
|
||||
cy.get('.form > .field > .dropdown > .visible').contains(name).click()
|
||||
|
||||
@@ -211,7 +415,7 @@ describe('Studyprogramme overview', () => {
|
||||
|
||||
cy.contains('Student statistics').click()
|
||||
cy.get('.prompt').type(student)
|
||||
cy.contains(student).click()
|
||||
cy.contains('a', student).click()
|
||||
cy.contains(name)
|
||||
|
||||
cy.go('back')
|
||||
@@ -221,7 +425,7 @@ describe('Studyprogramme overview', () => {
|
||||
|
||||
cy.contains('Student statistics').click()
|
||||
cy.get('.prompt').type(student)
|
||||
cy.contains(student).click()
|
||||
cy.contains('a', student).click()
|
||||
cy.contains(name).should('not.exist')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -16,7 +16,7 @@ describe('Teachers page tests', () => {
|
||||
cy.contains('Tietojenkäsittelytieteen kandiohjelma').click()
|
||||
cy.cs('course-providers').children('.icon').click()
|
||||
cy.get('.form > .fluid').click()
|
||||
cy.contains('Name')
|
||||
cy.contains('Teacher')
|
||||
cy.contains(teacher1)
|
||||
cy.contains(teacher2)
|
||||
})
|
||||
@@ -32,21 +32,13 @@ describe('Teachers page tests', () => {
|
||||
cy.url().should('include', '/teachers')
|
||||
cy.get('.borderless > :nth-child(3)').click()
|
||||
cy.get('.prompt').type(teacher1.split()[0])
|
||||
cy.contains(teacher1).click()
|
||||
cy.contains('td', teacher1).click()
|
||||
cy.contains('Aineopintojen harjoitustyö: Tietokantasovellus')
|
||||
})
|
||||
|
||||
it('Check leaderboad works', () => {
|
||||
cy.get('.borderless > :nth-child(2)').click({ force: true })
|
||||
cy.get(':nth-child(1) > .ui > .search').click({ force: true })
|
||||
cy.get(':nth-child(1) > .ui > .search').click({ force: true })
|
||||
cy.contains('2019-2020').click({ force: true })
|
||||
cy.contains('Recalculate this year').click({ force: true })
|
||||
cy.wait(5000)
|
||||
cy.reload()
|
||||
cy.contains('2019-2020').click({ force: true })
|
||||
cy.get(':nth-child(1) > .ui > .search').click({ force: true })
|
||||
cy.contains('Passed')
|
||||
cy.contains('Ahokas Jarmo Ilkka')
|
||||
cy.contains('2020-2021').click({ force: true })
|
||||
})
|
||||
})
|
||||
|
||||
@@ -4,16 +4,16 @@ describe('Users tests', () => {
|
||||
describe('Using user with just grp-oodikone-user, no other rights', () => {
|
||||
it('shows only frontpage, trends feedback', () => {
|
||||
cy.init('', 'norights') // login with norights user
|
||||
cy.get('.navBar').contains('Trends').should('exist')
|
||||
cy.get('.navBar').contains('Give feedback').should('exist')
|
||||
cy.get('[data-cy="navBar"]').contains('Trends').should('exist')
|
||||
cy.get('[data-cy="navBar"]').contains('Give feedback').should('exist')
|
||||
|
||||
cy.get('.navBar').contains('Study programme').should('not.exist')
|
||||
cy.get('.navBar').contains('Student statistics').should('not.exist')
|
||||
cy.get('.navBar').contains('Course statistics').should('not.exist')
|
||||
cy.get('.navBar').contains('Teachers').should('not.exist')
|
||||
cy.get('.navBar').contains('Users').should('not.exist')
|
||||
cy.get('.navBar').contains('Faculty').should('not.exist')
|
||||
cy.get('.navBar').contains('Updater').should('not.exist')
|
||||
cy.get('[data-cy="navBar"]').contains('Study programme').should('not.exist')
|
||||
cy.get('[data-cy="navBar"]').contains('Student statistics').should('not.exist')
|
||||
cy.get('[data-cy="navBar"]').contains('Course statistics').should('not.exist')
|
||||
cy.get('[data-cy="navBar"]').contains('Teachers').should('not.exist')
|
||||
cy.get('[data-cy="navBar"]').contains('Users').should('not.exist')
|
||||
cy.get('[data-cy="navBar"]').contains('Faculty').should('not.exist')
|
||||
cy.get('[data-cy="navBar"]').contains('Updater').should('not.exist')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -29,32 +29,25 @@ describe('Users tests', () => {
|
||||
})
|
||||
|
||||
it('should see more stuff than others', () => {
|
||||
cy.get('.navBar').contains('Study programme').should('exist')
|
||||
cy.get('.navBar').contains('Student statistics').should('exist')
|
||||
cy.get('.navBar').contains('Course statistics').should('exist')
|
||||
// TODO: admin should probably see this as well, since can access this page: cy.get('.navBar').contains('Teachers').should('exist')
|
||||
cy.get('.navBar').contains('Users').should('exist')
|
||||
cy.get('.navBar').contains('Trends').should('exist')
|
||||
cy.get('[data-cy="navBar"]').contains('Study programme').should('exist')
|
||||
cy.get('[data-cy="navBar"]').contains('Student statistics').should('exist')
|
||||
cy.get('[data-cy="navBar"]').contains('Course statistics').should('exist')
|
||||
// TODO: admin should probably see this as well, since can access this page: cy.get('[data-cy="navBar"]').contains('Teachers').should('exist')
|
||||
cy.get('[data-cy="navBar"]').contains('Users').should('exist')
|
||||
cy.get('[data-cy="navBar"]').contains('Trends').should('exist')
|
||||
})
|
||||
|
||||
it("mocking normal user shows only the mocked user's programmes", () => {
|
||||
cy.contains('mocking').should('not.exist')
|
||||
cy.contains('tr', 'basic').within($row => {
|
||||
cy.contains('.button', 'Edit').click()
|
||||
})
|
||||
cy.intercept({
|
||||
method: 'POST',
|
||||
path: '/api/superlogin/basic',
|
||||
}).as('superlogin')
|
||||
cy.cs('user-edit-button-basic').click()
|
||||
cy.get('i.spy').click()
|
||||
cy.wait('@superlogin')
|
||||
cy.contains('mocking as basic')
|
||||
cy.wait(1000)
|
||||
cy.contains('Study programme').click().siblings().contains('Search by class').click()
|
||||
cy.contains('label', 'Study programme')
|
||||
cy.contains('label', 'Study programme')
|
||||
.siblings()
|
||||
.within($row => {
|
||||
.within(() => {
|
||||
cy.get("div[role='option']").should('have.length', 2).contains('Tietojenkäsittelytieteen kandiohjelma')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -7,7 +7,7 @@ const adminUserHeaders = {
|
||||
uid: 'admin',
|
||||
displayname: 'Admin User',
|
||||
'shib-session-id': 'mock-cypress-session',
|
||||
hygroupcn: 'grp-oodikone-users;grp-oodikone-basic-users',
|
||||
hygroupcn: 'grp-oodikone-users;grp-oodikone-basic-users;grp-toska',
|
||||
edupersonaffiliation: 'member;employee;faculty',
|
||||
mail: 'grp-toska+mockadminuser@helsinki.fi',
|
||||
hypersonsisuid: 'hy-hlo-6666666',
|
||||
@@ -54,8 +54,6 @@ Cypress.Commands.add('init', (path = '', userId = 'basic') => {
|
||||
if (!headersToUse) throw Error(`${userId} is not valid user id!`)
|
||||
req.headers = headersToUse
|
||||
})
|
||||
// Hide SIS warning flag to avoid screen size problems when running headlessly
|
||||
window.localStorage.setItem('SIS_WARNING_FLAG', 'true')
|
||||
const baseUrl = Cypress.config().baseUrl
|
||||
cy.visit(baseUrl.concat(path))
|
||||
})
|
||||
@@ -63,24 +61,37 @@ Cypress.Commands.add('init', (path = '', userId = 'basic') => {
|
||||
/**
|
||||
* Shorthand for using "Cypress Selectors" (CS), i.e., `data-cy` attributes.
|
||||
*/
|
||||
Cypress.Commands.add('cs', name => {
|
||||
return cy.get(`[data-cy='${name}']`)
|
||||
Cypress.Commands.add('cs', { prevSubject: 'optional' }, (subject, name) => {
|
||||
const selector = `[data-cy="${name}"]`
|
||||
|
||||
if (subject === undefined) {
|
||||
return cy.get(selector)
|
||||
} else {
|
||||
return cy.wrap(subject).find(selector)
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* Select item specified by `index` number from a semantic-ui dropdown with
|
||||
* `data-cy` attribute value `name`.
|
||||
*/
|
||||
Cypress.Commands.add('selectFromDropdown', (name, index) => {
|
||||
Cypress.Commands.add('selectFromDropdown', { prevSubject: true }, (s, index) => {
|
||||
const subject = cy.wrap(s)
|
||||
const indexes = Array.isArray(index) ? index : [index]
|
||||
|
||||
indexes.forEach(i => {
|
||||
cy.cs(name).click().children('.menu').children().eq(i).click({ force: true })
|
||||
subject.click()
|
||||
|
||||
if (typeof i === 'number') {
|
||||
subject.children('.menu').children().eq(i).click({ force: true })
|
||||
} else {
|
||||
subject.children('.menu').containing(i).eq(0).click({ force: true })
|
||||
}
|
||||
})
|
||||
|
||||
// Close multiple selection so it does not block elements underneath it.
|
||||
if (Array.isArray(index)) {
|
||||
cy.cs(name).children('.icon').click({ force: true })
|
||||
cy.wrap(s).children('.icon').click({ force: true })
|
||||
}
|
||||
})
|
||||
|
||||
@@ -93,3 +104,18 @@ Cypress.Commands.add('selectStudyProgramme', name => {
|
||||
cy.cs('select-study-programme').click().children().contains(name).click()
|
||||
cy.contains('See population').click()
|
||||
})
|
||||
|
||||
Cypress.Commands.add('checkTableStats', (correctStats, tableName) => {
|
||||
cy.get(`[data-cy=Table-${tableName}]>tbody`).within(() => {
|
||||
correctStats.forEach((values, trIndex) => {
|
||||
cy.get('tr')
|
||||
.eq(trIndex)
|
||||
.within(() => {
|
||||
values.forEach((value, tdIndex) => {
|
||||
cy.get('td').eq(tdIndex).contains(value)
|
||||
})
|
||||
})
|
||||
})
|
||||
cy.get('tr').should('have.length', correctStats.length)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -14,3 +14,13 @@
|
||||
// ***********************************************************
|
||||
|
||||
import './commands'
|
||||
|
||||
// This error is benign and seems to be only present in Cypress
|
||||
const resizeObserverLoopErrRe = /^[^(ResizeObserver loop limit exceeded)]/
|
||||
|
||||
Cypress.on('uncaught:exception', err => {
|
||||
/* returning false here prevents Cypress from failing the test */
|
||||
if (resizeObserverLoopErrRe.test(err.message)) {
|
||||
return false
|
||||
}
|
||||
})
|
||||
|
||||
+82
-20
@@ -2,7 +2,6 @@ x-defaults: &defaults
|
||||
restart: unless-stopped
|
||||
|
||||
services:
|
||||
|
||||
backend:
|
||||
<<: *defaults
|
||||
container_name: backend
|
||||
@@ -10,17 +9,16 @@ services:
|
||||
- kone-db
|
||||
- redis
|
||||
- sis-db
|
||||
- user-db
|
||||
environment:
|
||||
CI: "true"
|
||||
CI: 'true'
|
||||
DB_URL_USER: postgres://postgres@user-db:5432/user-db
|
||||
DB_URL_KONE: postgres://postgres@kone-db:5432/kone-db
|
||||
FRONT_URL: http://localhost:1337
|
||||
REDIS: redis
|
||||
SECRET_TOKEN: dev
|
||||
SIS_DB_URL: postgres://postgres@sis-db:5432/sis-db
|
||||
SIS_UPDATER_URL: http://sis-updater-scheduler:8082
|
||||
TOKEN_SECRET: IWannaBeTheVeryBest
|
||||
USERSERVICE_SECRET: Luukk4inenApprov3dVitunMinttuSalainenAvain666NurmivaaraPJ2019
|
||||
USERSERVICE_URL: http://userservice:4567
|
||||
image: toska/oodikone2-backend:staging
|
||||
|
||||
frontend:
|
||||
@@ -31,11 +29,15 @@ services:
|
||||
kone-db:
|
||||
<<: *defaults
|
||||
container_name: kone-db
|
||||
environment:
|
||||
PGDATA: /data
|
||||
POSTGRES_DB: kone-db
|
||||
POSTGRES_HOST_AUTH_METHOD: trust
|
||||
image: registry.toska.cs.helsinki.fi/kone-db
|
||||
|
||||
redis:
|
||||
<<: *defaults
|
||||
command: "redis-server --appendonly yes"
|
||||
command: 'redis-server --appendonly yes'
|
||||
container_name: redis
|
||||
image: redis
|
||||
ports:
|
||||
@@ -45,23 +47,20 @@ services:
|
||||
<<: *defaults
|
||||
container_name: sis-db
|
||||
image: registry.toska.cs.helsinki.fi/sis-db
|
||||
environment:
|
||||
PGDATA: /data
|
||||
POSTGRES_DB: sis-db
|
||||
POSTGRES_HOST_AUTH_METHOD: trust
|
||||
shm_size: 2gb
|
||||
|
||||
user-db:
|
||||
<<: *defaults
|
||||
container_name: user-db
|
||||
image: registry.toska.cs.helsinki.fi/user-db
|
||||
|
||||
userservice:
|
||||
<<: *defaults
|
||||
container_name: userservice
|
||||
depends_on:
|
||||
- user-db
|
||||
environment:
|
||||
CI: "true"
|
||||
DB_URL: postgres://postgres@user-db:5432/user-db
|
||||
SECRET: Luukk4inenApprov3dVitunMinttuSalainenAvain666NurmivaaraPJ2019
|
||||
TOKEN_SECRET: IWannaBeTheVeryBest
|
||||
image: toska/oodikone2-userservice:staging
|
||||
PGDATA: /data
|
||||
POSTGRES_DB: user-db
|
||||
POSTGRES_HOST_AUTH_METHOD: trust
|
||||
image: registry.toska.cs.helsinki.fi/user-db
|
||||
|
||||
nginx:
|
||||
image: nginx:latest
|
||||
@@ -69,8 +68,71 @@ services:
|
||||
depends_on:
|
||||
- frontend
|
||||
- backend
|
||||
- userservice
|
||||
ports:
|
||||
- 1337:80
|
||||
- 1337:80
|
||||
volumes:
|
||||
- ./nginx.conf:/etc/nginx/nginx.conf
|
||||
|
||||
# Following services are used to run sis-updater-worker when anon sis-db needs to be updated. Could probably also use production version, but this certainly works.
|
||||
sis-importer-db:
|
||||
<<: *defaults
|
||||
container_name: sis-importer-db
|
||||
image: registry.toska.cs.helsinki.fi/sis-importer-db
|
||||
profiles:
|
||||
- updater
|
||||
|
||||
sis-updater-worker:
|
||||
<<: *defaults
|
||||
build:
|
||||
target: development
|
||||
context: updater/sis-updater-worker
|
||||
depends_on:
|
||||
- redis
|
||||
- sis-db
|
||||
- sis-importer-db
|
||||
- sis-updater-nats
|
||||
environment:
|
||||
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_NATS_TOKEN: dev
|
||||
SIS_NATS_URI: sis-updater-nats://sis-updater-nats:4222
|
||||
profiles:
|
||||
- updater
|
||||
|
||||
sis-updater-scheduler:
|
||||
<<: *defaults
|
||||
build:
|
||||
target: development
|
||||
context: updater/sis-updater-scheduler
|
||||
container_name: sis-updater-scheduler
|
||||
depends_on:
|
||||
- redis
|
||||
- sis-importer-db
|
||||
- sis-updater-nats
|
||||
environment:
|
||||
REDIS_URI: //redis:6379
|
||||
SECRET_TOKEN: dev
|
||||
SIS_IMPORTER_DATABASE: sis-importer-db
|
||||
SIS_IMPORTER_HOST: sis-importer-db
|
||||
SIS_IMPORTER_PASSWORD: postgres
|
||||
SIS_IMPORTER_USER: postgres
|
||||
SIS_NATS_TOKEN: dev
|
||||
SIS_NATS_URI: sis-updater-nats://sis-updater-nats:4222
|
||||
SCHEDULE_IMMEDIATE: '${SCHEDULE_IMMEDIATE}'
|
||||
EXIT_AFTER_IMMEDIATES: '${EXIT_AFTER_IMMEDIATES}'
|
||||
ports:
|
||||
- 8082:8082
|
||||
profiles:
|
||||
- updater
|
||||
|
||||
sis-updater-nats:
|
||||
<<: *defaults
|
||||
command: '-cid sis-updater-nats -m 8222 -p 4222 --max_channels 1000 --max_age 2h --hb_fail_count 2 --hb_timeout 10s --hb_interval 10s --auth dev --file_slice_max_bytes 0 --file_slice_max_age 60s --max_msgs 0 -store file -dir datastore'
|
||||
container_name: sis-updater-nats
|
||||
image: nats-streaming:0.16.2
|
||||
profiles:
|
||||
- updater
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
services:
|
||||
|
||||
backend:
|
||||
environment:
|
||||
DB_URL_USER: postgres://postgres@user-db:5432/user-db-real
|
||||
DB_URL_KONE: postgres://postgres@kone-db:5432/kone-db-real
|
||||
SIS_DB_URL: postgres://postgres@sis-db:5432/sis-db-real
|
||||
|
||||
@@ -18,9 +18,5 @@ services:
|
||||
DB_URL: postgres://postgres@sis-db:5432/sis-db-real
|
||||
SIS_IMPORTER_DATABASE: sis-importer-db-real
|
||||
|
||||
userservice:
|
||||
environment:
|
||||
DB_URL: postgres://postgres@user-db:5432/user-db-real
|
||||
|
||||
volumes:
|
||||
redis-data-real:
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
services:
|
||||
|
||||
userservice-test:
|
||||
build:
|
||||
context: services/userservice
|
||||
target: test
|
||||
container_name: userservice-test
|
||||
environment:
|
||||
SECRET: Luukk4inenApprov3dVitunMinttuSalainenAvain666NurmivaaraPJ2019
|
||||
TOKEN_SECRET: IWannaBeTheVeryBest
|
||||
+7
-25
@@ -1,3 +1,5 @@
|
||||
version: '2.1'
|
||||
|
||||
x-defaults: &defaults
|
||||
restart: unless-stopped
|
||||
|
||||
@@ -26,18 +28,17 @@ services:
|
||||
- kone-db
|
||||
- redis
|
||||
- sis-db
|
||||
- user-db
|
||||
env_file:
|
||||
- .env
|
||||
environment:
|
||||
DB_URL_KONE: postgres://postgres@kone-db:5432/kone-db
|
||||
DB_URL_USER: postgres://postgres@user-db:5432/user-db
|
||||
FRONT_URL: http://localhost:3000
|
||||
REDIS: redis
|
||||
SECRET_TOKEN: dev
|
||||
SIS_DB_URL: postgres://postgres@sis-db:5432/sis-db
|
||||
SIS_UPDATER_URL: http://sis-updater-scheduler:8082
|
||||
TOKEN_SECRET: IWannaBeTheVeryBest
|
||||
USERSERVICE_SECRET: Luukk4inenApprov3dVitunMinttuSalainenAvain666NurmivaaraPJ2019
|
||||
USERSERVICE_URL: http://userservice:4567
|
||||
ports:
|
||||
- 8080:8080
|
||||
profiles:
|
||||
@@ -98,7 +99,7 @@ services:
|
||||
|
||||
sis-updater-nats:
|
||||
<<: *defaults
|
||||
command: '-cid sis-updater-nats -m 8222 -p 4222 --max_age 2h --hb_fail_count 2 --hb_timeout 10s --hb_interval 10s --auth dev --file_slice_max_bytes 0 --file_slice_max_age 60s --max_msgs 0 -store file -dir datastore'
|
||||
command: '-cid sis-updater-nats -m 8222 -p 4222 --max_channels 1000 --max_age 2h --hb_fail_count 2 --hb_timeout 10s --hb_interval 10s --auth dev --file_slice_max_bytes 0 --file_slice_max_age 60s --max_msgs 0 -store file -dir datastore'
|
||||
container_name: sis-updater-nats
|
||||
image: nats-streaming:0.16.2
|
||||
profiles:
|
||||
@@ -123,6 +124,8 @@ services:
|
||||
SIS_IMPORTER_USER: postgres
|
||||
SIS_NATS_TOKEN: dev
|
||||
SIS_NATS_URI: sis-updater-nats://sis-updater-nats:4222
|
||||
SCHEDULE_IMMEDIATE: '${SCHEDULE_IMMEDIATE}'
|
||||
EXIT_AFTER_IMMEDIATES: '${EXIT_AFTER_IMMEDIATES}'
|
||||
ports:
|
||||
- 8082:8082
|
||||
profiles:
|
||||
@@ -165,26 +168,6 @@ services:
|
||||
volumes:
|
||||
- user-db-data:/data
|
||||
|
||||
userservice:
|
||||
<<: *defaults
|
||||
build:
|
||||
<<: *build_defaults
|
||||
context: services/userservice
|
||||
container_name: userservice
|
||||
depends_on:
|
||||
- user-db
|
||||
environment:
|
||||
DB_URL: postgres://postgres@user-db:5432/user-db
|
||||
SECRET: Luukk4inenApprov3dVitunMinttuSalainenAvain666NurmivaaraPJ2019
|
||||
TOKEN_SECRET: IWannaBeTheVeryBest
|
||||
ports:
|
||||
- 4567:4567
|
||||
profiles:
|
||||
- oodikone
|
||||
volumes:
|
||||
- userservice-nodemod:/usr/src/app/node_modules
|
||||
- ./services/userservice/:/usr/src/app
|
||||
|
||||
volumes:
|
||||
backend-nodemod:
|
||||
frontend-nodemod:
|
||||
@@ -195,4 +178,3 @@ volumes:
|
||||
sis-updater-scheduler-nodemod:
|
||||
sis-updater-worker-nodemod:
|
||||
user-db-data:
|
||||
userservice-nodemod:
|
||||
|
||||
Generated
+856
-6926
File diff suppressed because it is too large
Load Diff
+19
-14
@@ -15,7 +15,8 @@
|
||||
"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",
|
||||
"prepare": "husky install"
|
||||
"prepare": "husky install",
|
||||
"rapodiff": "docker exec backend node /usr/src/app/src/rapodiff.js"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -27,27 +28,31 @@
|
||||
},
|
||||
"homepage": "https://github.com/UniversityOfHelsinkiCS/oodikone#readme",
|
||||
"devDependencies": {
|
||||
"@babel/eslint-parser": "^7.15.0",
|
||||
"@babel/plugin-syntax-jsx": "^7.14.5",
|
||||
"concurrently": "^6.2.1",
|
||||
"cypress": "^8.3.1",
|
||||
"@babel/eslint-parser": "^7.17.0",
|
||||
"@babel/plugin-syntax-jsx": "^7.16.7",
|
||||
"concurrently": "^6.5.1",
|
||||
"cypress": "^8.7.0",
|
||||
"eslint": "^7.32.0",
|
||||
"eslint-config-airbnb": "^18.2.1",
|
||||
"eslint-config-prettier": "^8.3.0",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
"eslint-plugin-babel": "^5.3.1",
|
||||
"eslint-plugin-cypress": "^2.11.3",
|
||||
"eslint-plugin-import": "^2.24.2",
|
||||
"eslint-plugin-jsx-a11y": "^6.4.1",
|
||||
"eslint-plugin-cypress": "^2.12.1",
|
||||
"eslint-plugin-import": "^2.25.4",
|
||||
"eslint-plugin-jsx-a11y": "^6.5.1",
|
||||
"eslint-plugin-prefer-object-spread": "^1.2.1",
|
||||
"eslint-plugin-prettier": "^3.4.1",
|
||||
"eslint-plugin-react": "^7.24.0",
|
||||
"eslint-plugin-react-hooks": "^4.2.0",
|
||||
"husky": "^7.0.2",
|
||||
"lint-staged": "^11.1.2",
|
||||
"prettier": "^2.3.2",
|
||||
"eslint-plugin-react": "^7.29.3",
|
||||
"eslint-plugin-react-hooks": "^4.3.0",
|
||||
"husky": "^7.0.4",
|
||||
"lint-staged": "^11.2.6",
|
||||
"prettier": "^2.5.1",
|
||||
"stylelint": "^13.13.1",
|
||||
"stylelint-config-prettier": "^8.0.2",
|
||||
"stylelint-config-standard": "^22.0.0",
|
||||
"stylelint-prettier": "^1.2.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"lodash": "^4.17.21",
|
||||
"moment": "^2.29.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,12 +110,54 @@ reset_all_real_data() {
|
||||
reset_databases "${DATABASES[@]}"
|
||||
}
|
||||
|
||||
reset_sis_importer_data() {
|
||||
infomsg "Downloading sis-importer-db dump"
|
||||
local database=$SIS_IMPORTER_DB_NAME
|
||||
local url=$SIS_IMPORTER_DB_REAL_DUMP_URL
|
||||
download_real_dump $database $url
|
||||
reset_databases $database
|
||||
reset_single_database() {
|
||||
# Define custom shell prompt for the nested interactive select loop
|
||||
PS3="Please enter which database to reset: "
|
||||
|
||||
options=(
|
||||
"kone-db"
|
||||
"sis-db"
|
||||
"sis-importer-db"
|
||||
"user-db"
|
||||
"Return to main menu."
|
||||
)
|
||||
|
||||
local database=""
|
||||
local url=""
|
||||
|
||||
while true; do
|
||||
select opt in "${options[@]}"; do
|
||||
case $opt in
|
||||
"kone-db")
|
||||
local database=$KONE_DB_NAME
|
||||
local url=$KONE_DB_REAL_DUMP_URL
|
||||
break 2;;
|
||||
"sis-db")
|
||||
local database=$SIS_DB_NAME
|
||||
local url=$SIS_DB_REAL_DUMP_URL
|
||||
break 2;;
|
||||
"sis-importer-db")
|
||||
local database=$SIS_IMPORTER_DB_NAME
|
||||
local url=$SIS_IMPORTER_DB_REAL_DUMP_URL
|
||||
break 2;;
|
||||
"user-db")
|
||||
local database=$USER_DB_NAME
|
||||
local url=$USER_DB_REAL_DUMP_URL
|
||||
break 2;;
|
||||
"Return to main menu.")
|
||||
break 2;;
|
||||
*) msg "${RED}Invalid option:${NOFORMAT} $REPLY
|
||||
";;
|
||||
esac
|
||||
break
|
||||
done
|
||||
done
|
||||
|
||||
if [ -n "$database" ]; then
|
||||
infomsg "Downloading $database dump"
|
||||
download_real_dump $database $url
|
||||
reset_databases $database
|
||||
fi
|
||||
}
|
||||
|
||||
set_up_oodikone_from_scratch() {
|
||||
@@ -123,7 +165,6 @@ set_up_oodikone_from_scratch() {
|
||||
folders_to_set_up=(
|
||||
"$PROJECT_ROOT"
|
||||
"$PROJECT_ROOT/services/frontend"
|
||||
"$PROJECT_ROOT/services/userservice"
|
||||
"$PROJECT_ROOT/services/backend"
|
||||
"$PROJECT_ROOT/updater/sis-updater-scheduler"
|
||||
"$PROJECT_ROOT/updater/sis-updater-worker"
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
.dockerignore
|
||||
Dockerfile
|
||||
node_modules/
|
||||
requests/
|
||||
scripts/
|
||||
*.log
|
||||
@@ -1,3 +0,0 @@
|
||||
# Student diff output
|
||||
/student_diff_*
|
||||
/diff_partial_att_data.json
|
||||
@@ -17,7 +17,7 @@ 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
|
||||
RUN npm ci --production && apk add --no-cache vim curl nano
|
||||
COPY --chown=node:node . /usr/src/app/
|
||||
USER node
|
||||
CMD ["npm", "start"]
|
||||
|
||||
Generated
+291
-351
File diff suppressed because it is too large
Load Diff
@@ -4,36 +4,31 @@
|
||||
"scripts": {
|
||||
"start": "node --max_old_space_size=4096 index.js",
|
||||
"dev": "nodemon --max_old_space_size=4096 index.js",
|
||||
"diff:students": "node /usr/src/app/src/sisoodi_diff/students/index.js",
|
||||
"diff:populations": "node /usr/src/app/src/sisoodi_diff/populations.js",
|
||||
"diff:courses": "node /usr/src/app/src/sisoodi_diff/courses/index.js",
|
||||
"diff:transfers": "node /usr/src/app/src/sisoodi_diff/transfers.js",
|
||||
"diff:trends": "node /usr/src/app/src/sisoodi_diff/trends.js"
|
||||
"rapodiff": "node /usr/src/app/src/rapodiff.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@sentry/node": "^5.30.0",
|
||||
"@sentry/tracing": "^6.11.0",
|
||||
"async": "^3.2.1",
|
||||
"async-lock": "^1.3.0",
|
||||
"axios": "^0.21.2",
|
||||
"@sentry/tracing": "^6.18.2",
|
||||
"async": "^3.2.3",
|
||||
"async-lock": "^1.3.1",
|
||||
"axios": "^0.21.4",
|
||||
"bluebird": "^3.7.2",
|
||||
"cors": "^2.8.5",
|
||||
"cron": "^1.8.2",
|
||||
"express": "^4.17.1",
|
||||
"express": "^4.17.3",
|
||||
"express-async-errors": "^3.1.1",
|
||||
"jsonwebtoken": "^8.5.1",
|
||||
"lodash": "^4.17.21",
|
||||
"lru-cache": "^5.1.1",
|
||||
"moment": "^2.29.1",
|
||||
"moment-timezone": "^0.5.33",
|
||||
"moment-timezone": "^0.5.34",
|
||||
"morgan": "^1.10.0",
|
||||
"pg": "^8.7.1",
|
||||
"pg": "^8.7.3",
|
||||
"redis": "^3.1.2",
|
||||
"sequelize": "^6.6.5",
|
||||
"sequelize": "^6.17.0",
|
||||
"umzug": "^2.3.0",
|
||||
"unfuck-utf8-headers-middleware": "^1.0.1",
|
||||
"uuid": "^3.4.0",
|
||||
"winston": "^3.3.3"
|
||||
"winston": "^3.6.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"nodemon": "^1.19.4"
|
||||
|
||||
@@ -1,13 +1,9 @@
|
||||
require('express-async-errors')
|
||||
const express = require('express')
|
||||
const cors = require('cors')
|
||||
const Sentry = require('@sentry/node')
|
||||
const { baseUrl, backendPort, frontUrl } = require('./conf-backend')
|
||||
const { baseUrl, backendPort } = require('./conf-backend')
|
||||
const routes = require('./routes')
|
||||
const { startCron } = require('./events')
|
||||
const { initializeDatabaseConnection, dbConnections } = require('./database/connection')
|
||||
const initializeSentry = require('./util/sentry')
|
||||
const errorMiddleware = require('./middleware/errormiddleware')
|
||||
const logger = require('./util/logger')
|
||||
|
||||
initializeDatabaseConnection()
|
||||
@@ -21,28 +17,11 @@ initializeDatabaseConnection()
|
||||
dbConnections.on('error', () => {
|
||||
logger.error('Failed to connect to sis db!')
|
||||
})
|
||||
|
||||
const app = express()
|
||||
initializeSentry(app)
|
||||
app.use(Sentry.Handlers.requestHandler())
|
||||
app.use(Sentry.Handlers.tracingHandler())
|
||||
|
||||
startCron()
|
||||
|
||||
app.use(cors({ credentials: true, origin: frontUrl }))
|
||||
app.use(express.json())
|
||||
|
||||
const app = express()
|
||||
routes(app, baseUrl)
|
||||
|
||||
app.get('*', async (_, res) => {
|
||||
const results = { error: 'unknown endpoint' }
|
||||
res.status(404).json(results)
|
||||
})
|
||||
|
||||
app.use(Sentry.Handlers.errorHandler())
|
||||
|
||||
app.use(errorMiddleware)
|
||||
|
||||
const server = app.listen(backendPort, () => logger.info(`Backend listening on port ${backendPort}!`))
|
||||
process.on('SIGTERM', () => {
|
||||
server.close(() => {
|
||||
|
||||
@@ -18,7 +18,6 @@ const pateToken = process.env.PATE_API_TOKEN || ''
|
||||
|
||||
// Importer client
|
||||
const importerToken = process.env.IMPORTER_DB_API_TOKEN || ''
|
||||
const importerUrl = process.env.IMPORTER_DB_API_URL || ''
|
||||
|
||||
// Networking: Urls & ports
|
||||
const baseUrl = isDev ? '/api' : ''
|
||||
@@ -37,6 +36,7 @@ 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
|
||||
|
||||
let DB_MAX_CONNECTIONS = parseInt(process.env.DB_MAX_CONNECTIONS, 10)
|
||||
if (isNaN(DB_MAX_CONNECTIONS)) {
|
||||
@@ -48,10 +48,8 @@ if (DB_MAX_CRON_CONNECTIONS < 1) {
|
||||
}
|
||||
|
||||
const redis = process.env.REDIS
|
||||
const TOKEN_SECRET = process.env.TOKEN_SECRET
|
||||
const SECRET_TOKEN = process.env.SECRET_TOKEN
|
||||
const DB_SCHEMA_KONE = process.env.DB_SCHEMA_KONE || 'public'
|
||||
const USERSERVICE_URL = process.env.USERSERVICE_URL
|
||||
const SIS_UPDATER_URL = process.env.SIS_UPDATER_URL
|
||||
|
||||
const formatURL = url => {
|
||||
@@ -60,20 +58,14 @@ const formatURL = url => {
|
||||
|
||||
const addSlashToEnd = url => (url.endsWith('/') ? url : url + '/')
|
||||
|
||||
const ACCESS_TOKEN_HEADER_KEY = 'x-access-token'
|
||||
|
||||
const PWD = process.env.PWD || '/usr/src/app'
|
||||
|
||||
module.exports = {
|
||||
DB_URL_USER,
|
||||
DB_URL_KONE,
|
||||
DB_MAX_CONNECTIONS,
|
||||
DB_MAX_CRON_CONNECTIONS,
|
||||
redis,
|
||||
TOKEN_SECRET,
|
||||
SECRET_TOKEN,
|
||||
DB_SCHEMA_KONE,
|
||||
USERSERVICE_URL: formatURL(USERSERVICE_URL),
|
||||
ACCESS_TOKEN_HEADER_KEY,
|
||||
requiredGroup,
|
||||
SIS_UPDATER_URL,
|
||||
ANALYTICS_INFLUXDB_URL: formatURL(ANALYTICS_INFLUXDB_URL),
|
||||
@@ -91,6 +83,4 @@ module.exports = {
|
||||
frontUrl,
|
||||
backendPort,
|
||||
importerToken,
|
||||
importerUrl,
|
||||
PWD,
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ class DbConnection extends EventEmitter {
|
||||
pool: {
|
||||
max: 25,
|
||||
min: 0,
|
||||
acquire: 10000,
|
||||
acquire: 30000,
|
||||
idle: 300000000,
|
||||
},
|
||||
logging: false,
|
||||
@@ -39,54 +39,73 @@ class DbConnection extends EventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
// Old-style kone + user db connections
|
||||
const sequelizeKone = new Sequelize(conf.DB_URL_KONE, {
|
||||
schema: conf.DB_SCHEMA_KONE,
|
||||
searchPath: conf.DB_SCHEMA_KONE,
|
||||
logging: false,
|
||||
})
|
||||
|
||||
sequelizeKone.query(`SET SESSION search_path to ${conf.DB_SCHEMA_KONE}`)
|
||||
|
||||
const sequelizeUser = new Sequelize(conf.DB_URL_USER, {
|
||||
logging: false,
|
||||
})
|
||||
|
||||
const initializeDatabaseConnection = async () => {
|
||||
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms))
|
||||
const waitSeconds = 60
|
||||
for (let i = 1; i <= waitSeconds; i++) {
|
||||
try {
|
||||
await sequelizeKone.authenticate()
|
||||
break
|
||||
} catch (e) {
|
||||
if (i === waitSeconds) {
|
||||
logger.error(`Could not connect to database in ${waitSeconds} seconds`)
|
||||
throw e
|
||||
const rounds = 60
|
||||
|
||||
for (const [seq, dbName] of [
|
||||
[sequelizeKone, 'kone-db'],
|
||||
[sequelizeUser, 'user-db'],
|
||||
]) {
|
||||
logger.info(`Connecting to ${dbName}...`)
|
||||
for (let round = 1; round <= rounds; round++) {
|
||||
try {
|
||||
await seq.authenticate()
|
||||
break
|
||||
} catch (error) {
|
||||
if (round === rounds) {
|
||||
logger.error(`${dbName} database connection failed!`)
|
||||
throw error
|
||||
}
|
||||
await sleep(1000)
|
||||
}
|
||||
await sleep(1000)
|
||||
}
|
||||
}
|
||||
try {
|
||||
logger.info(`${dbName} database connection established`)
|
||||
|
||||
const schema = dbName === 'kone-db' ? conf.DB_SCHEMA_KONE : undefined
|
||||
const migrationsFolder = dbName === 'kone-db' ? 'migrations_kone' : 'migrations_user'
|
||||
const migrator = new Umzug({
|
||||
storage: 'sequelize',
|
||||
storageOptions: {
|
||||
sequelize: sequelizeKone,
|
||||
sequelize: seq,
|
||||
tableName: 'migrations',
|
||||
schema: conf.DB_SCHEMA_KONE,
|
||||
schema,
|
||||
},
|
||||
migrations: {
|
||||
params: [sequelizeKone.getQueryInterface(), Sequelize],
|
||||
path: `${process.cwd()}/src/database/migrations_kone`,
|
||||
params: [seq.getQueryInterface(), Sequelize],
|
||||
path: `${process.cwd()}/src/database/${migrationsFolder}`,
|
||||
pattern: /\.js$/,
|
||||
schema: conf.DB_SCHEMA_KONE,
|
||||
schema,
|
||||
},
|
||||
})
|
||||
const migrations = await migrator.up()
|
||||
logger.info({ message: 'Kone Migrations up to date', meta: migrations })
|
||||
} catch (e) {
|
||||
logger.error('Kone Migration error')
|
||||
throw e
|
||||
try {
|
||||
const migrations = await migrator.up()
|
||||
logger.info({ message: `${dbName} migrations up to date: `, meta: migrations.map(m => m.file) })
|
||||
} catch (error) {
|
||||
logger.error({ message: `${dbName} migrations failed`, meta: error })
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const dbConnections = new DbConnection()
|
||||
|
||||
module.exports = {
|
||||
dbConnections,
|
||||
sequelizeKone,
|
||||
sequelizeUser,
|
||||
initializeDatabaseConnection,
|
||||
}
|
||||
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
const { BIGINT, STRING, DATE } = require('sequelize')
|
||||
|
||||
module.exports = {
|
||||
up: async queryInterface => {
|
||||
await queryInterface.createTable(
|
||||
'study_guidance_group_tags',
|
||||
{
|
||||
id: {
|
||||
primaryKey: true,
|
||||
type: BIGINT,
|
||||
autoIncrement: true,
|
||||
},
|
||||
study_guidance_group_id: {
|
||||
type: STRING,
|
||||
unique: true,
|
||||
},
|
||||
study_programme: {
|
||||
type: STRING,
|
||||
},
|
||||
year: {
|
||||
type: STRING,
|
||||
},
|
||||
created_at: {
|
||||
type: DATE,
|
||||
},
|
||||
updated_at: {
|
||||
type: DATE,
|
||||
},
|
||||
},
|
||||
{ underscored: true, timestamps: true }
|
||||
)
|
||||
},
|
||||
down: async () => {},
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
module.exports = {
|
||||
up: async queryInterface => {
|
||||
await queryInterface.dropTable('thesis_courses')
|
||||
},
|
||||
down: async () => {},
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
module.exports = {
|
||||
up: async queryInterface => {
|
||||
await queryInterface.dropTable('user_affiliation')
|
||||
await queryInterface.sequelize.query('DROP SEQUENCE IF EXISTS user_affilation_id_seq')
|
||||
await queryInterface.dropTable('affiliations')
|
||||
await queryInterface.sequelize.query('DROP SEQUENCE IF EXISTS affiliations_id_seq')
|
||||
},
|
||||
down: async () => {},
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
module.exports = {
|
||||
up: async queryInterface => {
|
||||
await queryInterface.dropTable('user_hy_group')
|
||||
await queryInterface.sequelize.query('DROP SEQUENCE IF EXISTS user_hy_group_id_seq')
|
||||
await queryInterface.dropTable('hy_groups')
|
||||
await queryInterface.sequelize.query('DROP SEQUENCE IF EXISTS hy_groups_id_seq')
|
||||
},
|
||||
down: async () => {},
|
||||
}
|
||||
@@ -10,9 +10,8 @@ const {
|
||||
} = require('./services/trends')
|
||||
const { refreshAssociationsInRedis } = require('./services/studyrights')
|
||||
const { getAllProgrammes } = require('./services/studyrights')
|
||||
const { productivityStatsForStudytrack, throughputStatsForStudytrack } = require('./services/studyprogramme')
|
||||
const { updateBasicView, updateStudytrackView } = require('./services/studyprogrammeUpdates')
|
||||
const { findAndSaveTeachers } = require('./services/topteachers')
|
||||
const { setProductivity, setThroughput, patchProductivity, patchThroughput } = require('./services/analyticsService')
|
||||
const { isProduction } = require('./conf-backend')
|
||||
const { getCurrentSemester } = require('./services/semesters')
|
||||
const logger = require('./util/logger')
|
||||
@@ -24,47 +23,21 @@ const refreshStudyrightAssociations = async () => {
|
||||
logger.info('Studyright associations refreshed!')
|
||||
}
|
||||
|
||||
const refreshOverview = async () => {
|
||||
logger.info('Refreshing throughput and productivity for programmes...')
|
||||
const refreshNewOverviews = async () => {
|
||||
logger.info('Refreshing studyprogramme and studytrack overview statistics for all programmes')
|
||||
const codes = (await getAllProgrammes()).map(p => p.code).filter(code => code.includes('KH') || code.includes('MH'))
|
||||
let ready = 0
|
||||
for (const code of codes) {
|
||||
let programmeStatsSince = new Date('2017-07-31')
|
||||
if (code.includes('MH') || code.includes('KH')) {
|
||||
programmeStatsSince = new Date('2017-07-31')
|
||||
} else {
|
||||
programmeStatsSince = new Date('2000-07-31')
|
||||
}
|
||||
try {
|
||||
await patchThroughput({ [code]: { status: 'RECALCULATING' } })
|
||||
const data = await throughputStatsForStudytrack(code, programmeStatsSince.getFullYear())
|
||||
await setThroughput(data)
|
||||
await updateBasicView(code)
|
||||
await updateStudytrackView(code)
|
||||
ready += 1
|
||||
} catch (e) {
|
||||
logger.error({ message: `failed to recalculate throughput for ${code}`, meta: e })
|
||||
try {
|
||||
await patchThroughput({ [code]: { status: 'RECALCULATION ERRORED' } })
|
||||
} catch (e) {
|
||||
logger.error({ message: 'failed to update throughtput status to error', meta: e })
|
||||
}
|
||||
logger.error({ message: `Failed to update overview stats for programme ${code}`, meta: e })
|
||||
}
|
||||
try {
|
||||
await patchProductivity({ [code]: { status: 'RECALCULATING' } })
|
||||
const data = await productivityStatsForStudytrack(code, programmeStatsSince)
|
||||
await setProductivity(data)
|
||||
} catch (e) {
|
||||
logger.error({ message: `failed to recalculate productivity for ${code}`, meta: e })
|
||||
try {
|
||||
await patchProductivity({
|
||||
[code]: { status: 'RECALCULATION ERRORED' },
|
||||
})
|
||||
} catch (e) {
|
||||
logger.error({ message: 'failed to update productivity status to error', meta: e })
|
||||
}
|
||||
}
|
||||
ready += 1
|
||||
logger.info(`${ready}/${codes.length} programmes done`)
|
||||
}
|
||||
logger.info('Throughput and productivity for programmes refreshed')
|
||||
logger.info('Studyprogramme and studytrack overview stats refreshed!')
|
||||
}
|
||||
|
||||
const refreshTeacherLeaderboard = async () => {
|
||||
@@ -147,7 +120,7 @@ const refreshProtoCProgrammeToRedis = async () => {
|
||||
}
|
||||
|
||||
const refreshStatistics = async () => {
|
||||
const statfuncs = [refreshStudyrightAssociations, refreshOverview, refreshTeacherLeaderboard]
|
||||
const statfuncs = [refreshStudyrightAssociations, refreshTeacherLeaderboard]
|
||||
logger.info('Refreshing statistics')
|
||||
for (const func of statfuncs) {
|
||||
await func()
|
||||
@@ -166,9 +139,10 @@ const refreshTrends = async () => {
|
||||
|
||||
const startCron = () => {
|
||||
if (isProduction) {
|
||||
logger.info('Cronjob for refreshing stats started: runs at 3am every day.')
|
||||
// refresh 3am every day
|
||||
schedule('0 3 * * *', async () => {
|
||||
for (const func of [refreshStatistics, refreshTrends]) {
|
||||
for (const func of [refreshStatistics, refreshTrends, refreshNewOverviews]) {
|
||||
await func()
|
||||
}
|
||||
})
|
||||
|
||||
+6
-3
@@ -1,5 +1,6 @@
|
||||
const morgan = require('morgan')
|
||||
const logger = require('../util/logger')
|
||||
const _ = require('lodash')
|
||||
|
||||
// So this appears to be a hack to get neatly formatted stats like response-time etc. from morgan
|
||||
// without actually using morgan what it's used for (LOGGING REQUESTS!).
|
||||
@@ -7,7 +8,8 @@ const logger = require('../util/logger')
|
||||
// so don't e.g. make it into an async function or it'll print [object Promise] on every request
|
||||
const accessLogger = morgan((tokens, req, res) => {
|
||||
const fields = ['method', 'url', 'status', 'response-time', 'remote-addr', 'remote-user', 'user-agent', 'referrer']
|
||||
const meta = { ...req.decodedToken }
|
||||
const { user } = req
|
||||
const meta = { ...user }
|
||||
fields.forEach(field => (meta[field] = tokens[field](req, res)))
|
||||
meta['time'] = tokens['date'](req, res, 'iso')
|
||||
if (req.route) {
|
||||
@@ -15,7 +17,7 @@ const accessLogger = morgan((tokens, req, res) => {
|
||||
}
|
||||
|
||||
const message = [
|
||||
req.decodedToken.name,
|
||||
user.name,
|
||||
':',
|
||||
tokens['method'](req, res),
|
||||
tokens['url'](req, res),
|
||||
@@ -26,7 +28,8 @@ const accessLogger = morgan((tokens, req, res) => {
|
||||
].join(' ')
|
||||
|
||||
logger.info(message, {
|
||||
...meta,
|
||||
// don't log student list which might be huge
|
||||
..._.omit(meta, ['studentsUserCanAccess']),
|
||||
// pass this as a custom field so we can filter by it in graylog
|
||||
isUsageStats: true,
|
||||
})
|
||||
@@ -1,43 +1,8 @@
|
||||
const jwt = require('jsonwebtoken')
|
||||
const conf = require('../conf-backend')
|
||||
const blacklist = require('../services/blacklist')
|
||||
const { getUserDataFor } = require('../services/userService')
|
||||
const { ACCESS_TOKEN_HEADER_KEY } = require('../conf-backend')
|
||||
const { hasRequiredGroup, parseHyGroups } = require('../util/utils')
|
||||
|
||||
const TOKEN_VERSION = 1.1 // When token structure changes, increment in userservice, backend and frontend
|
||||
|
||||
const checkAuth = async (req, res, next) => {
|
||||
const token = req.headers[ACCESS_TOKEN_HEADER_KEY]
|
||||
const uid = req.headers['uid']
|
||||
|
||||
if (!token) {
|
||||
return res.status(403).json({ error: 'No token in headers' })
|
||||
}
|
||||
|
||||
jwt.verify(token, conf.TOKEN_SECRET, async (err, decoded) => {
|
||||
if (err) {
|
||||
return res.status(403).json(err)
|
||||
}
|
||||
|
||||
if (decoded.version !== TOKEN_VERSION) {
|
||||
return res.status(401).json({ error: 'Token needs to be refreshed - invalid version' })
|
||||
}
|
||||
|
||||
if (decoded.mockedBy ? decoded.mockedBy !== uid : decoded.userId !== uid) {
|
||||
return res.status(403).json({ error: 'User shibboleth id and token id did not match' })
|
||||
}
|
||||
|
||||
const userData = await getUserDataFor(decoded.userId)
|
||||
req.decodedToken = decoded
|
||||
Object.assign(req, userData)
|
||||
next()
|
||||
})
|
||||
}
|
||||
|
||||
const roles = requiredRoles => async (req, res, next) => {
|
||||
if (req.decodedToken) {
|
||||
const { roles } = req
|
||||
if (req.user) {
|
||||
const {
|
||||
user: { roles },
|
||||
} = req
|
||||
|
||||
if (requiredRoles.every(r => roles.indexOf(r) >= 0) || roles.includes('admin')) {
|
||||
return next()
|
||||
@@ -47,37 +12,6 @@ const roles = requiredRoles => async (req, res, next) => {
|
||||
res.status(403).json({ error: 'missing required roles' })
|
||||
}
|
||||
|
||||
const checkRequiredGroup = async (req, res, next) => {
|
||||
const hyGroups = parseHyGroups(req.headers['hygroupcn'])
|
||||
const enabled = hasRequiredGroup(hyGroups)
|
||||
const tokenOutdated = req.decodedToken.enabled !== enabled
|
||||
|
||||
if (tokenOutdated) {
|
||||
return res.status(401).json({
|
||||
error: 'Token needs to be refreshed - enabled does not match hy-group requirement',
|
||||
})
|
||||
}
|
||||
|
||||
if (!enabled) {
|
||||
return res.status(403).json({ error: 'User is not enabled' })
|
||||
}
|
||||
next()
|
||||
}
|
||||
|
||||
const checkUserBlacklisting = async (req, res, next) => {
|
||||
const { userId, createdAt } = req.decodedToken
|
||||
const isBlacklisted = await blacklist.isUserBlacklisted(userId, createdAt)
|
||||
|
||||
if (isBlacklisted) {
|
||||
return res.status(401).json({ error: 'Token needs to be refreshed - blacklisted' })
|
||||
}
|
||||
|
||||
next()
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
checkAuth,
|
||||
roles,
|
||||
checkRequiredGroup,
|
||||
checkUserBlacklisting,
|
||||
}
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
const Sentry = require('@sentry/node')
|
||||
const { ApplicationError } = require('../util/customErrors')
|
||||
const { getUser, getMockedUser } = require('../services/userService')
|
||||
const { requiredGroup } = require('../conf-backend')
|
||||
const _ = require('lodash')
|
||||
|
||||
const parseIamGroups = iamGroups => iamGroups?.split(';').filter(Boolean) ?? []
|
||||
|
||||
const hasRequiredIamGroup = iamGroups => _.intersection(iamGroups, requiredGroup).length > 0
|
||||
|
||||
const currentUserMiddleware = async (req, _res, next) => {
|
||||
const {
|
||||
'shib-session-id': sessionId,
|
||||
'x-show-as-user': showAsUser,
|
||||
displayname: name,
|
||||
hygroupcn,
|
||||
hypersonsisuid: sisId,
|
||||
mail: email,
|
||||
shib_logout_url: logoutUrl,
|
||||
uid: username,
|
||||
} = req.headers
|
||||
|
||||
if (!sessionId || !username) {
|
||||
throw new ApplicationError('Not enough data in request headers', 403, { logoutUrl })
|
||||
}
|
||||
|
||||
const iamGroups = parseIamGroups(hygroupcn)
|
||||
|
||||
if (!hasRequiredIamGroup(iamGroups)) {
|
||||
throw new ApplicationError('User does not have required iam group', 403, { logoutUrl })
|
||||
}
|
||||
|
||||
let user = await getUser({
|
||||
username,
|
||||
name,
|
||||
email,
|
||||
iamGroups,
|
||||
sisId,
|
||||
})
|
||||
|
||||
if (showAsUser && user.isAdmin) {
|
||||
user = await getMockedUser({ userToMock: showAsUser, mockedBy: username })
|
||||
}
|
||||
|
||||
Sentry.setUser({ username: user.mockedBy ?? username })
|
||||
|
||||
req.user = user
|
||||
req.logoutUrl = logoutUrl
|
||||
|
||||
next()
|
||||
}
|
||||
|
||||
module.exports = currentUserMiddleware
|
||||
@@ -1,13 +0,0 @@
|
||||
const Sentry = require('@sentry/node')
|
||||
|
||||
const sentryUserIdMiddleware = (req, res, next) => {
|
||||
Sentry.getCurrentHub().configureScope(scope => {
|
||||
scope.setUser({
|
||||
username: req.decodedToken && req.decodedToken.userId,
|
||||
})
|
||||
})
|
||||
|
||||
next()
|
||||
}
|
||||
|
||||
module.exports = sentryUserIdMiddleware
|
||||
@@ -0,0 +1,5 @@
|
||||
const headersMiddleware = require('unfuck-utf8-headers-middleware')
|
||||
|
||||
const headers = ['uid', 'displayName', 'shib-session-id', 'hyGroupCn', 'mail', 'hyPersonSisuId']
|
||||
|
||||
module.exports = headersMiddleware(headers)
|
||||
@@ -47,6 +47,9 @@ Course.init(
|
||||
substitutions: {
|
||||
type: JSONB,
|
||||
},
|
||||
course_unit_type: {
|
||||
type: STRING,
|
||||
},
|
||||
},
|
||||
{
|
||||
underscored: false,
|
||||
|
||||
@@ -75,6 +75,12 @@ Credit.init(
|
||||
updatedAt: {
|
||||
type: DATE,
|
||||
},
|
||||
language: {
|
||||
type: STRING,
|
||||
},
|
||||
is_open: {
|
||||
type: BOOLEAN,
|
||||
},
|
||||
},
|
||||
{
|
||||
underscored: false,
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
const { Model, DATE, INTEGER, STRING, BOOLEAN } = require('sequelize')
|
||||
const { dbConnections } = require('../database/connection')
|
||||
|
||||
class Enrollment extends Model {}
|
||||
|
||||
Enrollment.init(
|
||||
{
|
||||
id: {
|
||||
primaryKey: true,
|
||||
type: STRING,
|
||||
},
|
||||
studentnumber: {
|
||||
type: STRING,
|
||||
references: {
|
||||
model: 'student',
|
||||
key: 'studentnumber',
|
||||
},
|
||||
onUpdate: 'cascade',
|
||||
onDelete: 'cascade',
|
||||
},
|
||||
course_code: {
|
||||
type: STRING,
|
||||
},
|
||||
state: {
|
||||
type: STRING,
|
||||
},
|
||||
enrollment_date_time: {
|
||||
type: DATE,
|
||||
},
|
||||
course_id: {
|
||||
type: STRING,
|
||||
references: {
|
||||
model: 'course',
|
||||
key: 'id',
|
||||
},
|
||||
onUpdate: 'cascade',
|
||||
onDelete: 'cascade',
|
||||
},
|
||||
semester_composite: {
|
||||
type: STRING,
|
||||
references: {
|
||||
model: 'semesters',
|
||||
key: 'composite',
|
||||
},
|
||||
onUpdate: 'cascade',
|
||||
onDelete: 'cascade',
|
||||
},
|
||||
semestercode: {
|
||||
type: INTEGER,
|
||||
},
|
||||
createdAt: {
|
||||
type: DATE,
|
||||
},
|
||||
updatedAt: {
|
||||
type: DATE,
|
||||
},
|
||||
is_open: {
|
||||
type: BOOLEAN,
|
||||
},
|
||||
},
|
||||
{
|
||||
underscored: false,
|
||||
sequelize: dbConnections.sequelize,
|
||||
modelName: 'enrollment',
|
||||
tableName: 'enrollment',
|
||||
}
|
||||
)
|
||||
|
||||
module.exports = Enrollment
|
||||
@@ -10,6 +10,7 @@ const CreditType = require('./creditType')
|
||||
const Credit = require('./credit')
|
||||
const CreditTeacher = require('./creditTeacher')
|
||||
const ElementDetail = require('./elementDetail')
|
||||
const Studyplan = require('./studyplan')
|
||||
const StudyrightExtent = require('./studyrightExtent')
|
||||
const Studyright = require('./studyright')
|
||||
const StudyrightElement = require('./studyrightElement')
|
||||
@@ -17,6 +18,7 @@ const Transfer = require('./transfer')
|
||||
const ProgrammeModule = require('./programmeModule')
|
||||
const ProgrammeModuleChild = require('./programmeModuleChild')
|
||||
const ExcludedCourse = require('./excludedCourse')
|
||||
const Enrollment = require('./enrollment')
|
||||
|
||||
CourseType.hasMany(Course, { foreignKey: 'coursetypecode', sourceKey: 'coursetypecode' })
|
||||
Course.belongsTo(CourseType, { foreignKey: 'coursetypecode', targetKey: 'coursetypecode' })
|
||||
@@ -62,6 +64,9 @@ Teacher.belongsToMany(Credit, { through: CreditTeacher, foreignKey: 'teacher_id'
|
||||
Organization.hasMany(Studyright, { foreignKey: 'facultyCode', sourceKey: 'code' })
|
||||
Studyright.belongsTo(Organization, { foreignKey: 'facultyCode', targetKey: 'code' })
|
||||
|
||||
Studyplan.belongsTo(Student, { foreignKey: 'studentnumber', targetKey: 'studentnumber' })
|
||||
Student.hasMany(Studyplan, { foreignKey: 'studentnumber', sourceKey: 'studentnumber' })
|
||||
|
||||
Studyright.belongsTo(Student, { foreignKey: 'studentStudentnumber', targetKey: 'studentnumber' })
|
||||
Student.hasMany(Studyright, { foreignKey: 'studentStudentnumber', sourceKey: 'studentnumber' })
|
||||
|
||||
@@ -88,6 +93,12 @@ Studyright.hasMany(Transfer, { foreignKey: 'studyrightid', sourceKey: 'studyrigh
|
||||
Transfer.belongsTo(ElementDetail, { as: 'source', foreignKey: 'sourcecode' })
|
||||
Transfer.belongsTo(ElementDetail, { as: 'target', foreignKey: 'targetcode' })
|
||||
|
||||
Enrollment.belongsTo(Student, { foreignKey: 'studentnumber', targetKey: 'studentnumber' })
|
||||
Student.hasMany(Enrollment, { foreignKey: 'studentnumber', sourceKey: 'studentnumber' })
|
||||
Enrollment.belongsTo(Course, { foreignKey: 'course_id' })
|
||||
Course.hasMany(Enrollment, { foreignKey: 'course_id' })
|
||||
Enrollment.belongsTo(Semester, { foreignKey: { name: 'semester_composite', allowNull: false } })
|
||||
|
||||
ProgrammeModule.belongsToMany(ProgrammeModule, {
|
||||
as: 'parents',
|
||||
through: ProgrammeModuleChild,
|
||||
@@ -114,6 +125,7 @@ module.exports = {
|
||||
Credit,
|
||||
CreditTeacher,
|
||||
ElementDetail,
|
||||
Studyplan,
|
||||
StudyrightExtent,
|
||||
Studyright,
|
||||
StudyrightElement,
|
||||
@@ -121,4 +133,5 @@ module.exports = {
|
||||
ProgrammeModule,
|
||||
ProgrammeModuleChild,
|
||||
ExcludedCourse,
|
||||
Enrollment,
|
||||
}
|
||||
|
||||
@@ -104,20 +104,6 @@ const MandatoryCourseLabels = sequelizeKone.define('mandatory_course_labels', {
|
||||
},
|
||||
})
|
||||
|
||||
const ThesisCourse = sequelizeKone.define('thesis_courses', {
|
||||
programmeCode: {
|
||||
primaryKey: true,
|
||||
type: Sequelize.STRING,
|
||||
},
|
||||
courseCode: {
|
||||
primaryKey: true,
|
||||
type: Sequelize.STRING,
|
||||
},
|
||||
thesisType: {
|
||||
type: Sequelize.ENUM([ThesisTypeEnums.BACHELOR, ThesisTypeEnums.MASTER]),
|
||||
},
|
||||
})
|
||||
|
||||
const CustomPopulationSearch = sequelizeKone.define('custom_population_searches', {
|
||||
id: {
|
||||
primaryKey: true,
|
||||
@@ -135,6 +121,28 @@ const CustomPopulationSearch = sequelizeKone.define('custom_population_searches'
|
||||
},
|
||||
})
|
||||
|
||||
const StudyGuidanceGroupTag = sequelizeKone.define(
|
||||
'study_guidance_group_tags',
|
||||
{
|
||||
id: {
|
||||
primaryKey: true,
|
||||
type: Sequelize.BIGINT,
|
||||
autoIncrement: true,
|
||||
},
|
||||
studyGuidanceGroupId: {
|
||||
type: Sequelize.STRING,
|
||||
unique: true,
|
||||
},
|
||||
studyProgramme: {
|
||||
type: Sequelize.STRING,
|
||||
},
|
||||
year: {
|
||||
type: Sequelize.STRING,
|
||||
},
|
||||
},
|
||||
{ underscored: true, timestamps: true }
|
||||
)
|
||||
|
||||
MandatoryCourse.belongsTo(MandatoryCourseLabels, {
|
||||
foreignKey: 'label',
|
||||
sourceKey: 'id',
|
||||
@@ -151,10 +159,10 @@ module.exports = {
|
||||
MandatoryCourse,
|
||||
TagStudent,
|
||||
Tag,
|
||||
ThesisCourse,
|
||||
ThesisTypeEnums,
|
||||
MandatoryCourseLabels,
|
||||
CustomPopulationSearch,
|
||||
sequelizeKone,
|
||||
ExcludedCourse,
|
||||
StudyGuidanceGroupTag,
|
||||
}
|
||||
|
||||
+6
-52
@@ -1,7 +1,7 @@
|
||||
const Sequelize = require('sequelize')
|
||||
const { sequelize } = require('../database/connection')
|
||||
const { sequelizeUser } = require('../database/connection')
|
||||
|
||||
const UserElementDetails = sequelize.define(
|
||||
const UserElementDetails = sequelizeUser.define(
|
||||
'user_elementdetails',
|
||||
{
|
||||
userId: {
|
||||
@@ -18,7 +18,7 @@ const UserElementDetails = sequelize.define(
|
||||
}
|
||||
)
|
||||
|
||||
const AccessGroup = sequelize.define('access_group', {
|
||||
const AccessGroup = sequelizeUser.define('access_group', {
|
||||
id: {
|
||||
primaryKey: true,
|
||||
type: Sequelize.BIGINT,
|
||||
@@ -39,7 +39,7 @@ const AccessGroup = sequelize.define('access_group', {
|
||||
},
|
||||
})
|
||||
|
||||
const UserFaculties = sequelize.define('user_faculties', {
|
||||
const UserFaculties = sequelizeUser.define('user_faculties', {
|
||||
userId: {
|
||||
primaryKey: true,
|
||||
type: Sequelize.BIGINT,
|
||||
@@ -60,7 +60,7 @@ const UserFaculties = sequelize.define('user_faculties', {
|
||||
},
|
||||
})
|
||||
|
||||
const User = sequelize.define(
|
||||
const User = sequelizeUser.define(
|
||||
'users',
|
||||
{
|
||||
id: {
|
||||
@@ -83,41 +83,6 @@ const User = sequelize.define(
|
||||
timestamps: false,
|
||||
}
|
||||
)
|
||||
const HyGroup = sequelize.define('hy_group', {
|
||||
id: {
|
||||
primaryKey: true,
|
||||
type: Sequelize.BIGINT,
|
||||
autoIncrement: true,
|
||||
},
|
||||
|
||||
code: { type: Sequelize.STRING },
|
||||
|
||||
createdAt: {
|
||||
type: Sequelize.DATE,
|
||||
},
|
||||
|
||||
updatedAt: {
|
||||
type: Sequelize.DATE,
|
||||
},
|
||||
})
|
||||
|
||||
const Affiliation = sequelize.define('affiliations', {
|
||||
id: {
|
||||
primaryKey: true,
|
||||
type: Sequelize.BIGINT,
|
||||
autoIncrement: true,
|
||||
},
|
||||
|
||||
code: { type: Sequelize.STRING },
|
||||
|
||||
createdAt: {
|
||||
type: Sequelize.DATE,
|
||||
},
|
||||
|
||||
updatedAt: {
|
||||
type: Sequelize.DATE,
|
||||
},
|
||||
})
|
||||
|
||||
User.hasMany(UserElementDetails, { as: 'programme' })
|
||||
UserElementDetails.belongsTo(User)
|
||||
@@ -128,23 +93,12 @@ User.belongsToMany(AccessGroup, {
|
||||
})
|
||||
AccessGroup.belongsToMany(User, { through: 'user_accessgroup' })
|
||||
|
||||
User.belongsToMany(HyGroup, { through: 'user_hy_group', as: 'hy_group' })
|
||||
HyGroup.belongsToMany(User, { through: 'user_hy_group' })
|
||||
|
||||
User.belongsToMany(Affiliation, {
|
||||
through: 'user_affiliation',
|
||||
as: 'affiliation',
|
||||
})
|
||||
Affiliation.belongsToMany(User, { through: 'user_affiliation' })
|
||||
|
||||
User.hasMany(UserFaculties, { as: 'faculty', foreignKey: 'userId' })
|
||||
|
||||
module.exports = {
|
||||
User,
|
||||
UserElementDetails,
|
||||
AccessGroup,
|
||||
HyGroup,
|
||||
Affiliation,
|
||||
UserFaculties,
|
||||
sequelize,
|
||||
sequelizeUser,
|
||||
}
|
||||
@@ -17,7 +17,6 @@ Student.init(
|
||||
dateofuniversityenrollment: { type: DATE },
|
||||
email: { type: STRING },
|
||||
national_student_number: { type: STRING },
|
||||
home_county_id: { type: INTEGER },
|
||||
country_fi: { type: STRING },
|
||||
country_sv: { type: STRING },
|
||||
country_en: { type: STRING },
|
||||
@@ -25,6 +24,7 @@ Student.init(
|
||||
home_country_sv: { type: STRING },
|
||||
home_country_en: { type: STRING },
|
||||
gender_code: { type: INTEGER },
|
||||
sis_person_id: { type: STRING },
|
||||
createdAt: {
|
||||
type: DATE,
|
||||
},
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
const { Model, DATE, ARRAY, STRING, INTEGER } = require('sequelize')
|
||||
const { dbConnections } = require('../database/connection')
|
||||
|
||||
class Studyplan extends Model {}
|
||||
|
||||
Studyplan.init(
|
||||
{
|
||||
id: {
|
||||
primaryKey: true,
|
||||
type: INTEGER,
|
||||
autoIncrement: true,
|
||||
allowNull: false,
|
||||
},
|
||||
studentnumber: {
|
||||
type: STRING,
|
||||
references: {
|
||||
model: 'student',
|
||||
key: 'studentnumber',
|
||||
},
|
||||
onUpdate: 'cascade',
|
||||
onDelete: 'cascade',
|
||||
unique: 'source', // This and programme_code create a composite key for this table.
|
||||
allowNull: false,
|
||||
},
|
||||
programme_code: {
|
||||
type: STRING,
|
||||
allowNull: false,
|
||||
unique: 'source',
|
||||
},
|
||||
included_courses: {
|
||||
type: ARRAY(STRING),
|
||||
},
|
||||
createdAt: {
|
||||
type: DATE,
|
||||
},
|
||||
updatedAt: {
|
||||
type: DATE,
|
||||
},
|
||||
},
|
||||
{
|
||||
underscored: false,
|
||||
sequelize: dbConnections.sequelize,
|
||||
modelName: 'studyplan',
|
||||
tableName: 'studyplan',
|
||||
}
|
||||
)
|
||||
|
||||
module.exports = Studyplan
|
||||
@@ -9,9 +9,6 @@ Studyright.init(
|
||||
primaryKey: true,
|
||||
type: STRING,
|
||||
},
|
||||
canceldate: {
|
||||
type: DATE,
|
||||
},
|
||||
startdate: {
|
||||
type: DATE,
|
||||
},
|
||||
@@ -27,6 +24,9 @@ Studyright.init(
|
||||
graduated: {
|
||||
type: INTEGER,
|
||||
},
|
||||
active: {
|
||||
type: INTEGER,
|
||||
},
|
||||
// irtisanomisperuste
|
||||
studentStudentnumber: {
|
||||
type: STRING,
|
||||
|
||||
@@ -0,0 +1,363 @@
|
||||
/* eslint no-console: 0 */
|
||||
|
||||
/*
|
||||
depends on nodeproxy running in importer.cs.helsinki.fi, and env RAPO_NODEPROXY (see importer for a proper value)
|
||||
needs also acces to importer db api, uses env IMPORTER_API
|
||||
|
||||
known differences in APIs
|
||||
- rapo does not show students that have graduated (it however takes some days for rapo to notice a graduation)
|
||||
- rapo does not handle the starting year of transfers properly (shows in old staring population)
|
||||
- rapo show some students incorrectly passive, see https://github.com/UniversityOfHelsinkiCS/oodikone/issues/3521
|
||||
*/
|
||||
|
||||
const axios = require('axios').default
|
||||
|
||||
const _ = require('lodash')
|
||||
|
||||
const { optimizedStatisticsOf } = require('./services/populations')
|
||||
const { Student, Transfer, StudyrightElement, Studyright } = require('./models')
|
||||
|
||||
const token = process.env.RAPO_NODEPROXY
|
||||
|
||||
const getFromRapo = async (urlStart, urlEnd) => {
|
||||
const { data: rapoStuffLasna } = await axios.get(urlStart)
|
||||
const { data: rapoStuffLasnaMinus } = await axios.get(urlEnd)
|
||||
|
||||
const rapoStudents = _.difference(
|
||||
rapoStuffLasna.map(s => s.opiskelijanumero),
|
||||
rapoStuffLasnaMinus.map(s => s.opiskelijanumero)
|
||||
)
|
||||
|
||||
return rapoStudents
|
||||
}
|
||||
|
||||
const known_problems_in_oodikone = {
|
||||
KH30_001: ['013896191'],
|
||||
KH40_003: ['013200457'],
|
||||
KH55_001: ['013195834', '015119708', '015138336'],
|
||||
}
|
||||
|
||||
const known_problems_in_rapo = {
|
||||
KH55_001: ['015160142', '015135575', '015119708', '015138336'],
|
||||
}
|
||||
|
||||
const getFromOodikone = async (programme, year) => {
|
||||
const isNotAmongKnownProblems = student => {
|
||||
const hasProblems = known_problems_in_oodikone[programme] ? known_problems_in_oodikone[programme] : []
|
||||
return !hasProblems.includes(student.studentNumber)
|
||||
}
|
||||
|
||||
const theStudyrights = student => {
|
||||
const theOne = s => s.studyright_elements.find(e => e.code === programme)
|
||||
// in some rare cases student may have more than one studyright for same programme
|
||||
return student.studyrights.filter(theOne)
|
||||
}
|
||||
|
||||
const notGraduated = student => {
|
||||
const graduation = theStudyrights(student).filter(r => r.graduated === 1)
|
||||
return graduation.length === 0
|
||||
}
|
||||
|
||||
const notTransferred = student => {
|
||||
const transfer = student.transfers.find(t => t.targetcode === programme)
|
||||
return transfer === undefined
|
||||
}
|
||||
|
||||
const months = 12
|
||||
|
||||
const oodikoneQuery = {
|
||||
semesters: ['FALL', 'SPRING'],
|
||||
months,
|
||||
studyRights: { programme },
|
||||
year,
|
||||
}
|
||||
|
||||
const oodikoneStuff = (await optimizedStatisticsOf(oodikoneQuery)).students
|
||||
|
||||
return oodikoneStuff
|
||||
.filter(notGraduated)
|
||||
.filter(notTransferred)
|
||||
.filter(isNotAmongKnownProblems)
|
||||
.map(s => s.studentNumber)
|
||||
}
|
||||
|
||||
const isAcuallyActive = async student => {
|
||||
const currentSemester = 144
|
||||
const tokenImporterApi = process.env.IMPORTER_API
|
||||
|
||||
const url = `https://importer.cs.helsinki.fi/api/importer/students/${student}/rapo_semester_enrollments?token=${tokenImporterApi}`
|
||||
const { data } = await axios.get(url)
|
||||
const terms = data.reduce((all, s) => all.concat(s.terms), []).filter(t => t.semester_code === currentSemester)
|
||||
const active = terms.filter(t => t.type !== 'NEGLECTED')
|
||||
const negl = terms.filter(t => t.type === 'NEGLECTED')
|
||||
return active.length > 0 && negl.length > 0
|
||||
}
|
||||
|
||||
const hasTransferred = async (student, programme) => {
|
||||
const data = await Student.findByPk(student, {
|
||||
include: [
|
||||
{
|
||||
model: Transfer,
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
const transferTo = data.transfers.find(t => t.targetcode === programme)
|
||||
return transferTo !== undefined
|
||||
}
|
||||
|
||||
const graduatedVeryRecently = async (student, programme) => {
|
||||
const data = await Student.findByPk(student, {
|
||||
include: [
|
||||
{
|
||||
model: Studyright,
|
||||
include: [
|
||||
{
|
||||
model: StudyrightElement,
|
||||
required: true,
|
||||
where: {
|
||||
code: programme,
|
||||
},
|
||||
},
|
||||
],
|
||||
where: {
|
||||
graduated: 1,
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
if (!data || data.studyrights.length === 0) {
|
||||
return false
|
||||
}
|
||||
|
||||
const graduated = new Date(data.studyrights[0].enddate)
|
||||
const weekAgo = new Date(new Date().getTime() - 7 * 24 * 60 * 60 * 1000)
|
||||
return graduated > weekAgo
|
||||
}
|
||||
|
||||
const programme_diff_year = async (programme, facultyCode, year, brief = false) => {
|
||||
const oodikoneStudents = await getFromOodikone(programme, year)
|
||||
|
||||
const studyrightStartDate = `${year}-08-01`
|
||||
const studyrightEndDate = `${year + 1}-08-01`
|
||||
|
||||
const rapoUrlStart = presence =>
|
||||
encodeURI(
|
||||
`https://importer.cs.helsinki.fi/test/rapo/${facultyCode}/${programme}/${studyrightStartDate}/${presence}?token=${token}`
|
||||
)
|
||||
const rapoUrlEnd = presence =>
|
||||
encodeURI(
|
||||
`https://importer.cs.helsinki.fi/test/rapo/${facultyCode}/${programme}/${studyrightEndDate}/${presence}?token=${token}`
|
||||
)
|
||||
|
||||
const rapoStudentsLasna = await getFromRapo(rapoUrlStart('Läsnä'), rapoUrlEnd('Läsnä'))
|
||||
const rapoStudentsPoissa = await getFromRapo(rapoUrlStart('Poissa'), rapoUrlEnd('Poissa'))
|
||||
const rapoStudentPoissaLaki = await getFromRapo(
|
||||
rapoUrlStart('Poissa ei kuluta opintoaikaa'),
|
||||
rapoUrlEnd('Poissa ei kuluta opintoaikaa')
|
||||
)
|
||||
|
||||
const isNotAmongKnownProblems = student => {
|
||||
const hasProblems = known_problems_in_rapo[programme] ? known_problems_in_rapo[programme] : []
|
||||
return !hasProblems.includes(student.studentNumber)
|
||||
}
|
||||
|
||||
const rapoStudents = rapoStudentsLasna
|
||||
.concat(rapoStudentsPoissa)
|
||||
.concat(rapoStudentPoissaLaki)
|
||||
.filter(isNotAmongKnownProblems)
|
||||
|
||||
const onlyInOodikone = _.difference(oodikoneStudents, rapoStudents)
|
||||
const onlyInRapo = _.difference(rapoStudents, oodikoneStudents)
|
||||
|
||||
const wrongMissingInRapo = []
|
||||
for (let student of onlyInOodikone) {
|
||||
const status = await isAcuallyActive(student)
|
||||
if (status) {
|
||||
wrongMissingInRapo.push(student)
|
||||
}
|
||||
}
|
||||
|
||||
// these are in wrong RAPO-population
|
||||
const transferred = []
|
||||
for (let student of onlyInRapo) {
|
||||
const isTransfer = await hasTransferred(student, programme)
|
||||
if (isTransfer) {
|
||||
transferred.push(student)
|
||||
} else {
|
||||
const isRecentGraduation = await graduatedVeryRecently(student, programme)
|
||||
if (isRecentGraduation) {
|
||||
transferred.push(student)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const checkOodikoneOnly = _.difference(onlyInOodikone, wrongMissingInRapo)
|
||||
const checkRapoOnly = _.difference(onlyInRapo, transferred)
|
||||
|
||||
if (!brief) {
|
||||
console.log('vuosi', year)
|
||||
console.log('')
|
||||
console.log('oodikone', oodikoneStudents.length, ' transferred', transferred.length)
|
||||
console.log('rapo', rapoStudents.length, ' activeness wrong rapo', wrongMissingInRapo.length)
|
||||
console.log('only rapo', checkRapoOnly.length)
|
||||
console.log('only oodikone', checkOodikoneOnly.length)
|
||||
|
||||
console.log('')
|
||||
|
||||
if (checkRapoOnly.length > 0) {
|
||||
console.log('Only in Rapo:')
|
||||
console.log(checkRapoOnly.join('\n'))
|
||||
}
|
||||
|
||||
if (checkOodikoneOnly.length > 0) {
|
||||
console.log('Only in Oodikone:')
|
||||
console.log(checkOodikoneOnly.join('\n'))
|
||||
}
|
||||
}
|
||||
|
||||
if (checkRapoOnly.length + checkOodikoneOnly.length) {
|
||||
console.log('******************************', year)
|
||||
if (checkRapoOnly.length > 0) {
|
||||
console.log('Only in Rapo:')
|
||||
console.log(checkRapoOnly.join('\n'))
|
||||
}
|
||||
|
||||
if (checkOodikoneOnly.length > 0) {
|
||||
console.log('Only in Oodikone:')
|
||||
console.log(checkOodikoneOnly.join('\n'))
|
||||
}
|
||||
}
|
||||
|
||||
if (!brief) {
|
||||
console.log('')
|
||||
}
|
||||
}
|
||||
|
||||
const programme_diff = async (programme, faculty, brief) => {
|
||||
console.log()
|
||||
console.log(programme)
|
||||
console.log('--------', '\n')
|
||||
|
||||
await programme_diff_year(programme, faculty, 2017, brief)
|
||||
await programme_diff_year(programme, faculty, 2018, brief)
|
||||
await programme_diff_year(programme, faculty, 2019, brief)
|
||||
await programme_diff_year(programme, faculty, 2020, brief)
|
||||
await programme_diff_year(programme, faculty, 2021, brief)
|
||||
}
|
||||
|
||||
const faculty_diff = async (faculty, programmes, brief) => {
|
||||
console.log('\n')
|
||||
console.log(faculty)
|
||||
console.log('==')
|
||||
for (let programme of programmes) {
|
||||
await programme_diff(programme, faculty, brief)
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const msc_programme_diff_year = async (programme, facultyCode, year, brief = false) => {
|
||||
const oodikoneStudents = await getFromOodikone(programme, year)
|
||||
|
||||
const studyrightStartDate = `${year}-08-01`
|
||||
const studyrightEndDate = `${year + 1}-08-01`
|
||||
|
||||
const rapoUrlStart = presence =>
|
||||
encodeURI(
|
||||
`https://importer.cs.helsinki.fi/test/rapo/${facultyCode}/${programme}/${studyrightStartDate}/${presence}?token=${token}`
|
||||
)
|
||||
const rapoUrlEnd = presence =>
|
||||
encodeURI(
|
||||
`https://importer.cs.helsinki.fi/test/rapo/${facultyCode}/${programme}/${studyrightEndDate}/${presence}?token=${token}`
|
||||
)
|
||||
|
||||
const rapoStudentsLasna = await getFromRapo(rapoUrlStart('Läsnä'), rapoUrlEnd('Läsnä'))
|
||||
const rapoStudentsPoissa = await getFromRapo(rapoUrlStart('Poissa'), rapoUrlEnd('Poissa'))
|
||||
const rapoStudentPoissaLaki = await getFromRapo(
|
||||
rapoUrlStart('Poissa ei kuluta opintoaikaa'),
|
||||
rapoUrlEnd('Poissa ei kuluta opintoaikaa')
|
||||
)
|
||||
|
||||
const rapoStudents = rapoStudentsLasna.concat(rapoStudentsPoissa).concat(rapoStudentPoissaLaki)
|
||||
|
||||
const onlyInOodikone = _.difference(oodikoneStudents, rapoStudents)
|
||||
const onlyInRapo = _.difference(rapoStudents, oodikoneStudents)
|
||||
|
||||
const wrongInRapo = []
|
||||
for (let student of onlyInOodikone) {
|
||||
const status = await isAcuallyActive(student)
|
||||
if (status) {
|
||||
//wrongInRapo.push(student)
|
||||
}
|
||||
}
|
||||
|
||||
// in wrong RAPO population
|
||||
const transferred = []
|
||||
for (let student of onlyInRapo) {
|
||||
const status = await hasTransferred(student, programme)
|
||||
if (status) {
|
||||
transferred.push(student)
|
||||
}
|
||||
}
|
||||
|
||||
const checkOodikoneOnly = _.difference(onlyInOodikone, wrongInRapo)
|
||||
const checkRapoOnly = _.difference(onlyInRapo, transferred)
|
||||
const both = _.intersection(oodikoneStudents, rapoStudents)
|
||||
|
||||
console.log('vuosi', year)
|
||||
|
||||
if (!brief) {
|
||||
console.log('')
|
||||
console.log('oodikone', oodikoneStudents.length, ' transferred', transferred.length)
|
||||
console.log('rapo', rapoStudents.length, ' activeness wrong rapo', wrongInRapo.length)
|
||||
console.log('both', both.length)
|
||||
console.log('only rapo', checkRapoOnly.length)
|
||||
console.log('only oodikone', checkOodikoneOnly.length)
|
||||
|
||||
console.log('')
|
||||
|
||||
if (checkRapoOnly.length > 0) {
|
||||
console.log('Only in Rapo:')
|
||||
console.log(checkRapoOnly.join('\n'))
|
||||
}
|
||||
|
||||
if (checkOodikoneOnly.length > 0) {
|
||||
console.log('Only in Oodikone:')
|
||||
console.log(checkOodikoneOnly.join('\n'))
|
||||
}
|
||||
}
|
||||
|
||||
if (checkRapoOnly.length + checkOodikoneOnly.length) {
|
||||
console.log('******************************************************')
|
||||
}
|
||||
|
||||
if (!brief) {
|
||||
console.log('')
|
||||
}
|
||||
}
|
||||
|
||||
const bsc_priogrammes_of_faculties = {
|
||||
H10: ['KH10_001'],
|
||||
H20: ['KH20_001'],
|
||||
H30: ['KH30_001', 'KH30_002'],
|
||||
H40: ['KH40_001', 'KH40_002', 'KH40_003', 'KH40_004', 'KH40_005', 'KH40_006'],
|
||||
H50: ['KH50_001', 'KH50_002', 'KH50_003', 'KH50_004', 'KH50_005', 'KH50_006', 'KH50_007', 'KH50_008'],
|
||||
H55: ['KH55_001'],
|
||||
H60: ['KH60_001'],
|
||||
H70: ['KH70_001', 'KH70_002', 'KH70_003', 'KH70_004'],
|
||||
H74: ['KH74_001'],
|
||||
H80: ['KH80_001', 'KH80_002', 'KH80_003', 'KH80_004'],
|
||||
H90: ['KH90_001'],
|
||||
}
|
||||
|
||||
const main = async () => {
|
||||
for (let faculty of Object.keys(bsc_priogrammes_of_faculties)) {
|
||||
await faculty_diff(faculty, bsc_priogrammes_of_faculties[faculty], true)
|
||||
}
|
||||
|
||||
//await programme_diff_year('KH55_001', 'H55', 2021, true)
|
||||
//await programme_diff('KH60_001', 'H60', true)
|
||||
}
|
||||
|
||||
main().then(() => process.exit(0))
|
||||
@@ -1,50 +1,52 @@
|
||||
const shibbolethHeadersFix = require('unfuck-utf8-headers-middleware')
|
||||
const accessLogger = require('./middleware/accesslogger')
|
||||
const sentryUserId = require('./middleware/sentryUserId')
|
||||
const Sentry = require('@sentry/node')
|
||||
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 courses = require('./routes/courses')
|
||||
const students = require('./routes/students')
|
||||
const population = require('./routes/population')
|
||||
const login = require('./routes/login')
|
||||
const superlogin = require('./routes/superlogin')
|
||||
const language = require('./routes/language')
|
||||
const users = require('./routes/users')
|
||||
const elementdetails = require('./routes/elementdetails')
|
||||
const auth = require('./middleware/auth')
|
||||
const teachers = require('./routes/teachers')
|
||||
const providers = require('./routes/providers')
|
||||
const faculties = require('./routes/faculties')
|
||||
const semesters = require('./routes/semesters')
|
||||
const mandatoryCourses = require('./routes/mandatorycourses')
|
||||
const mandatoryCourseLabels = require('./routes/mandatorycourselabels')
|
||||
const ping = require('./routes/ping')
|
||||
const feedback = require('./routes/feedback')
|
||||
const tags = require('./routes/tags')
|
||||
const updater = require('./routes/updater')
|
||||
const tsaAnalytics = require('./routes/tsaAnalytics')
|
||||
const matomoInit = require('./routes/matomo-init')
|
||||
const customPopulationSearch = require('./routes/customPopulationSearch')
|
||||
const trends = require('./routes/trends')
|
||||
const programmeModules = require('./routes/programmeModules')
|
||||
const studyGuidanceGroups = require('./routes/studyGuidanceGroups')
|
||||
const studyProgramme = require('./routes/studyProgramme')
|
||||
const initializeSentry = require('./util/sentry')
|
||||
|
||||
const errorMiddleware = require('./middleware/errorMiddleware')
|
||||
|
||||
module.exports = (app, url) => {
|
||||
app.use(url, ping)
|
||||
app.use(
|
||||
shibbolethHeadersFix([
|
||||
'hyGroupCn',
|
||||
'SHIB_LOGOUT_URL',
|
||||
'eduPersonAffiliation',
|
||||
'uid',
|
||||
'displayName',
|
||||
'mail',
|
||||
'hyPersonSisuId',
|
||||
'preferredLanguage',
|
||||
])
|
||||
)
|
||||
initializeSentry(app)
|
||||
app.use(Sentry.Handlers.requestHandler())
|
||||
app.use(Sentry.Handlers.tracingHandler())
|
||||
|
||||
app.use(cors({ credentials: true, origin: frontUrl }))
|
||||
app.use(express.json())
|
||||
|
||||
app.use(shibbolethCharsetMiddleware)
|
||||
app.use(url, matomoInit)
|
||||
app.use(currentUserMiddleware)
|
||||
app.use(accessLogger)
|
||||
app.use(url, login)
|
||||
app.use(`${url}/superlogin`, superlogin)
|
||||
app.use(auth.checkAuth, auth.checkRequiredGroup, auth.checkUserBlacklisting, accessLogger, sentryUserId)
|
||||
app.use(url, elementdetails)
|
||||
app.use(url, courses)
|
||||
app.use(url, students)
|
||||
@@ -54,6 +56,7 @@ module.exports = (app, url) => {
|
||||
app.use(url, semesters)
|
||||
app.use(url, tags)
|
||||
app.use(url, programmeModules)
|
||||
app.use(url, studyProgramme)
|
||||
app.use(`${url}/faculties`, faculties)
|
||||
app.use(`${url}/updater`, auth.roles(['admin']), updater)
|
||||
app.use(`${url}/teachers`, auth.roles(['teachers']), teachers)
|
||||
@@ -65,4 +68,10 @@ module.exports = (app, url) => {
|
||||
app.use(`${url}/custom-population-search`, customPopulationSearch)
|
||||
app.use(`${url}/cool-data-science`, trends)
|
||||
app.use(`${url}/studyguidancegroups`, auth.roles(['studyGuidanceGroups']), studyGuidanceGroups)
|
||||
app.get('*', async (_, res) => {
|
||||
const results = { error: 'unknown endpoint' }
|
||||
res.status(404).json(results)
|
||||
})
|
||||
app.use(Sentry.Handlers.errorHandler())
|
||||
app.use(errorMiddleware)
|
||||
}
|
||||
|
||||
@@ -16,7 +16,9 @@ router.get('/v2/coursesmulti', async (req, res) => {
|
||||
})
|
||||
|
||||
router.get('/v3/courseyearlystats', async (req, res) => {
|
||||
const { rights, roles } = req
|
||||
const {
|
||||
user: { rights, roles },
|
||||
} = req
|
||||
|
||||
const allowedRoles = roles && ['admin', 'courseStatistics'].find(role => roles.includes(role))
|
||||
|
||||
@@ -42,6 +44,4 @@ router.get('/v3/courseyearlystats', async (req, res) => {
|
||||
}
|
||||
})
|
||||
|
||||
router.use('*', (req, res, next) => next())
|
||||
|
||||
module.exports = router
|
||||
|
||||
@@ -8,7 +8,7 @@ const {
|
||||
|
||||
router.get('/', async (req, res) => {
|
||||
const {
|
||||
decodedToken: { id },
|
||||
user: { id },
|
||||
} = req
|
||||
const customPopulationSearches = await getCustomPopulationSearchesByUser(id)
|
||||
res.json(customPopulationSearches)
|
||||
@@ -16,7 +16,7 @@ router.get('/', async (req, res) => {
|
||||
|
||||
router.post('/', async (req, res) => {
|
||||
const {
|
||||
decodedToken: { id },
|
||||
user: { id },
|
||||
body: { name, students },
|
||||
} = req
|
||||
|
||||
@@ -29,7 +29,7 @@ router.post('/', async (req, res) => {
|
||||
|
||||
router.put('/:id', async (req, res) => {
|
||||
const {
|
||||
decodedToken: { id: userId },
|
||||
user: { id: userId },
|
||||
body: { students },
|
||||
} = req
|
||||
const { id } = req.params
|
||||
@@ -46,7 +46,7 @@ router.put('/:id', async (req, res) => {
|
||||
|
||||
router.delete('/:id', async (req, res) => {
|
||||
const {
|
||||
decodedToken: { id: userId },
|
||||
user: { id: userId },
|
||||
} = req
|
||||
const { id } = req.params
|
||||
|
||||
|
||||
@@ -1,23 +1,6 @@
|
||||
const router = require('express').Router()
|
||||
const MandatoryCourses = require('../services/mandatoryCourses')
|
||||
const {
|
||||
productivityStatsForStudytrack,
|
||||
throughputStatsForStudytrack,
|
||||
optionData,
|
||||
} = require('../services/studyprogramme')
|
||||
const { findProgrammeTheses, createThesisCourse, deleteThesisCourse } = require('../services/thesis')
|
||||
const {
|
||||
getProductivity,
|
||||
setProductivity,
|
||||
getThroughput,
|
||||
setThroughput,
|
||||
patchProductivity,
|
||||
patchThroughput,
|
||||
} = require('../services/analyticsService')
|
||||
const { getAllProgrammes, getAllElementDetails } = require('../services/studyrights')
|
||||
const logger = require('../util/logger')
|
||||
|
||||
const programmeStatsSince = new Date('2017-07-31')
|
||||
|
||||
router.get('/elementdetails/all', async (req, res) => {
|
||||
const elementdetails = await getAllElementDetails()
|
||||
@@ -29,22 +12,6 @@ router.get('/studyprogrammes', async (req, res) => {
|
||||
res.json(studyrights)
|
||||
})
|
||||
|
||||
router.get('/v2/studyprogrammes/:id/optiondata', async (req, res) => {
|
||||
const code = req.params.id
|
||||
|
||||
let level
|
||||
if (code.includes('MH')) {
|
||||
level = 'MSC'
|
||||
} else if (code.includes('KH')) {
|
||||
level = 'BSC'
|
||||
} else {
|
||||
return res.json([])
|
||||
}
|
||||
|
||||
const data = await optionData(new Date('2017-07-31'), new Date(), code, level)
|
||||
res.json(data)
|
||||
})
|
||||
|
||||
router.get('/v2/studyprogrammes/:id/mandatory_courses', async (req, res) => {
|
||||
if (req.params.id) {
|
||||
const codes = await MandatoryCourses.byStudyprogramme(req.params.id)
|
||||
@@ -54,140 +21,4 @@ router.get('/v2/studyprogrammes/:id/mandatory_courses', async (req, res) => {
|
||||
}
|
||||
})
|
||||
|
||||
router.get('/v2/studyprogrammes/:id/productivity', async (req, res) => {
|
||||
const code = req.params.id
|
||||
if (code) {
|
||||
let data = null
|
||||
try {
|
||||
data = await getProductivity(code)
|
||||
} catch (e) {
|
||||
logger.error(`Failed to get code ${code} productivity`)
|
||||
}
|
||||
if (!data) {
|
||||
try {
|
||||
const stats = await productivityStatsForStudytrack(code, programmeStatsSince)
|
||||
data = await setProductivity(stats)
|
||||
} catch (e) {
|
||||
logger.error(`Failed to update code ${code} patching status to recalculation erroring`)
|
||||
}
|
||||
}
|
||||
return res.json(data)
|
||||
} else {
|
||||
res.status(422).end()
|
||||
}
|
||||
})
|
||||
|
||||
router.get('/v2/studyprogrammes/productivity/recalculate', async (req, res) => {
|
||||
const code = req.query.code
|
||||
|
||||
await patchThroughput({ [code]: { status: 'RECALCULATING' } })
|
||||
res.status(200).end()
|
||||
|
||||
try {
|
||||
if (code.includes('MH') || code.includes('KH')) {
|
||||
const data = await productivityStatsForStudytrack(code, new Date('2017-07-31'))
|
||||
await setProductivity(data)
|
||||
} else {
|
||||
const data = await productivityStatsForStudytrack(code, new Date('2000-07-31'))
|
||||
await setProductivity(data)
|
||||
}
|
||||
} catch (e) {
|
||||
try {
|
||||
await patchProductivity({
|
||||
[code]: { status: 'RECALCULATION ERRORED' },
|
||||
})
|
||||
} catch (e) {
|
||||
logger.error(`Failed to update code ${code} patching status to recalculation erroring`)
|
||||
return
|
||||
}
|
||||
|
||||
logger.error(`Failed to update productivity stats for code: ${code}, reason: ${e.message}`)
|
||||
}
|
||||
logger.info(`Produdtiviy stats recalculation for studyprogramme ${code} done`)
|
||||
})
|
||||
|
||||
router.get('/v2/studyprogrammes/:id/throughput', async (req, res) => {
|
||||
const code = req.params.id
|
||||
if (code) {
|
||||
const thesisPromise = findProgrammeTheses(req.params.id)
|
||||
|
||||
let data = null
|
||||
try {
|
||||
data = await getThroughput(code)
|
||||
} catch (e) {
|
||||
logger.error(`Failed to get code ${code} throughput`)
|
||||
}
|
||||
if (!data) {
|
||||
try {
|
||||
const result = await throughputStatsForStudytrack(req.params.id, programmeStatsSince.getFullYear())
|
||||
data = await setThroughput(result)
|
||||
} catch (e) {
|
||||
logger.error(`Failed to update code ${code} patching status to recalculation erroring`)
|
||||
}
|
||||
}
|
||||
return res.json({ ...data, thesis: await thesisPromise })
|
||||
} else {
|
||||
res.status(422).end()
|
||||
}
|
||||
})
|
||||
|
||||
router.get('/v2/studyprogrammes/throughput/recalculate', async (req, res) => {
|
||||
const code = req.query.code
|
||||
|
||||
await patchThroughput({ [code]: { status: 'RECALCULATING' } })
|
||||
res.status(200).end()
|
||||
|
||||
try {
|
||||
if (code.includes('MH') || code.includes('KH')) {
|
||||
const data = await throughputStatsForStudytrack(code, 2017)
|
||||
await setThroughput(data)
|
||||
} else {
|
||||
const data = await throughputStatsForStudytrack(code, 2000)
|
||||
await setThroughput(data)
|
||||
}
|
||||
} catch (e) {
|
||||
try {
|
||||
await patchThroughput({ [code]: { status: 'RECALCULATION ERRORED' } })
|
||||
} catch (e) {
|
||||
logger.error(`Failed to update code ${code} patching status to recalculation erroring`)
|
||||
return
|
||||
}
|
||||
logger.error(`Failed to update throughput stats for code: ${code}, reason: ${e.message}`)
|
||||
}
|
||||
logger.info(`Throughput stats recalculation for studyprogramme ${code} done`)
|
||||
})
|
||||
|
||||
router.get('/v2/studyprogrammes/:id/thesis', async (req, res) => {
|
||||
const { id } = req.params
|
||||
if (id) {
|
||||
const thesis = await findProgrammeTheses(id)
|
||||
res.json(thesis)
|
||||
} else {
|
||||
res.status(422).end()
|
||||
}
|
||||
})
|
||||
|
||||
router.post('/v2/studyprogrammes/:id/thesis', async (req, res) => {
|
||||
const { id } = req.params
|
||||
const { course, thesisType } = req.body
|
||||
if (id && course && thesisType) {
|
||||
const thesis = await createThesisCourse(id, course, thesisType)
|
||||
res.status(201).json(thesis)
|
||||
} else {
|
||||
res.status(422).end()
|
||||
}
|
||||
})
|
||||
|
||||
router.delete('/v2/studyprogrammes/:id/thesis/:course', async (req, res) => {
|
||||
const { id, course } = req.params
|
||||
if (id && course) {
|
||||
const deleted = await deleteThesisCourse(id, course)
|
||||
res.status(204).json(deleted)
|
||||
} else {
|
||||
res.status(422).end()
|
||||
}
|
||||
})
|
||||
|
||||
router.use('*', (req, res, next) => next())
|
||||
|
||||
module.exports = router
|
||||
|
||||
@@ -1,28 +1,19 @@
|
||||
const router = require('express').Router()
|
||||
const { getUserDataFor } = require('../services/userService')
|
||||
const { sendFeedbackToToska } = require('../services/mailservice')
|
||||
const logger = require('../util/logger')
|
||||
|
||||
router.post('/email', async (req, res) => {
|
||||
const { content } = req.body
|
||||
router.post('/email', async req => {
|
||||
const {
|
||||
decodedToken: { userId },
|
||||
body: { content },
|
||||
user,
|
||||
} = req
|
||||
|
||||
const { email, full_name } = await getUserDataFor(userId)
|
||||
|
||||
const result = await sendFeedbackToToska({
|
||||
await sendFeedbackToToska({
|
||||
feedbackContent: content,
|
||||
userId,
|
||||
userEmail: email,
|
||||
userFullName: full_name,
|
||||
user,
|
||||
})
|
||||
if (result.error) {
|
||||
return res.status(400).json(result).end()
|
||||
}
|
||||
logger.info(`${userId} succesfully sent some feedback mail to toska`)
|
||||
|
||||
res.status(200).json('success').end()
|
||||
logger.info(`${user.userId} succesfully sent feedback to toska`)
|
||||
})
|
||||
|
||||
module.exports = router
|
||||
|
||||
@@ -2,8 +2,10 @@ const router = require('express').Router()
|
||||
const userService = require('../services/userService')
|
||||
|
||||
router.post('/language', async (req, res) => {
|
||||
const { language } = req.body
|
||||
const { userId } = req.decodedToken
|
||||
const {
|
||||
body: { language },
|
||||
user: { userId },
|
||||
} = req
|
||||
if (!['fi', 'sv', 'en'].includes(language)) {
|
||||
return res.status(400).json('invalid language')
|
||||
}
|
||||
|
||||
@@ -1,47 +1,19 @@
|
||||
const router = require('express').Router()
|
||||
const userService = require('../services/userService')
|
||||
const { parseHyGroups } = require('../util/utils')
|
||||
const { sendNotificationAboutNewUser } = require('../services/mailservice')
|
||||
const logger = require('../util/logger')
|
||||
const { ApplicationError } = require('../util/customErrors')
|
||||
const _ = require('lodash')
|
||||
|
||||
router.post('/login', async (req, res) => {
|
||||
const uid = req.headers['uid']
|
||||
if (req.headers['shib-session-id'] && uid) {
|
||||
const full_name = req.headers.displayname || ''
|
||||
const mail = req.headers.mail || ''
|
||||
const hyGroups = parseHyGroups(req.headers['hygroupcn'])
|
||||
const affiliations = parseHyGroups(req.headers['edupersonaffiliation'])
|
||||
const hyPersonSisuId = req.headers.hypersonsisuid || ''
|
||||
logger.info(`${uid} trying to login, referring to userservice`)
|
||||
let { token, isNew } = await userService.login(uid, full_name, hyGroups, affiliations, mail, hyPersonSisuId)
|
||||
if (isNew) {
|
||||
const result = await sendNotificationAboutNewUser({ userId: uid, userFullName: full_name })
|
||||
if (result.error) {
|
||||
return res.status(400).json(result).end()
|
||||
}
|
||||
}
|
||||
res.status(200).json({ token })
|
||||
} else {
|
||||
res
|
||||
.status(401)
|
||||
.json({
|
||||
message: `Not enough headers login, uid: ${req.headers.uid}
|
||||
session-id ${req.headers['shib-session-id']}`,
|
||||
})
|
||||
.end()
|
||||
}
|
||||
})
|
||||
router.get('/login', async (req, res) => {
|
||||
const { user, logoutUrl } = req
|
||||
|
||||
router.delete('/logout', async (req, res) => {
|
||||
const logoutUrl = req.headers.shib_logout_url
|
||||
const { returnUrl } = req.body
|
||||
if (logoutUrl) {
|
||||
return res
|
||||
.status(200)
|
||||
.send({ logoutUrl: `${logoutUrl}?return=${returnUrl}` })
|
||||
.end()
|
||||
if (!user) {
|
||||
throw new ApplicationError('User not found', 404)
|
||||
}
|
||||
res.status(200).send({ logoutUrl: returnUrl }).end()
|
||||
|
||||
res.send({
|
||||
// don't send possibly huge list of students to frontend
|
||||
user: _.omit(user, ['studentsUserCanAccess']),
|
||||
logoutUrl,
|
||||
})
|
||||
})
|
||||
|
||||
module.exports = router
|
||||
|
||||
@@ -28,7 +28,7 @@ _paq.push(["enableLinkTracking"]);
|
||||
|
||||
const script = config.MATOMO_URL ? acual : ''
|
||||
|
||||
router.get('/matomo-init', (req, res) => {
|
||||
router.get('/matomo-init', (_, res) => {
|
||||
res.contentType('text/javascript; charset=UTF-8')
|
||||
res.send(script)
|
||||
})
|
||||
|
||||
@@ -1,85 +0,0 @@
|
||||
/* eslint-disable */
|
||||
|
||||
const { router, wrapper } = require('./router').routerWithWrapper()
|
||||
const axios = require('axios')
|
||||
|
||||
wrapper.get('/ping', async (req, res) => {
|
||||
res.json({
|
||||
message: 'pong'
|
||||
})
|
||||
})
|
||||
|
||||
wrapper.get('/v3/mandatory_courses/:code', async (req, res) => {
|
||||
const courses = []
|
||||
const included = new Set()
|
||||
|
||||
const dumb_flatten = module => {
|
||||
if (module.children) {
|
||||
module.children.forEach(child => {
|
||||
dumb_flatten(child)
|
||||
})
|
||||
} else if (module.code && !module.code.startsWith('KK') && !courses.map(c => c.code).includes(module.code)) {
|
||||
courses.push({
|
||||
name: {
|
||||
fi: module.name,
|
||||
en: module.name
|
||||
},
|
||||
code: module.code,
|
||||
label: { id: `${courses.length}`, label: module.code.replace('-', '').slice(0, 4), orderNumber: courses.length }
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const better_flatten = (module, label) => {
|
||||
if (module.children) {
|
||||
module.children.forEach(child => {
|
||||
better_flatten(child, module.module ? module.module.name : label)
|
||||
})
|
||||
} else if (module.code && !module.code.startsWith('KK')) {
|
||||
courses.push({
|
||||
name: {
|
||||
fi: module.name,
|
||||
en: module.name
|
||||
},
|
||||
code: module.code,
|
||||
label: { id: `${courses.length}`, label, orderNumber: courses.length }
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const superFlattenFlatten = (module, label) => {
|
||||
if (module.children) {
|
||||
module.children.forEach(child => {
|
||||
superFlattenFlatten(child, module.module ? module.module.name : label)
|
||||
})
|
||||
} else if (Array.isArray(module)) {
|
||||
module.forEach(elem => superFlattenFlatten(elem, label))
|
||||
} else if (module.code && !module.code.startsWith('KK')) {
|
||||
if (!included.has(`${label}${module.code}`)) {
|
||||
included.add(`${label}${module.code}`)
|
||||
courses.push({
|
||||
name: {
|
||||
fi: module.name,
|
||||
en: module.name
|
||||
},
|
||||
code: module.code,
|
||||
label: { id: `${courses.length}`, label, orderNumber: courses.length }
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const code = req.params.code
|
||||
const response = await axios.get(`http://sis-updater-scheduler:8082/v1/courses/${code}?token=dev&superFlatten=true`)
|
||||
|
||||
// dumb_flatten(response.data)
|
||||
// better_flatten(response.data)
|
||||
superFlattenFlatten(response.data)
|
||||
|
||||
const byCode = (c1, c2) => (c1.code < c2.code ? -1 : 1)
|
||||
|
||||
courses.sort(byCode)
|
||||
res.json(courses)
|
||||
})
|
||||
|
||||
module.exports = router
|
||||
@@ -1,15 +1,14 @@
|
||||
const crypto = require('crypto')
|
||||
const Sentry = require('@sentry/node')
|
||||
const router = require('express').Router()
|
||||
const _ = require('lodash')
|
||||
const Population = require('../services/populations')
|
||||
const Student = require('../services/students')
|
||||
const StudyrightService = require('../services/studyrights')
|
||||
const UserService = require('../services/userService')
|
||||
const TagService = require('../services/tags')
|
||||
const CourseService = require('../services/courses')
|
||||
const StatMergeService = require('../services/statMerger')
|
||||
const { getAllStudentsUserHasInGroups } = require('../services/studyGuidanceGroups')
|
||||
const { logger } = require('../util/logger')
|
||||
const { ApplicationError } = require('../util/customErrors')
|
||||
|
||||
const filterPersonalTags = (population, userId) => {
|
||||
return {
|
||||
@@ -80,20 +79,13 @@ router.post('/v2/populationstatistics/coursesbycoursecode', async (req, res) =>
|
||||
if (Math.abs(to - from + 1) > maxYearsToCreatePopulationFrom) {
|
||||
return res.status(400).json({ error: `Max years to create population from is ${maxYearsToCreatePopulationFrom}` })
|
||||
}
|
||||
let studentnumberlist
|
||||
const studentnumbers = await Student.findByCourseAndSemesters(coursecodes, from, to)
|
||||
|
||||
const {
|
||||
decodedToken: { userId },
|
||||
roles,
|
||||
user: { isAdmin, studentsUserCanAccess },
|
||||
} = req
|
||||
|
||||
if (roles && roles.includes('admin')) {
|
||||
studentnumberlist = studentnumbers
|
||||
} else {
|
||||
const unitsUserCanAccess = await UserService.getUnitsFromElementDetails(userId)
|
||||
const codes = unitsUserCanAccess.map(unit => unit.id)
|
||||
studentnumberlist = await Student.filterStudentnumbersByAccessrights(studentnumbers, codes)
|
||||
}
|
||||
const studentnumbers = await Student.findByCourseAndSemesters(coursecodes, from, to)
|
||||
const studentnumberlist = isAdmin ? studentnumbers : _.intersection(studentnumbers, studentsUserCanAccess)
|
||||
|
||||
const result = await Population.bottlenecksOf(
|
||||
{
|
||||
@@ -120,19 +112,12 @@ router.post('/v2/populationstatistics/coursesbytag', async (req, res) => {
|
||||
res.status(400).json({ error: 'The body should have a tag defined' })
|
||||
return
|
||||
}
|
||||
let studentnumberlist
|
||||
const studentnumbers = await Student.findByTag(tag)
|
||||
const {
|
||||
decodedToken: { userId },
|
||||
roles,
|
||||
user: { isAdmin, studentsUserCanAccess },
|
||||
} = req
|
||||
if (roles && roles.includes('admin')) {
|
||||
studentnumberlist = studentnumbers
|
||||
} else {
|
||||
const unitsUserCanAccess = await UserService.getUnitsFromElementDetails(userId)
|
||||
const codes = unitsUserCanAccess.map(unit => unit.id)
|
||||
studentnumberlist = await Student.filterStudentnumbersByAccessrights(studentnumbers, codes)
|
||||
}
|
||||
const studentnumbers = await Student.findByTag(tag)
|
||||
const studentnumberlist = isAdmin ? studentnumbers : _.intersection(studentnumbers, studentsUserCanAccess)
|
||||
|
||||
const result = await Population.bottlenecksOf(
|
||||
{
|
||||
year: 1900,
|
||||
@@ -154,27 +139,17 @@ router.post('/v2/populationstatistics/coursesbytag', async (req, res) => {
|
||||
})
|
||||
|
||||
router.post('/v2/populationstatistics/coursesbystudentnumberlist', async (req, res) => {
|
||||
if (!req.body.studentnumberlist) {
|
||||
res.status(400).json({ error: 'The body should have a studentnumberlist defined' })
|
||||
return
|
||||
}
|
||||
let studentnumberlist
|
||||
const {
|
||||
decodedToken: { userId, sisPersonId },
|
||||
roles,
|
||||
user: { isAdmin, studentsUserCanAccess },
|
||||
body: { studentnumberlist: studentnumbersInReq },
|
||||
} = req
|
||||
|
||||
if (roles && roles.includes('admin')) {
|
||||
studentnumberlist = req.body.studentnumberlist
|
||||
} else if (req.body.usingStudyGuidanceGroups) {
|
||||
const studentsUserCanAccess = await getAllStudentsUserHasInGroups(sisPersonId)
|
||||
studentnumberlist = req.body.studentnumberlist.filter(s => studentsUserCanAccess.has(s))
|
||||
} else {
|
||||
const unitsUserCanAccess = await UserService.getUnitsFromElementDetails(userId)
|
||||
const codes = unitsUserCanAccess.map(unit => unit.id)
|
||||
studentnumberlist = await Student.filterStudentnumbersByAccessrights(req.body.studentnumberlist, codes)
|
||||
if (!studentnumbersInReq) {
|
||||
throw new ApplicationError('The body should have a studentnumberlist defined', 400)
|
||||
}
|
||||
|
||||
const studentnumberlist = isAdmin ? studentnumbersInReq : _.intersection(studentnumbersInReq, studentsUserCanAccess)
|
||||
|
||||
const result = await Population.bottlenecksOf(
|
||||
{
|
||||
year: 1900,
|
||||
@@ -195,7 +170,7 @@ router.post('/v2/populationstatistics/coursesbystudentnumberlist', async (req, r
|
||||
|
||||
router.get('/v3/populationstatistics', async (req, res) => {
|
||||
const { year, semesters, studyRights: studyRightsJSON } = req.query
|
||||
const { decodedToken } = req
|
||||
const { user } = req
|
||||
|
||||
if (!year || !semesters || !studyRightsJSON) {
|
||||
res.status(400).json({ error: 'The query should have a year, semester and studyRights defined' })
|
||||
@@ -204,13 +179,13 @@ router.get('/v3/populationstatistics', async (req, res) => {
|
||||
let studyRights = null
|
||||
try {
|
||||
studyRights = JSON.parse(studyRightsJSON)
|
||||
const { rights, roles } = req
|
||||
const {
|
||||
user: { rights, isAdmin },
|
||||
} = req
|
||||
|
||||
if (!roles || !roles.includes('admin')) {
|
||||
if (!rights.includes(studyRights.programme)) {
|
||||
res.status(403).json([])
|
||||
return
|
||||
}
|
||||
if (!isAdmin && !rights.includes(studyRights.programme)) {
|
||||
res.status(403).json([])
|
||||
return
|
||||
}
|
||||
} catch (e) {
|
||||
res.status(400).json({ error: 'The query had invalid studyRights' })
|
||||
@@ -245,7 +220,7 @@ router.get('/v3/populationstatistics', async (req, res) => {
|
||||
return
|
||||
}
|
||||
|
||||
res.json(filterPersonalTags(result, decodedToken.id))
|
||||
res.json(filterPersonalTags(result, user.id))
|
||||
} else {
|
||||
const result = await Population.optimizedStatisticsOf({ ...req.query, studyRights })
|
||||
if (result.error) {
|
||||
@@ -254,32 +229,26 @@ router.get('/v3/populationstatistics', async (req, res) => {
|
||||
return
|
||||
}
|
||||
|
||||
res.json(filterPersonalTags(result, decodedToken.id))
|
||||
res.json(filterPersonalTags(result, user.id))
|
||||
}
|
||||
})
|
||||
|
||||
router.get('/v3/populationstatisticsbytag', async (req, res) => {
|
||||
const { tag, studyRights: studyRightsJSON, months, year } = req.query
|
||||
const { decodedToken } = req
|
||||
const {
|
||||
user: { id, isAdmin, studentsUserCanAccess },
|
||||
query: { tag, studyRights: studyRightsJSON, months, year },
|
||||
} = req
|
||||
|
||||
if (!tag) return res.status(400).json({ error: 'The query should have a tag defined' })
|
||||
const foundTag = await TagService.findTagById(tag)
|
||||
if (!foundTag) return res.status(404).json({ error: 'Tag not found' })
|
||||
|
||||
const semesters = ['FALL', 'SPRING']
|
||||
let studentnumberlist
|
||||
|
||||
const studentnumbers = await Student.findByTag(tag)
|
||||
const {
|
||||
decodedToken: { userId },
|
||||
roles,
|
||||
} = req
|
||||
if (roles && roles.includes('admin')) {
|
||||
studentnumberlist = studentnumbers
|
||||
} else {
|
||||
const unitsUserCanAccess = await UserService.getUnitsFromElementDetails(userId)
|
||||
const codes = unitsUserCanAccess.map(unit => unit.id)
|
||||
studentnumberlist = await Student.filterStudentnumbersByAccessrights(studentnumbers, codes)
|
||||
}
|
||||
|
||||
const studentnumberlist = isAdmin ? studentnumbers : _.intersection(studentnumbers, studentsUserCanAccess)
|
||||
|
||||
const studyRights = JSON.parse(studyRightsJSON)
|
||||
const newStartYear = await Population.getEarliestYear(studentnumberlist, studyRights)
|
||||
const yearDifference = Number(year) - Number(newStartYear)
|
||||
@@ -301,12 +270,14 @@ router.get('/v3/populationstatisticsbytag', async (req, res) => {
|
||||
return
|
||||
}
|
||||
|
||||
res.json(filterPersonalTags(result, decodedToken.id))
|
||||
res.json(filterPersonalTags(result, id))
|
||||
})
|
||||
|
||||
router.get('/v3/populationstatisticsbycourse', async (req, res) => {
|
||||
const { coursecodes, from, to, separate: sep } = req.query
|
||||
const { decodedToken, roles } = req
|
||||
const { coursecodes, from, to, separate: sep, unifyCourses } = req.query
|
||||
const {
|
||||
user: { id, isAdmin, studentsUserCanAccess: allStudentsUserCanAccess },
|
||||
} = req
|
||||
const separate = sep ? JSON.parse(sep) : false
|
||||
|
||||
if (!coursecodes || !from || !to) {
|
||||
@@ -322,7 +293,13 @@ router.get('/v3/populationstatisticsbycourse', async (req, res) => {
|
||||
}
|
||||
|
||||
const semesters = ['FALL', 'SPRING']
|
||||
const studentnumbers = await Student.findByCourseAndSemesters(JSON.parse(coursecodes), from, to, separate)
|
||||
const studentnumbers = await Student.findByCourseAndSemesters(
|
||||
JSON.parse(coursecodes),
|
||||
from,
|
||||
to,
|
||||
separate,
|
||||
unifyCourses
|
||||
)
|
||||
const result = await Population.optimizedStatisticsOf(
|
||||
{
|
||||
// Useless, because studentnumbers are already filtered above by from & to.
|
||||
@@ -335,14 +312,9 @@ router.get('/v3/populationstatisticsbycourse', async (req, res) => {
|
||||
studentnumbers
|
||||
)
|
||||
|
||||
let studentsUserCanAccess
|
||||
if (roles && roles.includes('admin')) {
|
||||
studentsUserCanAccess = new Set(studentnumbers)
|
||||
} else {
|
||||
const unitsUserCanAccess = await UserService.getUnitsFromElementDetails(decodedToken.userId)
|
||||
const codes = unitsUserCanAccess.map(unit => unit.id)
|
||||
studentsUserCanAccess = new Set(await Student.filterStudentnumbersByAccessrights(studentnumbers, codes))
|
||||
}
|
||||
const studentsUserCanAccess = isAdmin
|
||||
? new Set(studentnumbers)
|
||||
: new Set(_.intersection(studentnumbers, allStudentsUserCanAccess))
|
||||
|
||||
const randomHash = crypto.randomBytes(12).toString('hex')
|
||||
const obfuscateStudent = ({ studyrights, studentNumber, courses, gender_code }) => ({
|
||||
@@ -368,26 +340,16 @@ router.get('/v3/populationstatisticsbycourse', async (req, res) => {
|
||||
return
|
||||
}
|
||||
|
||||
res.json(filterPersonalTags(result, decodedToken.id))
|
||||
res.json(filterPersonalTags(result, id))
|
||||
})
|
||||
|
||||
router.post('/v3/populationstatisticsbystudentnumbers', async (req, res) => {
|
||||
const { studentnumberlist, usingStudyGuidanceGroups } = req.body
|
||||
const { roles, decodedToken } = req
|
||||
const { studentnumberlist } = req.body
|
||||
const {
|
||||
user: { isAdmin, id, studentsUserCanAccess },
|
||||
} = req
|
||||
|
||||
if (usingStudyGuidanceGroups) {
|
||||
if (!decodedToken.sisPersonId) {
|
||||
logger.error(
|
||||
`User ${decodedToken.userId} tried to get person groups but personId was ${decodedToken.sisPersonId} in header`
|
||||
)
|
||||
return res.status(400).json({ error: 'Not possible to get groups without personId' })
|
||||
}
|
||||
}
|
||||
|
||||
const studentsUserCanAccess = usingStudyGuidanceGroups
|
||||
? await getAllStudentsUserHasInGroups(decodedToken.sisPersonId)
|
||||
: await UserService.getStudentsUserCanAccess(studentnumberlist, roles, decodedToken.userId)
|
||||
const filteredStudentNumbers = studentnumberlist.filter(s => studentsUserCanAccess.has(s))
|
||||
const filteredStudentNumbers = isAdmin ? studentnumberlist : _.intersection(studentnumberlist, studentsUserCanAccess)
|
||||
|
||||
const result = await Population.optimizedStatisticsOf(
|
||||
{
|
||||
@@ -402,12 +364,14 @@ router.post('/v3/populationstatisticsbystudentnumbers', async (req, res) => {
|
||||
Sentry.captureException(new Error(result.error))
|
||||
return
|
||||
}
|
||||
res.status(200).json(filterPersonalTags(result, decodedToken.id))
|
||||
res.status(200).json(filterPersonalTags(result, id))
|
||||
})
|
||||
|
||||
router.get('/v3/populationstatistics/studyprogrammes', async (req, res) => {
|
||||
const { rights, roles } = req
|
||||
if (roles && roles.includes('admin')) {
|
||||
const {
|
||||
user: { rights, roles },
|
||||
} = req
|
||||
if (roles?.includes('admin')) {
|
||||
const studyrights = await StudyrightService.getAssociations()
|
||||
res.json(studyrights)
|
||||
} else {
|
||||
@@ -423,10 +387,23 @@ router.get('/v3/populationstatistics/studyprogrammes/unfiltered', async (req, re
|
||||
|
||||
router.get('/v3/populationstatistics/maxYearsToCreatePopulationFrom', async (req, res) => {
|
||||
const { courseCodes } = req.query
|
||||
const maxYearsToCreatePopulationFrom = await CourseService.maxYearsToCreatePopulationFrom(JSON.parse(courseCodes))
|
||||
return res.json(maxYearsToCreatePopulationFrom)
|
||||
const maxYearsToCreatePopulationFromOpen = await CourseService.maxYearsToCreatePopulationFrom(
|
||||
JSON.parse(courseCodes),
|
||||
'openStats'
|
||||
)
|
||||
const maxYearsToCreatePopulationFromUni = await CourseService.maxYearsToCreatePopulationFrom(
|
||||
JSON.parse(courseCodes),
|
||||
'reqularStats'
|
||||
)
|
||||
const maxYearsToCreatePopulationFromBoth = await CourseService.maxYearsToCreatePopulationFrom(
|
||||
JSON.parse(courseCodes),
|
||||
'unifyStats'
|
||||
)
|
||||
return res.json({
|
||||
openCourses: maxYearsToCreatePopulationFromOpen,
|
||||
uniCourses: maxYearsToCreatePopulationFromUni,
|
||||
unifyCourses: maxYearsToCreatePopulationFromBoth,
|
||||
})
|
||||
})
|
||||
|
||||
router.use('*', (req, res, next) => next())
|
||||
|
||||
module.exports = router
|
||||
|
||||
@@ -6,6 +6,4 @@ router.get('/semesters/codes', async (req, res) => {
|
||||
res.json(providers)
|
||||
})
|
||||
|
||||
router.use('*', (req, res, next) => next())
|
||||
|
||||
module.exports = router
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
const router = require('express').Router()
|
||||
const Student = require('../services/students')
|
||||
const userService = require('../services/userService')
|
||||
const Unit = require('../services/units')
|
||||
const { ApplicationError } = require('../util/customErrors')
|
||||
|
||||
const filterStudentTags = (student, userId) => {
|
||||
return {
|
||||
@@ -12,8 +11,7 @@ const filterStudentTags = (student, userId) => {
|
||||
|
||||
router.get('/students', async (req, res) => {
|
||||
const {
|
||||
roles,
|
||||
decodedToken: { userId },
|
||||
user: { isAdmin, studentsUserCanAccess },
|
||||
query: { searchTerm },
|
||||
} = req
|
||||
|
||||
@@ -25,55 +23,30 @@ router.get('/students', async (req, res) => {
|
||||
.slice(0, 2)
|
||||
.find(t => t.length > 3)
|
||||
) {
|
||||
return res.status(400).json({ error: 'at least one search term must be longer than 3 characters' })
|
||||
throw new ApplicationError('at least one search term must be longer than 3 characters', 400)
|
||||
}
|
||||
|
||||
if (roles && roles.includes('admin')) {
|
||||
let results = []
|
||||
if (trimmedSearchTerm) {
|
||||
results = await Student.bySearchTerm(trimmedSearchTerm)
|
||||
}
|
||||
return res.status(200).json(results).end()
|
||||
} else {
|
||||
const unitsUserCanAccess = await userService.getUnitsFromElementDetails(userId)
|
||||
const codes = unitsUserCanAccess.map(unit => unit.id)
|
||||
const matchingStudents = await Student.bySearchTermAndElements(trimmedSearchTerm, codes)
|
||||
res.json(matchingStudents)
|
||||
let results = []
|
||||
if (trimmedSearchTerm) {
|
||||
results = isAdmin
|
||||
? await Student.bySearchTerm(trimmedSearchTerm)
|
||||
: await Student.bySearchTermAndStudentNumbers(trimmedSearchTerm, studentsUserCanAccess)
|
||||
}
|
||||
res.json(results)
|
||||
})
|
||||
|
||||
router.get('/students/:id', async (req, res) => {
|
||||
const { id: studentId } = req.params
|
||||
const { roles, decodedToken } = req
|
||||
const {
|
||||
user: { id, isAdmin, studentsUserCanAccess },
|
||||
} = req
|
||||
|
||||
if (roles && roles.includes('admin')) {
|
||||
const results = await Student.withId(studentId)
|
||||
return results.error
|
||||
? res.status(400).json({ error: 'error finding student' }).end()
|
||||
: res.status(200).json(filterStudentTags(results, decodedToken.id)).end()
|
||||
if (!isAdmin && !studentsUserCanAccess.includes(studentId)) {
|
||||
throw new ApplicationError('Error finding student', 400)
|
||||
}
|
||||
|
||||
const uid = req.decodedToken.userId
|
||||
const student = await Student.withId(studentId)
|
||||
if (student.error) {
|
||||
return res.status(400).json({ error: 'error finding student' }).end()
|
||||
}
|
||||
const units = await userService.getUnitsFromElementDetails(uid)
|
||||
|
||||
const rights = await Promise.all(
|
||||
units.map(async unit => {
|
||||
const jtn = await Unit.hasStudent(unit.id, student.studentNumber)
|
||||
return jtn
|
||||
})
|
||||
)
|
||||
|
||||
if (rights.some(right => right !== null)) {
|
||||
res.status(200).json(filterStudentTags(student, decodedToken.id)).end()
|
||||
} else {
|
||||
res.status(400).json({ error: 'error finding student' }).end()
|
||||
}
|
||||
res.json(filterStudentTags(student, id))
|
||||
})
|
||||
|
||||
router.use('*', (req, res, next) => next())
|
||||
|
||||
module.exports = router
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
const router = require('express').Router()
|
||||
const { logger } = require('../util/logger')
|
||||
const { getAllGroupsAndStudents } = require('../services/studyGuidanceGroups')
|
||||
const { getAllGroupsAndStudents, changeGroupTags } = require('../services/studyGuidanceGroups')
|
||||
|
||||
router.get('/', async (req, res) => {
|
||||
const {
|
||||
decodedToken: { uid, sisPersonId },
|
||||
user: { uid, sisPersonId },
|
||||
} = req
|
||||
if (!sisPersonId) {
|
||||
logger.error(`User ${uid} tried to get person groups but personId was ${sisPersonId} in header`)
|
||||
@@ -13,4 +13,27 @@ router.get('/', async (req, res) => {
|
||||
return res.json(await getAllGroupsAndStudents(sisPersonId))
|
||||
})
|
||||
|
||||
router.put('/:id/tags', async (req, res) => {
|
||||
const {
|
||||
user: { uid, sisPersonId },
|
||||
body: tags,
|
||||
params: { id: groupId },
|
||||
} = req
|
||||
|
||||
if (!sisPersonId) {
|
||||
logger.error(`User ${uid} tried to change study guidance group tags but personId was ${sisPersonId} in header`)
|
||||
return res.status(400).json({ error: 'Not possible to change groups without personId' })
|
||||
}
|
||||
|
||||
const groupsUserHasAccessTo = (await getAllGroupsAndStudents(sisPersonId)).map(group => group.id)
|
||||
|
||||
if (!groupsUserHasAccessTo.includes(groupId)) {
|
||||
logger.error(
|
||||
`User ${uid} tried to change study guidance group tags, but didn't have permission for the group ${groupId}`
|
||||
)
|
||||
return res.status(400).json({ error: 'Access denied' })
|
||||
}
|
||||
return res.status(200).json(await changeGroupTags({ groupId, tags }))
|
||||
})
|
||||
|
||||
module.exports = router
|
||||
|
||||
@@ -0,0 +1,177 @@
|
||||
const router = require('express').Router()
|
||||
const { getBasicStatsForStudytrack } = require('../services/studyprogrammeBasics')
|
||||
const { getCreditStatsForStudytrack } = require('../services/studyprogrammeCredits')
|
||||
const { getGraduationStatsForStudytrack } = require('../services/studyprogrammeGraduations')
|
||||
const { getStudytrackStatsForStudyprogramme } = require('../services/studytrackStats')
|
||||
const {
|
||||
getBasicStats,
|
||||
setBasicStats,
|
||||
getCreditStats,
|
||||
setCreditStats,
|
||||
getGraduationStats,
|
||||
setGraduationStats,
|
||||
getStudytrackStats,
|
||||
setStudytrackStats,
|
||||
} = require('../services/analyticsService')
|
||||
const { updateBasicView, updateStudytrackView } = require('../services/studyprogrammeUpdates')
|
||||
const logger = require('../util/logger')
|
||||
|
||||
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
|
||||
|
||||
if (code) {
|
||||
let data = null
|
||||
try {
|
||||
data = await getBasicStats(code, yearType, specialGroups)
|
||||
} catch (e) {
|
||||
logger.error(`Failed to get code ${code} basic stats: ${e}`)
|
||||
}
|
||||
if (!data) {
|
||||
try {
|
||||
const result = await getBasicStatsForStudytrack({
|
||||
studyprogramme: req.params.id,
|
||||
settings: {
|
||||
isAcademicYear: yearType === 'ACADEMIC_YEAR',
|
||||
includeAllSpecials: specialGroups === 'SPECIAL_INCLUDED',
|
||||
},
|
||||
})
|
||||
data = await setBasicStats(result, yearType, specialGroups)
|
||||
} catch (e) {
|
||||
logger.error(`Failed to update code ${code} basic stats: ${e}`)
|
||||
}
|
||||
}
|
||||
return res.json(data)
|
||||
} else {
|
||||
res.status(422).end()
|
||||
}
|
||||
})
|
||||
|
||||
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
|
||||
|
||||
if (code) {
|
||||
let data = null
|
||||
try {
|
||||
data = await getCreditStats(code, yearType, specialGroups)
|
||||
} catch (e) {
|
||||
logger.error(`Failed to get code ${code} credit stats`)
|
||||
}
|
||||
if (!data) {
|
||||
try {
|
||||
const result = await getCreditStatsForStudytrack({
|
||||
studyprogramme: req.params.id,
|
||||
settings: {
|
||||
isAcademicYear: yearType === 'ACADEMIC_YEAR',
|
||||
includeAllSpecials: specialGroups === 'SPECIAL_INCLUDED',
|
||||
},
|
||||
})
|
||||
data = await setCreditStats(result, yearType, specialGroups)
|
||||
} catch (e) {
|
||||
logger.error(`Failed to update code ${code} credit stats`)
|
||||
}
|
||||
}
|
||||
return res.json(data)
|
||||
} else {
|
||||
res.status(422).end()
|
||||
}
|
||||
})
|
||||
|
||||
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
|
||||
|
||||
if (code) {
|
||||
let data = null
|
||||
try {
|
||||
data = await getGraduationStats(code, yearType, specialGroups)
|
||||
} catch (e) {
|
||||
logger.error(`Failed to get code ${code} graduation stats`)
|
||||
}
|
||||
if (!data) {
|
||||
try {
|
||||
const result = await getGraduationStatsForStudytrack({
|
||||
studyprogramme: req.params.id,
|
||||
settings: {
|
||||
isAcademicYear: yearType === 'ACADEMIC_YEAR',
|
||||
includeAllSpecials: specialGroups === 'SPECIAL_INCLUDED',
|
||||
},
|
||||
})
|
||||
data = await setGraduationStats(result, yearType, specialGroups)
|
||||
} catch (e) {
|
||||
logger.error(`Failed to update code ${code} graduation stats`)
|
||||
}
|
||||
}
|
||||
return res.json(data)
|
||||
} else {
|
||||
res.status(422).end()
|
||||
}
|
||||
})
|
||||
|
||||
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
|
||||
|
||||
if (code) {
|
||||
let data = null
|
||||
try {
|
||||
data = await getStudytrackStats(code, graduated, specialGroups)
|
||||
} catch (e) {
|
||||
logger.error(`Failed to get code ${code} studytrack stats: ${e}`)
|
||||
}
|
||||
if (!data) {
|
||||
try {
|
||||
const result = await getStudytrackStatsForStudyprogramme({
|
||||
studyprogramme: code,
|
||||
settings: {
|
||||
graduated: graduated === 'GRADUATED_INCLUDED',
|
||||
specialGroups: specialGroups === 'SPECIAL_INCLUDED',
|
||||
},
|
||||
})
|
||||
data = await setStudytrackStats(result, graduated, specialGroups)
|
||||
} catch (e) {
|
||||
logger.error(`Failed to update code ${code} studytrack stats: ${e}`)
|
||||
}
|
||||
}
|
||||
return res.json(data)
|
||||
} else {
|
||||
res.status(422).end()
|
||||
}
|
||||
})
|
||||
|
||||
router.get('/v2/studyprogrammes/:id/update_basicview', async (req, res) => {
|
||||
const code = req.params.id
|
||||
if (code) {
|
||||
let result = null
|
||||
try {
|
||||
result = await updateBasicView(code)
|
||||
} catch (e) {
|
||||
logger.error(`Failed to update code ${code} basic stats: ${e}`)
|
||||
}
|
||||
return res.json(result)
|
||||
} else {
|
||||
res.status(422).end()
|
||||
}
|
||||
})
|
||||
|
||||
router.get('/v2/studyprogrammes/:id/update_studytrackview', async (req, res) => {
|
||||
const code = req.params.id
|
||||
if (code) {
|
||||
let result = null
|
||||
try {
|
||||
result = await updateStudytrackView(code)
|
||||
} catch (e) {
|
||||
logger.error(`Failed to update code ${code} studytrack stats: ${e}`)
|
||||
}
|
||||
return res.json(result)
|
||||
} else {
|
||||
res.status(422).end()
|
||||
}
|
||||
})
|
||||
|
||||
module.exports = router
|
||||
@@ -1,21 +0,0 @@
|
||||
const router = require('express').Router()
|
||||
const userService = require('../services/userService')
|
||||
|
||||
router.post('/:uid', async (req, res) => {
|
||||
const uid = req.headers['uid']
|
||||
if (req.headers['shib-session-id'] && uid) {
|
||||
const asUser = req.params.uid
|
||||
const token = await userService.superlogin(uid, asUser)
|
||||
res.status(200).json(token)
|
||||
} else {
|
||||
res
|
||||
.status(401)
|
||||
.json({
|
||||
message: `Not enough headers login, uid:
|
||||
${req.headers.uid} session-id ${req.headers['shib-session-id']}`,
|
||||
})
|
||||
.end()
|
||||
}
|
||||
})
|
||||
|
||||
module.exports = router
|
||||
@@ -12,48 +12,50 @@ const filterRelevantStudentTags = (studentTags, userId) => {
|
||||
return studentTags.filter(({ tag }) => !tag.personal_user_id || tag.personal_user_id === userId)
|
||||
}
|
||||
|
||||
router.get('/tags', async (req, res) => {
|
||||
router.get('/tags', async (_req, res) => {
|
||||
const tags = await Tags.findTags()
|
||||
res.status(200).json(tags)
|
||||
})
|
||||
|
||||
router.get('/tags/:studytrack', async (req, res) => {
|
||||
const { studytrack } = req.params
|
||||
const { rights, roles, decodedToken } = req
|
||||
const {
|
||||
user: { rights, roles, id },
|
||||
} = req
|
||||
|
||||
if (!rights.includes(studytrack) && !(roles && roles.includes('admin'))) return res.status(403).end()
|
||||
if (!rights.includes(studytrack) && !roles?.includes('admin')) return res.status(403).end()
|
||||
|
||||
const tags = await Tags.findTagsByStudytrack(studytrack)
|
||||
res.status(200).json(filterRelevantTags(tags, decodedToken.id))
|
||||
res.status(200).json(filterRelevantTags(tags, id))
|
||||
})
|
||||
|
||||
router.post('/tags', async (req, res) => {
|
||||
const {
|
||||
tag: { studytrack, tagname, year, personal_user_id },
|
||||
} = req.body
|
||||
const { rights, roles, decodedToken } = req
|
||||
const {
|
||||
user: { rights, roles, id },
|
||||
} = req
|
||||
|
||||
if (!rights.includes(studytrack) && !(roles && roles.includes('admin'))) return res.status(403).end()
|
||||
if (!rights.includes(studytrack) && !roles?.includes('admin')) return res.status(403).end()
|
||||
|
||||
await Tags.createNewTag({ studytrack, tagname, year, personal_user_id })
|
||||
const tags = await Tags.findTagsByStudytrack(studytrack)
|
||||
res.status(200).json(filterRelevantTags(tags, decodedToken.id))
|
||||
res.status(200).json(filterRelevantTags(tags, id))
|
||||
})
|
||||
|
||||
router.delete('/tags', async (req, res) => {
|
||||
const { tag } = req.body
|
||||
const { rights, roles, decodedToken } = req
|
||||
const {
|
||||
user: { rights, roles, id },
|
||||
} = req
|
||||
|
||||
if (
|
||||
!rights.includes(tag.studytrack) &&
|
||||
!(roles && roles.includes('admin')) &&
|
||||
!(tag.personal_user_id === decodedToken.id)
|
||||
)
|
||||
if (!rights.includes(tag.studytrack) && !roles?.includes('admin') && !(tag.personal_user_id === id))
|
||||
return res.status(403).end()
|
||||
|
||||
await Tags.deleteTag(tag)
|
||||
const t = await Tags.findTagsByStudytrack(tag.studytrack)
|
||||
res.status(200).json(filterRelevantTags(t, decodedToken.id))
|
||||
res.status(200).json(filterRelevantTags(t, id))
|
||||
})
|
||||
|
||||
router.get('/studenttags', async (req, res) => {
|
||||
@@ -63,37 +65,43 @@ router.get('/studenttags', async (req, res) => {
|
||||
|
||||
router.get('/studenttags/:studytrack', async (req, res) => {
|
||||
const { studytrack } = req.params
|
||||
const { rights, roles, decodedToken } = req
|
||||
const {
|
||||
user: { rights, roles, id },
|
||||
} = req
|
||||
|
||||
if (!rights.includes(studytrack) && !(roles && roles.includes('admin'))) return res.status(403).end()
|
||||
if (!rights.includes(studytrack) && !roles?.includes('admin')) return res.status(403).end()
|
||||
|
||||
const result = await TagStudent.getStudentTagsByStudytrack(studytrack)
|
||||
res.status(200).json(filterRelevantStudentTags(result, decodedToken.id))
|
||||
res.status(200).json(filterRelevantStudentTags(result, id))
|
||||
})
|
||||
|
||||
router.get('/studenttags/:studentnumber', async (req, res) => {
|
||||
const { studentnumber } = req.params
|
||||
const { decodedToken } = req
|
||||
const { user } = req
|
||||
const result = await TagStudent.getStudentTagsByStudentnumber(studentnumber)
|
||||
res.status(200).json(filterRelevantStudentTags(result, decodedToken.id))
|
||||
res.status(200).json(filterRelevantStudentTags(result, user.id))
|
||||
})
|
||||
|
||||
router.post('/studenttags/:studentnumber', async (req, res) => {
|
||||
const { tag, studytrack } = req.body
|
||||
const { rights, roles, decodedToken } = req
|
||||
const {
|
||||
user: { roles, rights, id },
|
||||
} = req
|
||||
|
||||
if (!rights.includes(studytrack) && !(roles && roles.includes('admin'))) return res.status(403).end()
|
||||
if (!rights.includes(studytrack) && !roles?.includes('admin')) return res.status(403).end()
|
||||
|
||||
await TagStudent.createStudentTag(tag)
|
||||
const result = await TagStudent.getStudentTagsByStudytrack(studytrack)
|
||||
res.status(200).json(filterRelevantStudentTags(result, decodedToken.id))
|
||||
res.status(200).json(filterRelevantStudentTags(result, id))
|
||||
})
|
||||
|
||||
router.post('/studenttags', async (req, res) => {
|
||||
const { tags, studytrack } = req.body
|
||||
const { rights, roles, decodedToken } = req
|
||||
const {
|
||||
user: { rights, roles, id },
|
||||
} = req
|
||||
|
||||
if (!rights.includes(studytrack) && !(roles && roles.includes('admin'))) return res.status(403).end()
|
||||
if (!rights.includes(studytrack) && !roles?.includes('admin')) return res.status(403).end()
|
||||
|
||||
const existingtags = await Tags.findTagsByStudytrack(studytrack)
|
||||
const existingTagids = existingtags.map(t => t.tag_id)
|
||||
@@ -110,37 +118,39 @@ router.post('/studenttags', async (req, res) => {
|
||||
|
||||
await TagStudent.createMultipleStudentTags(tags)
|
||||
const result = await TagStudent.getStudentTagsByStudytrack(studytrack)
|
||||
res.status(200).json(filterRelevantStudentTags(result, decodedToken.id))
|
||||
res.status(200).json(filterRelevantStudentTags(result, id))
|
||||
})
|
||||
|
||||
router.delete('/studenttags/delete_one', async (req, res) => {
|
||||
const { tag_id, studentnumber, studytrack } = req.body
|
||||
const { rights, roles, decodedToken } = req
|
||||
const {
|
||||
user: { rights, roles, id },
|
||||
} = req
|
||||
|
||||
if (!rights.includes(studytrack) && !(roles && roles.includes('admin'))) return res.status(403).end()
|
||||
if (!rights.includes(studytrack) && !roles?.includes('admin')) return res.status(403).end()
|
||||
|
||||
const tags = await Tags.findTagsFromStudytrackById(studytrack, [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)
|
||||
res.status(200).json(filterRelevantStudentTags(result, decodedToken.id))
|
||||
res.status(200).json(filterRelevantStudentTags(result, id))
|
||||
})
|
||||
|
||||
router.delete('/studenttags/delete_many', async (req, res) => {
|
||||
const { tagId, studentnumbers, studytrack } = req.body
|
||||
const { rights, roles, decodedToken } = req
|
||||
const {
|
||||
user: { rights, roles, id },
|
||||
} = req
|
||||
|
||||
if (!rights.includes(studytrack) && !(roles && roles.includes('admin'))) return res.status(403).end()
|
||||
if (!rights.includes(studytrack) && !roles?.includes('admin')) return res.status(403).end()
|
||||
|
||||
const tags = await Tags.findTagsFromStudytrackById(studytrack, [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)
|
||||
res.status(200).json(filterRelevantStudentTags(result, decodedToken.id))
|
||||
res.status(200).json(filterRelevantStudentTags(result, id))
|
||||
})
|
||||
|
||||
router.use('*', (req, res, next) => next())
|
||||
|
||||
module.exports = router
|
||||
|
||||
@@ -42,7 +42,9 @@ router.get('/top/categories', async (req, res) => {
|
||||
})
|
||||
|
||||
router.get('/stats', async (req, res) => {
|
||||
const { rights, roles } = req
|
||||
const {
|
||||
user: { rights, roles },
|
||||
} = req
|
||||
|
||||
const { providers, semesterStart, semesterEnd } = req.query
|
||||
if (!providers || !semesterStart) {
|
||||
@@ -78,6 +80,4 @@ router.get('/:id', async (req, res) => {
|
||||
res.json(result)
|
||||
})
|
||||
|
||||
router.use('*', (req, res, next) => next())
|
||||
|
||||
module.exports = router
|
||||
|
||||
@@ -9,8 +9,12 @@ router.post('/event', (req, res) => {
|
||||
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(req.decodedToken.userId, { group, name, label, value }).catch(e => {
|
||||
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) {
|
||||
|
||||
@@ -1,18 +1,8 @@
|
||||
const router = require('express').Router()
|
||||
const userService = require('../services/userService')
|
||||
const blacklist = require('../services/blacklist')
|
||||
const { userDataCache } = require('../services/cache')
|
||||
const { sendNotificationAboutAccessToUser, previewNotificationAboutAccessToUser } = require('../services/mailservice')
|
||||
const logger = require('../util/logger')
|
||||
|
||||
const addUserToBlacklist = async (user, decodedToken) => {
|
||||
if (user) {
|
||||
const { username } = user
|
||||
userDataCache.del(username)
|
||||
if (username != decodedToken.userId) await blacklist.addUserToBlacklist(username)
|
||||
}
|
||||
}
|
||||
|
||||
router.get('/', async (req, res) => {
|
||||
const results = await userService.findAll()
|
||||
res.json(results)
|
||||
@@ -25,12 +15,7 @@ router.get('/access_groups', async (req, res) => {
|
||||
|
||||
router.post('/modifyaccess', async (req, res) => {
|
||||
try {
|
||||
const { username } = req.body
|
||||
const result = await userService.modifyAccess(req.body)
|
||||
const user = {
|
||||
username,
|
||||
}
|
||||
await addUserToBlacklist(user, req.decodedToken)
|
||||
res.status(200).json(result)
|
||||
} catch (e) {
|
||||
res.status(400).json(e)
|
||||
@@ -60,7 +45,6 @@ router.post('/:uid/elements', async (req, res) => {
|
||||
const { uid } = req.params
|
||||
const { codes } = req.body
|
||||
const user = await userService.enableElementDetails(uid, codes)
|
||||
await addUserToBlacklist(user, req.decodedToken)
|
||||
res.json(user)
|
||||
})
|
||||
|
||||
@@ -68,7 +52,6 @@ router.delete('/:uid/elements', async (req, res) => {
|
||||
const { uid } = req.params
|
||||
const { codes } = req.body
|
||||
const user = await userService.removeElementDetails(uid, codes)
|
||||
await addUserToBlacklist(user, req.decodedToken)
|
||||
res.json(user)
|
||||
})
|
||||
|
||||
@@ -76,7 +59,6 @@ router.post('/:uid/faculties', async (req, res) => {
|
||||
const { uid } = req.params
|
||||
const { faculties } = req.body
|
||||
const user = await userService.setFaculties(uid, faculties)
|
||||
await addUserToBlacklist(user, req.decodedToken)
|
||||
res.json(user)
|
||||
})
|
||||
|
||||
|
||||
@@ -1,21 +1,23 @@
|
||||
const { redisClient } = require('../services/redis')
|
||||
const moment = require('moment')
|
||||
|
||||
const createRedisKeyForProductivity = id => `PRODUCTIVITY_${id}`
|
||||
const createRedisKeyForThroughput = id => `THROUGHPUT_${id}`
|
||||
const createRedisKeyForBasicStats = (id, yearType, specialGroups) => `BASIC_STATS_${id}_${yearType}_${specialGroups}`
|
||||
const createRedisKeyForCreditStats = (id, yearType, specialGroups) => `CREDIT_STATS_${id}_${yearType}_${specialGroups}`
|
||||
const createRedisKeyForGraduationStats = (id, yearType, specialGroups) =>
|
||||
`GRADUATION_STATS_${id}_${yearType}_${specialGroups}`
|
||||
const createRedisKeyForStudytrackStats = (id, graduated, specialGroups) =>
|
||||
`STUDYTRACK_STATS_${id}_${graduated}_${specialGroups}`
|
||||
|
||||
const getProductivity = async id => {
|
||||
const redisKey = createRedisKeyForProductivity(id)
|
||||
const getBasicStats = async (id, yearType, specialGroups) => {
|
||||
const redisKey = createRedisKeyForBasicStats(id, yearType, specialGroups)
|
||||
const dataFromRedis = await redisClient.getAsync(redisKey)
|
||||
if (!dataFromRedis) return null
|
||||
return {
|
||||
[id]: JSON.parse(dataFromRedis),
|
||||
}
|
||||
return JSON.parse(dataFromRedis)
|
||||
}
|
||||
|
||||
const setProductivity = async data => {
|
||||
const setBasicStats = async (data, yearType, specialGroups) => {
|
||||
const { id } = data
|
||||
const redisKey = createRedisKeyForProductivity(id)
|
||||
const redisKey = createRedisKeyForBasicStats(id, yearType, specialGroups)
|
||||
const dataToRedis = {
|
||||
...data,
|
||||
status: 'DONE',
|
||||
@@ -23,75 +25,76 @@ const setProductivity = async data => {
|
||||
}
|
||||
const setOperationStatus = await redisClient.setAsync(redisKey, JSON.stringify(dataToRedis))
|
||||
if (setOperationStatus !== 'OK') return null
|
||||
return {
|
||||
[id]: dataToRedis,
|
||||
}
|
||||
return dataToRedis
|
||||
}
|
||||
|
||||
const patchProductivity = async data => {
|
||||
const { id } = data
|
||||
const redisKey = createRedisKeyForProductivity(id)
|
||||
const dataFromRedis = JSON.parse(await redisClient.getAsync(redisKey))
|
||||
const patchedData = {
|
||||
...dataFromRedis,
|
||||
...data,
|
||||
lastUpdated: moment().format(),
|
||||
}
|
||||
const setOperationStatus = await redisClient.setAsync(redisKey, JSON.stringify(patchedData))
|
||||
if (setOperationStatus !== 'OK') return null
|
||||
return {
|
||||
[id]: patchedData,
|
||||
}
|
||||
}
|
||||
|
||||
const getThroughput = async id => {
|
||||
const redisKey = createRedisKeyForThroughput(id)
|
||||
const getCreditStats = async (id, yearType, specialGroups) => {
|
||||
const redisKey = createRedisKeyForCreditStats(id, yearType, specialGroups)
|
||||
const dataFromRedis = await redisClient.getAsync(redisKey)
|
||||
if (!dataFromRedis) return null
|
||||
return {
|
||||
[id]: JSON.parse(dataFromRedis),
|
||||
}
|
||||
return JSON.parse(dataFromRedis)
|
||||
}
|
||||
|
||||
const setThroughput = async data => {
|
||||
const { id, data: dataToSet } = data
|
||||
const redisKey = createRedisKeyForThroughput(id)
|
||||
const setCreditStats = async (data, yearType, specialGroups) => {
|
||||
const { id } = data
|
||||
const redisKey = createRedisKeyForCreditStats(id, yearType, specialGroups)
|
||||
const dataToRedis = {
|
||||
data: dataToSet.years,
|
||||
totals: dataToSet.totals,
|
||||
stTotals: dataToSet.stTotals,
|
||||
...data,
|
||||
status: 'DONE',
|
||||
lastUpdated: moment().format(),
|
||||
}
|
||||
const setOperationStatus = await redisClient.setAsync(redisKey, JSON.stringify(dataToRedis))
|
||||
if (setOperationStatus !== 'OK') return null
|
||||
return {
|
||||
[id]: dataToRedis,
|
||||
}
|
||||
return dataToRedis
|
||||
}
|
||||
|
||||
const patchThroughput = async data => {
|
||||
const [id, dataToPatch] = Object.entries(data)[0]
|
||||
const redisKey = createRedisKeyForThroughput(id)
|
||||
const { status } = dataToPatch
|
||||
const dataFromRedis = JSON.parse(await redisClient.getAsync(redisKey))
|
||||
const patchedData = {
|
||||
...dataFromRedis,
|
||||
status,
|
||||
const getGraduationStats = async (id, yearType, specialGroups) => {
|
||||
const redisKey = createRedisKeyForGraduationStats(id, yearType, specialGroups)
|
||||
const dataFromRedis = await redisClient.getAsync(redisKey)
|
||||
if (!dataFromRedis) return null
|
||||
return JSON.parse(dataFromRedis)
|
||||
}
|
||||
|
||||
const setGraduationStats = async (data, yearType, specialGroups) => {
|
||||
const { id } = data
|
||||
const redisKey = createRedisKeyForGraduationStats(id, yearType, specialGroups)
|
||||
const dataToRedis = {
|
||||
...data,
|
||||
status: 'DONE',
|
||||
lastUpdated: moment().format(),
|
||||
}
|
||||
const setOperationStatus = await redisClient.setAsync(redisKey, JSON.stringify(patchedData))
|
||||
const setOperationStatus = await redisClient.setAsync(redisKey, JSON.stringify(dataToRedis))
|
||||
if (setOperationStatus !== 'OK') return null
|
||||
return {
|
||||
[id]: patchedData,
|
||||
return dataToRedis
|
||||
}
|
||||
|
||||
const getStudytrackStats = async (id, graduated, specialGroups) => {
|
||||
const redisKey = createRedisKeyForStudytrackStats(id, graduated, specialGroups)
|
||||
const dataFromRedis = await redisClient.getAsync(redisKey)
|
||||
if (!dataFromRedis) return null
|
||||
return JSON.parse(dataFromRedis)
|
||||
}
|
||||
|
||||
const setStudytrackStats = async (data, graduated, specialGroups) => {
|
||||
const { id } = data
|
||||
const redisKey = createRedisKeyForStudytrackStats(id, graduated, specialGroups)
|
||||
const dataToRedis = {
|
||||
...data,
|
||||
status: 'DONE',
|
||||
lastUpdated: moment().format(),
|
||||
}
|
||||
const setOperationStatus = await redisClient.setAsync(redisKey, JSON.stringify(dataToRedis))
|
||||
if (setOperationStatus !== 'OK') return null
|
||||
return dataToRedis
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getProductivity,
|
||||
setProductivity,
|
||||
patchProductivity,
|
||||
getThroughput,
|
||||
setThroughput,
|
||||
patchThroughput,
|
||||
getBasicStats,
|
||||
setBasicStats,
|
||||
getCreditStats,
|
||||
setCreditStats,
|
||||
getGraduationStats,
|
||||
setGraduationStats,
|
||||
getStudytrackStats,
|
||||
setStudytrackStats,
|
||||
}
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
const { redisClient } = require('./redis')
|
||||
const moment = require('moment')
|
||||
const logger = require('../util/logger')
|
||||
|
||||
const MAKE_REDIS_HASH_KEY = id => `BLACKLIST ${id}`
|
||||
|
||||
const addUserToBlacklist = async userId => {
|
||||
logger.info(`user ${userId} added to token blacklist`)
|
||||
await redisClient.setAsync(MAKE_REDIS_HASH_KEY(userId), moment().toISOString(false))
|
||||
}
|
||||
|
||||
const isUserBlacklisted = async (userId, tokenCreatedAt) => {
|
||||
const isBlacklisted = async () => {
|
||||
const blacklistTimestamp = await redisClient.getAsync(MAKE_REDIS_HASH_KEY(userId))
|
||||
return tokenCreatedAt && blacklistTimestamp && moment(tokenCreatedAt) < moment(blacklistTimestamp)
|
||||
}
|
||||
|
||||
const timer = () => new Promise(resolve => setTimeout(() => resolve(true), 5000))
|
||||
return Promise.race([isBlacklisted(), timer()])
|
||||
}
|
||||
|
||||
module.exports = { addUserToBlacklist, isUserBlacklisted }
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user